An NPC schedule for a Scientist. On start it chooses to look at a visible player, another visible NPC, or scan a nearby point at eye height, and queues look and wait tasks; On end it clears the look target.
using Sandbox.Npcs.Tasks;
namespace Sandbox.Npcs.Schedules;
/// <summary>
/// Idle in place — glance around at eye level, and watch the people we can see,
/// HL2 style: a visible player gets looked at for a while, then other NPCs, and
/// otherwise we just scan the room.
/// </summary>
public sealed class ScientistIdleSchedule : ScheduleBase
{
protected override void OnStart()
{
// A player we can actually see is the most interesting thing around
var player = Npc.Senses.GetNearestVisible( "player" );
if ( player.IsValid() && Game.Random.Float() < 0.5f )
{
Watch( player, Game.Random.Float( 2.5f, 5f ) );
return;
}
// Otherwise, watch another NPC we can see for a bit
var other = NearestVisibleNpc();
if ( other.IsValid() && Game.Random.Float() < 0.4f )
{
Watch( other, Game.Random.Float( 1.5f, 3.5f ) );
return;
}
// Nobody around - pick a direction within comfortable head range of where
// we're already facing, at eye height, so we scan the room rather than
// the floor. Kept inside MaxHeadAngle: the body doesn't turn during idle,
// so a wider glance would pin the head at its limit and never complete.
var forward = GameObject.WorldRotation.Forward.WithZ( 0 ).Normal;
var yawOffset = Game.Random.Float( -40f, 40f );
var lookDir = Rotation.FromAxis( Vector3.Up, yawOffset ) * forward;
var lookTarget = GameObject.WorldPosition + Vector3.Up * 60f + lookDir * 100f;
AddTask( new LookAt( lookTarget ) );
// wait a bit, with random deviation
AddTask( new Wait( Game.Random.Float( 1f, 3f ) ) );
}
protected override void OnEnd()
{
// Don't leave the glance point lingering into the next schedule
Npc.Animation.ClearLookTarget();
}
// Look someone in the eyes for a while
private void Watch( GameObject who, float duration )
{
Npc.Animation.AddLookTarget( who, duration );
AddTask( new Wait( duration ) );
}
// The nearest NPC we can actually see (field of view + line of sight)
private GameObject NearestVisibleNpc()
{
GameObject best = null;
var bestDistance = float.MaxValue;
foreach ( var npc in Npc.Senses.GetVisible( "npc" ) )
{
if ( !npc.IsValid() || npc == Npc.GameObject )
continue;
var distance = npc.WorldPosition.Distance( Npc.WorldPosition );
if ( distance < bestDistance )
{
bestDistance = distance;
best = npc;
}
}
return best;
}
}