Conveyance/SimpleAnimationController.cs
using Sandbox;
using System.Collections.Generic;

/// <summary>
/// Lightweight roaming-hazard controller for non-citizen creatures (rhino, giant ants,
/// robogator, ...). AnimGraph-agnostic: it drives animation by Set()-ing a handful of named
/// parameters on the SkinnedModelRenderer, so it works with any creature rig — just point the
/// param-name fields at whatever your model actually exposes.
///
/// STATES
///   Hibernating → dormant. The NavMeshAgent is DISABLED (almost free to tick) and the
///                 Hibernate anim param is set so the model can play a sleep/coiled/dormant
///                 loop. Optionally the creature becomes a pushable physics prop while asleep.
///   Patrol      → walk a fixed PatrolWaypoints route (if any waypoints are assigned).
///   Roam        → wander to random navmesh points around HomeArea (when there are no waypoints).
///   Following   → chase a target (a player who walked into the trigger, or a forced target).
///   Returning   → after losing the target, head home, then re-hibernate or resume roaming.
///
/// WAKING
///   • A player entering TriggerZone wakes it and is chased (toggle: WakeOnPlayerInTrigger).
///   • Game code can call Wake() / WakeAndPursue(target) / Hibernate() directly — e.g. a
///     wanted-level escalation, the creature getting shot, or a scripted reveal.
///   The "switch to the navagent on wake" is literal: the agent is enabled and the body is
///   snapped onto the navmesh so it can path immediately.
///
/// DEPENDENCIES: a NavMeshAgent on this GameObject.
///
/// QUICK SETUP
///   1. Add NavMeshAgent + this component to the creature GameObject.
///   2. Assign BodyRenderer (the SkinnedModelRenderer on the mesh).
///   3. (Optional) Assign TriggerZone — a trigger Collider on a child/sibling.
///   4. (Optional) Assign PatrolWaypoints, or leave empty to free-roam around HomeArea.
///   5. Match SpeedWalk/SpeedFollow and the anim param names to your model.
/// </summary>
[Title( "Simple Animal Controller" )]
[Category( "Game / NPC" )]
public sealed class SimpleAnimalController : Component
{
	// ── Inspector ──────────────────────────────────────────────────────────

	[Property] public SkinnedModelRenderer BodyRenderer { get; set; }
	[Property] public NavMeshAgent          Agent       { get; set; }

	[Header( "Activation" )]
	/// <summary>Start dormant and wait to be woken (the usual case for an ambush hazard).</summary>
	[Property] public bool StartHibernating { get; set; } = true;

	/// <summary>A player entering the trigger zone wakes the creature and gets chased.</summary>
	[Property] public bool WakeOnPlayerInTrigger { get; set; } = true;

	/// <summary>After losing its target and returning home, drop back into hibernation.
	/// If false, it keeps patrolling/roaming once woken until Hibernate() is called.</summary>
	[Property] public bool ReHibernateWhenIdle { get; set; } = true;

	[Header( "Trigger Zone" )]
	[Property] public Collider TriggerZone { get; set; }

	[Header( "Movement" )]
	[Property, Range( 20f, 300f )] public float SpeedWalk      { get; set; } = 80f;
	[Property, Range( 20f, 600f )] public float SpeedFollow    { get; set; } = 160f;

	/// <summary>Min distance to maintain from the followed target.</summary>
	[Property, Range( 30f, 200f )] public float FollowDistance { get; set; } = 80f;

	[Header( "Patrol / Roam" )]
	[Property] public List<GameObject> PatrolWaypoints { get; set; } = new();
	[Property, Range( 0.5f, 5f )] public float WaypointPauseDuration { get; set; } = 1.5f;

	/// <summary>Centre of the free-roam area (auto-set to spawn position if left at zero).</summary>
	[Property] public Vector3 HomeArea { get; set; }
	[Property, Range( 100f, 2000f )] public float RoamRadius { get; set; } = 500f;

	[Header( "Physics While Hibernating" )]
	/// <summary>Optional. If assigned and PhysicsWhileHibernating is on, this Rigidbody is
	/// enabled while dormant (a pushable sleeping beast) and disabled while active so the
	/// navmesh agent drives movement without the two fighting.</summary>
	[Property] public Rigidbody Body { get; set; }
	[Property] public bool PhysicsWhileHibernating { get; set; } = false;

	[Header( "Animation Parameter Names" )]
	[Property] public string AnimParamSpeed     { get; set; } = "Speed";
	[Property] public string AnimParamIsMoving  { get; set; } = "IsMoving";
	/// <summary>Bool set true while hibernating — drive a sleep/coiled/dormant loop off it.</summary>
	[Property] public string AnimParamHibernate { get; set; } = "Hibernate";

