swb_base/Weapon.Shoot.cs
using SWB.Base.Particles;
using SWB.Shared;
using System;

namespace SWB.Base;

public partial class Weapon
{
	public static readonly string[] BulletTraceIgnoreTags =
	{
		TagsHelper.Trigger,
		TagsHelper.PlayerClip,
		TagsHelper.PassBullets,
		TagsHelper.ViewModel,
		TagsHelper.Sky
	};

	public static readonly string[] TuckingTraceIgnoreTags =
	[
		..BulletTraceIgnoreTags,
		TagsHelper.Player,
		TagsHelper.DeadPlayer
	];

	/// <summary>
	/// Checks if the weapon can do the provided attack
	/// </summary>
	/// <param name="shootInfo">Attack information</param>
	/// <param name="lastAttackTime">Time since this attack</param>
	/// <param name="inputButton">The input button for this attack</param>
	/// <returns></returns>
	public virtual bool CanShoot( ShootInfo shootInfo, TimeSince lastAttackTime, string inputButton )
	{
		if ( (IsReloading && !ShellReloading) || (IsReloading && ShellReloading && !ShellReloadingShootCancel) || InBoltBack ) return false;
		if ( shootInfo is null || !Owner.IsValid() || (!Owner.IsBot && !Input.Down( inputButton )) || ((IsRunning || TimeSinceRunning < 0.1f) && Secondary is null) ) return false;
		if ( !HasAmmo() )
		{
			if ( Input.Pressed( inputButton ) )
			{
				// Check for auto reloading
				if ( Settings.AutoReload && Owner.AmmoCount( shootInfo.AmmoType ) > 0 && lastAttackTime > GetRealRPM( shootInfo.RPM ) )
				{
					TimeSincePrimaryShoot = 999;
					TimeSinceSecondaryShoot = 999;

					if ( ShellReloading )
						OnShellReload();
					else
						Reload();

					return false;
				}

				// Dry fire
				if ( shootInfo.DryShootSound is not null )
					PlaySound( shootInfo.DryShootSound );
			}

			return false;
		}

		if ( shootInfo.FiringType == FiringType.semi && !Owner.IsBot && !Input.Pressed( inputButton ) ) return false;
		if ( shootInfo.FiringType == FiringType.burst )
		{
			if ( burstCount > 2 ) return false;

			if ( (Owner.IsBot || Input.Down( inputButton )) && lastAttackTime > GetRealRPM( shootInfo.RPM ) )
			{
				burstCount++;
				return true;
			}

			return false;
		}
		;

		if ( shootInfo.RPM <= 0 ) return true;

		return lastAttackTime > GetRealRPM( shootInfo.RPM );
	}

	/// <summary>
	/// Checks if weapon can do the primary attack
	/// </summary>
	public virtual bool CanPrimaryShoot()
	{
		return CanShoot( Primary, TimeSincePrimaryShoot, InputButtonHelper.PrimaryAttack );
	}

	/// <summary>
	/// Checks if weapon can do the secondary attack
	/// </summary>
	public virtual bool CanSecondaryShoot()
	{
		return CanShoot( Secondary, TimeSinceSecondaryShoot, InputButtonHelper.SecondaryAttack );
	}

	public virtual void Shoot( ShootInfo shootInfo, bool isPrimary )
	{
		// Ammo
		if ( shootInfo.InfiniteAmmo != InfiniteAmmoType.clip )
			shootInfo.Ammo -= 1;

		// Animations
		var shootAnim = GetShootAnimation( shootInfo );
		if ( ViewModelRenderer is not null && !string.IsNullOrEmpty( shootAnim ) )
			ViewModelRenderer.Set( shootAnim, true );

		// Sound
		if ( shootInfo.ShootSound is not null )
			PlaySound( shootInfo.ShootSound );

		// Particles
		HandleShootEffects( isPrimary );

		if ( !Owner.IsBot )
		{
			// Barrel smoke
			barrelHeat += 1;

			// Recoil
			Owner.ApplyEyeAnglesOffset( GetRecoilAngles( shootInfo ) );

			// Screenshake
			if ( shootInfo.ScreenShake is not null )
				Owner.ShakeScreen( shootInfo.ScreenShake );

			// UI
			BroadcastUIEvent( "shoot", GetRealRPM( shootInfo.RPM ) );
		}

		// Bullet
		for ( int i = 0; i < shootInfo.Bullets; i++ )
		{
			var realSpread = GetRealSpread( shootInfo.Spread );
			var spreadOffset = shootInfo.BulletType.GetRandomSpread( realSpread );
			shootInfo?.BulletType?.Shoot( this, isPrimary, spreadOffset );
		}
	}

