Utils/PhysicsGrabber.cs
namespace ShrimpleRagdolls;

public class PhysicsGrabber : Component
{
	private PhysicsBody GrabbedBody;
	private GameObject GrabbedObject;
	private Vector3 GrabbedBodyLocal;
	private Vector3 GrabbedObjectLocal;
	private float GrabDistance;

	[Property] public float SpringStrength { get; set; } = 150f;
	[Property] public float Damping { get; set; } = 10f;
	[Property] public float MaxForceMultiplier { get; set; } = 100f;

	protected override void OnDisabled()
	{
		base.OnDisabled();
		Clear();
	}

	protected override void OnDestroy()
	{
		base.OnDestroy();
		Clear();
	}

	private void Clear()
	{
		GrabbedBody = null;
		GrabbedObject = null;
		GrabbedBodyLocal = default;
		GrabbedObjectLocal = default;
		GrabDistance = 0;
	}

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

		if ( GrabbedBody.IsValid() )
		{
			if ( !Input.Down( "attack1" ) )
			{
				Clear();
			}
			else
			{
				return;
			}
		}

		var tr = Scene.Trace.Ray( Scene.Camera.WorldPosition, Scene.Camera.WorldPosition + Scene.Camera.WorldRotation.Forward * 1000 )
			.IgnoreGameObjectHierarchy( GameObject.Root )
			.Run();

		if ( !tr.Hit || tr.Body is null )
			return;

		if ( tr.Body.BodyType == PhysicsBodyType.Static )
			return;

		if ( Input.Down( "attack1" ) )
		{
			GrabbedBody = tr.Body;
			GrabbedObject = tr.GameObject;
			GrabbedBodyLocal = GrabbedBody.Transform.PointToLocal( tr.HitPosition );
			GrabbedObjectLocal = GrabbedObject.WorldTransform.PointToLocal( tr.HitPosition );
			GrabDistance = tr.Distance;
		}
	}

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

		if ( !GrabbedBody.IsValid() )
			return;

		var camera = Scene.Camera;
		var targetPosition = camera.WorldPosition + camera.WorldRotation.Forward * GrabDistance;
		var currentPosition = GrabbedBody.Transform.PointToWorld( GrabbedBodyLocal );

		var displacement = targetPosition - currentPosition;
		var velocity = GrabbedBody.GetVelocityAtPoint( currentPosition );

		var springForce = displacement * SpringStrength - velocity * Damping;
		var force = springForce * GrabbedBody.Mass;

		var maxForce = MaxForceMultiplier * GrabbedBody.Mass * Scene.PhysicsWorld.Gravity.Length;
		if ( force.Length > maxForce )
		{
			force = force.Normal * maxForce;
		}

		GrabbedBody.ApplyForceAt( currentPosition, force );
	}

	protected override void OnPreRender()
	{
		base.OnPreRender();

		if ( !GrabbedObject.IsValid() )
		{
			var tr = Scene.Trace.Ray( Scene.Camera.ScreenNormalToRay( 0.5f ), 1000.0f )
						.IgnoreGameObjectHierarchy( GameObject.Root )
						.Run();

			if ( tr.Hit )
			{
				Gizmo.Draw.Color = Color.Cyan;
				Gizmo.Draw.SolidSphere( tr.HitPosition, 1 );
			}
		}
		else
		{
			var position = GrabbedObject.WorldTransform.PointToWorld( GrabbedObjectLocal );

			Gizmo.Draw.Color = Color.Cyan;
			Gizmo.Draw.SolidSphere( position, 1 );
		}
	}
}