From 8f2004c9a33e16979eda52a5347867249c0ead0a Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Fri, 17 Jul 2026 23:05:17 -0700 Subject: [PATCH] fix: enemies on path not resetting aggro radius correctly (#2015) tested that aggro is reset correctly and enemies no longer infinitely path to players. Tether now properly calculates the time to tether instead of guessing --- dGame/dComponents/BaseCombatAIComponent.cpp | 37 +++++++++++++-------- dGame/dComponents/BaseCombatAIComponent.h | 4 +++ dGame/dComponents/MovementAIComponent.cpp | 24 +++++++++++-- dGame/dComponents/MovementAIComponent.h | 7 ++++ 4 files changed, 56 insertions(+), 16 deletions(-) diff --git a/dGame/dComponents/BaseCombatAIComponent.cpp b/dGame/dComponents/BaseCombatAIComponent.cpp index 1bfc4039..9c3bc4be 100644 --- a/dGame/dComponents/BaseCombatAIComponent.cpp +++ b/dGame/dComponents/BaseCombatAIComponent.cpp @@ -65,7 +65,7 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t compo if (!componentResult.fieldIsNull("hardTetherRadius")) m_HardTetherRadius = componentResult.getFloatField("hardTetherRadius"); - + m_MinRoundLength = componentResult.getFloatField("minRoundLength"); m_MaxRoundLength = componentResult.getFloatField("maxRoundLength"); m_CombatRoundLength = componentResult.getFloatField("combatRoundLength"); @@ -161,15 +161,10 @@ void BaseCombatAIComponent::Update(const float deltaTime) { // Check if we should stop the tether effect if (m_TetherEffectActive) { m_TetherTime -= deltaTime; - if (m_Target != LWOOBJID_EMPTY || (NiPoint3::DistanceSquared( - m_StartPosition, - m_Parent->GetPosition()) < 20 * 20 && m_TetherTime <= 0) - ) { - GameMessages::SendStopFXEffect(m_Parent, true, "tether"); - m_TetherEffectActive = false; - } m_ForcedTetherTime -= deltaTime; if (m_ForcedTetherTime >= 0) return; + GameMessages::SendStopFXEffect(m_Parent, true, "tether"); + m_TetherEffectActive = false; } for (auto entry = m_RemovedThreatList.begin(); entry != m_RemovedThreatList.end();) { @@ -316,14 +311,17 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) { SetAiState(AiState::aggro); } else { SetAiState(AiState::idle); - if (m_MovementAI) m_MovementAI->SetMaxSpeed(1.0f); + // Don't clobber the pursuit speed set by TetherLogic while the tether is still active, + // otherwise the entity slows to wander speed and takes far longer than m_TetherTime to + // return to its spawn point. + if (m_MovementAI && !m_TetherEffectActive) m_MovementAI->SetMaxSpeed(1.0f); } if (!hasSkillToCast) return; if (m_Target == LWOOBJID_EMPTY) { SetAiState(AiState::idle); - if (m_MovementAI) m_MovementAI->SetMaxSpeed(1.0f); + if (m_MovementAI && !m_TetherEffectActive) m_MovementAI->SetMaxSpeed(1.0f); return; } @@ -810,7 +808,9 @@ void BaseCombatAIComponent::Wake() { void BaseCombatAIComponent::TetherLogic() { auto* destroyableComponent = m_Parent->GetComponent(); - if (destroyableComponent != nullptr && destroyableComponent->HasFaction(4)) { + const bool applyTetherEffect = destroyableComponent != nullptr && destroyableComponent->HasFaction(4); + + if (applyTetherEffect) { auto serilizationRequired = false; if (destroyableComponent->GetHealth() != destroyableComponent->GetMaxHealth()) { @@ -832,16 +832,27 @@ void BaseCombatAIComponent::TetherLogic() { GameMessages::SendPlayFXEffect(m_Parent->GetObjectID(), 6270, u"tether", "tether"); m_TetherEffectActive = true; - - m_TetherTime = 3.0f; } // Speed towards start position if (m_MovementAI != nullptr) { m_MovementAI->SetHaltDistance(0); m_MovementAI->SetMaxSpeed(m_PursuitSpeed); + m_MovementAI->SetCurrentSpeed(m_PursuitSpeed); m_MovementAI->SetDestination(m_StartPosition); } + + if (applyTetherEffect) { + // Use the actual navmesh path length (populated by SetDestination above) instead of the + // straight-line distance, otherwise the tether timer expires before the entity can walk + // around obstacles back to its spawn point. + const auto distance = m_MovementAI != nullptr + ? m_MovementAI->GetRemainingPathDistance() + : NiPoint3::Distance(m_Parent->GetPosition(), m_StartPosition); + const auto speed = m_PursuitSpeed * MovementAIComponent::GetBaseSpeed(m_Parent->GetLOT()); + m_TetherTime = speed > 0.0f ? distance / speed : 0.0f; + m_ForcedTetherTime = m_TetherTime; + } } void BaseCombatAIComponent::ForceTether() { diff --git a/dGame/dComponents/BaseCombatAIComponent.h b/dGame/dComponents/BaseCombatAIComponent.h index 728e0d4a..95a63c16 100644 --- a/dGame/dComponents/BaseCombatAIComponent.h +++ b/dGame/dComponents/BaseCombatAIComponent.h @@ -241,6 +241,10 @@ public: void SetStartingPosition(const NiPoint3& pos) { m_StartPosition = pos; } + bool GetOutOfCombat() const { return m_OutOfCombat; } + + bool GetIsTethering() const { return m_TetherEffectActive; } + private: /** * Returns the current target or the target that currently is the largest threat to this entity diff --git a/dGame/dComponents/MovementAIComponent.cpp b/dGame/dComponents/MovementAIComponent.cpp index 917e96c0..f5ef9f36 100644 --- a/dGame/dComponents/MovementAIComponent.cpp +++ b/dGame/dComponents/MovementAIComponent.cpp @@ -131,9 +131,13 @@ void MovementAIComponent::Update(const float deltaTime) { const auto approxPos = ApproximateLocation(); SetPosition(approxPos); - // Set the AIs new home based on where our current waypoint is IF we're idle, that way we can return to this + // Set the AIs new home based on where our current waypoint is IF wehave a path, we're idle, not out of combat (grace period if you left the radius), + // and we're not tethering to home, that way we can return to this // when resuming the pathing after losing aggro while moving the aggro hitbox with us - if (m_BaseCombatAI && m_BaseCombatAI->GetState() == AiState::idle) m_BaseCombatAI->SetStartingPosition(approxPos); + const bool idleNotResetting = m_Path && m_BaseCombatAI && m_BaseCombatAI->GetState() == AiState::idle && !m_BaseCombatAI->GetOutOfCombat() && !m_BaseCombatAI->GetIsTethering(); + if (idleNotResetting) { + m_BaseCombatAI->SetStartingPosition(approxPos); + } if (m_TimeTravelled < m_TimeToTravel) return; m_TimeTravelled = 0.0f; @@ -169,7 +173,7 @@ void MovementAIComponent::Update(const float deltaTime) { } } else { // Only try to renew or continue the path if we're in the idle or spawn state and we actually have a combatAI component - if (!m_BaseCombatAI || (m_BaseCombatAI && m_BaseCombatAI->GetState() == AiState::idle)) { + if (!m_BaseCombatAI || (idleNotResetting)) { // Check if there are more waypoints in the queue, if so set our next destination to the next waypoint const auto waypointNum = m_IsBounced ? m_CurrentPath.size() : m_CurrentPathWaypointCount - m_CurrentPath.size() - 1; RunWaypointCommands(waypointNum); @@ -205,6 +209,8 @@ void MovementAIComponent::Update(const float deltaTime) { m_CurrentPath.pop(); } + } else { + Stop(); } } @@ -341,6 +347,18 @@ float MovementAIComponent::GetBaseSpeed(LOT lot) { return speed; } +float MovementAIComponent::GetRemainingPathDistance() const { + if (m_InterpolatedWaypoints.empty() || m_PathIndex >= m_InterpolatedWaypoints.size()) { + return 0.0f; + } + + float total = NiPoint3::Distance(m_Parent->GetPosition(), m_InterpolatedWaypoints[m_PathIndex]); + for (size_t i = m_PathIndex; i + 1 < m_InterpolatedWaypoints.size(); ++i) { + total += NiPoint3::Distance(m_InterpolatedWaypoints[i], m_InterpolatedWaypoints[i + 1]); + } + return total; +} + void MovementAIComponent::SetPosition(const NiPoint3& value) { m_Parent->SetPosition(value); } diff --git a/dGame/dComponents/MovementAIComponent.h b/dGame/dComponents/MovementAIComponent.h index eeb871cf..f19007fc 100644 --- a/dGame/dComponents/MovementAIComponent.h +++ b/dGame/dComponents/MovementAIComponent.h @@ -209,6 +209,13 @@ public: */ static float GetBaseSpeed(LOT lot); + /** + * Returns the total distance remaining along the current path, following the + * interpolated waypoints from the entity's current position to the final destination. + * @return the total remaining path distance in world units + */ + float GetRemainingPathDistance() const; + bool IsPaused() const { return m_Paused; } bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);