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 );
}
}
}