diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs index 1af471f28a..710ee7c7cb 100644 --- a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs @@ -157,7 +157,7 @@ public sealed partial class GunSystem : SharedGunSystem var useKey = gun.UseKey ? EngineKeyFunctions.Use : EngineKeyFunctions.UseSecondary; - if (_inputSystem.CmdStates.GetState(useKey) != BoundKeyState.Down) + if (_inputSystem.CmdStates.GetState(useKey) != BoundKeyState.Down && !gun.BurstActivated) { if (gun.ShotCounter != 0) EntityManager.RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) }); diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.AutoFire.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.AutoFire.cs index 39cd2486ed..e5439cdb06 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.AutoFire.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.AutoFire.cs @@ -1,4 +1,6 @@ +using Content.Shared.Damage; using Content.Shared.Weapons.Ranged.Components; +using Robust.Shared.Map; namespace Content.Server.Weapons.Ranged.Systems; @@ -13,17 +15,28 @@ public sealed partial class GunSystem */ // Automatic firing without stopping if the AutoShootGunComponent component is exist and enabled - var query = EntityQueryEnumerator(); + var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var autoShoot, out var gun)) + while (query.MoveNext(out var uid, out var gun)) { - if (!autoShoot.Enabled) - continue; - if (gun.NextFire > Timing.CurTime) continue; - AttemptShoot(uid, gun); + if (TryComp(uid, out AutoShootGunComponent? autoShoot)) + { + if (!autoShoot.Enabled) + continue; + + AttemptShoot(uid, gun); + } + else if (gun.BurstActivated) + { + var parent = _transform.GetParentUid(uid); + if (HasComp(parent)) + AttemptShoot(parent, uid, gun, gun.ShootCoordinates ?? new EntityCoordinates(uid, gun.DefaultDirection)); + else + AttemptShoot(uid, gun); + } } } } diff --git a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs index b404221abf..98b1d2fe2a 100644 --- a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Audio; @@ -156,6 +157,30 @@ public sealed partial class GunComponent : Component [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] public int ShotsPerBurstModified = 3; + /// + /// How long time must pass between burstfire shots. + /// + [DataField, AutoNetworkedField] + public float BurstCooldown = 0.25f; + + /// + /// The fire rate of the weapon in burst fire mode. + /// + [DataField, AutoNetworkedField] + public float BurstFireRate = 8f; + + /// + /// Whether the burst fire mode has been activated. + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public bool BurstActivated = false; + + /// + /// The burst fire bullet count. + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public int BurstShotsCount = 0; + /// /// Used for tracking semi-auto / burst /// @@ -232,6 +257,12 @@ public sealed partial class GunComponent : Component /// [DataField] public bool ClumsyProof = false; + + /// + /// Firing direction for an item not being held (e.g. shuttle cannons, thrown guns still firing). + /// + [DataField] + public Vector2 DefaultDirection = new Vector2(0, -1); } [Flags] diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs index 794237b145..9bd786bbe0 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs @@ -218,7 +218,7 @@ public abstract partial class SharedGunSystem : EntitySystem /// public void AttemptShoot(EntityUid gunUid, GunComponent gun) { - var coordinates = new EntityCoordinates(gunUid, new Vector2(0, -1)); + var coordinates = new EntityCoordinates(gunUid, gun.DefaultDirection); gun.ShootCoordinates = coordinates; AttemptShoot(gunUid, gunUid, gun); gun.ShotCounter = 0; @@ -258,6 +258,9 @@ public abstract partial class SharedGunSystem : EntitySystem var fireRate = TimeSpan.FromSeconds(1f / gun.FireRateModified); + if (gun.SelectedMode == SelectiveFire.Burst || gun.BurstActivated) + fireRate = TimeSpan.FromSeconds(1f / gun.BurstFireRate); + // First shot // Previously we checked shotcounter but in some cases all the bullets got dumped at once // curTime - fireRate is insufficient because if you time it just right you can get a 3rd shot out slightly quicker. @@ -278,18 +281,24 @@ public abstract partial class SharedGunSystem : EntitySystem // Get how many shots we're actually allowed to make, due to clip size or otherwise. // Don't do this in the loop so we still reset NextFire. - switch (gun.SelectedMode) + if (!gun.BurstActivated) { - case SelectiveFire.SemiAuto: - shots = Math.Min(shots, 1 - gun.ShotCounter); - break; - case SelectiveFire.Burst: - shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter); - break; - case SelectiveFire.FullAuto: - break; - default: - throw new ArgumentOutOfRangeException($"No implemented shooting behavior for {gun.SelectedMode}!"); + switch (gun.SelectedMode) + { + case SelectiveFire.SemiAuto: + shots = Math.Min(shots, 1 - gun.ShotCounter); + break; + case SelectiveFire.Burst: + shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter); + break; + case SelectiveFire.FullAuto: + break; + default: + throw new ArgumentOutOfRangeException($"No implemented shooting behavior for {gun.SelectedMode}!"); + } + } else + { + shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter); } var attemptEv = new AttemptShootEvent(user, null); @@ -301,7 +310,8 @@ public abstract partial class SharedGunSystem : EntitySystem { PopupSystem.PopupClient(attemptEv.Message, gunUid, user); } - + gun.BurstActivated = false; + gun.BurstShotsCount = 0; gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds)); return; } @@ -328,6 +338,10 @@ public abstract partial class SharedGunSystem : EntitySystem var emptyGunShotEvent = new OnEmptyGunShotEvent(); RaiseLocalEvent(gunUid, ref emptyGunShotEvent); + gun.BurstActivated = false; + gun.BurstShotsCount = 0; + gun.NextFire += TimeSpan.FromSeconds(gun.BurstCooldown); + // Play empty gun sounds if relevant // If they're firing an existing clip then don't play anything. if (shots > 0) @@ -347,6 +361,22 @@ public abstract partial class SharedGunSystem : EntitySystem return; } + // Handle burstfire + if (gun.SelectedMode == SelectiveFire.Burst) + { + gun.BurstActivated = true; + } + if (gun.BurstActivated) + { + gun.BurstShotsCount += shots; + if (gun.BurstShotsCount >= gun.ShotsPerBurstModified) + { + gun.NextFire += TimeSpan.FromSeconds(gun.BurstCooldown); + gun.BurstActivated = false; + gun.BurstShotsCount = 0; + } + } + // Shoot confirmed - sounds also played here in case it's invalid (e.g. cartridge already spent). Shoot(gunUid, gun, ev.Ammo, fromCoordinates, toCoordinates.Value, out var userImpulse, user, throwItems: attemptEv.ThrowItems); var shotEv = new GunShotEvent(user, ev.Ammo); diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml index a22be1da04..5140a358e1 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml @@ -19,6 +19,7 @@ minAngle: 2 maxAngle: 16 fireRate: 8 + burstFireRate: 8 angleIncrease: 3 angleDecay: 16 selectedMode: FullAuto @@ -27,6 +28,7 @@ - FullAuto soundGunshot: path: /Audio/Weapons/Guns/Gunshots/smg.ogg + defaultDirection: 1, 0 - type: ChamberMagazineAmmoProvider soundRack: path: /Audio/Weapons/Guns/Cock/smg_cock.ogg @@ -140,12 +142,15 @@ - type: Gun minAngle: 21 maxAngle: 32 - fireRate: 6 - selectedMode: FullAuto + fireRate: 12 + burstFireRate: 12 + selectedMode: Burst soundGunshot: path: /Audio/Weapons/Guns/Gunshots/atreides.ogg availableModes: - - FullAuto + - Burst + shotsPerBurst: 3 + burstCooldown: 0.25 - type: ItemSlots slots: gun_magazine: @@ -250,6 +255,8 @@ angleDecay: 6 selectedMode: FullAuto shotsPerBurst: 5 + burstCooldown: 0.2 + burstFireRate: 7 availableModes: - SemiAuto - Burst