Files
OpenRA/OpenRA.Mods.Common/Activities/Move/MoveCooldownHelper.cs
let5sne.win10 9cf6ebb986
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled
Initial commit: OpenRA game engine
Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 21:46:54 +08:00

101 lines
3.7 KiB
C#

using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
namespace OpenRA.Mods.Common.Activities
{
/// <summary>
/// Activities that queue move activities via <see cref="IMove"/> can use this helper to decide
/// when moves with blocked destinations should be retried and to apply a cooldown between repeated moves.
/// </summary>
public sealed class MoveCooldownHelper
{
/// <summary>
/// If a move failed because the destination was blocked, indicates if we should try again.
/// When true, <see cref="Tick"/> will return null when the destination is blocked, after the cooldown has been applied.
/// When false, <see cref="Tick"/> will return true to indicate the activity should give up and complete.
/// Defaults to false.
/// </summary>
public bool RetryIfDestinationBlocked { get; set; }
/// <summary>
/// The cooldown delay in ticks. After a move with a blocked destination, the cooldown will be started.
/// Whilst the cooldown is in effect, <see cref="Tick"/> will return false.
/// After the cooldown finishes, <see cref="Tick"/> will return null to allow activity logic to resume.
/// This cooldown is important to avoid lag spikes caused by pathfinding every tick because the destination is unreachable.
/// Defaults to (20, 31).
/// </summary>
public (int MinTicksInclusive, int MaxTicksExclusive) Cooldown { get; set; } = (20, 31);
readonly World world;
readonly Mobile mobile;
bool wasMoving;
bool hasRunCooldown;
int cooldownTicks;
public MoveCooldownHelper(World world, Mobile mobile)
{
this.world = world;
this.mobile = mobile;
}
/// <summary>
/// Call this when queuing a move activity.
/// </summary>
public void NotifyMoveQueued()
{
wasMoving = true;
}
/// <summary>
/// Call this method within the <see cref="Activity.Tick(Actor)"/> method. It will return a tick result.
/// </summary>
/// <param name="targetIsHiddenActor">If the target is a hidden actor, forces the result to be true, once the move has completed.</param>
/// <returns>A result that should be returned from the calling Tick method.
/// A non-null result should be returned immediately.
/// On a null result, the method should continue with it's usual logic and perform any desired moves.</returns>
public bool? Tick(bool targetIsHiddenActor)
{
// We haven't moved yet, or we did move and we've finished the cooldown, allow the caller to resume with their logic.
if (!wasMoving)
return null;
if (!hasRunCooldown)
{
// The target is hidden, don't continue tracking it.
if (targetIsHiddenActor)
return true;
// Movement was cancelled, or we reached our destination, return immediately to allow the caller to perform their next steps.
if (mobile == null || mobile.MoveResult == MoveResult.CompleteCanceled || mobile.MoveResult == MoveResult.CompleteDestinationReached)
{
wasMoving = false;
return null;
}
// We couldn't reach the destination, don't try and keep going after the actor.
if (!RetryIfDestinationBlocked && mobile.MoveResult == MoveResult.CompleteDestinationBlocked)
return true;
// To avoid excessive pathfinding when the destination is blocked, wait for the cooldown before trying to move again.
// Applying some jitter to the wait time helps avoid multiple units repathing on the same tick and creating a lag spike.
hasRunCooldown = true;
cooldownTicks = world.SharedRandom.Next(Cooldown.MinTicksInclusive, Cooldown.MaxTicksExclusive);
return false;
}
else
{
if (cooldownTicks > 0)
cooldownTicks--;
if (cooldownTicks <= 0)
{
hasRunCooldown = false;
wasMoving = false;
}
return false;
}
}
}
}