Npcs/Scientist/ScientistInspectPropSchedule.cs
using Sandbox.Npcs.Layers;
using Sandbox.Npcs.Tasks;
namespace Sandbox.Npcs.Schedules;
/// <summary>
/// Walk to a nearby prop, look at it, and maybe comment on it.
/// If the prop is small enough, pick it up, carry it around, then drop it.
/// </summary>
public sealed class ScientistInspectPropSchedule : ScheduleBase
{
private static readonly string[] InspectLines =
[
"Hmm, interesting...",
"What's this?",
"Fascinating.",
"I should take notes.",
"Now how did this get here?",
"Curious...",
];
private static readonly string[] PickUpLines =
[
"I'll take this.",
"Ooh, let me grab this.",
"This is coming with me.",
"Mine now.",
];
private static readonly string[] DropLines =
[
"There we go.",
"That goes there.",
"Alright, done with that.",
];
public GameObject PropTarget { get; set; }
protected override void OnStart()
{
if ( !PropTarget.IsValid() ) return;
// Persistently track the prop — the NPC will keep looking at it
// throughout the entire schedule without needing repeated LookAt tasks.
Npc.Animation.SetLookTarget( PropTarget );
// Walk toward the prop — tracks it if it moves
AddTask( new MoveTo( PropTarget, 40f ) );
AddTask( new Wait( 1 ) );
var bounds = PropTarget.GetBounds();
var isSmall = bounds.Size.Length < 256;
var speech = Npc.Speech;
if ( isSmall )
{
// Comment on it, then pick it up
if ( speech is not null && speech.CanSpeak )
{
var line = PickUpLines[Game.Random.Int( 0, PickUpLines.Length - 1 )];
AddTask( new Say( line, 2f ) );
}
AddTask( new PickUpProp( PropTarget ) );
Log.Info($"Picked up prop, moving!" );
// Try to find a reachable wander point on the navmesh
var randomDir = Vector3.Random.WithZ( 0 ).Normal;
var wanderTarget = PropTarget.WorldPosition + randomDir * Game.Random.Float( 150f, 300f );
if ( Npc.Scene.NavMesh.GetClosestPoint( wanderTarget ) is Vector3 navPoint )
{
AddTask( new MoveTo( navPoint, 15f ) );
}
// Always added — even if MoveTo was skipped or would have failed,
// the NPC still holds the prop for a bit, then drops it properly.
AddTask( new Wait( Game.Random.Float( 3f, 6f ) ) );
AddTask( new DropProp( PropTarget ) );
if ( speech is not null )
{
var line = DropLines[Game.Random.Int( 0, DropLines.Length - 1 )];
AddTask( new Say( line, 2f ) );
}
}
else
{
// Just observe the big prop
if ( speech is not null && speech.CanSpeak && Game.Random.Float() < 0.5f )
{
var line = InspectLines[Game.Random.Int( 0, InspectLines.Length - 1 )];
AddTask( new Say( line, 2.5f ) );
}
AddTask( new Wait( Game.Random.Float( 2f, 4f ) ) );
}
}
protected override void OnEnd()
{
Npc.Animation.ClearLookTarget();
Npc.Animation.ClearHeldProp();
}
}