feat(combat): Add proactive LOS fixing with smart repositioning
Implements ProactiveLoSFixer component that intercepts spell cast attempts, checks LOS, and repositions the bot to a valid position before casting. Completes all missing LineOfSightManager method implementations and upgrades MovementIntegration to use smart position-finding instead of naive movement toward target. Key additions: - ProactiveLoSFixer: pre-cast LOS check with queued cast + reposition - Healer group LOS maintenance: proactive repositioning to see group - 30+ missing LineOfSightManager method implementations completed - LoSUtils::DoLinesIntersect segment intersection implementation - MovementIntegration::CheckLineOfSight upgraded to use FindBestLoSPosition Co-Authored-By: Claude Opus 4.6 <[email protected]> Signed-off-by: luis <[email protected]>
This commit is contained in:
committed by
luis
co-authored by
Claude Opus 4.6
parent
88f3e121fa
commit
552e674c0a
@@ -840,4 +840,810 @@ Position LoSUtils::GetLastVisiblePoint(const Position& from, const Position& to,
|
||||
return current;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MISSING METHOD IMPLEMENTATIONS
|
||||
// ============================================================================
|
||||
|
||||
bool LineOfSightManager::WillHaveLineOfSightAfterMovement(const Position& newPos, Unit* target)
|
||||
{
|
||||
if (!target)
|
||||
return false;
|
||||
|
||||
return HasLineOfSightFromPosition(newPos, target);
|
||||
}
|
||||
|
||||
Position LineOfSightManager::GetClosestUnblockedPosition(Unit* target)
|
||||
{
|
||||
if (!target)
|
||||
return _bot->GetPosition();
|
||||
|
||||
// Use FindBestLineOfSightPosition with a small preferred range to get closest
|
||||
float currentDist = ::std::sqrt(_bot->GetExactDistSq(target));
|
||||
Position best = FindBestLineOfSightPosition(target, currentDist > 5.0f ? currentDist : 10.0f);
|
||||
|
||||
// If FindBest returned our own position (no candidates found), try stepping toward target
|
||||
if (best.GetExactDist(_bot) < 1.0f)
|
||||
{
|
||||
// Step directly toward the target in increments to find the first visible point
|
||||
float angle = _bot->GetAbsoluteAngle(target);
|
||||
float totalDist = currentDist;
|
||||
|
||||
for (float step = 3.0f; step < totalDist; step += 3.0f)
|
||||
{
|
||||
Position candidate;
|
||||
candidate.m_positionX = _bot->GetPositionX() + step * ::std::cos(angle);
|
||||
candidate.m_positionY = _bot->GetPositionY() + step * ::std::sin(angle);
|
||||
candidate.m_positionZ = _bot->GetPositionZ();
|
||||
|
||||
// Correct Z to ground level
|
||||
Map* map = _bot->GetMap();
|
||||
if (map)
|
||||
{
|
||||
float groundZ = map->GetHeight(_bot->GetPhaseShift(), candidate.m_positionX,
|
||||
candidate.m_positionY, candidate.m_positionZ + 5.0f);
|
||||
if (groundZ > INVALID_HEIGHT)
|
||||
candidate.m_positionZ = groundZ + 0.5f;
|
||||
}
|
||||
|
||||
if (HasLineOfSightFromPosition(candidate, target))
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
::std::vector<ObjectGuid> LineOfSightManager::GetBlockingObjects(Unit* target)
|
||||
{
|
||||
::std::vector<ObjectGuid> blockingGuids;
|
||||
if (!target)
|
||||
return blockingGuids;
|
||||
|
||||
Position from = _bot->GetPosition();
|
||||
Position to = target->GetPosition();
|
||||
float searchRange = from.GetExactDist(&to);
|
||||
|
||||
Map* map = _bot->GetMap();
|
||||
if (!map)
|
||||
return blockingGuids;
|
||||
|
||||
DoubleBufferedSpatialGrid* spatialGrid = sSpatialGridManager.GetGrid(map);
|
||||
if (!spatialGrid)
|
||||
{
|
||||
sSpatialGridManager.CreateGrid(map);
|
||||
spatialGrid = sSpatialGridManager.GetGrid(map);
|
||||
if (!spatialGrid)
|
||||
return blockingGuids;
|
||||
}
|
||||
|
||||
::std::vector<ObjectGuid> nearbyGuids = spatialGrid->QueryNearbyGameObjectGuids(
|
||||
_bot->GetPosition(), searchRange);
|
||||
|
||||
for (ObjectGuid guid : nearbyGuids)
|
||||
{
|
||||
GameObject* obj = map->GetGameObject(guid);
|
||||
if (!obj || !obj->IsInWorld())
|
||||
continue;
|
||||
|
||||
if (obj->GetGoType() == GAMEOBJECT_TYPE_DOOR && obj->GetGoState() == GO_STATE_ACTIVE)
|
||||
continue;
|
||||
|
||||
Position objPos = obj->GetPosition();
|
||||
float objDist = ::std::sqrt(obj->GetExactDistSq(from));
|
||||
if (objDist < searchRange && objDist > 1.0f)
|
||||
{
|
||||
if (LoSUtils::DoLinesIntersect(from, to, objPos, objPos))
|
||||
blockingGuids.push_back(guid);
|
||||
}
|
||||
}
|
||||
|
||||
return blockingGuids;
|
||||
}
|
||||
|
||||
bool LineOfSightManager::IsObstructionTemporary(const LoSResult& result)
|
||||
{
|
||||
// Terrain and building obstructions are permanent
|
||||
if (result.blockedByTerrain || result.blockedByBuilding)
|
||||
return false;
|
||||
|
||||
// Unit obstructions are temporary (units move)
|
||||
if (result.blockedByUnit)
|
||||
return true;
|
||||
|
||||
// Object obstructions could be temporary (doors open/close)
|
||||
if (result.blockedByObject && !result.blockingObjectGuid.IsEmpty())
|
||||
{
|
||||
if (_dynamicObstructions.Contains(result.blockingObjectGuid))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
float LineOfSightManager::EstimateTimeUntilClearPath(Unit* target)
|
||||
{
|
||||
if (!target)
|
||||
return -1.0f;
|
||||
|
||||
LoSResult result = CheckLineOfSight(target, LoSCheckType::BASIC);
|
||||
if (result.hasLineOfSight)
|
||||
return 0.0f;
|
||||
|
||||
if (!IsObstructionTemporary(result))
|
||||
return -1.0f; // Permanent obstruction, won't clear
|
||||
|
||||
// Estimate based on unit movement speeds (~7 yards/sec average)
|
||||
if (result.blockedByUnit)
|
||||
return 2.0f; // Rough estimate: units typically move out of the way in ~2 sec
|
||||
|
||||
// Dynamic objects (doors) - could be 5-30 seconds
|
||||
return 10.0f;
|
||||
}
|
||||
|
||||
bool LineOfSightManager::IsAngleAcceptable(const Position& from, const Position& to, float maxAngle)
|
||||
{
|
||||
float dx = to.GetPositionX() - from.GetPositionX();
|
||||
float dy = to.GetPositionY() - from.GetPositionY();
|
||||
float angle = ::std::atan2(dy, dx);
|
||||
|
||||
// Compare against the source's orientation
|
||||
float orientation = from.GetOrientation();
|
||||
float diff = ::std::abs(angle - orientation);
|
||||
|
||||
// Normalize to [0, PI]
|
||||
while (diff > static_cast<float>(M_PI))
|
||||
diff = 2.0f * static_cast<float>(M_PI) - diff;
|
||||
|
||||
return diff <= maxAngle;
|
||||
}
|
||||
|
||||
bool LineOfSightManager::IsWithinViewingAngle(Unit* target, float maxAngle)
|
||||
{
|
||||
if (!target)
|
||||
return false;
|
||||
|
||||
return IsAngleAcceptable(_bot->GetPosition(), target->GetPosition(), maxAngle);
|
||||
}
|
||||
|
||||
float LineOfSightManager::CalculateViewingAngle(Unit* target)
|
||||
{
|
||||
if (!target)
|
||||
return static_cast<float>(M_PI);
|
||||
|
||||
float angle = _bot->GetAbsoluteAngle(target);
|
||||
float orientation = _bot->GetOrientation();
|
||||
float diff = ::std::abs(angle - orientation);
|
||||
|
||||
while (diff > static_cast<float>(M_PI))
|
||||
diff = 2.0f * static_cast<float>(M_PI) - diff;
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
bool LineOfSightManager::RequiresFacing(Unit* target, LoSCheckType checkType)
|
||||
{
|
||||
if (!target)
|
||||
return false;
|
||||
|
||||
// Movement doesn't require facing
|
||||
if (checkType == LoSCheckType::MOVEMENT)
|
||||
return false;
|
||||
|
||||
// Spell casting typically requires facing
|
||||
if (checkType == LoSCheckType::SPELL_CASTING || checkType == LoSCheckType::HEALING ||
|
||||
checkType == LoSCheckType::INTERRUPT)
|
||||
return true;
|
||||
|
||||
// Ranged combat requires facing
|
||||
if (checkType == LoSCheckType::RANGED_COMBAT)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Position LineOfSightManager::CalculateOptimalViewingPosition(Unit* target)
|
||||
{
|
||||
if (!target)
|
||||
return _bot->GetPosition();
|
||||
|
||||
// Find position that provides LOS and is at optimal range
|
||||
float optimalRange = 20.0f; // Default for ranged
|
||||
return FindBestLineOfSightPosition(target, optimalRange);
|
||||
}
|
||||
|
||||
uint32 LineOfSightManager::CountVisibleTargets(const ::std::vector<Unit*>& targets)
|
||||
{
|
||||
uint32 count = 0;
|
||||
for (Unit* target : targets)
|
||||
{
|
||||
if (target && CanSeeTarget(target))
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
Position LineOfSightManager::FindElevatedPosition(Unit* target)
|
||||
{
|
||||
if (!target)
|
||||
return _bot->GetPosition();
|
||||
|
||||
Position botPos = _bot->GetPosition();
|
||||
Position targetPos = target->GetPosition();
|
||||
Map* map = _bot->GetMap();
|
||||
if (!map)
|
||||
return botPos;
|
||||
|
||||
// Search for nearby positions that are higher than current
|
||||
Position bestPos = botPos;
|
||||
float bestElevation = botPos.GetPositionZ();
|
||||
|
||||
for (float angle = 0.0f; angle < 2.0f * static_cast<float>(M_PI); angle += static_cast<float>(M_PI) / 6.0f)
|
||||
{
|
||||
for (float dist = 5.0f; dist <= 20.0f; dist += 5.0f)
|
||||
{
|
||||
Position candidate;
|
||||
candidate.m_positionX = botPos.GetPositionX() + dist * ::std::cos(angle);
|
||||
candidate.m_positionY = botPos.GetPositionY() + dist * ::std::sin(angle);
|
||||
|
||||
float groundZ = map->GetHeight(_bot->GetPhaseShift(), candidate.m_positionX,
|
||||
candidate.m_positionY, botPos.GetPositionZ() + 20.0f);
|
||||
if (groundZ <= INVALID_HEIGHT)
|
||||
continue;
|
||||
|
||||
candidate.m_positionZ = groundZ + 0.5f;
|
||||
|
||||
if (candidate.m_positionZ > bestElevation && HasLineOfSightFromPosition(candidate, target))
|
||||
{
|
||||
bestElevation = candidate.m_positionZ;
|
||||
bestPos = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestPos;
|
||||
}
|
||||
|
||||
void LineOfSightManager::UpdateDynamicObstructions()
|
||||
{
|
||||
uint32 now = GameTime::GetGameTimeMS();
|
||||
if (now - _lastObstructionUpdate < OBSTRUCTION_UPDATE_INTERVAL)
|
||||
return;
|
||||
|
||||
_lastObstructionUpdate = now;
|
||||
|
||||
// Prune invalid obstructions
|
||||
::std::vector<ObjectGuid> toRemove;
|
||||
for (auto const& pair : _dynamicObstructions)
|
||||
{
|
||||
if (!pair.second || !pair.second->IsInWorld())
|
||||
toRemove.push_back(pair.first);
|
||||
}
|
||||
|
||||
for (ObjectGuid guid : toRemove)
|
||||
_dynamicObstructions.Remove(guid);
|
||||
}
|
||||
|
||||
void LineOfSightManager::RegisterDynamicObstruction(GameObject* obj)
|
||||
{
|
||||
if (!obj)
|
||||
return;
|
||||
|
||||
_dynamicObstructions.Insert(obj->GetGUID(), obj);
|
||||
}
|
||||
|
||||
void LineOfSightManager::UnregisterDynamicObstruction(GameObject* obj)
|
||||
{
|
||||
if (!obj)
|
||||
return;
|
||||
|
||||
_dynamicObstructions.Remove(obj->GetGUID());
|
||||
}
|
||||
|
||||
bool LineOfSightManager::IsDynamicObstructionActive(ObjectGuid guid)
|
||||
{
|
||||
auto result = _dynamicObstructions.Get(guid);
|
||||
if (!result.has_value())
|
||||
return false;
|
||||
|
||||
GameObject* obj = result.value();
|
||||
return obj && obj->IsInWorld();
|
||||
}
|
||||
|
||||
bool LineOfSightManager::HasSpellLineOfSight(Unit* target, uint32 spellId)
|
||||
{
|
||||
if (!target || !spellId)
|
||||
return false;
|
||||
|
||||
LoSResult result = CheckSpellLineOfSight(target, spellId);
|
||||
return result.hasLineOfSight;
|
||||
}
|
||||
|
||||
float LineOfSightManager::GetSpellMaxRange(uint32 spellId)
|
||||
{
|
||||
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId, DIFFICULTY_NONE);
|
||||
if (!spellInfo)
|
||||
return 0.0f;
|
||||
|
||||
return spellInfo->GetMaxRange();
|
||||
}
|
||||
|
||||
bool LineOfSightManager::IsSpellRangeBlocked(Unit* target, uint32 spellId)
|
||||
{
|
||||
if (!target || !spellId)
|
||||
return true;
|
||||
|
||||
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId, DIFFICULTY_NONE);
|
||||
if (!spellInfo)
|
||||
return true;
|
||||
|
||||
float maxRange = spellInfo->GetMaxRange();
|
||||
float dist = ::std::sqrt(_bot->GetExactDistSq(target));
|
||||
|
||||
return dist > maxRange;
|
||||
}
|
||||
|
||||
LoSValidation LineOfSightManager::GetSpellLoSRequirements(uint32 spellId)
|
||||
{
|
||||
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId, DIFFICULTY_NONE);
|
||||
if (!spellInfo)
|
||||
return LoSValidation::SPELL_LOS;
|
||||
|
||||
if (spellInfo->HasAttribute(SPELL_ATTR2_IGNORE_LINE_OF_SIGHT))
|
||||
return LoSValidation::NONE;
|
||||
|
||||
return LoSValidation::SPELL_LOS;
|
||||
}
|
||||
|
||||
bool LineOfSightManager::CanCastAoEAtPosition(const Position& targetPos, uint32 spellId)
|
||||
{
|
||||
if (!spellId)
|
||||
return false;
|
||||
|
||||
LoSResult result = CheckLineOfSight(targetPos, LoSCheckType::SPELL_CASTING);
|
||||
return result.hasLineOfSight;
|
||||
}
|
||||
|
||||
::std::vector<Unit*> LineOfSightManager::GetAoETargetsInLoS(const Position& centerPos, float radius)
|
||||
{
|
||||
::std::vector<Unit*> results;
|
||||
|
||||
Map* map = _bot->GetMap();
|
||||
if (!map)
|
||||
return results;
|
||||
|
||||
DoubleBufferedSpatialGrid* spatialGrid = sSpatialGridManager.GetGrid(map);
|
||||
if (!spatialGrid)
|
||||
{
|
||||
sSpatialGridManager.CreateGrid(map);
|
||||
spatialGrid = sSpatialGridManager.GetGrid(map);
|
||||
if (!spatialGrid)
|
||||
return results;
|
||||
}
|
||||
|
||||
::std::vector<ObjectGuid> nearbyGuids = spatialGrid->QueryNearbyCreatureGuids(
|
||||
centerPos, radius);
|
||||
|
||||
for (ObjectGuid guid : nearbyGuids)
|
||||
{
|
||||
::Unit* unit = ObjectAccessor::GetUnit(*_bot, guid);
|
||||
if (!unit || !unit->IsAlive())
|
||||
continue;
|
||||
|
||||
if (unit->GetExactDist(¢erPos) <= radius)
|
||||
{
|
||||
LoSContext context;
|
||||
context.bot = _bot;
|
||||
context.source = _bot;
|
||||
context.target = unit;
|
||||
context.sourcePos = _bot->GetPosition();
|
||||
context.targetPos = unit->GetPosition();
|
||||
context.checkType = LoSCheckType::AREA_CHECK;
|
||||
context.validationFlags = LoSValidation::BASIC_LOS;
|
||||
|
||||
LoSResult losResult = CheckLineOfSight(context);
|
||||
if (losResult.hasLineOfSight)
|
||||
results.push_back(unit);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
bool LineOfSightManager::IsAoEPositionOptimal(const Position& pos, const ::std::vector<Unit*>& targets)
|
||||
{
|
||||
if (targets.empty())
|
||||
return false;
|
||||
|
||||
uint32 inLoS = 0;
|
||||
for (Unit* target : targets)
|
||||
{
|
||||
if (target && HasLineOfSightFromPosition(pos, target))
|
||||
++inLoS;
|
||||
}
|
||||
|
||||
// Optimal if we can see at least 75% of targets
|
||||
return inLoS >= (targets.size() * 3 / 4);
|
||||
}
|
||||
|
||||
bool LineOfSightManager::IsPathClear(const ::std::vector<Position>& waypoints)
|
||||
{
|
||||
if (waypoints.size() < 2)
|
||||
return true;
|
||||
|
||||
for (size_t i = 0; i < waypoints.size() - 1; ++i)
|
||||
{
|
||||
if (CheckTerrainBlocking(waypoints[i], waypoints[i + 1]))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Position LineOfSightManager::GetFirstBlockedWaypoint(const ::std::vector<Position>& waypoints)
|
||||
{
|
||||
if (waypoints.size() < 2)
|
||||
return waypoints.empty() ? Position() : waypoints[0];
|
||||
|
||||
for (size_t i = 0; i < waypoints.size() - 1; ++i)
|
||||
{
|
||||
if (CheckTerrainBlocking(waypoints[i], waypoints[i + 1]))
|
||||
return waypoints[i + 1];
|
||||
}
|
||||
|
||||
return waypoints.back();
|
||||
}
|
||||
|
||||
bool LineOfSightManager::CanSeeDestination(const Position& destination)
|
||||
{
|
||||
return !CheckTerrainBlocking(_bot->GetPosition(), destination);
|
||||
}
|
||||
|
||||
::std::vector<Position> LineOfSightManager::GetVisibilityWaypoints(const Position& destination)
|
||||
{
|
||||
::std::vector<Position> waypoints;
|
||||
Position botPos = _bot->GetPosition();
|
||||
|
||||
if (!CheckTerrainBlocking(botPos, destination))
|
||||
{
|
||||
waypoints.push_back(destination);
|
||||
return waypoints;
|
||||
}
|
||||
|
||||
// Use LoSUtils::GetLastVisiblePoint to find intermediate waypoints
|
||||
Map* map = _bot->GetMap();
|
||||
if (map)
|
||||
{
|
||||
Position lastVisible = LoSUtils::GetLastVisiblePoint(botPos, destination, map);
|
||||
if (lastVisible.GetExactDist(&botPos) > 2.0f)
|
||||
{
|
||||
waypoints.push_back(lastVisible);
|
||||
waypoints.push_back(destination);
|
||||
}
|
||||
}
|
||||
|
||||
if (waypoints.empty())
|
||||
waypoints.push_back(destination);
|
||||
|
||||
return waypoints;
|
||||
}
|
||||
|
||||
bool LineOfSightManager::IsPositionInWorld(const Position& pos)
|
||||
{
|
||||
// Basic world coordinate bounds for WoW maps
|
||||
return pos.GetPositionX() > -17000.0f && pos.GetPositionX() < 17000.0f &&
|
||||
pos.GetPositionY() > -17000.0f && pos.GetPositionY() < 17000.0f;
|
||||
}
|
||||
|
||||
bool LineOfSightManager::IsPositionAccessible(const Position& pos)
|
||||
{
|
||||
Map* map = _bot->GetMap();
|
||||
if (!map)
|
||||
return false;
|
||||
|
||||
float groundZ = map->GetHeight(_bot->GetPhaseShift(), pos.GetPositionX(),
|
||||
pos.GetPositionY(), pos.GetPositionZ() + 10.0f);
|
||||
return groundZ > INVALID_HEIGHT;
|
||||
}
|
||||
|
||||
float LineOfSightManager::GetGroundLevel(const Position& pos)
|
||||
{
|
||||
Map* map = _bot->GetMap();
|
||||
if (!map)
|
||||
return INVALID_HEIGHT;
|
||||
|
||||
return map->GetHeight(_bot->GetPhaseShift(), pos.GetPositionX(),
|
||||
pos.GetPositionY(), pos.GetPositionZ() + 10.0f);
|
||||
}
|
||||
|
||||
bool LineOfSightManager::IsUnderground(const Position& pos)
|
||||
{
|
||||
float groundLevel = GetGroundLevel(pos);
|
||||
if (groundLevel <= INVALID_HEIGHT)
|
||||
return false;
|
||||
|
||||
return pos.GetPositionZ() < groundLevel - 2.0f;
|
||||
}
|
||||
|
||||
void LineOfSightManager::UpdateMetrics()
|
||||
{
|
||||
// Metrics are updated inline during TrackPerformance() calls
|
||||
// This method exists for explicit periodic metric aggregation if needed
|
||||
}
|
||||
|
||||
bool LineOfSightManager::CheckRangedCombatLineOfSight(Unit* target)
|
||||
{
|
||||
if (!target)
|
||||
return false;
|
||||
|
||||
// Ranged combat requires target within 40 yards and facing
|
||||
float distSq = _bot->GetExactDistSq(target);
|
||||
if (distSq > 40.0f * 40.0f)
|
||||
return false;
|
||||
|
||||
float angle = _bot->GetRelativeAngle(target);
|
||||
return ::std::abs(angle) <= static_cast<float>(M_PI) / 2.0f; // 90 degree cone
|
||||
}
|
||||
|
||||
void LineOfSightManager::CleanupCache()
|
||||
{
|
||||
ClearExpiredCacheEntries();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// REMAINING LOSUTILS STATIC METHOD IMPLEMENTATIONS
|
||||
// ============================================================================
|
||||
|
||||
bool LoSUtils::IsLoSBlocked(Player* source, Unit* target, ::std::string& reason)
|
||||
{
|
||||
if (!source || !target)
|
||||
{
|
||||
reason = "Invalid source or target";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!source->IsWithinLOSInMap(target))
|
||||
{
|
||||
reason = "Line of sight blocked by terrain/objects";
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LoSUtils::IsPointBehindPoint(const Position& observer, const Position& target, const Position& reference)
|
||||
{
|
||||
float angleToTarget = ::std::atan2(target.GetPositionY() - observer.GetPositionY(),
|
||||
target.GetPositionX() - observer.GetPositionX());
|
||||
float angleToRef = ::std::atan2(reference.GetPositionY() - observer.GetPositionY(),
|
||||
reference.GetPositionX() - observer.GetPositionX());
|
||||
|
||||
float diff = ::std::abs(angleToTarget - angleToRef);
|
||||
while (diff > static_cast<float>(M_PI))
|
||||
diff = 2.0f * static_cast<float>(M_PI) - diff;
|
||||
|
||||
// Behind means same general direction but farther
|
||||
return diff < static_cast<float>(M_PI) / 4.0f &&
|
||||
observer.GetExactDist(&target) > observer.GetExactDist(&reference);
|
||||
}
|
||||
|
||||
Position LoSUtils::GetLineIntersection(const Position& line1Start, const Position& line1End,
|
||||
const Position& line2Start, const Position& line2End)
|
||||
{
|
||||
float x1 = line1Start.GetPositionX(), y1 = line1Start.GetPositionY();
|
||||
float x2 = line1End.GetPositionX(), y2 = line1End.GetPositionY();
|
||||
float x3 = line2Start.GetPositionX(), y3 = line2Start.GetPositionY();
|
||||
float x4 = line2End.GetPositionX(), y4 = line2End.GetPositionY();
|
||||
|
||||
float denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
|
||||
if (::std::abs(denom) < 0.0001f)
|
||||
return line1Start; // Parallel lines, return start
|
||||
|
||||
float t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
|
||||
|
||||
Position result;
|
||||
result.m_positionX = x1 + t * (x2 - x1);
|
||||
result.m_positionY = y1 + t * (y2 - y1);
|
||||
result.m_positionZ = line1Start.GetPositionZ();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool LoSUtils::DoLinesIntersect(const Position& line1Start, const Position& line1End,
|
||||
const Position& line2Start, const Position& line2End)
|
||||
{
|
||||
float x1 = line1Start.GetPositionX(), y1 = line1Start.GetPositionY();
|
||||
float x2 = line1End.GetPositionX(), y2 = line1End.GetPositionY();
|
||||
float x3 = line2Start.GetPositionX(), y3 = line2Start.GetPositionY();
|
||||
float x4 = line2End.GetPositionX(), y4 = line2End.GetPositionY();
|
||||
|
||||
float denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
|
||||
if (::std::abs(denom) < 0.0001f)
|
||||
return false; // Parallel lines
|
||||
|
||||
float t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
|
||||
float u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
|
||||
|
||||
// Both parameters must be in [0, 1] for segments to intersect
|
||||
return (t >= 0.0f && t <= 1.0f && u >= 0.0f && u <= 1.0f);
|
||||
}
|
||||
|
||||
bool LoSUtils::IsAboveGround(const Position& pos, Map* map, float threshold)
|
||||
{
|
||||
if (!map)
|
||||
return false;
|
||||
|
||||
PhaseShift emptyPhaseShift;
|
||||
float groundZ = map->GetStaticHeight(emptyPhaseShift, pos.GetPositionX(),
|
||||
pos.GetPositionY(), pos.GetPositionZ());
|
||||
if (groundZ <= INVALID_HEIGHT)
|
||||
return false;
|
||||
|
||||
return (pos.GetPositionZ() - groundZ) > threshold;
|
||||
}
|
||||
|
||||
bool LoSUtils::IsBelowGround(const Position& pos, Map* map, float threshold)
|
||||
{
|
||||
if (!map)
|
||||
return false;
|
||||
|
||||
PhaseShift emptyPhaseShift;
|
||||
float groundZ = map->GetStaticHeight(emptyPhaseShift, pos.GetPositionX(),
|
||||
pos.GetPositionY(), pos.GetPositionZ() + 50.0f);
|
||||
if (groundZ <= INVALID_HEIGHT)
|
||||
return false;
|
||||
|
||||
return (groundZ - pos.GetPositionZ()) > threshold;
|
||||
}
|
||||
|
||||
float LoSUtils::GetVerticalClearance(const Position& pos, Map* map)
|
||||
{
|
||||
if (!map)
|
||||
return 0.0f;
|
||||
|
||||
PhaseShift emptyPhaseShift;
|
||||
float groundZ = map->GetStaticHeight(emptyPhaseShift, pos.GetPositionX(),
|
||||
pos.GetPositionY(), pos.GetPositionZ());
|
||||
if (groundZ <= INVALID_HEIGHT)
|
||||
return 0.0f;
|
||||
|
||||
return pos.GetPositionZ() - groundZ;
|
||||
}
|
||||
|
||||
bool LoSUtils::CanCastSpellAtPosition(Player* caster, const Position& pos, uint32 spellId)
|
||||
{
|
||||
if (!caster || !spellId)
|
||||
return false;
|
||||
|
||||
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId, DIFFICULTY_NONE);
|
||||
if (!spellInfo)
|
||||
return false;
|
||||
|
||||
float maxRange = spellInfo->GetMaxRange();
|
||||
if (caster->GetExactDist(&pos) > maxRange)
|
||||
return false;
|
||||
|
||||
return HasLoS(caster->GetPosition(), pos, caster->GetMap());
|
||||
}
|
||||
|
||||
float LoSUtils::GetEffectiveSpellRange(Player* caster, uint32 spellId)
|
||||
{
|
||||
if (!caster || !spellId)
|
||||
return 0.0f;
|
||||
|
||||
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId, DIFFICULTY_NONE);
|
||||
if (!spellInfo)
|
||||
return 0.0f;
|
||||
|
||||
return spellInfo->GetMaxRange();
|
||||
}
|
||||
|
||||
bool LoSUtils::IsAreaClear(const Position& center, float radius, Map* map)
|
||||
{
|
||||
if (!map)
|
||||
return false;
|
||||
|
||||
// Check 8 points around the center
|
||||
for (float angle = 0.0f; angle < 2.0f * static_cast<float>(M_PI); angle += static_cast<float>(M_PI) / 4.0f)
|
||||
{
|
||||
Position edge;
|
||||
edge.m_positionX = center.GetPositionX() + radius * ::std::cos(angle);
|
||||
edge.m_positionY = center.GetPositionY() + radius * ::std::sin(angle);
|
||||
edge.m_positionZ = center.GetPositionZ();
|
||||
|
||||
if (!HasLoS(center, edge, map))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
::std::vector<Position> LoSUtils::GetBlockedPositionsInArea(const Position& center, float radius, Map* map)
|
||||
{
|
||||
::std::vector<Position> blocked;
|
||||
if (!map)
|
||||
return blocked;
|
||||
|
||||
for (float angle = 0.0f; angle < 2.0f * static_cast<float>(M_PI); angle += static_cast<float>(M_PI) / 8.0f)
|
||||
{
|
||||
for (float dist = radius * 0.25f; dist <= radius; dist += radius * 0.25f)
|
||||
{
|
||||
Position pos;
|
||||
pos.m_positionX = center.GetPositionX() + dist * ::std::cos(angle);
|
||||
pos.m_positionY = center.GetPositionY() + dist * ::std::sin(angle);
|
||||
pos.m_positionZ = center.GetPositionZ();
|
||||
|
||||
if (!HasLoS(center, pos, map))
|
||||
blocked.push_back(pos);
|
||||
}
|
||||
}
|
||||
|
||||
return blocked;
|
||||
}
|
||||
|
||||
Position LoSUtils::GetClearedPositionNear(const Position& target, float searchRadius, Map* map)
|
||||
{
|
||||
if (!map)
|
||||
return target;
|
||||
|
||||
for (float angle = 0.0f; angle < 2.0f * static_cast<float>(M_PI); angle += static_cast<float>(M_PI) / 8.0f)
|
||||
{
|
||||
for (float dist = 2.0f; dist <= searchRadius; dist += 2.0f)
|
||||
{
|
||||
Position candidate;
|
||||
candidate.m_positionX = target.GetPositionX() + dist * ::std::cos(angle);
|
||||
candidate.m_positionY = target.GetPositionY() + dist * ::std::sin(angle);
|
||||
candidate.m_positionZ = target.GetPositionZ();
|
||||
|
||||
PhaseShift emptyPhaseShift;
|
||||
float groundZ = map->GetStaticHeight(emptyPhaseShift, candidate.m_positionX,
|
||||
candidate.m_positionY, candidate.m_positionZ + 10.0f);
|
||||
if (groundZ > INVALID_HEIGHT)
|
||||
{
|
||||
candidate.m_positionZ = groundZ + 0.5f;
|
||||
if (HasLoS(candidate, target, map))
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
::std::vector<Position> LoSUtils::GetLoSBreakpoints(const Position& from, const Position& to, Map* map)
|
||||
{
|
||||
::std::vector<Position> breakpoints;
|
||||
if (!map)
|
||||
return breakpoints;
|
||||
|
||||
float totalDist = from.GetExactDist(&to);
|
||||
uint32 steps = static_cast<uint32>(totalDist / 2.0f);
|
||||
if (steps < 2)
|
||||
return breakpoints;
|
||||
|
||||
float dx = (to.GetPositionX() - from.GetPositionX()) / steps;
|
||||
float dy = (to.GetPositionY() - from.GetPositionY()) / steps;
|
||||
float dz = (to.GetPositionZ() - from.GetPositionZ()) / steps;
|
||||
|
||||
Position prev = from;
|
||||
bool prevVisible = true;
|
||||
|
||||
for (uint32 i = 1; i <= steps; ++i)
|
||||
{
|
||||
Position current;
|
||||
current.m_positionX = from.GetPositionX() + dx * i;
|
||||
current.m_positionY = from.GetPositionY() + dy * i;
|
||||
current.m_positionZ = from.GetPositionZ() + dz * i;
|
||||
|
||||
bool currentVisible = HasLoS(from, current, map);
|
||||
|
||||
// Transition point: visible to blocked or blocked to visible
|
||||
if (currentVisible != prevVisible)
|
||||
breakpoints.push_back(current);
|
||||
|
||||
prevVisible = currentVisible;
|
||||
prev = current;
|
||||
}
|
||||
|
||||
return breakpoints;
|
||||
}
|
||||
|
||||
} // namespace Playerbot
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "MovementIntegration.h"
|
||||
#include "GameTime.h"
|
||||
#include "PositionManager.h" // Enterprise-grade positioning algorithms
|
||||
#include "LineOfSightManager.h"
|
||||
#include "Player.h"
|
||||
#include "Unit.h"
|
||||
#include "Group.h" // For healer group member queries
|
||||
@@ -14,6 +15,7 @@
|
||||
#include "Log.h"
|
||||
#include "../BotAI.h"
|
||||
#include "Core/PlayerBotHelpers.h"
|
||||
#include "../../Movement/BotMovementUtil.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
@@ -492,18 +494,35 @@ MovementCommand MovementIntegration::CheckLineOfSight()
|
||||
if (!target)
|
||||
return command;
|
||||
|
||||
// Check LoS
|
||||
// Check LoS using TrinityCore native check
|
||||
if (!_bot->IsWithinLOSInMap(target))
|
||||
{
|
||||
// Need to move to get LoS
|
||||
// Simple: move towards target
|
||||
float angle = _bot->GetAbsoluteAngle(target);
|
||||
float distance = 10.0f;
|
||||
// Use LineOfSightManager for smart position finding when available
|
||||
// This considers terrain, buildings, preferred range vs. naive "move toward target"
|
||||
LineOfSightManager losMgr(_bot);
|
||||
Position losPos = losMgr.FindBestLineOfSightPosition(target, 0.0f);
|
||||
|
||||
float x = _bot->GetPositionX() + distance * std::cos(angle);
|
||||
float y = _bot->GetPositionY() + distance * std::sin(angle);
|
||||
// If FindBest returned our own position (no candidates found), fall back to
|
||||
// stepping toward the target
|
||||
if (losPos.GetExactDist(_bot) < 2.0f)
|
||||
{
|
||||
losPos = losMgr.GetClosestUnblockedPosition(target);
|
||||
}
|
||||
|
||||
command.destination = Position(x, y, _bot->GetPositionZ());
|
||||
// Final fallback: move directly toward target
|
||||
if (losPos.GetExactDist(_bot) < 2.0f)
|
||||
{
|
||||
float angle = _bot->GetAbsoluteAngle(target);
|
||||
float moveDistance = std::min(10.0f, _bot->GetDistance(target) * 0.5f);
|
||||
float x = _bot->GetPositionX() + moveDistance * std::cos(angle);
|
||||
float y = _bot->GetPositionY() + moveDistance * std::sin(angle);
|
||||
losPos = Position(x, y, _bot->GetPositionZ());
|
||||
}
|
||||
|
||||
// Correct Z to ground level to prevent hovering/falling through terrain
|
||||
BotMovementUtil::CorrectPositionToGround(_bot, losPos);
|
||||
|
||||
command.destination = losPos;
|
||||
command.urgency = MovementUrgency::HIGH;
|
||||
command.reason = MovementReason::LINE_OF_SIGHT;
|
||||
command.acceptableRadius = 2.0f;
|
||||
|
||||
@@ -0,0 +1,713 @@
|
||||
/*
|
||||
* Copyright (C) 2024+ TrinityCore <http://www.trinitycore.org/>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*/
|
||||
|
||||
#include "ProactiveLoSFixer.h"
|
||||
#include "LineOfSightManager.h"
|
||||
#include "Player.h"
|
||||
#include "Unit.h"
|
||||
#include "Map.h"
|
||||
#include "Log.h"
|
||||
#include "SpellInfo.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "GameTime.h"
|
||||
#include "Group.h"
|
||||
#include "DBCEnums.h"
|
||||
#include "DB2Stores.h"
|
||||
#include "../../Movement/BotMovementUtil.h"
|
||||
|
||||
namespace Playerbot
|
||||
{
|
||||
|
||||
ProactiveLoSFixer::ProactiveLoSFixer(Player* bot)
|
||||
: _bot(bot)
|
||||
{
|
||||
}
|
||||
|
||||
void ProactiveLoSFixer::Initialize(LineOfSightManager* losMgr)
|
||||
{
|
||||
if (!_bot || !losMgr)
|
||||
{
|
||||
TC_LOG_ERROR("module.playerbot", "ProactiveLoSFixer::Initialize - null bot or losMgr");
|
||||
return;
|
||||
}
|
||||
|
||||
_losMgr = losMgr;
|
||||
_initialized = true;
|
||||
|
||||
TC_LOG_DEBUG("module.playerbot", "ProactiveLoSFixer: Initialized for bot {}", _bot->GetName());
|
||||
}
|
||||
|
||||
void ProactiveLoSFixer::Update(uint32 diff)
|
||||
{
|
||||
if (!_initialized || !_inCombat)
|
||||
return;
|
||||
|
||||
_updateTimer += diff;
|
||||
if (_updateTimer < UPDATE_INTERVAL_MS)
|
||||
return;
|
||||
_updateTimer = 0;
|
||||
|
||||
// Check if pending cast has expired
|
||||
if (_pendingCast.IsValid())
|
||||
{
|
||||
uint32 now = GameTime::GetGameTimeMS();
|
||||
|
||||
if (_pendingCast.IsExpired(now))
|
||||
{
|
||||
TC_LOG_DEBUG("module.playerbot", "ProactiveLoSFixer: Pending cast for spell {} timed out for bot {}",
|
||||
_pendingCast.spellId, _bot->GetName());
|
||||
_stats.repositionTimeouts++;
|
||||
ClearPendingCast();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we've arrived at the reposition target
|
||||
if (_isRepositioning && HasReachedRepositionTarget())
|
||||
{
|
||||
// Verify LOS from new position
|
||||
Unit* target = nullptr;
|
||||
if (!_pendingCast.targetGuid.IsEmpty())
|
||||
target = ObjectAccessor::GetUnit(*_bot, _pendingCast.targetGuid);
|
||||
|
||||
bool hasLos = false;
|
||||
if (target)
|
||||
hasLos = _losMgr->CanSeeTarget(target);
|
||||
else if (_pendingCast.isGroundTargeted)
|
||||
hasLos = _losMgr->CanMoveToPosition(_pendingCast.targetPosition);
|
||||
|
||||
if (hasLos)
|
||||
{
|
||||
_isRepositioning = false;
|
||||
_stats.repositionSuccesses++;
|
||||
TC_LOG_DEBUG("module.playerbot", "ProactiveLoSFixer: Bot {} reached LOS position for spell {}",
|
||||
_bot->GetName(), _pendingCast.spellId);
|
||||
// Pending cast is now ready - caller should check IsPendingCastReady()
|
||||
}
|
||||
else
|
||||
{
|
||||
// Arrived but still no LOS - try finding a new position
|
||||
TC_LOG_DEBUG("module.playerbot", "ProactiveLoSFixer: Bot {} arrived but still no LOS for spell {}",
|
||||
_bot->GetName(), _pendingCast.spellId);
|
||||
|
||||
uint32 now2 = GameTime::GetGameTimeMS();
|
||||
if (now2 - _lastPositionFindMs >= POSITION_FIND_COOLDOWN_MS)
|
||||
{
|
||||
_lastPositionFindMs = now2;
|
||||
Position newPos;
|
||||
if (target)
|
||||
newPos = FindCastPosition(target, _pendingCast.spellMaxRange);
|
||||
else
|
||||
newPos = FindCastPositionForGround(_pendingCast.targetPosition, _pendingCast.spellMaxRange);
|
||||
|
||||
if (newPos.GetExactDist(_bot) > REPOSITION_ARRIVAL_TOLERANCE)
|
||||
{
|
||||
_pendingCast.repositionTarget = newPos;
|
||||
MoveToLoSPosition(newPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No better position found, give up
|
||||
TC_LOG_DEBUG("module.playerbot", "ProactiveLoSFixer: Bot {} cannot find LOS for spell {}, giving up",
|
||||
_bot->GetName(), _pendingCast.spellId);
|
||||
_stats.noPositionFound++;
|
||||
ClearPendingCast();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Healer proactive LOS maintenance
|
||||
if (IsHealerRole())
|
||||
{
|
||||
_healerLoSCheckTimer += UPDATE_INTERVAL_MS;
|
||||
if (_healerLoSCheckTimer >= HEALER_LOS_CHECK_INTERVAL_MS)
|
||||
{
|
||||
_healerLoSCheckTimer = 0;
|
||||
// Only check healer LOS if we're not already repositioning for a cast
|
||||
if (!_isRepositioning)
|
||||
{
|
||||
CheckHealerGroupLoS();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProactiveLoSFixer::OnCombatStart()
|
||||
{
|
||||
_inCombat = true;
|
||||
ClearPendingCast();
|
||||
_updateTimer = 0;
|
||||
_healerLoSCheckTimer = 0;
|
||||
}
|
||||
|
||||
void ProactiveLoSFixer::OnCombatEnd()
|
||||
{
|
||||
_inCombat = false;
|
||||
ClearPendingCast();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CORE: PRE-CAST LOS CHECK
|
||||
// ============================================================================
|
||||
|
||||
LoSPreCastResult ProactiveLoSFixer::PreCastCheck(uint32 spellId, Unit* target)
|
||||
{
|
||||
if (!_initialized || !_losMgr)
|
||||
return LoSPreCastResult::CLEAR; // Fail open - don't block casting
|
||||
|
||||
_stats.totalPreCastChecks++;
|
||||
|
||||
if (!target)
|
||||
{
|
||||
// Self-cast or no target - LOS not relevant
|
||||
return LoSPreCastResult::CLEAR;
|
||||
}
|
||||
|
||||
// Check if spell ignores LOS
|
||||
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId, DIFFICULTY_NONE);
|
||||
if (!spellInfo)
|
||||
return LoSPreCastResult::CLEAR;
|
||||
|
||||
if (DoesSpellIgnoreLos(spellInfo))
|
||||
{
|
||||
_stats.spellIgnoredLos++;
|
||||
return LoSPreCastResult::SPELL_IGNORES;
|
||||
}
|
||||
|
||||
// Already repositioning for a different spell? Don't interrupt
|
||||
if (_isRepositioning && _pendingCast.IsValid() && _pendingCast.spellId != spellId)
|
||||
return LoSPreCastResult::ALREADY_MOVING;
|
||||
|
||||
// Check current LOS to target
|
||||
if (_bot->IsWithinLOSInMap(target))
|
||||
{
|
||||
// Also verify range
|
||||
float spellRange = GetEffectiveSpellRange(spellId);
|
||||
float dist = ::std::sqrt(_bot->GetExactDistSq(target));
|
||||
if (dist <= spellRange || spellRange <= 0.0f)
|
||||
{
|
||||
_stats.losWasClear++;
|
||||
// If we had a pending cast for this spell, clear it since LOS is now good
|
||||
if (_pendingCast.IsValid() && _pendingCast.spellId == spellId)
|
||||
ClearPendingCast();
|
||||
return LoSPreCastResult::CLEAR;
|
||||
}
|
||||
}
|
||||
|
||||
// LOS is blocked or out of range - need to reposition
|
||||
float spellRange = GetEffectiveSpellRange(spellId);
|
||||
float maxTheoreticalDist = ::std::sqrt(_bot->GetExactDistSq(target));
|
||||
|
||||
// If the target is way too far even after repositioning, don't bother
|
||||
if (maxTheoreticalDist > spellRange + 60.0f && spellRange > 0.0f)
|
||||
return LoSPreCastResult::TOO_FAR;
|
||||
|
||||
// Find the best position to cast from
|
||||
uint32 now = GameTime::GetGameTimeMS();
|
||||
if (now - _lastPositionFindMs < POSITION_FIND_COOLDOWN_MS && _pendingCast.IsValid() && _pendingCast.spellId == spellId)
|
||||
{
|
||||
// Already searching for this spell, don't spam position finding
|
||||
return LoSPreCastResult::REPOSITIONING;
|
||||
}
|
||||
|
||||
_lastPositionFindMs = now;
|
||||
Position castPos = FindCastPosition(target, spellRange);
|
||||
|
||||
// If castPos is basically where we are, no valid position was found
|
||||
if (castPos.GetExactDist(_bot) < REPOSITION_ARRIVAL_TOLERANCE)
|
||||
{
|
||||
// Try a simpler approach: just move toward the target
|
||||
float angle = _bot->GetAbsoluteAngle(target);
|
||||
float moveDistance = ::std::min(10.0f, maxTheoreticalDist * 0.5f);
|
||||
castPos.m_positionX = _bot->GetPositionX() + moveDistance * ::std::cos(angle);
|
||||
castPos.m_positionY = _bot->GetPositionY() + moveDistance * ::std::sin(angle);
|
||||
castPos.m_positionZ = _bot->GetPositionZ();
|
||||
|
||||
// Correct Z to ground
|
||||
BotMovementUtil::CorrectPositionToGround(_bot, castPos);
|
||||
|
||||
// Verify we'll have LOS from there
|
||||
if (!_losMgr->WillHaveLineOfSightAfterMovement(castPos, target))
|
||||
{
|
||||
_stats.noPositionFound++;
|
||||
return LoSPreCastResult::NO_POSITION;
|
||||
}
|
||||
}
|
||||
|
||||
// Queue the cast and start moving
|
||||
_pendingCast.spellId = spellId;
|
||||
_pendingCast.targetGuid = target->GetGUID();
|
||||
_pendingCast.targetPosition = target->GetPosition();
|
||||
_pendingCast.repositionTarget = castPos;
|
||||
_pendingCast.queueTimeMs = now;
|
||||
_pendingCast.maxWaitMs = MAX_REPOSITION_TIME_MS;
|
||||
_pendingCast.spellMaxRange = spellRange;
|
||||
_pendingCast.isGroundTargeted = false;
|
||||
|
||||
_isRepositioning = true;
|
||||
_stats.repositionAttempts++;
|
||||
|
||||
MoveToLoSPosition(castPos);
|
||||
|
||||
TC_LOG_DEBUG("module.playerbot", "ProactiveLoSFixer: Bot {} repositioning for spell {} (target: {}, dist: {:.1f})",
|
||||
_bot->GetName(), spellId, target->GetName(), castPos.GetExactDist(_bot));
|
||||
|
||||
return LoSPreCastResult::REPOSITIONING;
|
||||
}
|
||||
|
||||
LoSPreCastResult ProactiveLoSFixer::PreCastCheckPosition(uint32 spellId, Position const& targetPos)
|
||||
{
|
||||
if (!_initialized || !_losMgr)
|
||||
return LoSPreCastResult::CLEAR;
|
||||
|
||||
_stats.totalPreCastChecks++;
|
||||
|
||||
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId, DIFFICULTY_NONE);
|
||||
if (!spellInfo)
|
||||
return LoSPreCastResult::CLEAR;
|
||||
|
||||
if (DoesSpellIgnoreLos(spellInfo))
|
||||
{
|
||||
_stats.spellIgnoredLos++;
|
||||
return LoSPreCastResult::SPELL_IGNORES;
|
||||
}
|
||||
|
||||
// Check LOS to position
|
||||
if (_losMgr->CanCastAoEAtPosition(targetPos, spellId))
|
||||
{
|
||||
float spellRange = GetEffectiveSpellRange(spellId);
|
||||
float dist = _bot->GetExactDist(&targetPos);
|
||||
if (dist <= spellRange || spellRange <= 0.0f)
|
||||
{
|
||||
_stats.losWasClear++;
|
||||
return LoSPreCastResult::CLEAR;
|
||||
}
|
||||
}
|
||||
|
||||
// Need to reposition for ground-targeted spell
|
||||
float spellRange = GetEffectiveSpellRange(spellId);
|
||||
uint32 now = GameTime::GetGameTimeMS();
|
||||
|
||||
if (now - _lastPositionFindMs < POSITION_FIND_COOLDOWN_MS && _pendingCast.IsValid() && _pendingCast.spellId == spellId)
|
||||
return LoSPreCastResult::REPOSITIONING;
|
||||
|
||||
_lastPositionFindMs = now;
|
||||
Position castPos = FindCastPositionForGround(targetPos, spellRange);
|
||||
|
||||
if (castPos.GetExactDist(_bot) < REPOSITION_ARRIVAL_TOLERANCE)
|
||||
{
|
||||
_stats.noPositionFound++;
|
||||
return LoSPreCastResult::NO_POSITION;
|
||||
}
|
||||
|
||||
_pendingCast.spellId = spellId;
|
||||
_pendingCast.targetGuid.Clear();
|
||||
_pendingCast.targetPosition = targetPos;
|
||||
_pendingCast.repositionTarget = castPos;
|
||||
_pendingCast.queueTimeMs = now;
|
||||
_pendingCast.maxWaitMs = MAX_REPOSITION_TIME_MS;
|
||||
_pendingCast.spellMaxRange = spellRange;
|
||||
_pendingCast.isGroundTargeted = true;
|
||||
|
||||
_isRepositioning = true;
|
||||
_stats.repositionAttempts++;
|
||||
|
||||
MoveToLoSPosition(castPos);
|
||||
|
||||
return LoSPreCastResult::REPOSITIONING;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PENDING CAST MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
bool ProactiveLoSFixer::IsPendingCastReady() const
|
||||
{
|
||||
if (!_pendingCast.IsValid() || _isRepositioning)
|
||||
return false;
|
||||
|
||||
// Pending cast is ready when we've stopped repositioning (arrived at position)
|
||||
// and the cast hasn't expired
|
||||
uint32 now = GameTime::GetGameTimeMS();
|
||||
return !_pendingCast.IsExpired(now);
|
||||
}
|
||||
|
||||
void ProactiveLoSFixer::ClearPendingCast()
|
||||
{
|
||||
_pendingCast.Reset();
|
||||
_isRepositioning = false;
|
||||
}
|
||||
|
||||
void ProactiveLoSFixer::CancelPendingCast()
|
||||
{
|
||||
if (_isRepositioning)
|
||||
{
|
||||
BotMovementUtil::StopMovement(_bot);
|
||||
}
|
||||
ClearPendingCast();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HEALER LOS MAINTENANCE
|
||||
// ============================================================================
|
||||
|
||||
bool ProactiveLoSFixer::CheckHealerGroupLoS()
|
||||
{
|
||||
if (!_losMgr || !_bot)
|
||||
return false;
|
||||
|
||||
_stats.healerLoSChecks++;
|
||||
|
||||
Group* group = _bot->GetGroup();
|
||||
if (!group)
|
||||
return false;
|
||||
|
||||
// Count how many group members we can see
|
||||
uint32 totalMembers = 0;
|
||||
uint32 visibleMembers = 0;
|
||||
|
||||
for (auto const& slot : group->GetMemberSlots())
|
||||
{
|
||||
if (slot.guid == _bot->GetGUID())
|
||||
continue;
|
||||
|
||||
Player* member = ObjectAccessor::FindPlayer(slot.guid);
|
||||
if (!member || !member->IsInWorld() || !member->IsAlive())
|
||||
continue;
|
||||
|
||||
// Only check members on the same map
|
||||
if (member->GetMapId() != _bot->GetMapId())
|
||||
continue;
|
||||
|
||||
totalMembers++;
|
||||
|
||||
float distSq = _bot->GetExactDistSq(member);
|
||||
if (distSq > 40.0f * 40.0f) // Beyond heal range
|
||||
continue;
|
||||
|
||||
if (_bot->IsWithinLOSInMap(member))
|
||||
visibleMembers++;
|
||||
}
|
||||
|
||||
if (totalMembers == 0)
|
||||
return false;
|
||||
|
||||
float losPct = static_cast<float>(visibleMembers) / static_cast<float>(totalMembers);
|
||||
|
||||
if (losPct < HEALER_MIN_LOS_PCT)
|
||||
{
|
||||
// Need to reposition to see more group members
|
||||
Position healerPos = GetBestHealerPosition();
|
||||
if (healerPos.GetExactDist(_bot) > REPOSITION_ARRIVAL_TOLERANCE)
|
||||
{
|
||||
_stats.healerRepositions++;
|
||||
MoveToLoSPosition(healerPos);
|
||||
|
||||
TC_LOG_DEBUG("module.playerbot", "ProactiveLoSFixer: Healer {} repositioning for group LOS "
|
||||
"(visible: {}/{}, {:.0f}%)", _bot->GetName(), visibleMembers, totalMembers, losPct * 100.0f);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Position ProactiveLoSFixer::GetBestHealerPosition() const
|
||||
{
|
||||
if (!_losMgr || !_bot)
|
||||
return _bot ? _bot->GetPosition() : Position();
|
||||
|
||||
Group* group = _bot->GetGroup();
|
||||
if (!group)
|
||||
return _bot->GetPosition();
|
||||
|
||||
// Collect group member positions within heal range
|
||||
::std::vector<Position> memberPositions;
|
||||
for (auto const& slot : group->GetMemberSlots())
|
||||
{
|
||||
if (slot.guid == _bot->GetGUID())
|
||||
continue;
|
||||
|
||||
Player* member = ObjectAccessor::FindPlayer(slot.guid);
|
||||
if (!member || !member->IsInWorld() || !member->IsAlive())
|
||||
continue;
|
||||
if (member->GetMapId() != _bot->GetMapId())
|
||||
continue;
|
||||
|
||||
float distSq = _bot->GetExactDistSq(member);
|
||||
if (distSq <= 50.0f * 50.0f) // Slightly beyond heal range to consider
|
||||
memberPositions.push_back(member->GetPosition());
|
||||
}
|
||||
|
||||
if (memberPositions.empty())
|
||||
return _bot->GetPosition();
|
||||
|
||||
// Calculate centroid of group
|
||||
float centroidX = 0.0f, centroidY = 0.0f, centroidZ = 0.0f;
|
||||
for (Position const& pos : memberPositions)
|
||||
{
|
||||
centroidX += pos.GetPositionX();
|
||||
centroidY += pos.GetPositionY();
|
||||
centroidZ += pos.GetPositionZ();
|
||||
}
|
||||
float count = static_cast<float>(memberPositions.size());
|
||||
centroidX /= count;
|
||||
centroidY /= count;
|
||||
centroidZ /= count;
|
||||
|
||||
// Try positions around the centroid
|
||||
Position bestPos = _bot->GetPosition();
|
||||
uint32 bestVisible = 0;
|
||||
float bestMoveDist = 999.0f;
|
||||
|
||||
Position centroid;
|
||||
centroid.m_positionX = centroidX;
|
||||
centroid.m_positionY = centroidY;
|
||||
centroid.m_positionZ = centroidZ;
|
||||
|
||||
Map* map = _bot->GetMap();
|
||||
if (!map)
|
||||
return _bot->GetPosition();
|
||||
|
||||
for (float angle = 0.0f; angle < 2.0f * static_cast<float>(M_PI); angle += static_cast<float>(M_PI) / 6.0f)
|
||||
{
|
||||
for (float dist = 3.0f; dist <= 15.0f; dist += 4.0f)
|
||||
{
|
||||
Position candidate;
|
||||
candidate.m_positionX = centroidX + dist * ::std::cos(angle);
|
||||
candidate.m_positionY = centroidY + dist * ::std::sin(angle);
|
||||
candidate.m_positionZ = centroidZ;
|
||||
|
||||
// Correct Z to ground
|
||||
float groundZ = map->GetHeight(_bot->GetPhaseShift(), candidate.m_positionX,
|
||||
candidate.m_positionY, candidate.m_positionZ + 10.0f);
|
||||
if (groundZ <= INVALID_HEIGHT)
|
||||
continue;
|
||||
candidate.m_positionZ = groundZ + 0.5f;
|
||||
|
||||
// Count visible members from this position
|
||||
uint32 visible = 0;
|
||||
for (Position const& memberPos : memberPositions)
|
||||
{
|
||||
if (_losMgr->HasLineOfSightFromPosition(candidate, nullptr))
|
||||
{
|
||||
// Simple distance check as proxy (full LOS check too expensive for each candidate)
|
||||
float memberDist = candidate.GetExactDist(&memberPos);
|
||||
if (memberDist <= 40.0f)
|
||||
visible++;
|
||||
}
|
||||
}
|
||||
|
||||
float moveDist = candidate.GetExactDist(_bot);
|
||||
if (visible > bestVisible || (visible == bestVisible && moveDist < bestMoveDist))
|
||||
{
|
||||
bestVisible = visible;
|
||||
bestMoveDist = moveDist;
|
||||
bestPos = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestPos;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// QUERIES
|
||||
// ============================================================================
|
||||
|
||||
uint32 ProactiveLoSFixer::GetRepositioningTime() const
|
||||
{
|
||||
if (!_isRepositioning || !_pendingCast.IsValid())
|
||||
return 0;
|
||||
|
||||
uint32 now = GameTime::GetGameTimeMS();
|
||||
if (now < _pendingCast.queueTimeMs)
|
||||
return 0;
|
||||
|
||||
return now - _pendingCast.queueTimeMs;
|
||||
}
|
||||
|
||||
::std::string ProactiveLoSFixer::GetDebugSummary() const
|
||||
{
|
||||
::std::string summary = "ProactiveLoSFixer: ";
|
||||
summary += "checks=" + ::std::to_string(_stats.totalPreCastChecks);
|
||||
summary += " clear=" + ::std::to_string(_stats.losWasClear);
|
||||
summary += " repos=" + ::std::to_string(_stats.repositionAttempts);
|
||||
summary += " success=" + ::std::to_string(_stats.repositionSuccesses);
|
||||
summary += " timeout=" + ::std::to_string(_stats.repositionTimeouts);
|
||||
summary += " nopos=" + ::std::to_string(_stats.noPositionFound);
|
||||
|
||||
if (_pendingCast.IsValid())
|
||||
{
|
||||
summary += " [PENDING: spell=" + ::std::to_string(_pendingCast.spellId);
|
||||
summary += " time=" + ::std::to_string(GetRepositioningTime()) + "ms]";
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// INTERNAL METHODS
|
||||
// ============================================================================
|
||||
|
||||
Position ProactiveLoSFixer::FindCastPosition(Unit* target, float spellRange) const
|
||||
{
|
||||
if (!target || !_losMgr)
|
||||
return _bot->GetPosition();
|
||||
|
||||
// First, try LineOfSightManager's smart position finding
|
||||
float preferredRange = spellRange > 5.0f ? spellRange * 0.8f : 10.0f;
|
||||
Position bestPos = _losMgr->FindBestLineOfSightPosition(target, preferredRange);
|
||||
|
||||
// Verify the found position is within spell range
|
||||
if (bestPos.GetExactDist(target) > spellRange && spellRange > 0.0f)
|
||||
{
|
||||
// Position is out of spell range, try with a tighter radius
|
||||
bestPos = _losMgr->FindBestLineOfSightPosition(target, spellRange * 0.9f);
|
||||
}
|
||||
|
||||
// If still no good position, try GetClosestUnblockedPosition
|
||||
if (bestPos.GetExactDist(_bot) < 1.0f || (bestPos.GetExactDist(target) > spellRange && spellRange > 0.0f))
|
||||
{
|
||||
bestPos = _losMgr->GetClosestUnblockedPosition(target);
|
||||
}
|
||||
|
||||
// Correct Z to ground level
|
||||
BotMovementUtil::CorrectPositionToGround(_bot, bestPos);
|
||||
|
||||
return bestPos;
|
||||
}
|
||||
|
||||
Position ProactiveLoSFixer::FindCastPositionForGround(Position const& targetPos, float spellRange) const
|
||||
{
|
||||
if (!_losMgr)
|
||||
return _bot->GetPosition();
|
||||
|
||||
// For ground-targeted spells, find a position within range with LOS to the ground target
|
||||
Map* map = _bot->GetMap();
|
||||
if (!map)
|
||||
return _bot->GetPosition();
|
||||
|
||||
Position botPos = _bot->GetPosition();
|
||||
Position bestPos = botPos;
|
||||
float bestScore = -1.0f;
|
||||
|
||||
for (float angle = 0.0f; angle < 2.0f * static_cast<float>(M_PI); angle += static_cast<float>(M_PI) / 8.0f)
|
||||
{
|
||||
for (float dist = 3.0f; dist <= spellRange; dist += 3.0f)
|
||||
{
|
||||
Position candidate;
|
||||
candidate.m_positionX = targetPos.GetPositionX() + dist * ::std::cos(angle);
|
||||
candidate.m_positionY = targetPos.GetPositionY() + dist * ::std::sin(angle);
|
||||
candidate.m_positionZ = targetPos.GetPositionZ();
|
||||
|
||||
float groundZ = map->GetHeight(_bot->GetPhaseShift(), candidate.m_positionX,
|
||||
candidate.m_positionY, candidate.m_positionZ + 10.0f);
|
||||
if (groundZ <= INVALID_HEIGHT)
|
||||
continue;
|
||||
candidate.m_positionZ = groundZ + 0.5f;
|
||||
|
||||
float rangeDist = candidate.GetExactDist(&targetPos);
|
||||
if (rangeDist > spellRange)
|
||||
continue;
|
||||
|
||||
if (_losMgr->CanMoveToPosition(candidate))
|
||||
{
|
||||
float moveDist = candidate.GetExactDist(&botPos);
|
||||
float score = 100.0f - moveDist;
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestPos = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestPos;
|
||||
}
|
||||
|
||||
bool ProactiveLoSFixer::HasReachedRepositionTarget() const
|
||||
{
|
||||
if (!_pendingCast.IsValid())
|
||||
return false;
|
||||
|
||||
float dist = _bot->GetExactDist(&_pendingCast.repositionTarget);
|
||||
return dist <= REPOSITION_ARRIVAL_TOLERANCE;
|
||||
}
|
||||
|
||||
bool ProactiveLoSFixer::DoesSpellIgnoreLos(SpellInfo const* spellInfo) const
|
||||
{
|
||||
if (!spellInfo)
|
||||
return false;
|
||||
|
||||
// Check SPELL_ATTR2_IGNORE_LINE_OF_SIGHT
|
||||
if (spellInfo->HasAttribute(SPELL_ATTR2_IGNORE_LINE_OF_SIGHT))
|
||||
return true;
|
||||
|
||||
// Self-targeted spells don't need LOS
|
||||
if (spellInfo->GetMaxRange() <= 0.0f)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
float ProactiveLoSFixer::GetEffectiveSpellRange(uint32 spellId) const
|
||||
{
|
||||
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId, DIFFICULTY_NONE);
|
||||
if (!spellInfo)
|
||||
return 0.0f;
|
||||
|
||||
return spellInfo->GetMaxRange();
|
||||
}
|
||||
|
||||
bool ProactiveLoSFixer::IsHealerRole() const
|
||||
{
|
||||
if (!_bot)
|
||||
return false;
|
||||
|
||||
ChrSpecializationEntry const* spec = _bot->GetPrimarySpecializationEntry();
|
||||
if (!spec)
|
||||
return false;
|
||||
|
||||
return spec->GetRole() == ChrSpecializationRole::Healer;
|
||||
}
|
||||
|
||||
bool ProactiveLoSFixer::IsRangedRole() const
|
||||
{
|
||||
if (!_bot)
|
||||
return false;
|
||||
|
||||
ChrSpecializationEntry const* spec = _bot->GetPrimarySpecializationEntry();
|
||||
if (!spec)
|
||||
return false;
|
||||
|
||||
// Healers are ranged
|
||||
if (spec->GetRole() == ChrSpecializationRole::Healer)
|
||||
return true;
|
||||
|
||||
// Check Ranged or Caster flag
|
||||
if (spec->GetFlags().HasFlag(ChrSpecializationFlag::Ranged) ||
|
||||
spec->GetFlags().HasFlag(ChrSpecializationFlag::Caster))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ProactiveLoSFixer::MoveToLoSPosition(Position const& pos)
|
||||
{
|
||||
if (!_bot)
|
||||
return;
|
||||
|
||||
BotMovementUtil::MoveToPosition(_bot, pos, 0, 0.5f);
|
||||
}
|
||||
|
||||
} // namespace Playerbot
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* Copyright (C) 2024+ TrinityCore <http://www.trinitycore.org/>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* PROACTIVE LINE-OF-SIGHT FIXER
|
||||
*
|
||||
* Intercepts spell cast attempts and proactively repositions the bot to
|
||||
* a valid line-of-sight position before attempting the cast. This prevents
|
||||
* wasted GCDs on casts that would fail due to LOS and ensures smooth
|
||||
* combat flow.
|
||||
*
|
||||
* Architecture:
|
||||
* - Per-bot component, called before each spell cast attempt
|
||||
* - Maintains a pending cast queue: when LOS is broken, the spell is
|
||||
* queued and the bot is moved to a valid position first
|
||||
* - Uses LineOfSightManager::FindBestLineOfSightPosition() for smart
|
||||
* position selection (considers terrain, preferred range, movement cost)
|
||||
* - Healers maintain LOS to priority heal targets proactively
|
||||
* - Respects movement deduplication via BotMovementUtil
|
||||
*
|
||||
* Integration Points:
|
||||
* - LineOfSightManager: CheckLineOfSight, FindBestLineOfSightPosition,
|
||||
* GetClosestUnblockedPosition
|
||||
* - BotMovementUtil: MoveToPosition for safe movement
|
||||
* - SpellInfo: GetMaxRange, HasAttribute for spell-specific LOS rules
|
||||
* - CombatPhaseDetector: Phase-aware urgency (execute phase = more urgent)
|
||||
*
|
||||
* Flow:
|
||||
* 1. Bot wants to cast spell on target
|
||||
* 2. ProactiveLoSFixer::PreCastCheck(spellId, target) is called
|
||||
* 3. If LOS is clear, returns CLEAR (proceed to cast)
|
||||
* 4. If LOS is broken:
|
||||
* a. Finds best LOS position via LineOfSightManager
|
||||
* b. Queues the pending cast
|
||||
* c. Issues movement command via BotMovementUtil
|
||||
* d. Returns REPOSITIONING (don't cast yet)
|
||||
* 5. On next Update(), checks if bot has reached LOS position
|
||||
* 6. When in position, returns the pending cast via GetPendingCast()
|
||||
*
|
||||
* Performance:
|
||||
* - Update interval: 200ms (5 checks/sec)
|
||||
* - LOS checks are cached by LineOfSightManager (1s TTL)
|
||||
* - Position finding is throttled (max once per 500ms)
|
||||
* - Memory: ~200 bytes per bot
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Define.h"
|
||||
#include "ObjectGuid.h"
|
||||
#include "Position.h"
|
||||
#include <string>
|
||||
|
||||
class Player;
|
||||
class Unit;
|
||||
class SpellInfo;
|
||||
|
||||
namespace Playerbot
|
||||
{
|
||||
|
||||
class LineOfSightManager;
|
||||
|
||||
// ============================================================================
|
||||
// PRE-CAST CHECK RESULT
|
||||
// ============================================================================
|
||||
|
||||
/// Result of checking LOS before a spell cast
|
||||
enum class LoSPreCastResult : uint8
|
||||
{
|
||||
CLEAR = 0, // LOS is clear, proceed to cast
|
||||
REPOSITIONING = 1, // Bot is moving to LOS position, cast queued
|
||||
SPELL_IGNORES = 2, // Spell ignores LOS (proceed to cast)
|
||||
NO_TARGET = 3, // No valid target
|
||||
ALREADY_MOVING = 4, // Bot is already repositioning for a different cast
|
||||
NO_POSITION = 5, // Could not find a valid LOS position
|
||||
TOO_FAR = 6 // Target is out of max theoretical range even after repositioning
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// PENDING CAST INFO
|
||||
// ============================================================================
|
||||
|
||||
/// Information about a queued spell cast waiting for LOS repositioning
|
||||
struct PendingLoSCast
|
||||
{
|
||||
uint32 spellId = 0; // Spell to cast after reaching LOS
|
||||
ObjectGuid targetGuid; // Target for the cast
|
||||
Position targetPosition; // Snapshotted target position (for ground-targeted spells)
|
||||
Position repositionTarget; // Where the bot is moving to
|
||||
uint32 queueTimeMs = 0; // When the cast was queued (server time)
|
||||
uint32 maxWaitMs = 5000; // Maximum time to wait for repositioning (5s default)
|
||||
float spellMaxRange = 0.0f; // Max range of the queued spell
|
||||
bool isGroundTargeted = false; // Is this a ground-targeted AoE?
|
||||
|
||||
bool IsValid() const { return spellId > 0; }
|
||||
bool IsExpired(uint32 currentTimeMs) const
|
||||
{
|
||||
return currentTimeMs > queueTimeMs + maxWaitMs;
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
spellId = 0;
|
||||
targetGuid.Clear();
|
||||
targetPosition = Position();
|
||||
repositionTarget = Position();
|
||||
queueTimeMs = 0;
|
||||
maxWaitMs = 5000;
|
||||
spellMaxRange = 0.0f;
|
||||
isGroundTargeted = false;
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// PROACTIVE LOS STATISTICS
|
||||
// ============================================================================
|
||||
|
||||
struct ProactiveLoSStats
|
||||
{
|
||||
uint32 totalPreCastChecks = 0; // Total PreCastCheck calls
|
||||
uint32 losWasClear = 0; // Times LOS was already clear
|
||||
uint32 repositionAttempts = 0; // Times repositioning was initiated
|
||||
uint32 repositionSuccesses = 0; // Times repositioning led to successful cast
|
||||
uint32 repositionTimeouts = 0; // Times repositioning timed out
|
||||
uint32 spellIgnoredLos = 0; // Times spell ignored LOS rules
|
||||
uint32 noPositionFound = 0; // Times no valid LOS position was found
|
||||
uint32 healerLoSChecks = 0; // Healer proactive LOS maintenance checks
|
||||
uint32 healerRepositions = 0; // Healer proactive repositions for group LOS
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// PROACTIVE LOS FIXER
|
||||
// ============================================================================
|
||||
|
||||
class TC_GAME_API ProactiveLoSFixer
|
||||
{
|
||||
public:
|
||||
explicit ProactiveLoSFixer(Player* bot);
|
||||
~ProactiveLoSFixer() = default;
|
||||
|
||||
// ========================================================================
|
||||
// LIFECYCLE
|
||||
// ========================================================================
|
||||
|
||||
/// Initialize the fixer (requires LineOfSightManager to be available)
|
||||
void Initialize(LineOfSightManager* losMgr);
|
||||
|
||||
/// Update pending cast state. Call once per combat update.
|
||||
/// @param diff Time since last update in milliseconds
|
||||
void Update(uint32 diff);
|
||||
|
||||
/// Reset state on combat start
|
||||
void OnCombatStart();
|
||||
|
||||
/// Reset state on combat end
|
||||
void OnCombatEnd();
|
||||
|
||||
// ========================================================================
|
||||
// CORE: PRE-CAST LOS CHECK
|
||||
// ========================================================================
|
||||
|
||||
/// Check LOS before attempting a spell cast.
|
||||
/// If LOS is blocked, queues the cast and initiates repositioning.
|
||||
/// @param spellId Spell to cast
|
||||
/// @param target Target unit (or nullptr for self-cast)
|
||||
/// @return LoSPreCastResult indicating whether to proceed or wait
|
||||
LoSPreCastResult PreCastCheck(uint32 spellId, Unit* target);
|
||||
|
||||
/// Check LOS for a ground-targeted AoE spell
|
||||
/// @param spellId Spell to cast
|
||||
/// @param targetPos Ground target position
|
||||
/// @return LoSPreCastResult indicating whether to proceed or wait
|
||||
LoSPreCastResult PreCastCheckPosition(uint32 spellId, Position const& targetPos);
|
||||
|
||||
// ========================================================================
|
||||
// PENDING CAST MANAGEMENT
|
||||
// ========================================================================
|
||||
|
||||
/// Is there a pending cast waiting for repositioning to complete?
|
||||
bool HasPendingCast() const { return _pendingCast.IsValid(); }
|
||||
|
||||
/// Get the pending cast info (for the caller to execute after repositioning)
|
||||
PendingLoSCast const& GetPendingCast() const { return _pendingCast; }
|
||||
|
||||
/// Is the pending cast ready to execute? (bot has reached LOS position)
|
||||
bool IsPendingCastReady() const;
|
||||
|
||||
/// Clear the pending cast (after it's been executed or abandoned)
|
||||
void ClearPendingCast();
|
||||
|
||||
/// Cancel any pending repositioning and cast
|
||||
void CancelPendingCast();
|
||||
|
||||
// ========================================================================
|
||||
// HEALER LOS MAINTENANCE
|
||||
// ========================================================================
|
||||
|
||||
/// For healers: proactively check and maintain LOS to group members.
|
||||
/// Call periodically to ensure the healer can see heal targets.
|
||||
/// @return true if the healer needs to reposition for group LOS
|
||||
bool CheckHealerGroupLoS();
|
||||
|
||||
/// Get the best position for a healer to see all priority targets
|
||||
/// @return Position that maximizes LOS to group, or current position if already good
|
||||
Position GetBestHealerPosition() const;
|
||||
|
||||
// ========================================================================
|
||||
// QUERIES
|
||||
// ========================================================================
|
||||
|
||||
/// Is the bot currently repositioning for LOS?
|
||||
bool IsRepositioning() const { return _isRepositioning; }
|
||||
|
||||
/// Get time spent repositioning (milliseconds)
|
||||
uint32 GetRepositioningTime() const;
|
||||
|
||||
/// Get statistics
|
||||
ProactiveLoSStats const& GetStats() const { return _stats; }
|
||||
|
||||
/// Get a text summary for debugging
|
||||
::std::string GetDebugSummary() const;
|
||||
|
||||
private:
|
||||
// ========================================================================
|
||||
// INTERNAL METHODS
|
||||
// ========================================================================
|
||||
|
||||
/// Find the best position to cast from considering spell range and LOS
|
||||
Position FindCastPosition(Unit* target, float spellRange) const;
|
||||
|
||||
/// Find position to cast a ground-targeted spell from
|
||||
Position FindCastPositionForGround(Position const& targetPos, float spellRange) const;
|
||||
|
||||
/// Check if the bot has arrived at the repositioning target
|
||||
bool HasReachedRepositionTarget() const;
|
||||
|
||||
/// Check if a spell ignores LOS requirements
|
||||
bool DoesSpellIgnoreLos(SpellInfo const* spellInfo) const;
|
||||
|
||||
/// Get the effective max range for a spell (accounting for talents, etc.)
|
||||
float GetEffectiveSpellRange(uint32 spellId) const;
|
||||
|
||||
/// Determine combat role for LOS urgency
|
||||
bool IsHealerRole() const;
|
||||
bool IsRangedRole() const;
|
||||
|
||||
/// Issue movement command to reposition for LOS
|
||||
void MoveToLoSPosition(Position const& pos);
|
||||
|
||||
// ========================================================================
|
||||
// STATE
|
||||
// ========================================================================
|
||||
|
||||
Player* _bot;
|
||||
LineOfSightManager* _losMgr = nullptr;
|
||||
bool _initialized = false;
|
||||
bool _inCombat = false;
|
||||
bool _isRepositioning = false;
|
||||
|
||||
/// Currently pending cast
|
||||
PendingLoSCast _pendingCast;
|
||||
|
||||
/// Statistics
|
||||
ProactiveLoSStats _stats;
|
||||
|
||||
/// Update throttle
|
||||
uint32 _updateTimer = 0;
|
||||
static constexpr uint32 UPDATE_INTERVAL_MS = 200; // 5 checks/sec
|
||||
|
||||
/// Position-finding throttle (expensive operation)
|
||||
uint32 _lastPositionFindMs = 0;
|
||||
static constexpr uint32 POSITION_FIND_COOLDOWN_MS = 500;
|
||||
|
||||
/// Maximum repositioning time before giving up
|
||||
static constexpr uint32 MAX_REPOSITION_TIME_MS = 5000;
|
||||
|
||||
/// Arrival tolerance for repositioning (yards)
|
||||
static constexpr float REPOSITION_ARRIVAL_TOLERANCE = 3.0f;
|
||||
|
||||
/// Healer: minimum LOS targets threshold (at least this % of group visible)
|
||||
static constexpr float HEALER_MIN_LOS_PCT = 0.6f;
|
||||
|
||||
/// Healer: check interval for group LOS maintenance
|
||||
uint32 _healerLoSCheckTimer = 0;
|
||||
static constexpr uint32 HEALER_LOS_CHECK_INTERVAL_MS = 2000;
|
||||
};
|
||||
|
||||
} // namespace Playerbot
|
||||
@@ -658,6 +658,8 @@ set(PLAYERBOT_COMBAT_SOURCES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/AI/Combat/FormationManager.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/AI/Combat/LineOfSightManager.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/AI/Combat/LineOfSightManager.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/AI/Combat/ProactiveLoSFixer.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/AI/Combat/ProactiveLoSFixer.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/AI/Combat/InterruptDatabase.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/AI/Combat/InterruptDatabase.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/AI/Combat/CombatAIIntegrator.cpp
|
||||
|
||||
Reference in New Issue
Block a user