	// ── State ──────────────────────────────────────────────────────────────

	private enum AnimalState { Hibernating, Patrol, Roam, Following, Returning }
	private AnimalState _state;

	private readonly HashSet<GameObject> _zoneOccupants = new();
	private GameObject _followTarget;
	private int        _waypointIndex;
	private float      _pauseUntil;
	private Vector3    _roamTarget;
	private bool       _hasRoamTarget;

	public bool IsHibernating => _state == AnimalState.Hibernating;
	public bool IsAwake       => _state != AnimalState.Hibernating;

	// ── Lifecycle ──────────────────────────────────────────────────────────

	protected override void OnAwake()
	{
		Agent ??= Components.Get<NavMeshAgent>();

		if ( Agent.IsValid() )
			Agent.UpdateRotation = false; // we rotate manually

		if ( HomeArea == Vector3.Zero )
			HomeArea = WorldPosition;

		if ( TriggerZone.IsValid() )
		{
			TriggerZone.OnTriggerEnter += OnTriggerEntered;
			TriggerZone.OnTriggerExit  += OnTriggerExited;
		}

		if ( StartHibernating )
			EnterHibernate();
		else
			EnterActive();
	}

	protected override void OnUpdate()
	{
		switch ( _state )
		{
			case AnimalState.Hibernating: break; // dormant — nothing to tick
			case AnimalState.Patrol:    UpdatePatrol();    break;
			case AnimalState.Roam:      UpdateRoam();      break;
			case AnimalState.Following: UpdateFollowing(); break;
			case AnimalState.Returning: UpdateReturning(); break;
		}

		UpdateAnimation();

		if ( IsAwake )
			UpdateFacing();
	}

	// ── Public activation API ────────────────────────────────────────────────

	/// <summary>Wake the creature; it will patrol (if it has waypoints) or free-roam.</summary>
	public void Wake()
	{
		if ( IsAwake ) return;
		EnterActive();
	}

	/// <summary>Wake and immediately chase a specific target (e.g. whoever just shot it).</summary>
	public void WakeAndPursue( GameObject target )
	{
		_followTarget = target;
		EnterActive();
		_state = AnimalState.Following;
	}

	/// <summary>Force the creature back to sleep right now.</summary>
	public void Hibernate() => EnterHibernate();

	// Back-compat with the previous API.
	public void FollowTarget( GameObject target ) => WakeAndPursue( target );
	public void StopFollowing()
	{
		_followTarget = null;
		_state = ReHibernateWhenIdle
			? AnimalState.Returning
			: ( PatrolWaypoints.Count > 0 ? AnimalState.Patrol : AnimalState.Roam );
	}

	// ── State entry ────────────────────────────────────────────────────────

	private void EnterActive()
	{
		// Switch from "physics prop" to "navmesh agent": drop physics, snap to the mesh, enable.
		if ( PhysicsWhileHibernating && Body.IsValid() )
			Body.Enabled = false;

		if ( Agent.IsValid() )
		{
			if ( Scene.NavMesh is not null && Scene.NavMesh.GetClosestPoint( WorldPosition ) is { } navPos )
				WorldPosition = navPos;

			Agent.Enabled = true;
		}

		_hasRoamTarget = false;
		_state = PatrolWaypoints.Count > 0 ? AnimalState.Patrol : AnimalState.Roam;
	}

	private void EnterHibernate()
	{
		_followTarget  = null;
		_hasRoamTarget = false;

		if ( Agent.IsValid() )
		{
			Agent.Stop();
			Agent.Enabled = false;
		}

		if ( PhysicsWhileHibernating && Body.IsValid() )
			Body.Enabled = true;

		_state = AnimalState.Hibernating;
	}

	// ── State behaviours ───────────────────────────────────────────────────

	private void UpdatePatrol()
	{
		if ( PatrolWaypoints.Count == 0 ) { _state = AnimalState.Roam; return; }
		if ( !Agent.IsValid() ) return;
		if ( Time.Now < _pauseUntil ) return;

		var wp = PatrolWaypoints[_waypointIndex];
		if ( !wp.IsValid() )
		{
			_waypointIndex = (_waypointIndex + 1) % PatrolWaypoints.Count;
			return;
		}

		var target = wp.WorldPosition;

		if ( WorldPosition.Distance( target ) < 30f )
		{
			_pauseUntil    = Time.Now + WaypointPauseDuration;
			_waypointIndex = (_waypointIndex + 1) % PatrolWaypoints.Count;
			Agent.Stop();
		}
		else
		{
			Agent.MaxSpeed = SpeedWalk;
			Agent.MoveTo( target );
		}
	}

