things/items/Item.cs

Item entity class for the game, inherits Thing. Manages rendering, lifetime, fading, flying trajectories, magnetize behavior toward players, attraction forces, collision response and removal effects.

Networking
using System;
using System.Numerics;
using Sandbox;

public class Item : Thing
{
	[Property] public ModelRenderer ModelRenderer { get; set; }
	[Property] public HighlightOutline HighlightOutline { get; set; }
	public bool IsVisible { get; private set; }

	public float Lifetime { get; set; }

	public float BaseZPos { get; set; }

	private float _flyTimer;
	private float _flyTime;
	private Vector2 _flyStartPos;
	private Vector2 _flyTargetPos;
	private float _flyHeight;
	public bool FlyHalfway { get; private set; }
	public Player FlyTargetPlayer { get; private set; }

	private bool _isFalling;

	public bool DidFly { get; set; }

	public int NumBounces { get; set; }

	public virtual bool DontDisappear => false;
	private TimeSince _timeSinceFlash;
	public virtual float BlinkTimeRemainingStart => 5f;

	private bool _hasFinishedFadingIn;
	public const float FADE_IN_TIME = 0.06f;

	public virtual bool CantBeCollected => IsInTheAir && NumBounces == 0;

	protected virtual float AttractRangeFactor => 0f;
	protected virtual float AttractStrengthFactor => 0f;

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

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

		ModelRenderer.Tint = Color.White.WithAlpha( 0f );

