Conveyance/KartAssembler.cs
/// <summary>
/// Attach this to the kartbase. Drag the four wheel GameObjects into the slots in the editor.
/// Rewrites WheelJoint LocalFrames from actual world positions before physics initialises,
/// so stale baked frames from the toolgun scene are irrelevant.
/// </summary>
public class KartAssembler : Component, IPlayerControllable
{
	// ── Wheel slots — drag in from scene hierarchy ────────────────────────────

	[Property, Group( "Wheels" )] public GameObject FrontLeft  { get; set; }
	[Property, Group( "Wheels" )] public GameObject FrontRight { get; set; }
	[Property, Group( "Wheels" )] public GameObject RearLeft   { get; set; }
	[Property, Group( "Wheels" )] public GameObject RearRight  { get; set; }

	// ── Tuning ────────────────────────────────────────────────────────────────

	[Property, Range( 0f, 1f )] public float DrivePower      { get; set; } = 0.7f;
	[Property, Range( 0f, 1f )] public float DriveSpeed      { get; set; } = 0.65f;
	[Property, Range( 1f, 60f )] public float MaxSteerAngle  { get; set; } = 28f;
	[Property, Range( 0f, 1f )] public float BrakePower      { get; set; } = 1f;
	[Property, Range( 0f, 5f )] public float StabilityDamping { get; set; } = 2f;

	// ── Input ─────────────────────────────────────────────────────────────────

	[Property, Group( "Input" )] public string ThrottleAction   { get; set; } = "Forward";
	[Property, Group( "Input" )] public string BrakeAction      { get; set; } = "Backward";
	[Property, Group( "Input" )] public string SteerLeftAction  { get; set; } = "Left";
	[Property, Group( "Input" )] public string SteerRightAction { get; set; } = "Right";
	[Property, Group( "Input" )] public string HandbrakeAction  { get; set; } = "Jump";

	// ── Internal ──────────────────────────────────────────────────────────────

	Rigidbody _chassis;

	// Y offset of the axis child in wheel-local space (from basic.prefab)
	const float AxleLocalY = 5.492f;

	// Run in OnAwake so we write frames before WheelJoint.OnStart initialises the physics joint
	protected override void OnAwake()
	{
		_chassis = GetComponent<Rigidbody>();

		AttachWheel( FrontLeft,  steer: true  );
		AttachWheel( FrontRight, steer: true  );
		AttachWheel( RearLeft,   steer: false );
		AttachWheel( RearRight,  steer: false );
	}

	void AttachWheel( GameObject wheel, bool steer )
	{
		if ( wheel == null ) return;

		var joint = wheel.GetComponentInChildren<WheelJoint>();
		if ( !joint.IsValid() ) return;

		// Disable and re-enable to force the joint to reinitialise with our new frames
		joint.Enabled = false;

		// ── LocalFrame1: axle anchor in wheel-root local space ────────────────
		// The axis child sits at Y=5.492 in wheel-local space.
		// We want the spin axis to be wheel local-X, so rotate 90° around Z.
		joint.LocalFrame1 = new Transform(
			new Vector3( 0f, AxleLocalY, 0f ),
			Rotation.FromAxis( Vector3.Forward, 90f )
		);

		// ── LocalFrame2: attachment point in chassis local space ──────────────
		// Take the wheel's actual world position and express it in chassis-local space.
		var localPos = _chassis.GameObject.WorldTransform.PointToLocal( wheel.WorldPosition );

		// Axle points outward: +X for left-side wheels, -X for right-side wheels
		var axleDir = localPos.x >= 0f ? Vector3.Right : Vector3.Left;

		joint.LocalFrame2 = new Transform(
			localPos,
			Rotation.LookAt( axleDir, Vector3.Up )
		);

		// Switch to manual frames and re-enable so the joint initialises with our values
		joint.Attachment = Sandbox.Joint.AttachmentMode.LocalFrames;
		joint.Enabled = true;

		// ── Steering ──────────────────────────────────────────────────────────
		joint.EnableSteering       = steer;
		joint.EnableSteeringLimit  = steer;
		if ( steer )
		{
			joint.SteeringLimits       = new Vector2( -MaxSteerAngle, MaxSteerAngle );
			joint.SteeringHertz        = 10f;
			joint.SteeringDampingRatio = 1f;
		}
	}

