Conveyance/KartController.cs
/// <summary>
/// Drives all WheelEntity components on a kart chassis from a single seated player.
/// Attach this to the chassis root alongside a BaseChair (or on the same prefab root).
///
/// Wheel naming convention expected in the prefab:
///   wheel_fl  – front-left  (steers + drives)
///   wheel_fr  – front-right (steers + drives)
///   wheel_rl  – rear-left   (drives only)
///   wheel_rr  – rear-right  (drives only)
///
/// ControlSystem already scopes ClientInput to the seated player before calling OnControl(),
/// so we read Connection.Down() directly – same pattern as WheelEntity.
/// </summary>
public class KartController : Component, IPlayerControllable
{
	// ── Tuning ────────────────────────────────────────────────────────────────

	[Property, Range( 0f, 1f ), Title( "Drive Power" )]
	public float DrivePower { get; set; } = 0.7f;

	[Property, Range( 0f, 1f ), Title( "Drive Speed" )]
	public float DriveSpeed { get; set; } = 0.65f;

	[Property, Range( 1f, 60f ), Title( "Max Steer Angle (degrees)" )]
	public float MaxSteerAngle { get; set; } = 28f;

	[Property, Range( 0f, 1f ), Title( "Brake Power" )]
	public float BrakePower { get; set; } = 1f;

	/// <summary>
	/// Damps chassis pitch and roll each frame while driving to resist tipping.
	/// 0 = no damping, 2 = recommended for go-kart stability.
	/// </summary>
	[Property, Range( 0f, 5f ), Title( "Stability Damping" )]
	public float StabilityDamping { get; set; } = 2f;

	// ── Input actions (match your project's input map) ────────────────────────

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

	// ── Wheel references (auto-discovered by child GameObject name if not set) ─

	[Property, Group( "Wheels" ), Title( "Front Left" )]  public WheelEntity WheelFL { get; set; }
	[Property, Group( "Wheels" ), Title( "Front Right" )] public WheelEntity WheelFR { get; set; }
	[Property, Group( "Wheels" ), Title( "Rear Left" )]   public WheelEntity WheelRL { get; set; }
	[Property, Group( "Wheels" ), Title( "Rear Right" )]  public WheelEntity WheelRR { get; set; }

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

	Rigidbody _body;

	protected override void OnStart()
	{
		_body = GetComponent<Rigidbody>();

		// Auto-discover by GameObject name so the prefab works without manual wiring
		WheelFL ??= FindWheelByName( "wheel_fl" );
		WheelFR ??= FindWheelByName( "wheel_fr" );
		WheelRL ??= FindWheelByName( "wheel_rl" );
		WheelRR ??= FindWheelByName( "wheel_rr" );
	}

	WheelEntity FindWheelByName( string goName )
	{
		foreach ( var go in GameObject.GetAllObjects( true ) )
		{
			if ( string.Equals( go.Name, goName, StringComparison.OrdinalIgnoreCase ) )
			{
				var w = go.GetComponent<WheelEntity>();
				if ( w != null ) return w;
			}
		}
		return null;
	}

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

	public void OnStartControl() { }

	public void OnEndControl()
	{
		// Neutralise all wheels so the kart doesn't drive off without a pilot
		ApplyToAllWheels( ( joint, _ ) =>
		{
			joint.EnableSpinMotor = true;
			joint.SpinMotorSpeed  = 0f;
			joint.MaxSpinTorque   = 20000f;   // light hold, not a brick wall
		} );
	}

	public void OnControl()
	{
		// ClientInput.Current is the seated player; read their connection's inputs
		var conn = ClientInput.Current?.Network?.Owner;

		float throttle   = AnalogDown( ThrottleAction,   conn );
		float brake      = AnalogDown( BrakeAction,      conn );
		float steerLeft  = AnalogDown( SteerLeftAction,  conn );
		float steerRight = AnalogDown( SteerRightAction, conn );
		bool  handbrake  = IsDown(     HandbrakeAction,  conn );

		float drive = MathX.Clamp( throttle - brake, -1f, 1f );
		float steer = MathX.Clamp( steerRight - steerLeft, -1f, 1f );

		SetDrive( WheelFL, drive, handbrake );
		SetDrive( WheelFR, drive, handbrake );
		SetDrive( WheelRL, drive, handbrake );
		SetDrive( WheelRR, drive, handbrake );

		SetSteer( WheelFL,  steer );
		SetSteer( WheelFR,  steer );
		SetSteer( WheelRL,  0f );    // rear wheels don't steer
		SetSteer( WheelRR,  0f );

		ApplyStabilityDamping();
	}

	// ── Wheel control helpers ─────────────────────────────────────────────────

	void SetDrive( WheelEntity wheel, float drive, bool handbrake )
	{
		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 )
		{
			// Coasting – let it roll freely
			joint.EnableSpinMotor = false;
		}
		else
		{
			joint.EnableSpinMotor = true;
			joint.SpinMotorSpeed  = -2000f * drive * DriveSpeed;
			joint.MaxSpinTorque   = 200000f * DrivePower;
		}
	}

	void SetSteer( WheelEntity wheel, float steer )
	{
		if ( wheel == null ) return;
		var joint = wheel.GetComponentInChildren<WheelJoint>();
		if ( !joint.IsValid() ) return;

		joint.EnableSteering       = true;
		joint.SteeringDampingRatio = 1f;
		joint.MaxSteeringTorque    = 500000f;
		joint.SteeringLimits       = new Vector2( -MaxSteerAngle, MaxSteerAngle );
		joint.TargetSteeringAngle  = MaxSteerAngle * steer;
	}

	void ApplyStabilityDamping()
	{
		if ( _body == null || StabilityDamping <= 0f ) return;

		// Damp pitch (X) and roll (Z) in local space; leave yaw (Y) free for steering feel
		var worldAng = _body.PhysicsBody.AngularVelocity;
		var localAng = WorldRotation.Inverse * worldAng;

		float dampFactor = MathX.Clamp( StabilityDamping * Time.Delta * 10f, 0f, 1f );
		localAng.x *= 1f - dampFactor;
		localAng.z *= 1f - dampFactor;

		_body.PhysicsBody.AngularVelocity = WorldRotation * localAng;
	}

	// ── Utility ───────────────────────────────────────────────────────────────

	void ApplyToAllWheels( System.Action<WheelJoint, WheelEntity> action )
	{
		foreach ( var wheel in new[] { WheelFL, WheelFR, WheelRL, WheelRR } )
		{
			if ( wheel == null ) continue;
			var joint = wheel.GetComponentInChildren<WheelJoint>();
			if ( joint.IsValid() ) action( joint, wheel );
		}
	}

	static float AnalogDown( string action, Connection conn )
	{
		if ( string.IsNullOrWhiteSpace( action ) || conn == null ) return 0f;
		return conn.Down( action ) ? 1f : 0f;
	}

	static bool IsDown( string action, Connection conn )
	{
		if ( string.IsNullOrWhiteSpace( action ) || conn == null ) return false;
		return conn.Down( action );
	}
}