things/ReviveSoul.cs
using Sandbox;
using static Manager;

public class ReviveSoul : Thing
{
	public TimeSince SpawnTime { get; private set; }

	public float Lifetime { get; set; }

	private TimeSince _timeSinceBlink;

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

		//OffsetY = -0.14f;

		Scale = 0.6f;

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

		if ( IsProxy )
			return;

		//BasePivotY = 0.225f;

		SpawnTime = 0f;
		Lifetime = 60f;
		Radius = 0.175f;

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

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

		if ( !Manager.Instance.ShouldUpdateThings )
			return;

		Sprite.LocalScale = new Vector3( Scale + MathF.Cos( SpawnTime * 8f ) * 0.025f, Scale + Utils.FastSin( SpawnTime * 8f ) * 0.025f, 1f ) * Globals.SPRITE_SCALE;
		ShadowScale = 0.8f + Utils.FastSin( SpawnTime * 8f ) * 0.035f;
		ShadowSpriteDirty = true;

		float opacity = 0.3f + Utils.FastSin( SpawnTime * 5f ) * 0.2f;
		Sprite.Tint = Color.White.WithAlpha( opacity );
		ShadowSprite.Tint = Color.Black.WithAlpha( opacity );

		if ( IsProxy )
			return;

		float dt = Time.Delta;

		Position2D += Velocity * dt;
		Position2D = new Vector2( MathX.Clamp( Position2D.x, Manager.Instance.BOUNDS_MIN.x + Radius, Manager.Instance.BOUNDS_MAX.x - Radius ), MathX.Clamp( Position2D.y, Manager.Instance.BOUNDS_MIN.y + Radius, Manager.Instance.BOUNDS_MAX.y - Radius ) );
		WorldPosition = WorldPosition.WithZ( Globals.GetZPos( Position2D.y ) );
		Velocity *= (1f - dt * 3.5f);

		if ( SpawnTime > Lifetime - 7f )
		{
			if ( _timeSinceBlink > Utils.Map( SpawnTime, Lifetime - 7f, Lifetime, 0.25f, 0.03f, EasingType.SineIn ) )
			{
				_timeSinceBlink = 0f;
				Sprite.Enabled = !Sprite.Enabled;
			}

			if ( SpawnTime > Lifetime )
			{
				var cloud = Manager.Instance.SpawnCloud( Position2D );
				cloud.Velocity = new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ).Normal * Game.Random.Float( 0.2f, 0.6f );

				Remove();
				return;
			}
		}

		if ( SpawnTime > 0.1f )
		{
			foreach ( Player player in Manager.Instance.GetPlayers( alive: false ) )
			{
				var dist_sqr = (Position2D - player.Position2D).LengthSquared;
				var req_dist_sqr = MathF.Pow( player.Stats[PlayerStat.CoinAttractRange], 2f );
				if ( dist_sqr < req_dist_sqr )
				{
					Velocity += (player.Position2D - Position2D).Normal * Utils.Map( dist_sqr, req_dist_sqr, 0f, 0f, 1f, EasingType.Linear ) * player.Stats[PlayerStat.CoinAttractStrength] * dt;
				}
			}
		}

		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 && SpawnTime > 0.1f )
		{
			if ( player.IsDead )
			{
				player.Revive();
				Manager.Instance.PlaySfxNearby( "heal", Position2D, pitch: Utils.Map( player.Health / player.Stats[PlayerStat.MaxHp], 0f, 1f, 1.5f, 1f ), volume: 1.5f, maxDist: 5f );
				Remove();
			}
			else
			{
				Velocity += (Position2D - player.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * player.Stats[PlayerStat.PushStrength] * (1f + player.TempWeight) * dt;
			}
		}
	}
}