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.
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();
}
}