Npcs/Diner/DinerInspectFoodSchedule.cs
using Sandbox.Npcs.Tasks;
namespace Sandbox.Npcs.Schedules;
/// <summary>
/// Walk over to a nearby food item (anything tagged with DinerNpc.FoodTag — served by the
/// emitters, or a stray peach), eye it up, react, and if it's small enough pick it up, carry
/// it off a short way, and set it back down.
///
/// Adapted from ScientistInspectPropSchedule, with two deliberate differences:
/// • Targets a specific food GameObject handed in by DinerNpc (tag-filtered), so customers
/// never grab kart wheels or other props — only actual food.
/// • Diner-flavoured reactions, with separate lines for "real" food vs a rogue peach.
///
/// The food target is chosen by DinerNpc.FindNearbyFood; this schedule just acts on it.
/// </summary>
public class DinerInspectFoodSchedule : ScheduleBase
{
private static readonly string[] InspectLines =
[
"Ooh, is this mine?",
"That smells incredible.",
"Finally, some service!",
"Now THAT'S a breakfast.",
"Don't mind if I do.",
"Is this the special?",
];
private static readonly string[] PickUpLines =
[
"I'll have this, thank you.",
"Mine, all mine.",
"Come to papa.",
"Saving this for later.",
];
private static readonly string[] PeachLines =
[
"...why is there a peach?",
"Who's throwing fruit?!",
"A peach? In MY diner?",
"This isn't on the menu.",
];
private static readonly string[] DropLines =
[
"Lovely.",
"There we are.",
"Best meal all week.",
"Compliments to the chef.",
];
public GameObject FoodTarget { get; set; }
/// <summary>Set by DinerNpc so we can theme the reaction (a peach gets a different line).</summary>
public bool IsPeach { get; set; }
protected override void OnStart()
{
if ( !FoodTarget.IsValid() ) return;
var speech = Npc.Speech;
// Keep eyes on the food the whole time.
Npc.Animation.SetLookTarget( FoodTarget );
// Approach — MoveTo tracks the object if it rolls/gets knocked around.
AddTask( new MoveTo( FoodTarget, 40f ) );
AddTask( new Wait( Game.Random.Float( 0.5f, 1f ) ) );
// React on arrival.
if ( speech is not null && speech.CanSpeak )
{
var lines = IsPeach ? PeachLines : InspectLines;
AddTask( new Say( lines[Game.Random.Int( 0, lines.Length - 1 )], 2.5f ) );
}
// Only pick up genuinely small items so we don't try to hoist a whole turkey.
var bounds = FoodTarget.GetBounds();
var isSmall = bounds.Size.Length < 96f;
if ( isSmall )
{
if ( speech is not null && speech.CanSpeak && !IsPeach )
AddTask( new Say( PickUpLines[Game.Random.Int( 0, PickUpLines.Length - 1 )], 2f ) );
AddTask( new PickUpProp( FoodTarget ) );
// Carry it off toward home so food drifts back to the tables, not across the map.
var diner = Npc as Sandbox.Npcs.Diner.DinerNpc;
var home = diner?.HomeArea ?? Npc.WorldPosition;
var dir = Vector3.Random.WithZ( 0 ).Normal;
var carryTarget = home + dir * Game.Random.Float( 60f, 160f );
if ( Npc.Scene.NavMesh.GetClosestPoint( carryTarget ) is { } navPoint )
AddTask( new MoveTo( navPoint, 15f ) );
AddTask( new Wait( Game.Random.Float( 2f, 5f ) ) );
AddTask( new DropProp( FoodTarget ) );
if ( speech is not null && speech.CanSpeak )
AddTask( new Say( DropLines[Game.Random.Int( 0, DropLines.Length - 1 )], 2f ) );
}
else
{
// Too big to lift — just admire it for a moment.
AddTask( new Wait( Game.Random.Float( 2f, 4f ) ) );
}
}
protected override void OnEnd()
{
Npc.Animation.ClearLookTarget();
Npc.Animation.ClearHeldProp();
}
}