Npcs/Schedules/MoveAsideSchedule.cs

A schedule for an NPC to step aside when a player (Blocker) is walking into them. It computes the player's travel direction, chooses a perpendicular side that the NPC is already offset toward, sets a look target on the blocker, then queues a MoveTo and a short Wait task. It clears the look target when finished.

using Sandbox.Npcs.Layers;
using Sandbox.Npcs.Tasks;

namespace Sandbox.Npcs.Schedules;

/// <summary>
/// A player is walking into us -- step sideways out of their path and let them
/// through, HL2 companion style. We step to whichever side of their path we're
/// already closest to, so we clear it instead of crossing in front of them,
/// and watch them as they pass.
/// </summary>
public sealed class MoveAsideSchedule : ScheduleBase
{
	/// <summary>The player barging into us.</summary>
	public GameObject Blocker { get; set; }

	/// <summary>How far to step out of the way.</summary>
	public float StepDistance { get; set; } = 80f;

	// Above ambient behaviour like following and idling, below fleeing and combat --
	// we step out of the way unless something more important is going on.
	public override int Priority => SchedulePriority.Alert;

	protected override void OnStart()
	{
		if ( !Blocker.IsValid() )
			return;

		// The direction they're travelling; if they've already stopped, fall back
		// to the line from them to us
		var travel = SensesLayer.GetVelocity( Blocker ).WithZ( 0 );
		if ( travel.Length < 1f )
			travel = (Npc.WorldPosition - Blocker.WorldPosition).WithZ( 0 );

		if ( travel.Length < 1f )
			return;

		// Step perpendicular to their path, to the side we're already offset toward
		var side = travel.Normal.Cross( Vector3.Up );
		var toUs = (Npc.WorldPosition - Blocker.WorldPosition).WithZ( 0 );
		if ( side.Dot( toUs ) < 0f )
			side = -side;

		// Watch them go by while we make room, strafing aside rather than
		// turning our back on them
		Npc.Animation.AddLookTarget( Blocker, 2f );

		AddTask( new MoveTo( Npc.WorldPosition + side * StepDistance, 8f ) { FaceTarget = Blocker } );
		AddTask( new Wait( 0.5f ) );
	}

	protected override void OnEnd()
	{
		Npc.Animation.ClearLookTarget();
	}

	protected override bool ShouldCancel() => !Blocker.IsValid();
}