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

/// <summary>
/// Ghost-like enemy that spawns with an animation, then moves in a random
/// cardinal direction, bouncing off screen edges.
///
/// Detects the player via pixel-colour collision: reads screen pixels at
/// its "up" frame positions and checks for the player's body colour
/// (200, 200, 100, 255).
/// </summary>
public class Enemy : Entity
{
	private const float MOVE_SPEED = 10f;
	private Vector2 _direction;

	private Queue<PixelPoint> _lastPositions = new();
	private const int NUM_LAST_POSITIONS = 2; // 2 trailing ghost positions drawn behind the enemy.

	// Cached pixel list for the "up" frame — used every frame for ghost trail
	// and pixel-color collision. Computed once; the frame data never changes.
	private List<PixelData> _upFramePixels;

	private bool _touchedPlayer;

	public Enemy( int x, int y, TransposerScene scene )
	{
		PixelPosition = new PixelPoint( x, y );
		_scene = scene;
		_type = "enemy";
		PlayAnimation( "spawn" );
		SetAnimCompleteCallback( StartMoving );
		_layer = Globals.DEPTH_ENEMY;

		// Cache up-frame pixel list once — reused every frame in UpdateEntity and Draw.
		_upFramePixels = GetPixelDataList( "enemy", "up", 0, false, false );
	}

	private void StartMoving()
	{
		int rand = Game.Random.Next( 0, 4 );
		_direction = rand switch
		{
			0 => new Vector2( 1, 0 ),
			1 => new Vector2( -1, 0 ),
			2 => new Vector2( 0, 1 ),
			_ => new Vector2( 0, -1 )
		};

		UpdateAnim();
		_collideable = true;
	}

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

		if ( _touchedPlayer )
			return;

		Move( _direction, deltaTime );

		// ── Pixel-colour collision: detect player body colour ────────────
		// Note: uses screen-pixel reads rather than AABB because grid transposing
		// moves pixels visually without changing entity logical positions.
		if ( _collideable && !_touchedPlayer )
		{
			if ( _upFramePixels is null ) return;

			foreach ( PixelData pd in _upFramePixels )
			{
				PixelPoint pos = PixelPosition + pd.Position;
				Color32 existing = Screen.GetPixel( pos.X, pos.Y );

				// Player body colour: (200, 200, 100, 255)
				if ( existing.r == 200 && existing.g == 200 && existing.b == 100 && existing.a == 255 )
				{
					((GameScene)_scene).PlayerTouchedEnemy( this );
					_touchedPlayer = true;
					PlayAnimation( "killplayer" );
					break;
				}
			}
		}
	}

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

		if ( IsInBounds( newPx.X, newPx.Y ) )
		{
			if ( newPx != PixelPosition )
				AddLastPosition( PixelPosition );
			ExactPos = newPos;
		}
		else
		{
			_direction = new Vector2( _direction.x * -1, _direction.y * -1 );
			UpdateAnim();
		}
	}

	private void UpdateAnim()
	{
		if ( _direction.x == -1 )
		{
			PlayAnimation( "left" );
			_flipX = false;
		}
		else if ( _direction.x == 1 )
		{
			// No separate "right" animation — reuse "left" with horizontal flip.
			PlayAnimation( "left" );
			_flipX = true;
		}
		else if ( _direction.y == 1 )
		{
			PlayAnimation( "up" );
		}
		else
		{
			PlayAnimation( "down" );
		}
	}

	public override void Draw()
	{
		// Draw ghost trail at previous positions.
		if ( _upFramePixels is not null )
		{
			List<PixelData> trailPixels = _upFramePixels;
			float opacity = 0.5f;
			foreach ( PixelPoint pos in _lastPositions )
			{
				foreach ( PixelData pd in trailPixels )
				{
					PixelPoint pp = pos + pd.Position;
					Screen.AddPixel( pp.X, pp.Y, new Color32( 0, 0, 0, (byte)(opacity * 255f) ) );
				}
			}
		}

		base.Draw();
	}

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