		if ( IsProxy )
			return;
	}

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

		if ( HighlightOutline == null )
		{
			HighlightOutline = Components.Create<HighlightOutline>();
			HighlightOutline.Width = 0.2f;
			HighlightOutline.Color = new Color( 1f, 1f, 1f, 0.3f );
			HighlightOutline.Enabled = false;
		}

		if ( IsProxy )
			return;

		CollideWithTags.Add( "player" );
		CollideWithTags.Add( "enemy" );
		CollideWithTags.Add( "item" );
		CollideWithTags.Add( "obstacle" );
	}

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

		if ( !_hasFinishedFadingIn )
		{
			if ( TimeSinceSpawn < FADE_IN_TIME )
			{
				ModelRenderer.Tint = ModelRenderer.Tint.WithAlpha( Utils.Map( TimeSinceSpawn, 0f, FADE_IN_TIME, 0f, 1f ) );
			}
			else
			{
				ModelRenderer.Tint = ModelRenderer.Tint.WithAlpha( 1f );
				_hasFinishedFadingIn = true;
			}
		}

		if ( !DontDisappear )
		{
			if ( TimeSinceSpawn > Lifetime - BlinkTimeRemainingStart )
			{
				float delay = Utils.Map( TimeSinceSpawn, Lifetime - BlinkTimeRemainingStart, Lifetime, 0.125f, 0.025f, EasingType.QuadIn );
				if ( _timeSinceFlash > delay )
				{
					SetVisible( !IsVisible );
					_timeSinceFlash = 0f;
				}

				//if ( TimeSinceSpawn > Lifetime )
				//	GameObject.Clone( "prefabs/effects/cloud.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( WorldPosition.WithZ( 10f ) ) } );
			}
		}

		if ( IsProxy )
			return;

		UpdateMagnetize();

		if ( IsInTheAir )
		{
			_flyTimer += Time.Delta;
			if ( _flyTimer > _flyTime )
			{
				IsInTheAir = false;
				var pos = FlyTargetPlayer.IsValid() ? FlyTargetPlayer.Position2D : _flyTargetPos;
				WorldPosition = new Vector3( pos.x, pos.y, BaseZPos );

				if ( NumBounces == 0 || (NumBounces < 3 && Game.Random.Float( 0f, 1f ) < 0.4f) && !FlyTargetPlayer.IsValid() )
				{
					Fly(
						Position2D + (_flyTargetPos - _flyStartPos) * Game.Random.Float( 0.2f, 0.4f ),
						_flyTime * Game.Random.Float( 0.2f, 0.35f ),
						_flyHeight * Game.Random.Float( 0.1f, 0.35f ) * (_isFalling ? 0.25f : 1f)
					);

					NumBounces++;

					// todo: sfx
					//if ( NumBounces < 2 )
					//	Manager.Instance.PlaySfxNearbyRpc( "plop", Position2D, pitch: Game.Random.Float( 1.3f, 1.4f ) * Utils.Map( NumBounces, 0, 3, 1f, 0.8f ), volume: Utils.Map( NumBounces, 0, 3, 1f, 0.5f ), maxDist: 200f );
				}

				FlyTargetPlayer = null;
			}
			else
			{
				Vector2 targetPos = FlyTargetPlayer.IsValid() ? FlyTargetPlayer.Position2D : _flyTargetPos;

				float progress = Utils.Map( _flyTimer, 0f, _flyTime, 0f, 1f );
				var pos = Vector2.Lerp( _flyStartPos, targetPos, progress );

				var zPos = _isFalling
					? BaseZPos + Utils.Map( progress, 0f, 1f, 1f, 0f, EasingType.SineIn ) * _flyHeight
					: BaseZPos + Utils.MapReturn( progress, 0f, 1f, 0f, 1f, EasingType.QuadOut ) * _flyHeight;

				WorldPosition = new Vector3( pos.x, pos.y, zPos );

				float flyProgress = _isFalling
					? Utils.Map( progress, 0f, 1f, 0f, 1f )
					: Utils.MapReturn( progress, 0f, 1f, 1f, 0.5f );

				FlyHalfway = _flyTimer > _flyTime * 0.5f;
			}
		}
		else
		{
			if ( AttractRangeFactor > 0f && AttractStrengthFactor > 0f )
			{
				foreach ( var player in Manager.Instance.AlivePlayers )
				{
					var dist_sqr = (Position2D - player.Position2D).LengthSquared;
					var req_dist_sqr = MathF.Pow( player.GetSyncStat( PlayerStat.CoinAttractRange ) * AttractRangeFactor, 2f );

					if ( dist_sqr < req_dist_sqr )
					{
						if ( (player.Position2D - Position2D).LengthSquared > Manager.TOUCH_DIST_REQUIRED_SQR )
						{
							var strength = player.GetSyncStat( PlayerStat.CoinAttractStrength );

							if ( player.GetSyncStat( PlayerStat.CurseXpRepel ) > 0f && this is Coin )
								strength *= -0.5f;

							Velocity += (player.Position2D - Position2D).Normal * Utils.Map( dist_sqr, req_dist_sqr, 0f, 0f, 1f, EasingType.Linear ) * AttractStrengthFactor * strength * Time.Delta * Manager.Instance.GlobalMovespeedModifier;
						}
					}
				}
			}

			if ( Manager.Instance.IsWindActive )
				Velocity += Manager.Instance.GlobalWindForce * 0.25f * Time.Delta;

			WorldPosition += (Vector3)Velocity * Time.Delta;
			Velocity *= Math.Max( 1f - Time.Delta * Deceleration * Manager.Instance.GlobalFrictionModifier, 0f );
		}

		if ( !DontDisappear && TimeSinceSpawn > Lifetime )
		{
			Remove();
		}
	}

	[Rpc.Broadcast]
	public virtual void MagnetizeRpc( Player player )
	{
		HighlightOutline.Enabled = true;

		if ( IsProxy )
			return;

		Magnetize( player );
	}

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

	[Rpc.Broadcast]
	public virtual void StopMagnetizeRpc()
	{
		HighlightOutline.Enabled = false;

		if ( IsProxy )
			return;

		IsMagnetized = false;
		PlayerMagnetized = null;
	}

	protected virtual 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 )
			{
				Velocity += (PlayerMagnetized.Position2D - Position2D).Normal * 500f * Utils.Map( MagnetizeTime, 0f, MAGNETIZE_DURATION, 1f, 0f, EasingType.QuadIn ) * Time.Delta;
			}
		}
	}

	public virtual void SetVisible( bool visible )
	{
		IsVisible = visible;
		ModelRenderer.Enabled = visible;
	}

	public void Fly( Vector2 targetPos, float time, float height, bool isFalling = false )
	{
		targetPos = Manager.Instance.ClampPosToBounds( targetPos );

		IsInTheAir = true;
		_flyStartPos = Position2D;
		_flyTargetPos = targetPos;
		_flyTime = time;
		_flyHeight = height;
		_flyTimer = 0f;
		DidFly = true;
		_isFalling = isFalling;
		FlyTargetPlayer = null;

		if ( _isFalling )
		{
			WorldPosition = new Vector3( Position2D.x, Position2D.y, height );
		}
	}

	public void FlyToPlayer( Player targetPlayer, float time, float height, bool isFalling = false )
	{
		Fly( Position2D, time, height, isFalling );
		FlyTargetPlayer = targetPlayer;
	}

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

		if ( other is Enemy enemy )
		{
			if ( !Position2D.Equals( enemy.Position2D ) )
				Velocity += (Position2D - enemy.Position2D).Normal * Math.Min( 300f, enemy.PushStrength ) * percent * enemy.SpawnProgress * dt; // *  (enemy.Weight / Weight)
		}
		else if ( other is Item item )
		{
			if ( IsInTheAir )
				return;

			if ( !Position2D.Equals( other.Position2D ) )
				Velocity += (Position2D - other.Position2D).Normal * other.PushStrength * percent * (other.Weight / Weight) * dt;
		}
		else
		{
			if ( IsInTheAir )
				return;

			// obstacle
			if ( !Position2D.Equals( other.Position2D ) )
				Velocity += (Position2D - other.Position2D).Normal * other.PushStrength * percent * (other.Weight / Weight) * dt;

			var maxVel = 175f;
			if ( Velocity.LengthSquared > maxVel * maxVel )
				Velocity = Velocity.Normal * maxVel;
		}
	}

	public virtual void Remove()
	{
		Manager.Instance.SpawnCloudRpc( WorldPosition.WithZ( Game.Random.Float( 10f, 13f ) ), velocity: Vector2.Zero, deceleration: 4f, bright: true );

		GameObject.Destroy();
	}
}