ShrimpleRagdoll.Utils.cs
namespace ShrimpleRagdolls;

public partial class ShrimpleRagdoll
{
	/// <summary>
	/// Move the ragdoll without affecting its velocity or simulating collisions<br />
	/// </summary>
	/// <param name="target">The target transform, the entire ragdoll will be moved so that its root matches</param>
	public void Move( Transform target )
	{
		WakePhysics();

		foreach ( var body in Bodies )
		{
			if ( !body.Component.IsValid() )
				continue;

			var targetTransform = target.ToWorld( Renderer.WorldTransform.ToLocal( body.Component.WorldTransform ) );
			body.Component.WorldTransform = targetTransform;
		}
	}

	/// <summary>
	/// Apply a velocity to the ragdoll as a whole rather than on every body individually
	/// </summary>
	/// <param name="velocity">The velocity applied</param>
	public void ApplyVelocity( Vector3 velocity )
	{
		WakePhysics();

		foreach ( var body in Bodies )
			if ( body.Component.IsValid() )
				body.Component.Velocity += velocity;
	}

	/// <summary>
	/// Apply an angular velocity to the ragdoll, spinning it around the mass center
	/// </summary>
	/// <param name="angularVelocity">The axis to spin around and speed in radians per second</param>
	public void ApplyAngularVelocity( Vector3 angularVelocity )
	{
		WakePhysics();

		var spinAxis = angularVelocity.Normal;
		var spinSpeed = angularVelocity.Length;
		var normalizedAngularVelocity = spinAxis * spinSpeed;
		var massCenter = GetMassCenter();

		foreach ( var body in Bodies )
		{
			if ( !body.Component.IsValid() )
				continue;

			var bodyVelocity = Vector3.Cross( normalizedAngularVelocity, body.Component.WorldPosition - massCenter );
			body.Component.Velocity += bodyVelocity;
			body.Component.AngularVelocity += normalizedAngularVelocity;
		}
	}

	/// <summary>
	/// Apply a torque to the ragdoll, causing angular acceleration based on each body's inertia
	/// </summary>
	/// <param name="torque">The torque vector (axis and magnitude)</param>
	public void ApplyTorque( Vector3 torque )
	{
		WakePhysics();

		foreach ( var body in Bodies )
		{
			if ( !body.Component.IsValid() )
				continue;

			body.Component.PhysicsBody.ApplyTorque( torque );
		}
	}

	/// <summary>
	/// Apply a force to the ragdoll, causing acceleration based on each body's mass
	/// </summary>
	/// <param name="force">The force vector</param>
	public void ApplyForce( Vector3 force )
	{
		WakePhysics();

		foreach ( var body in Bodies )
		{
			if ( !body.Component.IsValid() )
				continue;

			body.Component.PhysicsBody.ApplyForce( force );
		}
	}

	/// <summary>
	/// Apply an impulse to the ragdoll, instantly changing velocity based on each body's mass
	/// </summary>
	/// <param name="impulse">The impulse vector</param>
	public void ApplyImpulse( Vector3 impulse )
	{
		WakePhysics();

		foreach ( var body in Bodies )
		{
			if ( !body.Component.IsValid() )
				continue;

			body.Component.PhysicsBody.ApplyImpulse( impulse );
		}
	}

	/// <summary>
	/// Get a body by bone name
	/// </summary>
	public ModelPhysics.Body? GetBodyByBoneName( string boneName )
	{
		if ( !Renderer.IsValid() || !Renderer.Model.IsValid() )
			return null;

		return Bodies.FirstOrDefault( x => x.Bone == Renderer.Model.Bones.GetBone( boneName ).Index );
	}

	/// <summary>
	/// Get a body by bone index
	/// </summary>
	public ModelPhysics.Body? GetBodyByBoneIndex( int boneIndex )
	{
		if ( !Renderer.IsValid() || !Renderer.Model.IsValid() )
			return null;

		return Bodies[boneIndex];
	}

	/// <summary>
	/// Get a body by bone
	/// </summary>
	public ModelPhysics.Body? GetBodyByBone( BoneCollection.Bone bone )
	{
		if ( bone == null )
			return null;

		return Bodies.FirstOrDefault( x => x.Bone == bone.Index );
	}

