Npcs/Roller/Tasks/RollerRollTask.cs
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;
}
}