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;
}
}