using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
namespace OpenRA.Mods.Common.Activities
{
///
/// Activities that queue move activities via can use this helper to decide
/// when moves with blocked destinations should be retried and to apply a cooldown between repeated moves.
///
public sealed class MoveCooldownHelper
{
///
/// If a move failed because the destination was blocked, indicates if we should try again.
/// When true, will return null when the destination is blocked, after the cooldown has been applied.
/// When false, will return true to indicate the activity should give up and complete.
/// Defaults to false.
///
public bool RetryIfDestinationBlocked { get; set; }
///
/// The cooldown delay in ticks. After a move with a blocked destination, the cooldown will be started.
/// Whilst the cooldown is in effect, will return false.
/// After the cooldown finishes, 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).
///
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;
}
///
/// Call this when queuing a move activity.
///
public void NotifyMoveQueued()
{
wasMoving = true;
}
///
/// Call this method within the method. It will return a tick result.
///
/// If the target is a hidden actor, forces the result to be true, once the move has completed.
/// 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.
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;
}
}
}
}