things/Boomerang.cs

A game Thing that implements a boomerang projectile. It handles movement homing back to the shooting player, collisions with enemies, players, other boomerangs, shields and obstacles, applies damage/forces, plays SFX/particles via Manager RPCs, and notifies a boomerang perk on destroy.

NetworkingNative Interop
using System;
using Sandbox;

public class Boomerang : Thing
{
	[Property] public ModelRenderer Model { get; set; }

	public Player Shooter { get; set; }

	public Dictionary<Thing, float> HitThings { get; private set; }
	private const float HIT_COOLDOWN = 0.65f;

	public float Damage { get; set; }

	private float _personalFriction;
	private float _personalSpeedMin;
	private float _personalSpeedMax;
	private float _personalRangeMax;
	private float _personalRotateSpeed;
	private float _hitStopTimer;

	private bool _sizeDirty;

	public Vector2 Dir { get; set; }

	public bool FromBoomerangPerk { get; set; }
	public bool FromBoomerangKillEnemyPerk { get; set; }
	public int NumSelfBounces { get; set; }

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

		Radius = 5f;

		if ( IsProxy )
			return;

		CollideWithTags.Add( "enemy" );
		CollideWithTags.Add( "orbiter_shield_enemy" );
		CollideWithTags.Add( "player" );
		CollideWithTags.Add( "boomerang" );
		CollideWithTags.Add( "obstacle" );

		HitThings = new();

		_personalFriction = 0.3f;
		//_personalSpeedMin = 150f;
		_personalSpeedMin = 700f;
		_personalSpeedMax = 2000f;
		_personalRangeMax = 295f;
		_personalRotateSpeed = Game.Random.Float( 1800f, 2400f );