	/// <summary> A single bullet trace from start to end with a certain radius.</summary>
	public static SceneTraceResult TraceBullet( GameObject toIgnoreGO, Vector3 start, Vector3 end, float radius = 2.0f, string[] ignoreTags = null )
	{
		// TODO: find another solution when water becomes more available
		// var startsInWater = SurfaceUtil.IsPointWater( start );
		// if ( startsInWater )
		//	 withoutTags.Add( TagsHelper.Water );

		var tr = Game.ActiveScene.Trace.Ray( start, end )
				.UseHitboxes()
				.WithoutTags( ignoreTags ?? BulletTraceIgnoreTags )
				.Size( radius )
				.IgnoreGameObjectHierarchy( toIgnoreGO )
				.Run();

		// Log.Info( tr.GameObject );

		return tr;
	}

	/// <summary> A single bullet trace from start to end with a certain radius.</summary>
	public virtual SceneTraceResult TraceBullet( Vector3 start, Vector3 end, float radius = 2.0f, string[] ignoreTags = null )
	{
		return TraceBullet( Owner.GameObject, start, end, radius, ignoreTags );
	}

	[Rpc.Broadcast( NetFlags.Unreliable )]
	public virtual void HandleShootEffects( bool isPrimary )
	{
		if ( !IsValid || Owner is null || Application.IsDedicatedServer ) return;

		// Player
		Owner.TriggerAnimation( Shared.Animations.Attack );

		// Weapon
		var shootInfo = GetShootInfo( isPrimary );
		if ( shootInfo is null ) return;

		// Bullet eject
		if ( shootInfo.BulletEjectParticle is not null )
		{
			if ( !BoltBack )
			{
				if ( !ShellReloading || (ShellReloading && ShellEjectDelay == 0) )
				{
					CreateBulletEjectParticle( shootInfo.BulletEjectParticle, "ejection_point" );
				}
				else
				{
					var delayedEject = async () =>
					{
						await GameTask.DelaySeconds( ShellEjectDelay );
						if ( !IsValid ) return;
						CreateBulletEjectParticle( shootInfo.BulletEjectParticle, "ejection_point" );
					};
					delayedEject();
				}
			}
			else if ( shootInfo.Ammo > 0 )
			{
				AsyncBoltBack( GetRealRPM( shootInfo.RPM ) );
			}
		}

		var muzzleObj = GetMuzzleObject();
		if ( muzzleObj is null ) return;

		var muzzleScale = CanSeeViewModel ? shootInfo.VMParticleScale : shootInfo.WMMuzzleParticleScale;

		// Muzzle flash
		if ( shootInfo.MuzzleFlashParticle is not null )
			CreateParticle( shootInfo.MuzzleFlashParticle, muzzleObj, muzzleScale );

		// Barrel smoke
		if ( !IsProxy && !Owner.IsBot && shootInfo.BarrelSmokeParticle is not null && barrelHeat >= shootInfo.ClipSize * 0.75 )
			CreateParticle( shootInfo.BarrelSmokeParticle, muzzleObj, muzzleScale );
	}

	/// <summary>Create a bullet impact effect</summary>
	public static GameObject CreateBulletImpact( SceneTraceResult tr )
	{
		return CreateBulletImpact( tr.HitPosition, tr.Normal, tr.Surface?.SoundCollection.Bullet, tr.Surface?.PrefabCollection.BulletImpact );
	}

