Effects/ScreenShake.cs
/// <summary>
/// Abstract base for screen shake effects. Active shakes are stored in a static list
/// and applied to the scene camera each frame via ScreenShake.Apply().
/// </summary>
public abstract class ScreenShake
{
	private static readonly List<ScreenShake> _active = new();

	/// <summary>Called each frame. Return false to remove this shake.</summary>
	public abstract bool Update();

	/// <summary>Apply all active screen shakes to the scene camera.</summary>
	public static void Apply()
	{
		var camera = Game.ActiveScene?.Camera;
		if ( camera == null ) return;

		for ( int i = _active.Count - 1; i >= 0; i-- )
		{
			if ( !_active[i].Update() )
				_active.RemoveAt( i );
		}
	}

	public static void Add( ScreenShake shake ) => _active.Add( shake );
	public static void Remove( ScreenShake shake ) => _active.Remove( shake );
	public static void ClearAll() => _active.Clear();

	// -----------------------------------------------------------------------
	// Speed-based continuous vibration (scales with player speed)
	// -----------------------------------------------------------------------
	public sealed class Charge : ScreenShake
	{
		public float Size { get; set; }

		public Charge() => Add( this );

		public void Destroy() => Remove( this );

		public override bool Update()
		{
			var camera = Game.ActiveScene?.Camera;
			if ( camera == null ) return true;

			var right = camera.WorldRotation.Right;
			var up = camera.WorldRotation.Up;
			var rand = new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) );

			camera.WorldPosition += (right * rand.x + up * rand.y) * Size;
			return true; // keep alive indefinitely (removed when player is replaced)
		}
	}

	// -----------------------------------------------------------------------
	// Time-decaying random shake (used for impacts/explosions)
	// -----------------------------------------------------------------------
	public sealed class Random : ScreenShake
	{
		public float Length { get; set; }
		public float Size { get; set; }
		private readonly RealTimeSince _started = 0f;

		public Random( float length, float size )
		{
			Length = length;
			Size = size;
			Add( this );
		}

		public override bool Update()
		{
			var delta = _started / Length;
			if ( delta >= 1f ) return false;

			var camera = Game.ActiveScene?.Camera;
			if ( camera == null ) return false;

			var right = camera.WorldRotation.Right;
			var up = camera.WorldRotation.Up;
			var rand = new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) );

			camera.WorldPosition += (right * rand.x + up * rand.y) * Size * (1f - delta);
			return true;
		}
	}
}

// -----------------------------------------------------------------------
// Speed-based continuous vibration (scales with player speed)
// -----------------------------------------------------------------------
public sealed class Charge : ScreenShake
{
	public float Size { get; set; }

	public Charge() => Add( this );

	public override bool Update()
	{
		var camera = Game.ActiveScene?.Camera;
		if ( camera == null ) return true;

		var right = camera.WorldRotation.Right;
		var up = camera.WorldRotation.Up;
		var rand = new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) );

		camera.WorldPosition += (right * rand.x + up * rand.y) * Size;
		return true; // keep alive indefinitely (removed when player is replaced)
	}
}

// -----------------------------------------------------------------------
// Time-decaying random shake (used for impacts/explosions)
// -----------------------------------------------------------------------
public sealed class Random : ScreenShake
{
	public float Length { get; set; }
	public float Size { get; set; }
	private readonly RealTimeSince _started = 0f;

	public Random( float length, float size )
	{
		Length = length;
		Size = size;
		Add( this );
	}

	public override bool Update()
	{
		var delta = _started / Length;
		if ( delta >= 1f ) return false;

		var camera = Game.ActiveScene?.Camera;
		if ( camera == null ) return false;

		var right = camera.WorldRotation.Right;
		var up = camera.WorldRotation.Up;
		var rand = new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) );

		camera.WorldPosition += (right * rand.x + up * rand.y) * Size * (1f - delta);
		return true;
	}
}