things/EnemyBullet.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static Manager;

public class EnemyBullet : Thing
{
	public TimeSince TimeSinceSpawn { get; private set; }

	public float Damage { get; set; }
	public float Lifetime { get; set; }
	public Vector2 Direction { get; set; }
	public float Speed { get; set; }
	public bool IsHoming { get; set; }
	public Thing Target { get; set; }
	private float _homingDeceleration;

	public bool IsCharmed { get; private set; }
	public Enemy Creator { get; set; }

	private float _startingSpriteY;

	protected override void OnAwake()
	{
		base.OnAwake();

		//OffsetY = -0.4f;
		Sprite.LocalPosition = new Vector3( 0f, 0.55f, 0f );

		Radius = 0.066f;

		Scale = 0.35f;
		Sprite.LocalScale *= Scale * Globals.SPRITE_SCALE;
		//Sprite.Size = new Vector2( 1f, 1f ) * Scale;

		Sprite.PlaybackSpeed = 2f;

		//Sprite.Tint = Color.Yellow;

		ShadowOpacity = 0.8f;
		ShadowScale = 0.6f;
		SpawnShadow( ShadowScale, ShadowOpacity );

		//Speed = 2f;
		Lifetime = 6f;

		if ( Manager.Instance.Difficulty == -1f )
			Lifetime *= 0.66f;

		Damage = 9f;
		TimeSinceSpawn = 0f;

		if ( IsProxy )
			return;

		CollideWith.Add( typeof( Player ) );
		CollideWith.Add( typeof( Enemy ) );

		_startingSpriteY = Sprite.LocalPosition.y;
	}

	public void SetColor(Color color)
	{
		Sprite.Tint = color;
	}

	protected override void OnUpdate()
	{
		base.OnUpdate();

		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.Text( $"Sprite.Tint: {Sprite.Tint}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.7f, 0f ) ) );

		//Gizmo.Draw.Color = Color.White.WithAlpha( 0.05f );
		//Gizmo.Draw.LineSphere( (Vector3)Position2D, Radius );

		//if ( !Manager.Instance.ShouldUpdateThings )
		//	return;

		float dt = Time.Delta;

		// todo: flip horizontal if moving left

		if(IsHoming)
		{
			if(Target.IsValid())
			{
				float speed = 7f;
				float leadingSpeed = Utils.Map( (Target.Position2D - Position2D).LengthSquared, 0f, 7f * 7f, 0f, 5f, EasingType.Linear );

				var dir = Vector2.Lerp( ((Target.Position2D + Target.Velocity * leadingSpeed) - Position2D).Normal, Velocity.Normal, Utils.Map( (Target.Position2D - Position2D).LengthSquared, 0f, 1.2f, 1f, 0f, EasingType.QuadOut ) );
				Velocity += dir * speed * dt;
				Velocity *= (1f - _homingDeceleration * dt);
			}

			Sprite.Tint = Color.Lerp( Color.Magenta, Color.Yellow, 0.5f + Utils.FastSin( TimeSinceSpawn + Time.Now * 28f ) * 0.5f );

			Position2D += Velocity * dt;
		}
		else
		{
			var speed = Speed * Utils.Map( TimeSinceSpawn, 0f, 0.5f, 0f, 1f, EasingType.QuadInOut ) * (Manager.Instance.Difficulty < 0 ? 0.75f : (Manager.Instance.Difficulty >= 1 ? 1.1f : 1f));
			Position2D += Direction * speed * dt;
		}

		WorldPosition = WorldPosition.WithZ( Globals.GetZPos( Position2D.y ) );

		if ( TimeSinceSpawn > Lifetime )
		{
			var cloud = Manager.Instance.SpawnCloud( Position2D );
			cloud.Lifetime = Game.Random.Float( 0.15f, 0.3f );
			cloud.Velocity = Velocity * Game.Random.Float( 0f, 1f );
			cloud.Deceleration = Game.Random.Float( 10f, 12f );
			cloud.LocalScale *= Game.Random.Float( 0.4f, 0.75f );

			Remove();
			return;
		}

		float progress = Utils.Map( TimeSinceSpawn, 0f, Lifetime, 0f, 1f, EasingType.QuadIn );
		Sprite.LocalPosition = Sprite.LocalPosition.WithY( Utils.Map( progress, 0f, 1f, _startingSpriteY, Radius * 0.8f ) );
		float shadowSize = Utils.Map( progress, 0f, 1f, ShadowScale, ShadowScale * 1.25f );
		ShadowSprite.LocalScale = new Vector3( shadowSize * Globals.SPRITE_SCALE, shadowSize * Globals.SPRITE_SCALE, 1f );
		ShadowSprite.Tint = Color.Black.WithAlpha( Utils.Map( progress, 0f, 1f, 0.5f, 1f ) );

		for ( int dx = -1; dx <= 1; dx++ )
		{
			for ( int dy = -1; dy <= 1; dy++ )
			{
				Manager.Instance.HandleThingCollisionForGridSquare( this, new GridSquare( GridPos.x + dx, GridPos.y + dy ), dt );

				if ( IsRemoved )
					return;
			}
		}
	}

	public override void Colliding( Thing other, float percent, float dt )
	{
		base.Colliding( other, percent, dt );

		if ( other is Player player && !player.IsDead && !IsCharmed )
		{
			float dmg = player.CheckDamageAmount( Damage, DamageType.Ranged );

			if ( !player.IsInvulnerable && !player.IsTimePausedForChoosing )
			{
				Manager.Instance.PlaySfxNearby( "splash", Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 4f );
				player.Damage( dmg );
				player.AddVelocity( Direction * 2f );
			}

			if( !player.IsDead && ( !player.IsInvulnerable || Manager.Instance.Difficulty <= 0 ) )
				Remove();
		}
		else if( other is Enemy enemy && (IsCharmed != enemy.IsCharmed) )
		{
			enemy.Damage( Damage, null, addVel: Direction * Game.Random.Float(0.75f, 1.5f), addTempWeight: 0f, isCrit: false, DamageType.Ranged );
			
			if(Creator.IsValid())
				enemy.Target = Creator;

			Manager.Instance.PlaySfxNearby( "splash", Position2D, pitch: Game.Random.Float( 1.1f, 1.15f ), volume: 0.6f, maxDist: 3f );

			Remove();
		}
	}

	public void BecomeCharmed(bool elite = false)
	{
		IsCharmed = true;
		Sprite.Tint = elite ? new Color( 1f, 0.2f, 1f, 0.3f ) : new Color(1f, 0f, 1f, 0.6f);

		CollideWith.Remove( typeof( Player ) );

		Damage = 9f * Manager.Instance.Player.Stats[PlayerStat.CharmedEnemyDmgDealtMultiplier];
	}

	public void BecomeHoming(Vector2 velocity, Thing target)
	{
		IsHoming = true;
		Velocity = velocity;
		Target = target;
		Radius = 0.08f;
		Scale = 0.45f;
		Sprite.LocalScale = Scale * Globals.SPRITE_SCALE;
		Lifetime = Game.Random.Float(4f, 7f);
		_homingDeceleration = Game.Random.Float( 1f, 1.25f );
	}
}