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

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

	public float Lifetime { get; set; }

	private TimeSince _timeSinceBlink;

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

		//OffsetY = -0.16f;

		Scale = 0.6f;

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

		Sprite.LocalScale = new Vector3( 1f ) * Scale * Globals.SPRITE_SCALE;

		if ( IsProxy )
			return;

		//BasePivotY = 0.225f;

		SpawnTime = 0f;
		Lifetime = 60f;

		if ( Manager.Instance.Difficulty < 0 )
			Lifetime *= 1.5f;

		Radius = 0.175f;

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

	protected override void OnUpdate()
	{
		base.OnUpdate();
		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.Text( $"Stats[BulletStat.Damage]: {Stats[BulletStat.Damage]}\nStats[BulletStat.Lifetime]: {Stats[BulletStat.Lifetime]}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.4f, 0f ) ) );

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

		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;

		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() )
			{
				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 Enemy enemy && !enemy.IsDying )
		{
			Velocity += (Position2D - other.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * 20f * (1f + other.TempWeight) * dt;
		}
		else if ( other is Player player )
		{
			if ( !player.IsDead && SpawnTime > 0.1f )
			{
				var amount = player.Stats[PlayerStat.HealthPackAmount];

				if ( amount > 0f )
				{
					player.Heal( amount, 0.2f );
					Manager.Instance.PlaySfxNearby( "heal", Position2D, pitch: Utils.Map( player.Health / player.Stats[PlayerStat.MaxHp], 0f, 1f, 1.5f, 1f ), volume: 1.5f, maxDist: 5f );

					//var particle = DamageNumbersLegacy.Create( amount, player.Position2D + new Vector2( 0.2f + Game.Random.Float( -0.1f, 0.1f ), player.Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) ), color: Color.Green, sizeMultiplier: 1f );
					//Vector3 velocity = new Vector3( 0f, 0f, 0f );
					//Vector3 gravity = new Vector3( 0f, 1f, 0f );
					//particle.SetVector( 1, velocity );
					//particle.SetNamedValue( "Gravity", gravity );

					var pos = Position2D + new Vector2( Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) );
					float size = Utils.Map( amount, 1, 30, 1.2f, 1.6f, EasingType.QuadOut );
					Manager.Instance.SpawnDamageNumber( pos, amount, Color.Green, size, FloaterType.Heal );
				}
				else
				{
					player.Damage( Math.Abs(amount), PlayerDamageType.Self );
					Manager.Instance.PlaySfxNearby( "zombie.attack.player", player.Position2D, pitch: Game.Random.Float( 1.45f, 1.55f ), volume: 0.9f, maxDist: 3f );
				}

				Remove();
			}
		}
		else if ( other is HealthPack healthPack || other is RerollPickup rerollPickup )
		{
			Velocity += (Position2D - other.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * 20f * dt;
		}
	}
}