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

/// <summary>
/// Collectible coin entity. Spins in place, detects the player via pixel-colour
/// collision, and smoothly relocates to a random position when collected.
/// Spawns Shine sparkle particles periodically.
/// </summary>
public class Coin : Entity
{
	// Monotonically-increasing counter shared across all Coin instances.
	// Each new coin gets a unique depth so their draw order is stable and
	// consistent even when multiple coins exist simultaneously.
	private static int _depthCounter;

	private bool _isMoving;
	private Vector2 _moveVector;
	private float _moveTimer;
	private const float MOVE_TIME = 0.75f;
	private Vector2 _originalPosition;
	private PixelPoint _targetPosition;

	private float _sparkleTimer;
	private const float SPARKLE_INTERVAL_IDLE   = 1f / 1.2f; // ~1.2 sparkles/s when stationary
	private const float SPARKLE_INTERVAL_MOVING = 1f / 12f;  // ~12 sparkles/s when moving

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

	public Coin( int x, int y, TransposerScene scene )
	{
		PixelPosition = new PixelPoint( x, y );
		_scene = scene;
		_type = "coin";
		PlayAnimation( "spin" );
		_layer = Globals.DEPTH_COIN;
		_depth = _depthCounter++;
	}

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

		if ( _isMoving )
		{
			PixelPoint oldPos = PixelPosition;

			// SineOut easing: fast start, decelerates smoothly into the target position.
			float moveAmount = PixelUtils.Map( _moveTimer, MOVE_TIME, 0f, 0f, 1f, EaseType.SineOut );
			ExactPos = _originalPosition + _moveVector * moveAmount;

			if ( PixelPosition != oldPos )
				AddLastPosition( oldPos );

			_moveTimer -= deltaTime;
			if ( _moveTimer <= 0f )
			{
				_isMoving = false;
				PixelPosition = _targetPosition;
				PlayAnimation( "spin" );
				_lastPositions.Clear();
			}
		}
		else
		{
			// Only check for player collision while stationary — skip during relocation
			// so the coin can't be re-collected mid-flight after a swap.
			List<PixelData> pixelDataList = GetPixelDataList( "coin", "spin", _currentFrameNumber, false, false );
			if ( pixelDataList is not null )
			{
				foreach ( PixelData pd in pixelDataList )
				{
					PixelPoint pos = PixelPosition + pd.Position;
					Color32 existing = Screen.GetPixel( pos.X, pos.Y );

					if ( existing.r == 200 && existing.g == 200 && existing.b == 100 && existing.a == 255 )
					{
						((GameScene)_scene).PlayerTouchedCoin( this );
						break;
					}
				}
			}
		}

		// Framerate-independent sparkle particles.
		float sparkleInterval = _isMoving ? SPARKLE_INTERVAL_MOVING : SPARKLE_INTERVAL_IDLE;
		_sparkleTimer -= deltaTime;
		if ( _sparkleTimer <= 0f )
		{
			_sparkleTimer += sparkleInterval;
			((GameScene)_scene).CreateShine(
				PixelX + Game.Random.Next( -1, 4 ),
				PixelY + Game.Random.Next( -1, 4 ) );
		}
	}

	/// <summary>
	/// Start a smooth relocation to a random position at least 40px away.
	/// </summary>
	public void Relocate()
	{
		const int MIN_DISTANCE = 40;
		int newX, newY;

		do
		{
			newX = Game.Random.Next( 2, Screen.PixelWidth - 12 );
			newY = Game.Random.Next( 2, Screen.PixelHeight - 12 );
		}
		while ( Math.Abs( newX - PixelX ) < MIN_DISTANCE && Math.Abs( newY - PixelY ) < MIN_DISTANCE );

		_isMoving = true;
		_moveTimer = MOVE_TIME;
		_sparkleTimer = 0f;
		PlayAnimation( "moving" );
		_lastPositions.Clear();

		_originalPosition = ExactPos;
		_targetPosition = new PixelPoint( newX, newY );
		_moveVector = new Vector2( newX - ExactPos.x, newY - ExactPos.y );
	}

	public override void Draw()
	{
		// Draw golden ghost trail.
		List<PixelData> trailPixels = GetPixelDataList( "coin", "moving", 0, false, false );
		if ( trailPixels is not null )
		{
			float opacity = 0.30f;
			foreach ( PixelPoint pos in _lastPositions )
			{
				foreach ( PixelData pd in trailPixels )
				{
					PixelPoint pp = pos + pd.Position;
					Screen.AddPixel( pp.X, pp.Y, new Color32( 191, 191, 0, (byte)(opacity * 255f) ) );
				}
			}
		}

		base.Draw();
	}

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