		_sizeDirty = true;
	}

	void DetermineSize()
	{
		var scale = Damage < 30f
			? Utils.Map( Damage, 0f, 30f, 1.2f, 2.5f, EasingType.QuadOut )
			: Utils.Map( Damage, 30f, 150f, 2.5f, 3.75f, EasingType.QuadIn );

		Radius = 5f * scale;

		WorldScale = new Vector3( scale );
		
		//_pfakeshadow.Set("Size", 9f * Scale);

		_sizeDirty = false;
	}

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

		if ( IsProxy )
			return;

		if( !Shooter.IsValid() || Shooter.IsDying )
		{
			GameObject.Destroy();
			return;
		}

		if( _hitStopTimer > 0f )
		{
			_hitStopTimer -= Time.Delta;
		}
		else
		{
			WorldPosition = WorldPosition + (Vector3)Velocity * Time.Delta;

			//Gizmo.Draw.Color = Color.White;
			//Gizmo.Draw.Line( WorldPosition, WorldPosition + (Vector3)Dir * 150f );

			var distSqr = (Position2D - Shooter.Position2D).LengthSquared;
			var velFactor = Utils.Map( distSqr, 0f, _personalRangeMax * _personalRangeMax, 0f, 1.3f, EasingType.SineIn );

			Dir = Vector2.Lerp( Dir, ((Shooter.Position2D + Shooter.Velocity * velFactor) - Position2D).Normal, Utils.Map( distSqr, 0f, _personalRangeMax * _personalRangeMax, 0f, 10f ) * Time.Delta );
			//if(distSqr > 150f * 150f)
			//	Dir = (Shooter.Position2D - Position2D).Normal;


			//Velocity *= (1f - Time.Delta * _personalFriction);
			//Velocity *= (1f - Time.Delta * Utils.Map( distSqr, 0f, 200f * 200f, _personalFriction, _personalFriction * 10f ));
			Velocity *= (1f - Time.Delta * Utils.Map( distSqr, 0f, _personalRangeMax * _personalRangeMax, _personalFriction, _personalFriction * 10f ));

			if ( distSqr > Manager.TOUCH_DIST_REQUIRED_SQR )
			{
				//var velFactor = Utils.Map( distSqr, 0f, 320f * 320f, 0f, 1.5f );
				//Velocity += ((Shooter.Position2D + Shooter.Velocity * velFactor) - Position2D).Normal * Utils.Map( distSqr, 0f, _personalRangeMax * _personalRangeMax, _personalSpeedMin, _personalSpeedMax ) * Time.Delta;

				Velocity += Dir.Normal * Utils.Map( distSqr, 0f, _personalRangeMax * _personalRangeMax, _personalSpeedMin, _personalSpeedMax ) * Time.Delta;
			}

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

			float MAX_SPEED = 800f;
			if ( Velocity.LengthSquared > MathF.Pow( MAX_SPEED, 2f ) )
				Velocity = Velocity.Normal * MAX_SPEED;

			WorldRotation = Rotation.FromYaw( WorldRotation.Yaw() - _personalRotateSpeed * Time.Delta );
		}

		if ( HitThings.Count > 0 )
		{
			var pair = HitThings.ElementAt( 0 );

			if ( Time.Now > pair.Value + HIT_COOLDOWN )
				HitThings.Remove( pair.Key );
		}

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

		if ( HitThings.ContainsKey( other ) )
			return;

		if ( other is Enemy enemy )
		{
			if ( enemy.IsSpawning && enemy.SpawnProgress < 0.7f )
				return;

			Vector2 dir = Velocity.Normal;

			float damage = Damage;

			var shouldFlinch =  damage < enemy.MaxHealth * 0.05f ? false : true;
			var force = dir * damage * 5f;

			enemy.DamageRpc( Damage, Shooter, DamageType.Boomerang, WorldPosition, force, isCrit: false, shouldFlinch );

			Velocity += (Position2D - other.Position2D).Normal * Game.Random.Float( 100f, 400f );

			HitThing( other );
		}
		else if ( other is Boomerang boomerang )
		{
			if ( !Position2D.Equals( other.Position2D ) )
			{
				//Velocity += (Position2D - other.Position2D).Normal * Game.Random.Float( 2000f, 5000f ) * dt;
				//Velocity += (Position2D - other.Position2D).Normal * Game.Random.Float( 200f, 500f );

				Velocity *= 0.33f;
				Velocity += (Position2D - other.Position2D).Normal * Game.Random.Float( 200f, 500f );

				HitThing( other );

				Manager.Instance.PlaySfxNearbyRpc( "bullet.impact", Position2D, pitch: Game.Random.Float( 1.7f, 1.8f ), volume: 0.65f, maxDist: 220f );
			}
		}
		else if ( other is Player player )
		{
			if ( !(Shooter.IsValid() && Shooter == player && percent > 0.35f && TimeSinceSpawn > 0.5f) )
				return;

			if ( player.IsProxy )
				return;

			if ( player.IsInvincible )
				return;

			if ( player.Stats[PlayerStat.BoomerangSelfDamagePercent] > 0f && (Position2D - other.Position2D).LengthSquared > Manager.TOUCH_DIST_REQUIRED_SQR )
			{
				Vector2 dir = Velocity.Normal;
				var hitPos = WorldPosition;
				var damage = Damage * player.Stats[PlayerStat.BoomerangSelfDamagePercent];
				var force = damage * 5f;

				var damageFlags = PlayerDamageFlags.SelfInflicted;
				float damageDone = player.Damage( Damage * player.Stats[PlayerStat.BoomerangSelfDamagePercent], DamageType.Boomerang, hitPos, dir, upwardAmount: 0f, force, ragdollForce: force * 0.01f, enemySource: null, enemyType: EnemyType.None, cantKill: true, damageFlags: damageFlags );

				if ( damageDone <= 0f )
				{
					HitThing( other );
					return;
				}
			}

			if ( player.Stats[PlayerStat.BoomerangBounceSelfNum] > 0 && NumSelfBounces < (int)Shooter.Stats[PlayerStat.BoomerangBounceSelfNum] )
			{
				Velocity *= 0.33f;
				Velocity += (Position2D - other.Position2D).Normal * Game.Random.Float( 400f, 550f );
				NumSelfBounces++;
				HitThing( other );

				Manager.Instance.PlaySfxNearbyRpc( "bullet.impact", player.Position2D, pitch: Game.Random.Float( 1.3f, 1.4f ), volume: 1f, maxDist: 220f );

				player.ScaleHeightRpc( amount: 1.5f, time: Game.Random.Float( 0.05f, 0.06f ) );

				return;
			}

			Manager.Instance.PlaySfxNearbyRpc( "bullet.impact", player.Position2D, pitch: Game.Random.Float( 1.1f, 1.2f ), volume: 1f, maxDist: 220f );

			var duckDir = (Position2D - player.Position2D).Normal;
			player.DodgeDuckRpc( duckDir, time: Game.Random.Float( 0.075f, 0.1f ), shouldFlinch: true );

			GameObject.Destroy();
		}
		else if ( other is OrbiterShieldEnemy orbiterShieldEnemy )
		{
			if ( orbiterShieldEnemy.IsActive )
			{
				orbiterShieldEnemy.Block( Position2D );

				Manager.Instance.SpawnBulletImpactParticlesRpc( WorldPosition.WithZ( 10f ), Vector3.Up, Color.White, 1f );

				GameObject.Destroy();
			}
		}
		else if ( other is Obstacle obstacle )
		{
			if ( !Position2D.Equals( other.Position2D ) )
			{
				Velocity *= 0.33f;
				Velocity += (Position2D - other.Position2D).Normal * Game.Random.Float( 400f, 500f );

				HitThing( other );

				var normal = (Position2D - other.Position2D).Normal;
				Manager.Instance.SpawnBulletImpactParticlesRpc( WorldPosition - (Vector3)normal * Radius, normal, Color.White );

				Manager.Instance.PlaySfxNearbyRpc( "bullet.impact", Position2D, pitch: Game.Random.Float( 1.1f, 1.2f ), volume: 0.85f, maxDist: 220f );
			}
		}
	}

	void HitThing( Thing other )
	{
		HitThings.Add( other, Time.Now );
		_hitStopTimer = 0.075f;
		_personalRotateSpeed = Game.Random.Float( 1200f, 2000f );
	}

	[Rpc.Broadcast]
	public void RemoveRpc()
	{
		if ( IsProxy )
			return;

		GameObject.Destroy();
	}

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

		if ( Shooter.IsValid() )
		{
			if( FromBoomerangPerk )
			{
				var perkType = TypeLibrary.GetType( typeof( PerkBoomerang ) );
				if ( Shooter.HasPerk( perkType ) )
				{
					var boomerangPerk = Shooter.GetPerk( perkType ) as PerkBoomerang;
					if ( boomerangPerk != null )
						boomerangPerk.BoomerangDestroyed();
				}
			}
		}
	}
}