things/enemyProjectiles/SpitterProjectileHoming.cs

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.

Networking
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;
			}
		}
	}
}