Homing enemy projectile class for a spitter. Extends SpitterProjectile, tracks a target player, updates homing steering, applies per-type stats (turn speed, acceleration, friction, damage, lifetime), handles collisions with other homing projectiles and occasional target reacquire checks.
using System;
using Sandbox;
public class SpitterProjectileHoming : SpitterProjectile
{
public Unit TargetUnit { get; set; }
private TimeSince _timeSinceCheckTarget;
private float _checkTargetDelay;
private float CHECK_TARGET_TIME_MIN = 0.6f;
private float CHECK_TARGET_TIME_MAX = 1.2f;
private float _personalTurnSpeed;
private float _personalAcceleration;
private float _personalFriction;
public override float RequiredPlayerCollisionPercent => 0.6f;
private float _hitStopTimer;
public override bool ShouldSpin => false;
protected override void OnStart()
{
base.OnStart();
Renderer.SceneModel.CurrentSequence.Name = "skull_idle_anim";
Renderer.SceneModel.PlaybackRate = Game.Random.Float( 1.5f, 2.2f );
if ( IsProxy )
return;
_velocityScale = 1f;
_groundHeight = 4f;
CollideWithTags.Add( "enemy_projectile_homing" );
_zPosEasingType = EasingType.ExpoIn;
}
public override void SetProjectileType( EnemyProjectileType projectileType )
{
base.SetProjectileType( projectileType );
switch ( projectileType )
{
case EnemyProjectileType.Normal:
default:
_personalTurnSpeed = Game.Random.Float( 18f, 22f );
_personalAcceleration = Game.Random.Float( 160f, 220f );
_personalFriction = Game.Random.Float( 0.9f, 1.25f );
HitForce = 320f;
Damage = Utils.Select( Manager.Instance.Difficulty, 10f, 12f, 12f );
Lifetime = 6f;
break;
case EnemyProjectileType.Acid:
_personalTurnSpeed = Game.Random.Float( 23f, 26f );
_personalAcceleration = Game.Random.Float( 160f, 220f );
_personalFriction = Game.Random.Float( 0.9f, 0.95f );
HitForce = -100f;
Damage = Utils.Select( Manager.Instance.Difficulty, 6f, 7f, 8f );
Lifetime = 4.5f;
break;
case EnemyProjectileType.Fire:
_personalTurnSpeed = Game.Random.Float( 12f, 22f );
_personalAcceleration = Game.Random.Float( 150f, 180f );
_personalFriction = Game.Random.Float( 1.3f, 1.35f );
HitForce = 220f;
Damage = Utils.Select( Manager.Instance.Difficulty, 2f, 3f, 4f );
Lifetime = 7.5f;
break;
case EnemyProjectileType.Freeze:
_personalTurnSpeed = Game.Random.Float( 6f, 8f );
_personalAcceleration = Game.Random.Float( 160f, 220f );
_personalFriction = Game.Random.Float( 0.6f, 0.7f );
HitForce = 200f;
Damage = Utils.Select( Manager.Instance.Difficulty, 5f, 6f, 7f );
Lifetime = 9f;
break;
case EnemyProjectileType.Poison:
_personalTurnSpeed = Game.Random.Float( 18f, 20f );
_personalAcceleration = Game.Random.Float( 160f, 220f );
_personalFriction = Game.Random.Float( 0.5f, 0.6f );
HitForce = 60f;
Damage = 4f;
Lifetime = 5.5f;
break;
case EnemyProjectileType.Curse:
_personalTurnSpeed = Game.Random.Float( 6f, 8f );
_personalAcceleration = Game.Random.Float( 150f, 170f );
_personalFriction = Game.Random.Float( 0.6f, 0.7f );
HitForce = 50f;
Damage = Utils.Select( Manager.Instance.Difficulty, 3f, 3f, 3f );
Lifetime = 4.75f;
break;
}
Lifetime *= Utils.Select( Manager.Instance.Difficulty, 0.8f, 1f, 1.8f );
}
protected override void OnUpdate()
{
base.OnUpdate();
if ( IsProxy )
return;
if ( _hitStopTimer > 0f )
{
_hitStopTimer -= Time.Delta;
return;
}
if ( TargetUnit != null )
{
float targetAngle = Rotation.LookAt( TargetUnit.Position2D - Position2D ).Yaw();
WorldRotation = Rotation.Lerp( WorldRotation, Rotation.FromYaw( targetAngle ), Utils.Map( TimeSinceSpawn, 0f, Lifetime, _personalTurnSpeed, 0f ) * Time.Delta );
}
Velocity += (Vector2)WorldRotation.Forward * _personalAcceleration * Time.Delta * Utils.Map( TimeSinceSpawn, 0f, Lifetime, 1f, 2.5f );
if ( Manager.Instance.IsWindActive )
Velocity += Manager.Instance.GlobalWindForce * Time.Delta;
// todo: needs a max speed? - can get way too fast if you teleport away from it as it homes
Velocity *= Math.Max( 1f - Time.Delta * _personalFriction, 0f );
//WorldRotation = Rotation.LookAt( Velocity, Vector3.Up );
if ( _timeSinceCheckTarget > _checkTargetDelay )
CheckForClosestTarget();
}
protected override void HandleRotation()
{
}
public void CheckForClosestTarget()
{
TargetUnit = Manager.Instance.GetClosestPlayer( Position2D );
_timeSinceCheckTarget = 0f;
_checkTargetDelay = Game.Random.Float( CHECK_TARGET_TIME_MIN, CHECK_TARGET_TIME_MAX );
}
private TimeSince _timeSinceBounce;
public override void Colliding( Thing other, float percent, float dt )
{
base.Colliding( other, percent, dt );
if ( _timeSinceBounce < 0.1f )
return;
if ( other is SpitterProjectileHoming homingProjectile )
{
if ( !Position2D.Equals( other.Position2D ) )
{
Velocity *= 0.33f;
Velocity += (Position2D - other.Position2D).Normal * Game.Random.Float( 100f, 200f );
//Velocity += (Position2D - other.Position2D).Normal * 1000f * percent * dt;
_timeSinceBounce = 0f;
_hitStopTimer = 0.05f;
}
}
}
}