Objects/Pellet.cs
using System;
using Clover.Carriable;
using Clover.Interfaces;

namespace Clover.Objects;

public class Pellet : Component, Component.ICollisionListener, Component.ITriggerListener
{
	[Property] public Collider Hitbox { get; set; }
	
	[Property] public SoundEvent HitSound { get; set; }

	public float Speed;

	public Vector3 StartPosition;

	private TimeSince _shot;

	public PelletGun PelletGun { get; set; }

	public Action<GameObject, PelletGun> OnHit;
	public Action OnTimeout;

	protected override void OnStart()
	{
		_shot = 0;
	}

	void ICollisionListener.OnCollisionStart( Collision collision )
	{
		Log.Info( "Pellet collided with " + collision.Other.GameObject.Name );
		OnHitObject( collision.Other.GameObject );
	}
	
	void ICollisionListener.OnCollisionStop( CollisionStop collision )
	{
		Log.Info( "Pellet stopped colliding with " + collision.Other.GameObject.Name );
	}

	void ITriggerListener.OnTriggerEnter( Collider other )
	{
		Log.Info( "Pellet entered trigger with " + other.GameObject.Name );
		OnHitObject( other.GameObject );
	}
	
	void ITriggerListener.OnTriggerExit( Collider other )
	{
		Log.Info( "Pellet exited trigger with " + other.GameObject.Name );
	}

	void ICollisionListener.OnCollisionUpdate( Collision collision )
	{
		Log.Info( "Pellet collided with " + collision.Other.GameObject.Name );
	}
	
	private void OnHitObject( GameObject otherGameObject )
	{
		
		OnHit?.Invoke( otherGameObject, PelletGun );
		
		Log.Info( $"Pellet hit {otherGameObject.Name}" );
		
		if ( otherGameObject.Components.TryGet<IShootable>( out var shootable, FindMode.EverythingInSelfAndAncestors ) )
		{
			shootable.OnShot( this );
		}

		PlayHitSound();
		DestroyGameObject();
	}

	private void PlayHitSound()
	{
		SoundEx.Play( HitSound, WorldPosition );
	}

	protected override void OnFixedUpdate()
	{
		WorldPosition += WorldRotation.Forward * Speed * Time.Delta;
		
		var trace = Scene.Trace.Ray( WorldPosition, WorldPosition + WorldRotation.Forward * 8f )
			.IgnoreGameObject( GameObject )
			.IgnoreGameObjectHierarchy( PelletGun.Player.GameObject )
			.Run();

		if ( trace.Hit && trace.GameObject.IsValid() )
		{
			OnHitObject( trace.GameObject );
			return;
		}

		if ( _shot > 6 )
		{
			Log.Info( "Pellet timed out" );
			OnTimeout?.Invoke();
			DestroyGameObject();
		}
		else if ( WorldPosition.Distance( StartPosition ) > 1024 )
		{
			Log.Info( "Pellet went too far" );
			OnTimeout?.Invoke();
			DestroyGameObject();
		}
	}
}