Player/FlightController.cs
/// <summary>
/// Handles flight physics for the PlayerPawn: acceleration, deceleration, 
/// boosting, rotation, lean banking, velocity and position.
/// </summary>
[Title( "Flight Controller" ), Icon( "speed" )]
public sealed class FlightController : Component
{
	private PlayerPawn Player => Components.Get<PlayerPawn>( FindMode.InSelf );

	/// <summary>Speed multiplier when not holding forward (cruise).</summary>
	[Property, Range( 1f, 2000f )] public float CruiseMultiplier { get; set; } = 20f;

	/// <summary>Speed multiplier when holding forward (throttle).</summary>
	[Property, Range( 1f, 5000f )] public float ThrottleMultiplier { get; set; } = 50f;

	protected override void OnFixedUpdate()
	{
		var player = Player;
		if ( player == null || player.IsProxy || !player.IsAlive ) return;

		player.Speed = player.Speed.Clamp( player.MinSpeed, player.MaxSpeed );
		player.BoostCoolDown = player.BoostCoolDown.Clamp( 0f, player.BoostAmount );
		var rawAnalogMove = player.IsBot ? player.InputDirection : Input.AnalogMove;
		var moveX = rawAnalogMove.x.Clamp( -1f, 1f );
		var moveY = rawAnalogMove.y.Clamp( -1f, 1f );
		const float moveDeadzone = 0.05f;

		// ── Throttle ──────────────────────────────────────────────
		if ( moveX > moveDeadzone )
			player.Speed += player.Data.Acceleration * Time.Delta;
		else
			player.Speed -= player.Data.Deceleration * Time.Delta;

		// ── Brake / reverse ───────────────────────────────────────
		var wantsBrake = player.IsBot
			? player.BotWantsBrake
			: (moveX < -moveDeadzone || Input.Down( "back" ) || Input.Down( "backward" ));
		if ( wantsBrake )
		{
			player.Speed -= (player.IsBot ? 6f : 15f) * Time.Delta;
			player.MinSpeed = player.MinSpeed.LerpTo( player.Data.MinSpeed, Time.Delta * 2f );
			player.BreakLean = player.BreakLean.LerpTo( 1f, Time.Delta * player.Speed );
		}
		else
		{
			player.MinSpeed = player.MinSpeed.LerpTo( player.CappedMaxSpeed, Time.Delta * 2f );
			player.BreakLean = player.BreakLean.LerpTo( 0f, Time.Delta * 10f );
		}

		// ── Boost ─────────────────────────────────────────────────
		var wantsBoost = player.IsBot ? player.BotWantsBoost : Input.Down( "run" );
		if ( wantsBoost && player.BoostCoolDown != 0 )
		{
			player.MaxSpeed = player.MaxSpeed.LerpTo( player.BoostSpeed, Time.Delta * 2f );
			player.Speed += player.BoostSpeed * Time.Delta;
			player.BoostCoolDown--;
			player.TrackBoostUsage( Time.Delta );
		}
		else
		{
			player.MaxSpeed = player.MaxSpeed.LerpTo( player.IdleSpeed, Time.Delta * 1.25f );
		}

		if ( !player.IsBot && Input.Pressed( "run" ) && player.BoostCoolDown != 0 )
			Sound.Play( "jetbooststart", player.WorldPosition );

		if ( !wantsBoost && player.BoostCoolDown < player.BoostAmount )
			player.BoostCoolDown += player.BoostRegenRate * Time.Delta;

		// ── Rotation (match reference: lerp toward aim, then apply lean) ─────
		var targetRotation = Rotation.From( player.ViewAngles );
		float lerpFactor;
		if ( player.IsBot )
		{
			var bot = player.Components.Get<BotController>();
			var ts  = bot?.TurnSpeed ?? 3f;
			// Turn faster during attack runs so the bot actually tracks the target
			if ( bot?.State == BotController.BotState.Attack ) ts *= 1.6f;
			lerpFactor = ts * Time.Delta;
		}
		else
		{
			lerpFactor = Input.MouseDelta.Length.Remap( 0f, 1000f, 3f, 9f ) * Time.Delta;
		}

		// Set base velocity BEFORE multiplier — lean is computed from this small value
		var baseVelocity = player.WorldRotation.Forward * player.Speed * Time.Delta;

		// Rotate toward aim
		player.WorldRotation = Rotation.Lerp( player.WorldRotation, targetRotation, lerpFactor );

		// ── Banking lean (from pre-multiplier velocity, matching reference) ───
		var movement = new Vector3( moveX, moveY, 0f );
		float leanTarget = baseVelocity.Dot( player.WorldRotation.Right * 1.25f ) * 10.01f;
		leanTarget += moveY * 0.035f;
		player.Lean = player.Lean.LerpTo( leanTarget, Time.Delta * 10f );

		// Apply lean: pitch, yaw, roll — AFTER rotation lerp, BEFORE velocity multiply
		player.WorldRotation *= Rotation.From( player.BreakLean * 2f, player.Lean * 5f, player.Lean * -10f );

		// ── Velocity: multiply AFTER lean (matching reference order) ─────────
		// speedtime normalizes Speed to 0-1 range, matching reference's Curve evaluation
		var speedtime = player.Speed.Remap( player.MinSpeed, player.MaxSpeed, 0f, 1f );

		player.Velocity = player.WorldRotation.Forward * player.Speed * Time.Delta;
		player.Velocity *= moveX > moveDeadzone ? ThrottleMultiplier : CruiseMultiplier;
		player.Velocity += player.Velocity.LerpTo(
			player.WorldRotation.Left * moveY * 50f * speedtime,
			20f * Time.Delta );

		// ── Movement with surface sliding (MoveHelper) ───────────────────────
		var pos = player.WorldPosition;
		var baseTrace = Scene.Trace.Ray( pos, pos )
			.Size( 32f )
			.IgnoreGameObject( player.GameObject )
			.WithoutTags( "player" ); // ships pass through each other; weapons handle combat hits

		var helper = new MoveHelper( baseTrace, pos, player.Velocity );
		helper.TryMove( 1f ); // Velocity already has Time.Delta baked in

		player.HitWall = helper.HitWall;
		player.WallHitNormal = helper.WallHitNormal;
		player.WorldPosition = helper.Position;

		if ( helper.HitWall )
		{
			player.Speed -= 50f * Time.Delta;
			player.RequestWallDamage( 0.5f );
		}
	}
}