	/// <summary>
	/// Returns the signed angle in degrees that <paramref name="rot"/> rotates around <paramref name="axis"/>.
	/// Picks a reference vector perpendicular to the axis, rotates it, projects onto the plane, then atan2s the result.
	/// </summary>
	public static float GetSignedAngleAroundAxis( Rotation rot, Vector3 axis )
	{
		// I really don't understand this math, but my implementation was so long and crap I asked an AI to fix it and I guess this is what it's meant to look?
		axis = axis.Normal;

		// Pick a stable reference vector perpendicular to the axis
		var reference = MathF.Abs( Vector3.Dot( axis, Vector3.Up ) ) < 0.99f
			? Vector3.Cross( axis, Vector3.Up ).Normal
			: Vector3.Cross( axis, Vector3.Right ).Normal;

		// Rotate the reference, then flatten it back onto the axis-perpendicular plane
		var rotated = rot * reference;
		rotated = (rotated - axis * Vector3.Dot( rotated, axis )).Normal;

		return MathF.Atan2( Vector3.Dot( Vector3.Cross( reference, rotated ), axis ),
							Vector3.Dot( reference, rotated ) ) * (180f / MathF.PI);
	}

	/// <summary>
	/// Get the ragdoll's ideal transform from the provided bone
	/// </summary>
	/// <param name="boneIndex">Which bone to base off of</param>
	/// <param name="mergedBoneTransforms">The final renderer's transform should match the bone's transform</param>
	/// <returns></returns>
	public Transform GetRagdollTransform( int boneIndex, bool mergedBoneTransforms = true )
	{
		if ( !Renderer.IsValid() || !Renderer.SceneModel.IsValid() )
			return WorldTransform;

		var currentTransform = Bodies[boneIndex].Component.GameObject.WorldTransform;
		var targetTransform = currentTransform;

		if ( mergedBoneTransforms )
		{
			var localTransform = Renderer.Model.GetBoneTransform( boneIndex );
			var invRotation = localTransform.Rotation.Inverse;

			// Transform the bone's world transform back to root space
			var rotatedLocalPos = currentTransform.Rotation * (localTransform.Position * invRotation);
			targetTransform = new Transform(
				currentTransform.Position - rotatedLocalPos,
				currentTransform.Rotation * invRotation
			);
		}

		return targetTransform;
	}

	/// <summary>
	/// Returns the given bone and all of its descendants in the skeleton
	/// </summary>
	/// <param name="rootBone">The root bone</param>
	public IEnumerable<BoneCollection.Bone> GetDescendantBones( BoneCollection.Bone rootBone )
	{
		var included = new HashSet<int>() { rootBone.Index };

		foreach ( var bone in Renderer.Model.Bones.AllBones )
		{
			if ( bone.Parent != null && included.Contains( bone.Parent.Index ) )
				included.Add( bone.Index );

			if ( included.Contains( bone.Index ) )
				yield return bone;
		}
	}

	/// <summary>
	/// Returns the given bone and all of its descendants in the skeleton
	/// </summary>
	/// <param name="boneName">The root bone</param>
	public IEnumerable<BoneCollection.Bone> GetDescendantBones( string boneName )
		=> GetDescendantBones( Renderer?.Model?.Bones?.GetBone( boneName ) );

	/// <summary>
	/// Returns the given bone and all of its descendants in the skeleton
	/// </summary>
	/// <param name="boneIndex">The root bone</param>
	public IEnumerable<BoneCollection.Bone> GetDescendantBones( int boneIndex )
		=> GetDescendantBones( Renderer?.Model?.Bones?.AllBones[boneIndex] );

	public void MultiplyJointLimits( float multiplier = 1f )
	{
		foreach ( var joint in Joints )
		{
			if ( joint.Component is BallJoint ballJoint )
			{
				ballJoint.SwingLimit *= multiplier;
				ballJoint.TwistLimit *= multiplier;
			}
			else if ( joint.Component is HingeJoint hingeJoint )
			{
				hingeJoint.MinAngle *= multiplier;
				hingeJoint.MaxAngle *= multiplier;
			}
		}

		_currentJointLimits *= multiplier;
	}
}