BulletHomingDetector.cs

Component attached to a bullet that detects nearby valid targets and tells the Bullet to home in. It listens for trigger enter/exit to track potential targets, checks distance and timing conditions each update, then calls Bullet.ApplyHoming when a closest target is chosen.

NetworkingFile Access
using System;
using Sandbox;

public sealed class BulletHomingDetector : Component, Component.ITriggerListener
{
	[Property] public SphereCollider SphereCollider { get; set; }
	public Bullet Bullet { get; set; }

	public TimeSince TimeSinceHome { get; set; }
	public float HomingDelay { get; set; }

	private List<Thing> _targets = new();
	public float Radius { get; set; }

	public bool CanTargetPlayers { get; set; } // todo: 

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

		TimeSinceHome = 0f;
		HomingDelay = 0.17f;
	}

	public void Refresh()
	{
		Bullet.HasHomed = false;
		TimeSinceHome = 0f;
	}

	protected override void OnUpdate()
	{
		//Gizmo.Draw.Color = _targets.Count == 0 ? Color.White.WithAlpha( 0.5f ) : (HasHomed || TimeSinceHome < HomingDelay ? Color.Red.WithAlpha( 0.5f ) : new Color( 0f, 0f, 1f, 0.9f ));
		//Gizmo.Draw.LineSphere( WorldPosition, Radius );

		if ( Bullet.HasHomed || TimeSinceHome < HomingDelay || _targets.Count == 0 )
			return;

		if ( Bullet.Stats[BulletStat.ArcHeight] > 0f )
		{
			var lifetimeProgress = Bullet.TimeSinceSpawn / Bullet.Stats[BulletStat.Lifetime];
			if ( lifetimeProgress < 0.5f || MathF.Abs( Bullet.WorldPosition.z - Bullet.BaseZPos ) > 15f )
				return;
		}

		float closestDistSqr = float.MaxValue;
		Thing closestTarget = null;

		foreach ( var target in _targets )
		{
			if ( !target.IsValid() )
				continue;

			var distSqr = (target.Position2D - Bullet.Position2D).LengthSquared;

			if ( distSqr > MathF.Pow( Radius, 2f ) )
				continue;

			if ( distSqr < closestDistSqr )
			{
				closestDistSqr = distSqr;
				closestTarget = target;
			}
		}

		if ( closestTarget.IsValid() )
			Home( (closestTarget.Position2D - Bullet.Position2D).Normal );
	}

	void ITriggerListener.OnTriggerEnter( Collider collider )
	{
		if ( !collider.IsTrigger )
			return;

		if ( collider.Tags.Has( "enemy" ) )
		{
			var enemy = collider.GetComponent<Enemy>();
			if( enemy.IsValid() )
			{
				_targets.Add( collider.GetComponent<Thing>() );
			}
		}
	}

	void ITriggerListener.OnTriggerExit( Collider collider )
	{
		if ( !collider.IsTrigger )
			return;

		if ( collider.Tags.Has( "enemy" ) )
		{
			_targets.Remove( collider.GetComponent<Thing>() );
		}
	}

	public void Home(Vector2 dir)
	{
		Bullet.ApplyHoming( dir );
		Bullet.HasHomed = true;
		TimeSinceHome = 0f;
	}
}