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

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

	public bool IsMagnetized { get; private set; }
	public Player PlayerMagnetized { get; private set; }
	public TimeSince MagnetizeTime { get; private set; }
	private const float MAGNETIZE_DURATION = 12f;

	[Property] public int Value { get; set; }

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

		//OffsetY = -0.12f;
		Sprite.LocalScale = new Vector3( 0.4f * Globals.SPRITE_SCALE, 0.4f * Globals.SPRITE_SCALE, 1f );
		Sprite.LocalRotation = new Angles( 0f, -90f, 0f );

		Scale = 0.4f;

		ShadowOpacity = 0.8f;
		ShadowScale = 0.4f;

		SpawnShadow( ShadowScale, ShadowOpacity );

		//BasePivotY = 0.225f;

		//Scale = new Vector2( 1f, 1f ) * 0.4f;
		SpawnTime = 0f;
		Radius = 0.125f;

		if ( IsProxy )
			return;

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

	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;

		if ( IsProxy )
			return;

		float dt = Time.Delta;

		if ( IsMagnetized )
		{
			if ( MagnetizeTime > MAGNETIZE_DURATION || PlayerMagnetized == null || !PlayerMagnetized.IsValid() || PlayerMagnetized.IsDead )
			{
				IsMagnetized = false;
				PlayerMagnetized = null;
			}
			else
			{
				if ( !PlayerMagnetized.Position2D.Equals( Position2D ) )
				{
					var strength = 10f;

					if ( PlayerMagnetized.Stats[PlayerStat.XpRepel] > 0f && (PlayerMagnetized.Position2D - Position2D).LengthSquared < 2f * 2f )
						strength = -4f;

					Velocity += (PlayerMagnetized.Position2D - Position2D).Normal * dt * strength * Utils.Map( MagnetizeTime, 0f, MAGNETIZE_DURATION, 1f, 0f, EasingType.QuadIn );
				}
			}
		}

		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 * 0.92f);

		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 )
			{
				if ( !player.Position2D.Equals( Position2D ) )
				{
					var strength = player.Stats[PlayerStat.CoinAttractStrength];
					if ( player.Stats[PlayerStat.XpRepel] > 0f )
						strength *= -0.5f;

					Velocity += (player.Position2D - Position2D).Normal * Utils.Map( dist_sqr, req_dist_sqr, 0f, 0f, 1f, EasingType.Linear ) * strength * 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 )
		{
			if ( !Position2D.Equals( other.Position2D ) )
				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 )
			{
				player.AddExperience( Value );
				Manager.Instance.PlaySfxNearby( "xp", Position2D, pitch: Utils.Map( player.ExperienceCurrent, 0, player.ExperienceRequired, 0.65f, 1.5f, EasingType.QuadIn ), volume: 1f, maxDist: 6f );
				Remove();
			}
		}
		else if ( other is Coin coin )
		{
			SetValue( Value + coin.Value );
			SpawnCloudClient( coin.Position2D, Vector2.Zero );

			if ( !IsMagnetized && coin.IsMagnetized )
			{
				PlayerMagnetized = coin.PlayerMagnetized;
				IsMagnetized = true;
				MagnetizeTime = coin.MagnetizeTime;
			}

			coin.Remove();
		}
	}

	public void SetValue( int value )
	{
		Value = value;

		int tier_shape = 1 + MathX.FloorToInt( (value - 1) / 5f );
		int tier_color = value % 5;

		switch ( tier_shape )
		{
			case 1:
				Sprite.PlayAnimation( "xp_1" );
				//BasePivotY = 0.225f;
				break;
			case 2:
				Sprite.PlayAnimation( "xp_2" );
				//BasePivotY = 0.225f;
				break;
			case 3:
				Sprite.PlayAnimation( "xp_3" );
				//BasePivotY = 0.15f;
				break;
			case 4:
				Sprite.PlayAnimation( "xp_4" );
				//BasePivotY = 0.1f;
				break;
			case 5:
			default:
				Sprite.PlayAnimation( "xp_5" );
				//BasePivotY = 0.05f;
				break;
		}

		switch ( tier_color )
		{
			case 1:
				Sprite.Tint = new Color( 0.2f, 0.2f, 1f );
				break;
			case 2:
				Sprite.Tint = new Color( 1f, 0.2f, 0.2f );
				break;
			case 3:
				Sprite.Tint = new Color( 1f, 1f, 0.2f );
				break;
			case 4:
				Sprite.Tint = new Color( 0.2f, 1f, 0.3f );
				break;
			case 5:
			default:
				Sprite.Tint = new Color( 1f, 1f, 1f ) * 2f;
				break;
		}
	}

	public void Magnetize( Player player )
	{
		PlayerMagnetized = player;
		IsMagnetized = true;
		MagnetizeTime = 0f;
	}
}