	// ── IPlayerControllable ───────────────────────────────────────────────────

	public void OnStartControl() { }

	public void OnEndControl()
	{
		foreach ( var w in Wheels() ) NeutraliseWheel( w );
	}

	public void OnControl()
	{
		var conn = ClientInput.Current?.Network?.Owner;

		float drive = MathX.Clamp(
			( IsDown( ThrottleAction, conn ) ? 1f : 0f ) - ( IsDown( BrakeAction, conn ) ? 1f : 0f ),
			-1f, 1f );
		float steer = MathX.Clamp(
			( IsDown( SteerRightAction, conn ) ? 1f : 0f ) - ( IsDown( SteerLeftAction, conn ) ? 1f : 0f ),
			-1f, 1f );
		bool handbrake = IsDown( HandbrakeAction, conn );

		DriveWheel( FrontLeft,  drive, handbrake, steer * MaxSteerAngle );
		DriveWheel( FrontRight, drive, handbrake, steer * MaxSteerAngle );
		DriveWheel( RearLeft,   drive, handbrake, 0f );
		DriveWheel( RearRight,  drive, handbrake, 0f );

		ApplyStabilityDamping();
	}

	// ── Wheel driving ─────────────────────────────────────────────────────────

	void DriveWheel( GameObject wheel, float drive, bool handbrake, float steerAngle )
	{
		if ( wheel == null ) return;
		var joint = wheel.GetComponentInChildren<WheelJoint>();
		if ( !joint.IsValid() ) return;

		if ( handbrake )
		{
			joint.EnableSpinMotor = true;
			joint.SpinMotorSpeed  = 0f;
			joint.MaxSpinTorque   = 500000f * BrakePower;
		}
		else if ( MathF.Abs( drive ) < 0.01f )
		{
			joint.EnableSpinMotor = false;
		}
		else
		{
			joint.EnableSpinMotor = true;
			joint.SpinMotorSpeed  = -2000f * drive * DriveSpeed;
			joint.MaxSpinTorque   = 200000f * DrivePower;
		}

		if ( joint.EnableSteering )
		{
			joint.MaxSteeringTorque   = 500000f;
			joint.TargetSteeringAngle = steerAngle;
		}
	}

	void NeutraliseWheel( GameObject wheel )
	{
		if ( wheel == null ) return;
		var joint = wheel.GetComponentInChildren<WheelJoint>();
		if ( !joint.IsValid() ) return;
		joint.EnableSpinMotor = true;
		joint.SpinMotorSpeed  = 0f;
		joint.MaxSpinTorque   = 20000f;
	}

	void ApplyStabilityDamping()
	{
		if ( _chassis == null || StabilityDamping <= 0f ) return;
		var world = _chassis.PhysicsBody.AngularVelocity;
		var local = WorldRotation.Inverse * world;
		float damp = MathX.Clamp( StabilityDamping * Time.Delta * 10f, 0f, 1f );
		local.x *= 1f - damp;
		local.z *= 1f - damp;
		_chassis.PhysicsBody.AngularVelocity = WorldRotation * local;
	}

	// ── Helpers ───────────────────────────────────────────────────────────────

	IEnumerable<GameObject> Wheels()
	{
		if ( FrontLeft  != null ) yield return FrontLeft;
		if ( FrontRight != null ) yield return FrontRight;
		if ( RearLeft   != null ) yield return RearLeft;
		if ( RearRight  != null ) yield return RearRight;
	}

	static bool IsDown( string action, Connection conn )
		=> !string.IsNullOrWhiteSpace( action ) && conn != null && conn.Down( action );
}