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