things/items/Bomb.cs

Item class for a bomb pickup. It handles spawning, visual flashing before detonation, optional stickiness to nearby enemies, physics tweaks, and triggers an explosion RPC when its lifetime expires.

NetworkingFile Access
using System;
using Sandbox;

public class Bomb : Item
{
	[Property] public Material FlashMaterial { get; set; }

	private float _rotateTimeOffset;
	private float _personalRotateSpeed;

	private const float EXPLOSION_RADIUS = 110f;

	public override Vector3 SpawnScale => new Vector3( 0.8f );

	public override bool DontDisappear => true;

	private bool _isFlashing;
	private TimeSince _timeSinceFlash;

	public virtual float FlashTimeRemainingStart => 2f;
	private float _flashTimeOffset;

	public Player Player { get; set; }

	public float Damage { get; set; } = 40f;

	public float StickyStrength { get; set; }
	private Enemy _closestEnemy;
	private const float STICKY_RANGE = 180f;
	private TimeSince _timeSinceGetClosestUnit;
	private const float CHECK_CLOSEST_UNIT_DELAY = 0.2f;

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

		Lifetime = 7.5f;
		ShouldCheckBounds = true;
		PushStrength = 500f;

		BaseZPos = -5f;
		WorldPosition = WorldPosition.WithZ( BaseZPos );

		_flashTimeOffset = Game.Random.Float( 0f, 0.2f );

		if ( IsProxy )
			return;

		Deceleration = StickyStrength > 0f
			? Utils.Map( StickyStrength, 1000f, 4000f, 1.2f, 1.8f )
			: 0.9f;

		_rotateTimeOffset = Game.Random.Float( 0f, 10f );
		_personalRotateSpeed = Game.Random.Float( 4f, 8f );
	}

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

		if ( Manager.Instance.IsGameOver )
			return;

		//Gizmo.Draw.Color = Color.White.WithAlpha(0.2f);
		//Gizmo.Draw.LineSphere( WorldPosition.WithZ(1f), 105f, 32 );

		if ( TimeSinceSpawn > Lifetime - FlashTimeRemainingStart - _flashTimeOffset )
		{
			float delay = Utils.Map( TimeSinceSpawn, Lifetime - FlashTimeRemainingStart - _flashTimeOffset, Lifetime, 0.125f, 0.025f, EasingType.QuadIn );
			if ( _timeSinceFlash > delay )
			{
				SetFlashing( !_isFlashing );
				_timeSinceFlash = 0f;

				//Manager.Instance.SpawnRing( Position2D, 105f, Color.Red, 0.5f );
			}
		}

		if ( IsProxy )
			return;

		LocalRotation = Rotation.From( new Angles( 0f, -90f, Utils.FastSin( _rotateTimeOffset + Time.Now * _personalRotateSpeed ) * Utils.Map( TimeSinceSpawn, 0f, Lifetime, 2f, 7f, EasingType.QuadIn ) ) );

		var scaleAmount = Utils.FastSin( Time.Now * 16f ) * Utils.Map( TimeSinceSpawn, 0f, Lifetime, 0f, 0.2f, EasingType.QuadIn );
		LocalScale = new Vector3( 1f + scaleAmount, 1f + scaleAmount, 1f - scaleAmount );

		if ( !IsInTheAir )
		{
			float bounceAmount = Utils.Map( TimeSinceSpawn, 0f, Lifetime, 0f, 3f, EasingType.QuadIn );
			WorldPosition = WorldPosition.WithZ( BaseZPos + bounceAmount + Utils.MapReturn( Utils.FastSin( _rotateTimeOffset + Time.Now * _personalRotateSpeed ), -1f, 1f, -1f, 1f, EasingType.Linear ) * bounceAmount );
		}

		HandleStickiness();

		if ( TimeSinceSpawn > Lifetime )
		{
			Explode();
		}
	}
	
	void HandleStickiness()
	{
		//Gizmo.Draw.Color = Color.Red.WithAlpha( 0.3f );
		//Gizmo.Draw.LineSphere( Position2D, STICKY_RANGE );

		if ( StickyStrength > 0f && TimeSinceSpawn > 0.25f )
		{
			if ( _timeSinceGetClosestUnit > CHECK_CLOSEST_UNIT_DELAY )
			{
				_closestEnemy = Manager.Instance.GetClosestEnemy( Position2D, STICKY_RANGE );
				_timeSinceGetClosestUnit = 0f;
			}

			if ( _closestEnemy.IsValid() )
			{
				//Gizmo.Draw.Color = Color.Cyan;
				//Gizmo.Draw.Line( Position2D, _closestUnit.Position2D );

				var distSqr = (_closestEnemy.Position2D - Position2D).LengthSquared;


				var radiusSqr = MathF.Pow( _closestEnemy.Radius + Radius, 2f );

				var percent = Utils.Map( distSqr, 0f, radiusSqr, 0f, 1f, EasingType.QuadIn ) * Utils.Map( distSqr, 0f, MathF.Pow( STICKY_RANGE, 2f ), 1f, 0f, EasingType.QuadOut );

				Velocity += (_closestEnemy.Position2D - Position2D).Normal * StickyStrength * percent * Time.Delta;
			}
		}
	}

	void SetFlashing( bool flashing )
	{
		_isFlashing = flashing;

		if( flashing )
		{
			ModelRenderer.SetMaterial( FlashMaterial );
		}
		else
		{
			ModelRenderer.ClearMaterialOverrides();
		}
	}

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

		if ( other is Player player )
		{
			if ( IsInTheAir || player.IsDead )
				return;

			if ( !Position2D.Equals( other.Position2D ) )
			{
				Velocity += (Position2D - other.Position2D).Normal * other.PushStrength * percent * (other.Weight / Weight) * dt;
			}
		}
		//else if( StickyStrength > 0f && other is Enemy enemy )
		//{
		//	if ( !Position2D.Equals( enemy.Position2D ) )
		//		Velocity += (Position2D - enemy.Position2D).Normal * Math.Min( 300f, enemy.PushStrength ) * percent * 5f * enemy.SpawnProgress * dt; // *  (enemy.Weight / Weight)
		//}
	}

	void Explode()
	{
		float radius = EXPLOSION_RADIUS * (Player.IsValid() ? Player.Stats[PlayerStat.RadiusMultiplier] * Player.Stats[PlayerStat.ExplosionSizeMultiplier] : 1f);
		float damage = Damage * (Player.IsValid() ? Player.Stats[PlayerStat.ExplosionDamageMultiplier] : 1f);

		var repelRadius = radius * 1.3f;
		var force = 800f;
		Manager.Instance.CreateExplosionRpc( Position2D, radius, Damage, repelRadius, force, Player, enemySource: null, enemyType: EnemyType.None, Color.Red );

		GameObject.Destroy();
	}
}