AI task for a Roller NPC that applies force and torque to roll toward the current visible target. It checks distance to succeed when within LeapRange, fails if the NPC, rigidbody, or target are invalid, and detects being stuck to apply a vertical impulse.
using Sandbox.Npcs.Roller;
namespace Sandbox.Npcs.Roller.Tasks;
/// <summary>
/// Rolls toward the current target by applying force and torque.
/// Succeeds when within LeapRange. Fails if target is lost.
/// Force/torque values are configured on RollerNpc.
/// </summary>
public sealed class RollerRollTask : TaskBase
{
/// <summary>If lateral speed stays below this for StuckTime, jump to un-stick.</summary>
private const float StuckSpeedThreshold = 40f;
private const float StuckTime = 1.2f;
private TimeSince _stuckTimer;
private bool _stuckTimerRunning;
protected override TaskStatus OnUpdate()
{
var rollermine = Npc as RollerNpc;
if ( rollermine is null ) return TaskStatus.Failed;
var rb = rollermine.Rigidbody;
if ( !rb.IsValid() ) return TaskStatus.Failed;
var target = Npc.Senses.GetNearestVisible();
if ( !target.IsValid() ) return TaskStatus.Failed;
var toTarget = target.WorldPosition - Npc.WorldPosition;
var flatToTarget = toTarget.WithZ( 0 );
var dist = flatToTarget.Length;
if ( dist <= rollermine.LeapRange )
return TaskStatus.Success;
var dir = flatToTarget.Normal;
// Apply rolling force
rb.ApplyForce( dir * rollermine.RollForce );
// Spin in the direction of travel (torque axis is perpendicular to direction in XY plane)
var torqueAxis = new Vector3( -dir.y, dir.x, 0f );
rb.ApplyTorque( torqueAxis * rollermine.RollTorque );
// Stuck detection — jump if barely moving
var lateralSpeed = rb.Velocity.WithZ( 0 ).Length;
if ( lateralSpeed < StuckSpeedThreshold )
{
if ( !_stuckTimerRunning )
{
_stuckTimer = 0f;
_stuckTimerRunning = true;
}
else if ( _stuckTimer > StuckTime )
{
rb.ApplyImpulse( Vector3.Up * rollermine.StuckJumpForce );
_stuckTimerRunning = false;
}
}
else
{
_stuckTimerRunning = false;
}
return TaskStatus.Running;
}
protected override void Reset()
{
_stuckTimerRunning = false;
}
}