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

/// <summary>
/// Bouncing title text that floats randomly around the screen with a ghost trail.
/// Used for "transposer" on the main menu, rendered at 2× scale in red.
/// </summary>
public class TitleText : TextDisplay
{
	private float _moveSpeed = 30f;
	private Vector2 _direction;

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

	public TitleText( string text, TransposerScene scene, int x, int y, string fontName, int scale = 1 )
		: base( text, scene, x, y, fontName, Color.Black, scale )
	{
		float angle = Game.Random.Next( 0, 360 );
		_direction = GetVectorFromRotation( angle );
	}

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

		// Nudge the direction by a small random angle each frame to create
		// a "drunk walk" — the title drifts around rather than moving in a straight line.
		float angleChange = Game.Random.Float( -7f, 7f );
		float currentAngle = GetRotationFromVector( _direction ) * (180f / MathF.PI);
		_direction = GetVectorFromRotation( currentAngle + angleChange );
	}

	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 ( IsInBounds( newPixelPos.X, newPixelPos.Y, 3 ) )
		{
			if ( newPixelPos != PixelPosition )
				AddLastPosition( PixelPosition );
			ExactPos = newExactPos;
		}
		else
		{
			float angle = Game.Random.Next( 0, 360 );
			_direction = GetVectorFromRotation( angle );
		}
	}

	public override void Draw()
	{
		// Draw ghost trail at previous positions in black.
		float opacity = 0.45f;
		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 red, semi-transparent.
		SetOverriddenColor( new Color( 1f, 0f, 0f, 0.5f ) );
		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 );
	}

	// ── Vector/rotation helpers (ported from Utils.cs) ───────────────────

	// Converts degrees to a unit direction vector.
	// Uses -sin for X so that 0° points up (+Y), 90° points right (+X) — matching
	// the convention used in the original Unity port where 0° = "up" not "right".
	private static Vector2 GetVectorFromRotation( float degrees )
	{
		float rad = degrees * (MathF.PI / 180f);
		return new Vector2( MathF.Sin( -rad ), MathF.Cos( rad ) );
	}

	// Converts a direction vector back to degrees in the same convention as above.
	// Subtracts Atan2(1,0) (= 90°) to rotate the reference frame so 0° aligns with +Y.
	private static float GetRotationFromVector( Vector2 vector )
	{
		return MathF.Atan2( vector.y, vector.x ) - MathF.Atan2( 1f, 0f );
	}
}