Weapons/HornetGun/HornetProjectile.cs
/// <summary>
/// An Hornet projectile. Will follow a target.
/// </summary>
public partial class HornetProjectile : Projectile, Component.ITriggerListener, Component.IDamageable
{
	// Not sure why this exists?
	public enum FireMode
	{
		Normal,
		Turbo
	}

	[Property] public SoundEvent LoopingSound { get; set; }
	[Property] public SoundEvent DieSound { get; set; }
	[Property] public SoundEvent MunchSound { get; set; }
	[Property] public float Speed { get; set; } = 512;
	[Property] public float SpeedTurbo { get; set; } = 1024;
	[RequireComponent] public Explosive Explosive { get; set; }
	public GameObject Weapon { get; set; }
	public FireMode Mode { get; set; }
	Player TargetPlayer { get; set; }
	TimeSince timeSinceThink;

	SoundHandle LoopingSoundHandle;

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

		if ( LoopingSound.IsValid() )
			LoopingSoundHandle = Sound.Play( LoopingSound, WorldPosition );

		if ( IsProxy )
			return;

		Rigidbody.Gravity = false;
		timeSinceThink = 0;
	}

	protected override void OnDisabled()
	{
		LoopingSoundHandle?.Stop();
	}

	protected override void OnUpdate()
	{
		if ( LoopingSoundHandle is not null )
			LoopingSoundHandle.Position = WorldPosition;
	}

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

		if ( !Networking.IsHost ) return;

		if ( Mode == FireMode.Turbo ) return;

		if ( TargetPlayer.IsValid() )
		{
			UpdateWithTarget( TargetPlayer.EyeTransform.Position, 256f );
			return;
		}

		if ( timeSinceThink > 0.3f )
		{
			float yaw = Random.Shared.Float( -15f, 15f );
			float pitch = Random.Shared.Float( -2f, 2f );

			WorldRotation *= Rotation.From( pitch, yaw, 0 );
			Rigidbody.Velocity = WorldRotation.Forward * Rigidbody.Velocity.Length;
			timeSinceThink = 0;
		}
	}

	protected override void OnHit( Collision collision = default )
	{
		var player = collision.Other.GameObject.GetComponentInParent<Player>();
		if ( player.IsValid() )
		{
			if ( Instigator.IsValid() && player.PlayerData == Instigator )
				return;

			NotifyHit( player, collision.Contact.Point );

			var dmg = new DamageInfo( 10, Instigator?.Player?.GameObject, GameObject );

			var snd = GameObject.PlaySound( MunchSound );
			snd.Pitch = Random.Shared.Float( 0.8f, 1.2f );

			player.OnDamage( dmg );
			GameObject.Destroy();
			return;
		}

		if ( Mode == FireMode.Turbo )
		{
			GameObject.PlaySound( DieSound );
			GameObject.Destroy();
			return;
		}

		var reflect = Vector3.Reflect( -collision.Contact.Speed, collision.Contact.Normal ).Normal;
		UpdateDirection( reflect, GetSpeed() );
	}

	/// <summary>
	/// This is meant to be called continuously, updates the target, rotates slowly to it and moves at a set speed.
	/// </summary>
	/// <param name="target"></param>
	/// <param name="speed"></param>
	internal void UpdateWithTarget( Vector3 target, float speed )
	{
		var direction = (target - WorldPosition).Normal;
		var targetRotation = Rotation.LookAt( direction, Vector3.Up );

		float alignDelta = Vector3.Dot( WorldTransform.Forward, direction );
		if ( alignDelta <= 0 )
		{
			alignDelta = 0.25f;
		}

		Rigidbody.Velocity = (WorldTransform.Forward + direction).Normal * speed * alignDelta;
		WorldRotation = Rotation.LookAt( Rigidbody.Velocity, Vector3.Up );
	}


	public float GetSpeed() => Mode switch
	{
		FireMode.Turbo => SpeedTurbo,
		_ => Speed,
	};

	/// <summary>
	/// Called when a gameobject enters the trigger.
	/// </summary>
	void ITriggerListener.OnTriggerEnter( GameObject other )
	{
		if ( !Networking.IsHost ) return;
		if ( GameObject.IsDestroyed ) return;

		var player = other.GetComponent<Player>();
		if ( !player.IsValid() )
			return;

		if ( Instigator.IsValid() && player.PlayerData == Instigator ) return;

		TargetPlayer = player;

		// Reset the explosive timer
		Explosive.TimeSinceActive = 0;
	}

	public void OnDamage( in DamageInfo damage )
	{
		GameObject.PlaySound( DieSound );
		GameObject.Destroy();
	}
}