Transposer/Entities/BonusText.cs
namespace Sandbox.Transposer;

/// <summary>
/// Floating bonus text ("+64", "+32", etc.) that drifts upward and fades out.
/// Spawned when the player collects a coin.
/// </summary>
public class BonusText : TextDisplay
{
	private float _moveSpeed = 40f;
	private Vector2 _direction;

	private Queue<PixelPoint> _lastPositions = new();
	private const int NUM_LAST_POSITIONS = 3;

	private float _elapsedTime;
	private const float LIFETIME = 2.5f;

	public BonusText( string text, TransposerScene scene, int x, int y, string fontName, int scale = 1 )
		: base( text, scene, x, y, fontName, Color.Black, scale )
	{
		_direction = new Vector2( 0, 1 );
	}

	public override void UpdateEntity( float deltaTime )
	{
		base.UpdateEntity( deltaTime );
		Move( _direction, deltaTime );

		_elapsedTime += deltaTime;
		if ( _elapsedTime >= LIFETIME )
			_scene.RemoveEntity( this );
	}

	private void Move( Vector2 moveVector, float deltaTime )
	{
		Vector2 newExactPos = ExactPos + moveVector * _moveSpeed * deltaTime;
		PixelPoint newPixelPos = new( (int)MathF.Round( newExactPos.x ), (int)MathF.Round( newExactPos.y ) );

		if ( newPixelPos.Y + _letterHeight > Screen.PixelHeight - 4 )
		{
			_direction = new Vector2( 0, -1 );
		}
		else
		{
			if ( newPixelPos != PixelPosition )
				AddLastPosition( PixelPosition );
			ExactPos = newExactPos;
		}

		_moveSpeed *= 0.98f; // Decelerate each frame — text slows to a drift then stops.
	}

	public override void Draw()
	{
		// Draw ghost trail in black.
		float opacity = PixelUtils.Map( _elapsedTime, 0f, LIFETIME, 1f, 0f, EaseType.SineOut );
		foreach ( PixelPoint pos in _lastPositions )
		{
			SetOverriddenColor( new Color( 0, 0, 0, opacity ) );
			int currentX = pos.X;
			int currentY = pos.Y;

			for ( int i = 0; i < _text.Length; i++ )
			{
				char c = _text[i];
				if ( c == '\n' ) { currentY -= _letterHeight * _scale; currentX = PixelX; continue; }
				if ( c == ' ' ) { currentX += (_letterWidth + _spacing) * _scale; continue; }
				List<PixelData> pxList = GetPixelDataList( _fontName, c.ToString() );
				if ( pxList is not null ) DrawPixels( pxList, currentX, currentY, _scale );
				currentX += (_letterWidth + _spacing) * _scale;
			}
		}

		// Draw main text in white, fading.
		opacity = PixelUtils.Map( _elapsedTime, 0f, LIFETIME, 1f, 0f, EaseType.CubicOut );
		SetOverriddenColor( new Color( 1f, 1f, 1f, opacity ) );

		int currX = PixelX;
		int currY = PixelY;
		for ( int i = 0; i < _text.Length; i++ )
		{
			char c = _text[i];
			if ( c == '\n' ) { currY -= _letterHeight * _scale; currX = PixelX; continue; }
			if ( c == ' ' ) { currX += (_letterWidth + _spacing) * _scale; continue; }
			List<PixelData> pxList = GetPixelDataList( _fontName, c.ToString() );
			if ( pxList is not null ) DrawPixels( pxList, currX, currY, _scale );
			currX += (_letterWidth + _spacing) * _scale;
		}
	}

	private void AddLastPosition( PixelPoint pos )
	{
		if ( _lastPositions.Count >= NUM_LAST_POSITIONS )
			_lastPositions.Dequeue();
		_lastPositions.Enqueue( pos );
	}
}