HookProjectile.cs

Projectile component for a hook gun. It moves from a spawn origin along Velocity, traces for impacts up to MaxRange, latches to world surfaces (ignoring the firing player, weapon, and dead-tagged objects), notifies the OwnerGun on attach or miss, and destroys itself on miss.

Networking
using Sandbox;
using System;

/// <summary>Travels from the hook gun, latches on world surfaces, then reports attach/miss to the owner.</summary>
public sealed class HookProjectile : Component
{
	public Vector3 Velocity { get; set; }
	public GameObject WeaponObject { get; set; }
	public HookGun OwnerGun { get; set; }
	public Vector3 SpawnOrigin { get; set; }
	public float MaxRange { get; set; } = 2500f;

	private bool _latched;
	private bool _notified;

	protected override void OnUpdate()
	{
		if ( _latched )
			return;

		if ( (WorldPosition - SpawnOrigin).Length >= MaxRange )
		{
			NotifyMissed();
			GameObject.Destroy();
			return;
		}

		Vector3 frameMovement = Velocity * Time.Delta;
		Vector3 nextPosition = WorldPosition + frameMovement;

		var trace = Scene.Trace.Ray( WorldPosition, nextPosition )
			.Size( 1f )
			.UseHitboxes()
			.WithoutTags( "dead" )
			.IgnoreGameObject( GameObject )
			.IgnoreGameObject( WeaponObject );

		if ( Player.CurrentPlayer.IsValid() )
			trace = trace.IgnoreGameObjectHierarchy( Player.CurrentPlayer.GameObject );

		var result = trace.Run();

		if ( result.Hit )
		{
			HandleImpact( result );
			return;
		}

		WorldPosition = nextPosition;
	}

	private void HandleImpact( SceneTraceResult trace )
	{
		var health = trace.GameObject?.Components.Get<HealthComponent>( FindMode.InSelf );
		if ( health.IsValid() )
		{
			NotifyMissed();
			GameObject.Destroy();
			return;
		}

		_latched = true;
		WorldPosition = trace.HitPosition;
		Velocity = Vector3.Zero;
		OwnerGun?.OnAttached( trace.HitPosition );
	}

	protected override void OnDestroy()
	{
		if ( !_latched )
			NotifyMissed();
	}

	private void NotifyMissed()
	{
		if ( _notified )
			return;

		_notified = true;
		OwnerGun?.OnHookMissed();
	}
}