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 );
}
}