Weapons/RangedWeapon.cs
using Opium;

public partial class RangedWeapon : BaseWeapon
{
	[Property, Category( "Ranged" )] public int BulletCount { get; set; } = 1;
	[Property, Category( "Ranged" )] public float BulletSpread { get; set; } = 0.1f;
	[Property, Category( "Ranged" )] public float MaxRange { get; set; } = 4096f;
	[Property, Category( "Ranged" )] public float BulletSize { get; set; } = 1f;

	//
	[Property, Category( "FX" )] public GameObject MuzzleFlashPrefab { get; set; }
	[Property, Category( "FX" )] public GameObject EjectPrefab { get; set; }
	[Property, Category( "FX" )] public SoundEvent FireSound { get; set; }

	[Property] public CameraEffect ShootCameraEffect { get; set; }
	[Property] public CameraEffect AimingCameraEffect { get; set; }

	[Property] public int CurrentAmmo { get; set; } = 2;
	[Property] public int MaximumAmmo { get; set; } = 2;

	/// <summary>
	/// Runs a trace with all the data we have supplied it, and returns the result
	/// </summary>
	/// <returns></returns>
	protected virtual IEnumerable<SceneTraceResult> GetShootTrace()
	{
		for ( int i = 0; i < BulletCount; i++ )
		{
			var cameraOrigin = Actor.CameraObject.Transform;

			var forward = cameraOrigin.Rotation.Forward;
			forward += (Vector3.Random + Vector3.Random + Vector3.Random + Vector3.Random) * BulletSpread * 0.25f;
			forward = forward.Normal;

			var ray = new Ray( cameraOrigin.Position, forward );

			var tr = Scene.Trace.Ray( ray, MaxRange )
				.IgnoreGameObjectHierarchy( GameObject.Root )
				.Size( BulletSize )
				.UseHitboxes()
				.Run();

			yield return tr;
		}
	}

	void DryFire()
	{
		// TODO - animation
		// TODO - effects
	}

	public bool IsAiming { get; set; }

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

		if ( Input.Down( "Attack2" ) )
		{
			IsAiming = true;

			if ( !AimingCameraEffect.Enabled && Actor is Opium.PlayerController player )
			{
				AimingCameraEffect.Player = player;
				AimingCameraEffect.Enabled = true;
			}
		}
		else
		{
			IsAiming = false;
		}
	}


	public override void Shoot()
	{
		if ( CurrentAmmo <= 0 )
		{
			DryFire();
			return;
		}

		base.Shoot();

		foreach ( var tr in GetShootTrace() )
		{
			// Inflict damage on whatever we find.
			var damageInfo = Opium.DamageInfo.Bullet( BaseDamage, Actor.GameObject, GameObject, tr.Hitbox );
			CalculateDamage( damageInfo );

			tr.GameObject.TakeDamage( damageInfo );

			var rb = tr.GameObject.Components.Get<Rigidbody>( FindMode.EnabledInSelfAndChildren );
			if ( rb is not null )
			{
				rb.ApplyImpulseAt( tr.EndPosition, tr.Normal * -10000f );
			}

			var skinnedModelRenderer = tr.GameObject.Components.Get<SkinnedModelRenderer>();
			if ( skinnedModelRenderer is not null && tr.Hitbox is not null )
			{
				CreateImpactEffects( skinnedModelRenderer.GetBoneObject( tr.Hitbox.Bone ), GetSurfaceFromTrace( tr ), tr.EndPosition, tr.Normal );
			}
			else
				CreateImpactEffects( tr.GameObject, GetSurfaceFromTrace( tr ), tr.EndPosition, tr.Normal );
		}

		EventListener?.Invoke( "shoot" );

		var gameObject = Actor.CameraObject ?? GameObject;

		gameObject.PlaySound( FireSound, true );

		CurrentAmmo--;

		Actor.TriggerEvent( "ammo_loss", CurrentAmmo );

		if ( ShootCameraEffect is not null && Actor is Opium.PlayerController player )
		{
			ShootCameraEffect.Player = player;
			ShootCameraEffect.Enabled = true;
		}
	}
}