	private void UpdateRoam()
	{
		if ( !Agent.IsValid() ) return;
		if ( Time.Now < _pauseUntil ) return;

		if ( !_hasRoamTarget )
		{
			var dir       = Vector3.Random.WithZ( 0 ).Normal;
			var candidate = HomeArea + dir * Game.Random.Float( RoamRadius * 0.3f, RoamRadius );

			_roamTarget = Scene.NavMesh is not null && Scene.NavMesh.GetClosestPoint( candidate ) is { } nav
				? nav
				: candidate;

			_hasRoamTarget = true;
			Agent.MaxSpeed = SpeedWalk;
			Agent.MoveTo( _roamTarget );
			return;
		}

		if ( WorldPosition.Distance( _roamTarget ) < 40f )
		{
			_hasRoamTarget = false;
			_pauseUntil    = Time.Now + WaypointPauseDuration;
			Agent.Stop();
		}
	}

	private void UpdateFollowing()
	{
		if ( !_followTarget.IsValid() )
		{
			_state = AnimalState.Returning;
			return;
		}

		if ( !Agent.IsValid() ) return;

		Agent.MaxSpeed = SpeedFollow;

		if ( WorldPosition.Distance( _followTarget.WorldPosition ) > FollowDistance + 20f )
			Agent.MoveTo( _followTarget.WorldPosition );
		else
			Agent.Stop();
	}

	private void UpdateReturning()
	{
		if ( !Agent.IsValid() ) { EnterHibernate(); return; }

		// A new target showed up while we were heading home.
		if ( _followTarget.IsValid() )
		{
			_state = AnimalState.Following;
			return;
		}

		var home = PatrolWaypoints.Count > 0 && PatrolWaypoints[_waypointIndex].IsValid()
			? PatrolWaypoints[_waypointIndex].WorldPosition
			: HomeArea;

		if ( WorldPosition.Distance( home ) < 40f )
		{
			if ( ReHibernateWhenIdle )
				EnterHibernate();
			else
				_state = PatrolWaypoints.Count > 0 ? AnimalState.Patrol : AnimalState.Roam;
			return;
		}

		Agent.MaxSpeed = SpeedWalk;
		Agent.MoveTo( home );
	}

	// ── Animation ──────────────────────────────────────────────────────────

	private void UpdateAnimation()
	{
		if ( BodyRenderer is null ) return;

		var speed  = Agent.IsValid() ? Agent.Velocity.Length : 0f;
		var moving  = speed > 5f;

		if ( !string.IsNullOrEmpty( AnimParamSpeed ) )
			BodyRenderer.Set( AnimParamSpeed, speed );

		if ( !string.IsNullOrEmpty( AnimParamIsMoving ) )
			BodyRenderer.Set( AnimParamIsMoving, moving );

		if ( !string.IsNullOrEmpty( AnimParamHibernate ) )
			BodyRenderer.Set( AnimParamHibernate, IsHibernating );
	}

	private void UpdateFacing()
	{
		if ( !Agent.IsValid() ) return;

		var vel = Agent.Velocity;
		if ( vel.LengthSquared < 1f ) return;

		var targetRot = Rotation.LookAt( vel.Normal, Vector3.Up );
		WorldRotation = Rotation.Lerp( WorldRotation, targetRot, Time.Delta * 8f );
	}

	// ── Trigger callbacks ────────────────────────────────────────────────────

	private void OnTriggerEntered( Collider other )
	{
		if ( !WakeOnPlayerInTrigger ) return;
		if ( !other.Tags.Has( "player" ) ) return;

		_zoneOccupants.Add( other.GameObject );
		_followTarget = other.GameObject;

		if ( IsHibernating )
			EnterActive();

		_state = AnimalState.Following;
	}

	private void OnTriggerExited( Collider other )
	{
		_zoneOccupants.Remove( other.GameObject );

		// Someone else still in the zone? Chase them instead.
		var next = AnyOccupant();
		if ( next.IsValid() )
		{
			_followTarget = next;
			if ( IsAwake )
				_state = AnimalState.Following;
			return;
		}

		_followTarget = null;

		if ( IsAwake )
		{
			_state = ReHibernateWhenIdle
				? AnimalState.Returning
				: ( PatrolWaypoints.Count > 0 ? AnimalState.Patrol : AnimalState.Roam );
		}
	}

	private GameObject AnyOccupant()
	{
		foreach ( var go in _zoneOccupants )
		{
			if ( go.IsValid() )
				return go;
		}
		return null;
	}
}