VehicleLights component for a vehicle. It handles driver input to toggle headlights and high/low beams, updates spotlight entities and plays switch sounds, and suppresses the player's normal flashlight while driving.
/// <summary>
/// A vehicle's headlights (the low beams) and its light bar (the high beams), which comes on as well as them.
///
/// The driver taps the flashlight key to switch the headlights on and off, and holds it to switch between low and
/// high beam. While they're driving the key is ours: we cancel the player's own flashlight through
/// <see cref="Global.IPlayerEvents.OnPlayerFlashlight"/>.
/// </summary>
[Title( "Vehicle Lights" ), Category( "Vehicles" ), Icon( "highlight" )]
public sealed class VehicleLights : Component, IPlayerControllable, Global.IPlayerEvents
{
public enum HeadlightMode
{
Off,
Low,
High,
}
[Property] public FourWheelVehicle Vehicle { get; set; }
[Property, Group( "Input" )] public ClientInput Switch { get; set; } = new() { Action = "Flashlight" };
/// <summary>
/// How long the key has to be held to switch beams, seconds. A shorter press switches the lights on or off.
/// </summary>
[Property, Group( "Input" )] public float HoldTime { get; set; } = 0.35f;
[Property] public Color LightColor { get; set; } = new( 1f, 0.94f, 0.84f );
/// <summary>
/// How long the bulbs take to come up to full brightness or go out, seconds.
/// </summary>
[Property] public float WarmUpTime { get; set; } = 0.12f;
[Property, Group( "Low Beam" )] public List<SpotLight> LowBeams { get; set; } = [];
[Property, Group( "Low Beam" )] public Texture LowBeamCookie { get; set; }
[Property, Group( "Low Beam" )] public float LowBeamBrightness { get; set; } = 14f;
[Property, Group( "Low Beam" )] public float LowBeamRange { get; set; } = 1800f;
[Property, Group( "Low Beam" )] public float LowBeamCone { get; set; } = 62f;
/// <summary>
/// The light bar. It comes on as well as the low beams.
/// </summary>
[Property, Group( "High Beam" )] public List<SpotLight> HighBeams { get; set; } = [];
[Property, Group( "High Beam" )] public Texture HighBeamCookie { get; set; }
[Property, Group( "High Beam" )] public float HighBeamBrightness { get; set; } = 28f;
[Property, Group( "High Beam" )] public float HighBeamRange { get; set; } = 4000f;
[Property, Group( "High Beam" )] public float HighBeamCone { get; set; } = 44f;
[Property, Group( "Sound" )] public SoundEvent SwitchOnSound { get; set; }
[Property, Group( "Sound" )] public SoundEvent SwitchOffSound { get; set; }
[Sync( SyncFlags.FromHost )] public HeadlightMode Mode { get; set; }
private bool _wasDown;
private bool _holdUsed;
private TimeSince _pressed;
private float _lowLevel;
private float _highLevel;
/// <summary>
/// Tap to switch the lights, hold to switch beams. Runs on the host, from the driver's seat.
/// </summary>
void IPlayerControllable.OnControl()
{
var down = Switch.Down();
if ( down && !_wasDown )
{
_pressed = 0;
_holdUsed = false;
}
if ( down && !_holdUsed && _pressed >= HoldTime )
{
_holdUsed = true;
SetMode( Mode == HeadlightMode.High ? HeadlightMode.Low : HeadlightMode.High );
}
if ( !down && _wasDown && !_holdUsed )
{
SetMode( Mode == HeadlightMode.Off ? HeadlightMode.Low : HeadlightMode.Off );
}
_wasDown = down;
}
/// <summary>
/// The driver's flashlight key works the headlights instead.
/// </summary>
void Global.IPlayerEvents.OnPlayerFlashlight( PlayerFlashlightEvent e )
{
if ( IsDriver( e.Player ) )
e.Cancelled = true;
}
private bool IsDriver( Player player )
{
var seat = Vehicle?.Seat;
if ( !player.IsValid() || !seat.IsValid() || !seat.IsOccupied ) return false;
return seat.GetOccupant()?.GetComponent<Player>() == player;
}
private void SetMode( HeadlightMode mode )
{
if ( mode == Mode ) return;
Mode = mode;
PlaySwitchSound( mode != HeadlightMode.Off );
}
[Rpc.Broadcast]
private void PlaySwitchSound( bool on )
{
var sound = on ? SwitchOnSound : SwitchOffSound;
if ( sound.IsValid() )
Sound.Play( sound, WorldPosition );
}
protected override void OnFixedUpdate()
{
// let go of the key if the driver got out holding it
if ( Networking.IsHost && !(Vehicle?.Seat?.IsOccupied ?? false) )
_wasDown = false;
}
protected override void OnUpdate()
{
var rate = Time.Delta / MathF.Max( WarmUpTime, 0.001f );
_lowLevel = _lowLevel.Approach( Mode != HeadlightMode.Off ? 1 : 0, rate );
_highLevel = _highLevel.Approach( Mode == HeadlightMode.High ? 1 : 0, rate );
UpdateLights( LowBeams, _lowLevel, LowBeamCookie, LowBeamBrightness, LowBeamRange, LowBeamCone );
UpdateLights( HighBeams, _highLevel, HighBeamCookie, HighBeamBrightness, HighBeamRange, HighBeamCone );
}
private void UpdateLights( List<SpotLight> lights, float level, Texture cookie, float brightness, float range, float cone )
{
foreach ( var light in lights )
{
if ( !light.IsValid() ) continue;
light.Enabled = level > 0;
light.Cookie = cookie;
light.Radius = range;
light.ConeOuter = cone;
light.ConeInner = cone * 0.8f;
light.LightColor = (LightColor * (brightness * level)).WithAlpha( 1 );
}
}
}