NPC/PissingGuy.cs

An NPC subclass called PissingGuy. It defines model, movement, vision and attack parameters, returns to a starting position using a grid A* cell navigation, finds and targets players/Doobs, plays effects/sounds when within kill range, and handles simple animation toggles and periodic actions.

using System.Linq;
using System.Threading.Tasks;
using Sandbox;
using GridAStar;

namespace BrickJam;

/// <summary>
/// Stationary-ish monster that returns to its post when it loses its target. Scene-System port of
/// legacy <c>PissingGuy</c>, now on grid A*. (Piss particle + stomp footstep deferred to effects.)
/// </summary>
[Title( "Pissing Guy" )]
[Category( "NPC" )]
public sealed partial class PissingGuy : NPC
{
	public override string ModelPath { get; set; } = "models/pissing_guy/pissing_guy.vmdl";
	public override float WalkSpeed { get; set; } = 100f;
	public override float RunSpeed { get; set; } = 100f;
	public override float MaxVisionAngle { get; set; } = 360f;
	public override float MaxVisionRange { get; set; } = 200f;
	public override float MaxVisionRangeWhenChasing { get; set; } = 1024f;
	public override float MaxVisionAngleWhenChasing { get; set; } = 180f;
	public override float MaxRememberTime { get; set; } = 3f;
	public override float KillRange { get; set; } = 40f;
	public override string IdleSound => "";
	public override string AttackSound => "sounds/piss/pissattack.sound";
	public override float AttackVolume => 3f;

	public Vector3 StartingPosition { get; set; } = Vector3.Zero;
	public Rotation StartingRotation { get; set; } = Rotation.Identity;

	private bool fixing = true;

	protected override void OnStart()
	{
		base.OnStart();
		_ = Funny();
	}

	public override void ComputeIdleAndSeek()
	{
		if ( InVision.Count > 0 )
		{
			Target = InVision.Where( x => x.Key.IsValid() )
				.OrderBy( x => x.Key.WorldPosition.Distance( WorldPosition ) )
				.FirstOrDefault().Key;

			if ( Target is Player player && player.Doob.IsValid() )
				Target = player.Doob;

			LastTarget = Target;
			nextIdle = MansionGame.Random.NextSingle() * 3f + 3f;
		}
		else
		{
			Target = null;
		}

		if ( Target is null && !IsFollowingPath && StartingPosition != Vector3.Zero )
		{
			if ( WorldPosition.Distance( StartingPosition ) <= 30f )
			{
				WorldRotation = Rotation.Lerp( WorldRotation, StartingRotation, Time.Delta * 5f );
			}
			else
			{
				var targetCell = CurrentGrid?.GetCell( StartingPosition, false );
				if ( targetCell != null )
					NavigateTo( targetCell );
			}

			LastTarget = null;
		}

		if ( Target.IsValid() && Target.WorldPosition.Distance( WorldPosition ) <= KillRange )
		{
			MansionGame.Instance?.PlayEffect( "prefabs/particles/piss.prefab", Target.WorldPosition, WorldRotation );

			if ( Target is Player p )
				_ = CatchPlayer( p );
			else if ( Target is Doob d )
				_ = CatchDoob( d );
		}
	}

	private TimeUntil nextDoor = 0f;
	public override void ComputeOpenDoors()
	{
		if ( nextDoor )
		{
			base.ComputeOpenDoors();
			nextDoor = MansionGame.Random.NextSingle() * 0.1f + 0.1f;
		}
	}

	private TimeUntil nextFind = 0f;
	public override void FindTargets()
	{
		if ( nextFind )
		{
			base.FindTargets();
			nextFind = MansionGame.Random.NextSingle() * 0.1f + 0.1f;
		}
	}

	// Legacy animation kick: toggle "walking" once on spawn so the animgraph settles.
	private async Task Funny()
	{
		await Task.DelayRealtimeSeconds( 0.5f );
		Body?.Set( "walking", true );
		await Task.DelayRealtimeSeconds( 0.5f );
		Body?.Set( "walking", false );
		fixing = false;
	}

	private TimeUntil nextStomp = 0f;

	public override void ComputeAnimations()
	{
		if ( !fixing )
			Body?.Set( "walking", Velocity.WithZ( 0 ).Length >= 5f );

		// Heavy footstep stomp while actually moving. Host-driven (Think) but broadcast so every client
		// hears it (see SoundExtensions.BroadcastPlay).
		if ( nextStomp && Velocity.WithZ( 0 ).Length >= 20f )
		{
			SoundExtensions.BroadcastPlay( "sounds/piss/pisstomp.sound", WorldPosition );
			nextStomp = 0.4f;
		}
	}

	// PissingGuy doesn't tag cells for Doob avoidance.
	public override void AssignNearbyTags() { }
}