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

/// <summary>
/// A single leaderboard entry that scrolls upward from the bottom of the screen
/// and removes itself once it exits the top. Renders with the same per-pixel
/// static style as ScoreText, but biased toward rank tints:
///   rank 1 = gold, rank 2 = silver, rank 3 = bronze, others = subtle neutral static.
/// </summary>
public class LeaderboardScrollText : TextDisplay
{
	public const float SCROLL_SPEED = 40f;

	private readonly int _rank;

	public LeaderboardScrollText( string text, TransposerScene scene, int x, int y, string fontName, int rank )
		: base( text, scene, x, y, fontName, Color.Black, 1 )
	{
		_rank = rank;
	}

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

		ExactY += SCROLL_SPEED * deltaTime;

		if ( PixelY > Screen.PixelHeight + _letterHeight )
			_scene.RemoveEntity( this );
	}

	public override void Draw()
	{
		int currentX = PixelX;
		int currentY = PixelY;

		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> pixelDataList = GetPixelDataList( _fontName, c.ToString() );
			if ( pixelDataList is not null )
				DrawTintedStatic( pixelDataList, currentX, currentY );

			currentX += (_letterWidth + _spacing) * _scale;
		}
	}

	/// <summary>
	/// Replicates the per-pixel randomColor loop from Entity.DrawPixels, but
	/// biases channel ranges toward the rank tint and raises the alpha floor
	/// so the static is more subtle than the raw 0–100 / 0–255 original.
	/// </summary>
	private void DrawTintedStatic( List<PixelData> pixels, int x, int y )
	{
		foreach ( PixelData pixel in pixels )
		{
			Color32 color = RandomTintedColor();

			for ( int xOff = 0; xOff < _scale; xOff++ )
			{
				for ( int yOff = 0; yOff < _scale; yOff++ )
				{
					int px = x + pixel.Position.X * _scale + xOff;
					int py = y + pixel.Position.Y * _scale + yOff;

					if ( color.a == 255 )
						Screen.SetPixel( px, py, color );
					else if ( color.a > 0 )
						Screen.AddPixel( px, py, color );
				}
			}
		}
	}

	private Color32 RandomTintedColor()
	{
		// alpha: 140–255 (higher floor = less disappearing flicker than original 0–255)
		byte a = (byte)Game.Random.Next( 140, 256 );

		return _rank switch
		{
			// Gold: R high, G medium-high, B low
			1 => new Color32(
				(byte)Game.Random.Next( 170, 236 ),
				(byte)Game.Random.Next( 130, 196 ),
				(byte)Game.Random.Next(   0,  50 ),
				a ),
			// Silver: all channels medium-high, slight blue tint
			2 => new Color32(
				(byte)Game.Random.Next( 150, 216 ),
				(byte)Game.Random.Next( 150, 216 ),
				(byte)Game.Random.Next( 165, 226 ),
				a ),
			// Bronze: R high, G medium, B low
			3 => new Color32(
				(byte)Game.Random.Next( 155, 221 ),
				(byte)Game.Random.Next(  75, 146 ),
				(byte)Game.Random.Next(   0,  55 ),
				a ),
			// Others: dark neutral static
			_ => new Color32(
				(byte)Game.Random.Next( 20,  90 ),
				(byte)Game.Random.Next( 20,  90 ),
				(byte)Game.Random.Next( 20,  90 ),
				a ),
		};
	}
}