things/items/Coin.cs

A game Item subclass representing a collectible coin. It manages value, scale, tint, physics behaviors like rotation, bounce, magnetization, combining with other coins, and awarding XP to players on pickup.

Networking
using System;
using System.Numerics;
using Sandbox;
using Sandbox.Diagnostics;
using Sandbox.Utility;

public class Coin : Item
{
	[Property, Hide] public int Value { get; private set; }

	public bool HasBeenUsed { get; set; }

	private float _personalRotSpeed;
	private float _personalBounceSpeed;

	public override bool DontDisappear => true;

	//public override Vector3 SpawnScale => new Vector3( 0.75f );
	public override bool UseSpawnScale => false;

	protected override float AttractRangeFactor => 1f;
	protected override float AttractStrengthFactor => 1f;

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

		ShouldCheckBounds = true;
		PushStrength = 300f;

		BaseZPos = 8f;
		WorldPosition = WorldPosition.WithZ( BaseZPos );

		RefreshTint( Value );

		if ( IsProxy )
			return;

		Deceleration = 0.92f;

		_personalRotSpeed = Game.Random.Float( -400f, -600f );
		_personalBounceSpeed = Game.Random.Float( 10f, 20f );
	}

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

		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.Text( $"{Value} - scale: {WorldScale}", new global::Transform( WorldPosition ) );

		//if ( Manager.Instance.IsGameOver )
		//	return;

		if ( IsProxy )
			return;

		LocalRotation = new Angles( 0f, LocalRotation.Yaw() + Time.Delta * _personalRotSpeed, 0f );

		if ( !IsInTheAir )
			WorldPosition = WorldPosition.WithZ( BaseZPos + 3f + Utils.FastSin( Time.Now * _personalBounceSpeed ) * 4f );
	}

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

		if ( HasBeenUsed )
			return;

		if ( other is Player player )
		{
			if ( CantBeCollected )
				return;

			if ( !player.IsDead )
			{
				if ( Manager.Instance.IsCommunismActive )
					Manager.Instance.AddCommunismXp( Value, XpSource.Coin, spawnFloater: true, playSfx: true );
				else
					player.AddXpRpc( Value, XpSource.Coin );

				HasBeenUsed = true;

				player.ScaleHeightRpc( amount: Utils.Map( Value, 1, 10, 1.2f, 1.5f ), time: Utils.Map( Value, 1, 10, 0.03f, 0.08f ) * Game.Random.Float( 0.95f, 1.05f ) );

				Remove();
			}
		}
		else if ( other is Coin coin )
		{
			if ( IsInTheAir || (coin.IsMagnetized && !IsMagnetized) )
				return;

			var newValue = Value + coin.Value;
			SetValue( newValue );

			var sphereCollider = Collider as SphereCollider;
			Radius = sphereCollider.Radius * WorldScale.x;

			RefreshTint( newValue );
			//SS2Game.CreateParticle( "particles/gameplay/coincombine/coincombine.vpcf", Position + Vector3.Up + Value * 7.5f );

			// todo: bounce scale

			coin.HasBeenUsed = true;
			coin.Remove();
		}
	}

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

		var scale = 0.75f;
		if ( value <= 3 )
			scale = Utils.Map( value, 1, 3, 0.75f, 0.9f );
		else if ( value <= 7 )
			scale = Utils.Map( value, 4, 7, 0.9f, 1.1f );
		else if ( value <= 12 )
			scale = Utils.Map( value, 8, 12, 1.1f, 1.15f );
		else if ( value <= 18 )
			scale = Utils.Map( value, 13, 18, 1.15f, 1.2f );
		else if ( value <= 24 )
			scale = Utils.Map( value, 19, 24, 1.2f, 1.25f );
		else if ( value <= 30 )
			scale = Utils.Map( value, 25, 30, 1.25f, 1.3f );
		else if ( value <= 35 )
			scale = Utils.Map( value, 31, 35, 1.3f, 1.325f );
		else if ( value <= 40 )
			scale = Utils.Map( value, 36, 40, 1.325f, 1.35f );
		else
			scale = Utils.Map( value, 41, 100, 1.35f, 1.4f );

		WorldScale = new Vector3( scale );
		BaseZPos = Utils.Map( scale, 0.75f, 1.4f, 8.5f, 15f );
	}

	[Rpc.Broadcast]
	public void RefreshTint( int value )
	{
		Color color;
		if ( value <= 3 )
			color = Color.Lerp( new Color( 0.3f, 0.3f, 1f ), new Color( 0f, 0f, 1f ), Utils.Map( value, 1, 3, 0f, 1f, EasingType.SineOut ) );
		else if ( value <= 7 )
			color = Color.Lerp( new Color( 1f, 0f, 1f ), new Color( 1f, 0.5f, 0.5f ), Utils.Map( value, 4, 7, 0f, 1f ) );
		else if ( value <= 12 )
			color = Color.Lerp( new Color( 1f, 0.3f, 0.3f ), new Color( 1f, 0f, 0f ), Utils.Map( value, 8, 12, 0f, 1f ) );
		else if ( value <= 18 )
			color = Color.Lerp( new Color( 1f, 0.2f, 0.1f ), new Color( 1f, 1f, 0.6f ), Utils.Map( value, 13, 18, 0f, 1f ) );
		else if ( value <= 24 )
			color = Color.Lerp( new Color( 1f, 1f, 0.4f ), new Color( 1f, 1f, 0f ), Utils.Map( value, 19, 24, 0f, 1f ) );
		else if ( value <= 30 )
			color = Color.Lerp( new Color( 0.8f, 1f, 0.2f ), new Color( 0.5f, 1f, 0.5f ), Utils.Map( value, 25, 30, 0f, 1f ) );
		else if ( value <= 35 )
			color = Color.Lerp( new Color( 0.4f, 1f, 0.4f ), new Color( 0f, 1f, 0f ), Utils.Map( value, 31, 35, 0f, 1f ) );
		else if ( value <= 40 )
			color = Color.Lerp( new Color( 0.2f, 0.95f, 0.2f ), new Color( 0.8f, 0.8f, 0.8f ), Utils.Map( value, 36, 40, 0f, 1f ) );
		else
			color = Color.Lerp( new Color( 0.83f, 0.83f, 0.83f ), new Color( 1f, 1f, 1f ), Utils.Map( value, 41, 100, 0f, 1f ) );

		ModelRenderer.Tint = color.WithAlpha( ModelRenderer.Tint.a );
	}

	protected override void UpdateMagnetize()
	{
		if ( !IsMagnetized )
			return;

		if ( MagnetizeTime > MAGNETIZE_DURATION || PlayerMagnetized == null || !PlayerMagnetized.IsValid || PlayerMagnetized.IsDead )
		{
			StopMagnetizeRpc();
		}
		else
		{
			if ( (PlayerMagnetized.Position2D - Position2D).LengthSquared > Manager.TOUCH_DIST_REQUIRED_SQR )
			{
				var strength = 500f;

				if ( PlayerMagnetized.GetSyncStat( PlayerStat.CurseXpRepel ) > 0f && (PlayerMagnetized.Position2D - Position2D).LengthSquared < 200f * 200f )
					strength = -200f;

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