	/// <summary>Create a bullet impact effect</summary>
	public static GameObject CreateBulletImpact( Vector3 pos, Vector3 normal, SoundEvent sound, GameObject particles )
	{
		// Sound
		SoundHandle soundHandle = null;

		if ( sound is not null )
		{
			sound.Distance = 10000;
			soundHandle = Sound.Play( sound );
		}

		soundHandle ??= Sound.Play( "impact-bullet-generic" );
		soundHandle.Position = pos;

		// Decal & Particles
		if ( !particles.IsValid() ) return null;

		var cloneConfig = new CloneConfig()
		{
			Name = "bullet_decal",
			StartEnabled = true,
			Transform = new()
			{
				Position = pos,
				Rotation = Rotation.LookAt( -normal ),
			},
			//Parent = tr.GameObject,
		};
		var decalGO = particles.Clone( cloneConfig );
		decalGO.NetworkMode = NetworkMode.Never;
		decalGO.DestroyAsync( 30f );

		WeaponParticleManager.Instance?.AddDecal( decalGO );

		return decalGO;
	}

	/// <summary>Create a bullet eject particle (always world)</summary>
	public virtual GameObject CreateBulletEjectParticle( GameObject particle, string attachment, Action<GameObject> OnParticleCreated = null )
	{
		var effectRenderer = GetEffectRenderer();
		if ( effectRenderer is null || effectRenderer.SceneModel is null ) return null;

		var transform = effectRenderer.GetAttachment( attachment );
		if ( !transform.HasValue ) return null;

		// Rotate bullet with attachment yaw
		var pitch = CanSeeViewModel ? ViewModelHandler.WorldRotation.Pitch() : WorldRotation.Pitch();
		var yaw = transform.Value.Rotation.Yaw();
		var newRot = Rotation.From( new Angles( 0, yaw, -pitch ) );
		transform = transform.Value.WithRotation( newRot );

		if ( CanSeeViewModel )
		{
			var viewSpacePos = CameraUtil.ProjectToViewSpace( transform.Value.Position, Owner.ViewModelCamera, Owner.Camera );
			transform = transform.Value.WithPosition( viewSpacePos );
		}

		var go = CreateParticle( particle, null, transform.Value, 1, false, OnParticleCreated );
		WeaponParticleManager.Instance?.AddEject( go );

		// Attach owner
		var ejectParticle = go.GetComponentInChildren<BulletEjectParticle>();
		ejectParticle?.Owner = Owner;

		return go;
	}

	/// <summary>Create a weapon particle</summary>
	public virtual GameObject CreateParticle( GameObject particle, GameObject parent, float scale, Action<GameObject> OnParticleCreated = null )
	{
		return CreateParticle( particle, parent, new Transform(), scale, OnParticleCreated );
	}

	/// <summary>Create a weapon particle</summary>
	public virtual GameObject CreateParticle( GameObject particle, Transform transform, float scale, Action<GameObject> OnParticleCreated = null )
	{
		return CreateParticle( particle, null, transform, scale, OnParticleCreated );
	}

	/// <summary>Create a weapon particle</summary>
	public virtual GameObject CreateParticle( GameObject particle, GameObject parent, Transform transform, float scale, Action<GameObject> OnParticleCreated = null )
	{
		return CreateParticle( particle, parent, transform, scale, CanSeeViewModel, OnParticleCreated );
	}

	public virtual GameObject CreateParticle( GameObject particle, GameObject parent, Transform transform, float scale, bool forViewModel, Action<GameObject> OnParticleCreated = null )
	{
		var go = particle.Clone( transform.WithScale( scale ), parent );

		if ( forViewModel )
			go.Tags.Add( TagsHelper.ViewModel );

		if ( OnParticleCreated is not null )
		{
			var p = go.GetComponentInChildren<ParticleEffect>();
			p.OnParticleCreated += ( p ) =>
			{
				OnParticleCreated.Invoke( go );
			};
		}

		return go;
	}
}