Npcs/Npc.cs
using Sandbox.Npcs.Layers;

namespace Sandbox.Npcs;

[Hide]
public partial class Npc : Component, IKillSource
{
	[Property]
	public bool ShowDebugOverlay { get; set; }

	[Property]
	public SkinnedModelRenderer Renderer { get; set; }

	/// <summary>
	/// The name shown in the kill feed when this NPC is killed.
	/// </summary>
	[Property]
	public string DisplayName { get; set; } = "NPC";

	// IKillSource
	string IKillSource.DisplayName => DisplayName;
	string IKillSource.Tags => "npc";

	private Rigidbody _rigidbody;
	private NavMeshAgent _navAgent;
	private TimeSince _timeSincePhysicsEnabled;

	protected override void OnStart()
	{
		GameObject.Tags.Add( "npc" );
		_rigidbody = GetComponent<Rigidbody>();
		_navAgent = GetComponent<NavMeshAgent>();
	}

	protected override void OnFixedUpdate()
	{
		if ( IsProxy || !_rigidbody.IsValid() || !_navAgent.IsValid() ) return;

		if ( _rigidbody.MotionEnabled )
		{
			// Physics is active (physgun grabbed us), so stop NavMesh from fighting the physics position.
			if ( _navAgent.UpdatePosition )
			{
				_navAgent.UpdatePosition = false;
				_timeSincePhysicsEnabled = 0;
			}

			// Once no longer constrained by a joint and velocity has settled, hand control back to navmesh
			var isJointHeld = _rigidbody.Joints.Count > 0;
			if ( !isJointHeld && _timeSincePhysicsEnabled > 0.5f && _rigidbody.Velocity.Length < 20f )
			{
				_rigidbody.MotionEnabled = false;
				_navAgent.Enabled = false;

				// Re-register the agent at the physics landing position by disabling and re-enabling it.
				_navAgent.Enabled = true;
				_navAgent.Stop();
				_navAgent.UpdatePosition = true;
			}
		}
		else if ( !_navAgent.UpdatePosition )
		{
			// MotionEnabled was cleared externally (eg. physgun), so re-enable NavMesh.
			_navAgent.UpdatePosition = true;
		}
	}

	protected override void OnUpdate()
	{
		if ( IsProxy )
			return;

		TickSchedule();

		if ( ShowDebugOverlay )
		{
			DrawDebugString();
		}
	}

	/// <summary>
	/// Spawns a ragdoll at the NPC's current position, copying the renderer and clothing,
	/// and optionally applies a launch velocity from the attacker.
	/// </summary>
	[Rpc.Broadcast( NetFlags.HostOnly )]
	protected void CreateRagdoll( Vector3 velocity, Vector3 origin, float duration = 30 )
	{
		if ( !Renderer.IsValid() )
			return;

		var batch = Scene.BatchGroup();

		var go = new GameObject( true, "Ragdoll" );
		go.Tags.Add( "ragdoll" );
		go.WorldTransform = WorldTransform;

		var mainBody = go.Components.Create<SkinnedModelRenderer>();
		mainBody.CopyFrom( Renderer );
		mainBody.UseAnimGraph = false;

		// copy the clothes
		foreach ( var clothing in Renderer.GameObject.Children.SelectMany( x => x.Components.GetAll<SkinnedModelRenderer>() ) )
		{
			if ( !clothing.IsValid() ) continue;

			var newClothing = new GameObject( true, clothing.GameObject.Name );
			newClothing.Parent = go;

			var item = newClothing.Components.Create<SkinnedModelRenderer>();
			item.CopyFrom( clothing );
			item.BoneMergeTarget = mainBody;
		}

		var physics = go.Components.Create<ModelPhysics>();
		physics.Model = mainBody.Model;
		physics.Renderer = mainBody;
		batch.Dispose();

		physics.CopyBonesFrom( Renderer, true );

		ApplyRagdollForce( physics, velocity, origin );

		//
		// Destroy after a while
		//
		mainBody.Invoke( duration, mainBody.DestroyGameObject );
	}

	void ApplyRagdollForce( ModelPhysics physics, Vector3 force, Vector3 origin )
	{
		if ( !physics.IsValid() ) return;
		if ( force.Length < 1 ) return;

		foreach ( var body in physics.Bodies )
		{
			var rb = body.Component;
			if ( !rb.IsValid() ) continue;
			rb.ApplyImpulse( Vector3.Direction( origin, rb.WorldPosition ) * force.Length * rb.Mass );
		}
	}

	/// <summary>
	/// Resolves the attacker's current velocity from whatever movement source it has.
	/// </summary>
	protected Vector3 GetAttackerVelocity( GameObject attacker )
	{
		if ( !attacker.IsValid() )
			return Vector3.Zero;

		if ( attacker.GetComponent<Rigidbody>() is { } rb )
			return rb.Velocity;

		return Vector3.Zero;
	}

	/// <summary>
	/// Calculates the launch velocity for a ragdoll based on the damage source.
	/// For explosions, uses the direction from the blast origin to this NPC.
	/// Otherwise, falls back to the attacker's physical velocity.
	/// </summary>
	protected Vector3 GetDeathLaunchVelocity( in DamageInfo damage )
	{
		if ( damage.Tags.Contains( DamageTags.Explosion ) && damage.Origin != Vector3.Zero )
		{

			var dist = (WorldPosition - damage.Origin).Length;
			var strength = MathX.Remap( dist, 0, 512, 500, 1500 ).Clamp( 500, 1500 );

			var dir = (WorldPosition - damage.Origin).Normal;
			dir += Vector3.Up * 1.0f;
			dir = dir.Normal;

			return dir * strength;
		}

		return GetAttackerVelocity( damage.Attacker );
	}

	/// <summary>
	/// Notifies the kill feed, spawns a ragdoll, and destroys this NPC.
	/// Call from subclass OnDamage when health drops below zero.
	/// Override to add NPC-specific behaviour before/after death.
	/// </summary>
	protected virtual void Die( in DamageInfo damage )
	{
		GameManager.Current?.OnNpcDeath( DisplayName, damage );
		CreateRagdoll( GetDeathLaunchVelocity( damage ), damage.Origin );
		GameObject.Destroy();
	}
}