]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add events for GunComponent values, muzzle flashes and cartridge spread (#24077)
authorDrSmugleaf <DrSmugleaf@users.noreply.github.com>
Sun, 28 Jan 2024 23:32:42 +0000 (15:32 -0800)
committerGitHub <noreply@github.com>
Sun, 28 Jan 2024 23:32:42 +0000 (10:32 +1100)
* Add a modifier event for GunComponent values

* Add docs

* Add VV readwrite to modified values

* Add more docs

* More docs

* Add Gun parameter to GunRefreshModifiersEvent

* Add another event for handling cartridge spread

* Fix pneumatic speed

16 files changed:
Content.Client/Weapons/Ranged/GunSpreadOverlay.cs
Content.Client/Weapons/Ranged/Systems/GunSystem.cs
Content.Server/Administration/Components/AdminMinigunComponent.cs [new file with mode: 0644]
Content.Server/Administration/Systems/AdminGunSystem.cs [new file with mode: 0644]
Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs
Content.Server/PneumaticCannon/PneumaticCannonSystem.cs
Content.Server/Weapons/Ranged/Systems/GunSystem.cs
Content.Shared/PneumaticCannon/PneumaticCannonComponent.cs
Content.Shared/Weapons/Ranged/Components/GunComponent.cs
Content.Shared/Weapons/Ranged/Events/GunGetAmmoSpreadEvent.cs [new file with mode: 0644]
Content.Shared/Weapons/Ranged/Events/GunMuzzleFlashAttemptEvent.cs [new file with mode: 0644]
Content.Shared/Weapons/Ranged/Events/GunRefreshModifiersEvent.cs [new file with mode: 0644]
Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs
Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Interactions.cs
Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
Content.Shared/Wieldable/WieldableSystem.cs

index 559c465e6dc8415b76e151ee3c9bdc157b4eba38..84b6ce4048018d985ca82f1bb87f55af2f3e652b 100644 (file)
@@ -56,11 +56,11 @@ public sealed class GunSpreadOverlay : Overlay
             return;
 
         // (☞゚ヮ゚)☞
-        var maxSpread = gun.MaxAngle;
-        var minSpread = gun.MinAngle;
+        var maxSpread = gun.MaxAngleModified;
+        var minSpread = gun.MinAngleModified;
         var timeSinceLastFire = (_timing.CurTime - gun.NextFire).TotalSeconds;
-        var currentAngle = new Angle(MathHelper.Clamp(gun.CurrentAngle.Theta - gun.AngleDecay.Theta * timeSinceLastFire,
-            gun.MinAngle.Theta, gun.MaxAngle.Theta));
+        var currentAngle = new Angle(MathHelper.Clamp(gun.CurrentAngle.Theta - gun.AngleDecayModified.Theta * timeSinceLastFire,
+            gun.MinAngleModified.Theta, gun.MaxAngleModified.Theta));
         var direction = (mousePos.Position - mapPos.Position);
 
         worldHandle.DrawLine(mapPos.Position, mousePos.Position + direction, Color.Orange);
index e4a95ac2c54d12dcfe424313931436d5d4c942ba..ce43057d631022c0d5b85275d15ac7a19f3d96df 100644 (file)
@@ -3,7 +3,6 @@ using Content.Client.Items;
 using Content.Client.Weapons.Ranged.Components;
 using Content.Shared.Camera;
 using Content.Shared.CombatMode;
-using Robust.Shared.Spawners;
 using Content.Shared.Weapons.Ranged;
 using Content.Shared.Weapons.Ranged.Components;
 using Content.Shared.Weapons.Ranged.Events;
@@ -195,7 +194,7 @@ public sealed partial class GunSystem : SharedGunSystem
         {
             if (throwItems)
             {
-                Recoil(user, direction, gun.CameraRecoilScalar);
+                Recoil(user, direction, gun.CameraRecoilScalarModified);
                 if (IsClientSide(ent!.Value))
                     Del(ent.Value);
                 else
@@ -210,8 +209,8 @@ public sealed partial class GunSystem : SharedGunSystem
                     {
                         SetCartridgeSpent(ent!.Value, cartridge, true);
                         MuzzleFlash(gunUid, cartridge, user);
-                        Audio.PlayPredicted(gun.SoundGunshot, gunUid, user);
-                        Recoil(user, direction, gun.CameraRecoilScalar);
+                        Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
+                        Recoil(user, direction, gun.CameraRecoilScalarModified);
                         // TODO: Can't predict entity deletions.
                         //if (cartridge.DeleteOnSpawn)
                         //    Del(cartridge.Owner);
@@ -228,16 +227,16 @@ public sealed partial class GunSystem : SharedGunSystem
                     break;
                 case AmmoComponent newAmmo:
                     MuzzleFlash(gunUid, newAmmo, user);
-                    Audio.PlayPredicted(gun.SoundGunshot, gunUid, user);
-                    Recoil(user, direction, gun.CameraRecoilScalar);
+                    Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
+                    Recoil(user, direction, gun.CameraRecoilScalarModified);
                     if (IsClientSide(ent!.Value))
                         Del(ent.Value);
                     else
                         RemoveShootable(ent.Value);
                     break;
                 case HitscanPrototype:
-                    Audio.PlayPredicted(gun.SoundGunshot, gunUid, user);
-                    Recoil(user, direction, gun.CameraRecoilScalar);
+                    Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
+                    Recoil(user, direction, gun.CameraRecoilScalarModified);
                     break;
             }
         }
diff --git a/Content.Server/Administration/Components/AdminMinigunComponent.cs b/Content.Server/Administration/Components/AdminMinigunComponent.cs
new file mode 100644 (file)
index 0000000..368d3d3
--- /dev/null
@@ -0,0 +1,7 @@
+namespace Content.Server.Administration.Components;
+
+[RegisterComponent]
+public sealed partial class AdminMinigunComponent : Component
+{
+
+}
diff --git a/Content.Server/Administration/Systems/AdminGunSystem.cs b/Content.Server/Administration/Systems/AdminGunSystem.cs
new file mode 100644 (file)
index 0000000..6270481
--- /dev/null
@@ -0,0 +1,17 @@
+using Content.Server.Administration.Components;
+using Content.Shared.Weapons.Ranged.Events;
+
+namespace Content.Server.Administration.Systems;
+
+public sealed class AdminGunSystem : EntitySystem
+{
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<AdminMinigunComponent, GunRefreshModifiersEvent>(OnGunRefreshModifiers);
+    }
+
+    private void OnGunRefreshModifiers(Entity<AdminMinigunComponent> ent, ref GunRefreshModifiersEvent args)
+    {
+        args.FireRate = 15;
+    }
+}
index 7dbbacb34090fcf4ac0a18712da7cd1131957497..296e48274c600c7699d61ede5416af37cb562741 100644 (file)
@@ -12,6 +12,7 @@ using Content.Server.Power.EntitySystems;
 using Content.Server.Stack;
 using Content.Server.Station.Components;
 using Content.Server.Station.Systems;
+using Content.Server.Weapons.Ranged.Systems;
 using Content.Shared.Access;
 using Content.Shared.Access.Components;
 using Content.Shared.Access.Systems;
@@ -52,6 +53,7 @@ public sealed partial class AdminVerbSystem
     [Dependency] private readonly BatterySystem _batterySystem = default!;
     [Dependency] private readonly SharedTransformSystem _xformSystem = default!;
     [Dependency] private readonly MetaDataSystem _metaSystem = default!;
+    [Dependency] private readonly GunSystem _gun = default!;
 
     private void AddTricksVerbs(GetVerbsEvent<Verb> args)
     {
@@ -697,7 +699,8 @@ public sealed partial class AdminVerbSystem
                 Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Weapons/Guns/HMGs/minigun.rsi"), "icon"),
                 Act = () =>
                 {
-                    gun.FireRate = 15;
+                    EnsureComp<AdminMinigunComponent>(args.Target);
+                    _gun.RefreshModifiers((args.Target, gun));
                 },
                 Impact = LogImpact.Medium,
                 Message = Loc.GetString("admin-trick-minigun-fire-description"),
index 9db9aa296d839a4db7fc047cae940a27b9ef22b6..60f0603074d8d2e6ff679dbae47cad35153e8390 100644 (file)
@@ -2,12 +2,14 @@ using Content.Server.Atmos.Components;
 using Content.Server.Atmos.EntitySystems;
 using Content.Server.Storage.EntitySystems;
 using Content.Server.Stunnable;
+using Content.Server.Weapons.Ranged.Systems;
 using Content.Shared.Containers.ItemSlots;
 using Content.Shared.Interaction;
 using Content.Shared.PneumaticCannon;
 using Content.Shared.StatusEffect;
 using Content.Shared.Tools.Components;
 using Content.Shared.Weapons.Ranged.Components;
+using Content.Shared.Weapons.Ranged.Events;
 using Content.Shared.Weapons.Ranged.Systems;
 using Robust.Shared.Containers;
 
@@ -17,6 +19,7 @@ public sealed class PneumaticCannonSystem : SharedPneumaticCannonSystem
 {
     [Dependency] private readonly AtmosphereSystem _atmos = default!;
     [Dependency] private readonly GasTankSystem _gasTank = default!;
+    [Dependency] private readonly GunSystem _gun = default!;
     [Dependency] private readonly StunSystem _stun = default!;
     [Dependency] private readonly ItemSlotsSystem _slots = default!;
 
@@ -27,6 +30,7 @@ public sealed class PneumaticCannonSystem : SharedPneumaticCannonSystem
         SubscribeLocalEvent<PneumaticCannonComponent, InteractUsingEvent>(OnInteractUsing, before: new []{ typeof(StorageSystem) });
         SubscribeLocalEvent<PneumaticCannonComponent, GunShotEvent>(OnShoot);
         SubscribeLocalEvent<PneumaticCannonComponent, ContainerIsInsertingAttemptEvent>(OnContainerInserting);
+        SubscribeLocalEvent<PneumaticCannonComponent, GunRefreshModifiersEvent>(OnGunRefreshModifiers);
     }
 
     private void OnInteractUsing(EntityUid uid, PneumaticCannonComponent component, InteractUsingEvent args)
@@ -47,10 +51,9 @@ public sealed class PneumaticCannonSystem : SharedPneumaticCannonSystem
         Popup.PopupEntity(Loc.GetString("pneumatic-cannon-component-change-power",
             ("power", component.Power.ToString())), uid, args.User);
 
+        component.ProjectileSpeed = GetProjectileSpeedFromPower(component);
         if (TryComp<GunComponent>(uid, out var gun))
-        {
-            gun.ProjectileSpeed = GetProjectileSpeedFromPower(component);
-        }
+            _gun.RefreshModifiers((uid, gun));
 
         args.Handled = true;
     }
@@ -105,6 +108,12 @@ public sealed class PneumaticCannonSystem : SharedPneumaticCannonSystem
         _slots.TryEject(uid, PneumaticCannonComponent.TankSlotId, args.User, out _);
     }
 
+    private void OnGunRefreshModifiers(Entity<PneumaticCannonComponent> ent, ref GunRefreshModifiersEvent args)
+    {
+        if (ent.Comp.ProjectileSpeed is { } speed)
+            args.ProjectileSpeed = speed;
+    }
+
     /// <summary>
     ///     Returns whether the pneumatic cannon has enough gas to shoot an item, as well as the tank itself.
     /// </summary>
index 4e4bdf78b101f4dd9dfa275e88185d2d5a61e4ce..007f30a28454d185827a2eb53e9dedb0d4206cdd 100644 (file)
@@ -10,7 +10,6 @@ using Content.Shared.Damage;
 using Content.Shared.Damage.Systems;
 using Content.Shared.Database;
 using Content.Shared.Effects;
-using Content.Shared.FixedPoint;
 using Content.Shared.Interaction.Components;
 using Content.Shared.Projectiles;
 using Content.Shared.Weapons.Melee;
@@ -19,11 +18,9 @@ using Content.Shared.Weapons.Ranged.Components;
 using Content.Shared.Weapons.Ranged.Events;
 using Content.Shared.Weapons.Ranged.Systems;
 using Content.Shared.Weapons.Reflect;
-using Robust.Server.GameObjects;
 using Robust.Shared.Audio;
 using Robust.Shared.Map;
 using Robust.Shared.Physics;
-using Robust.Shared.Physics.Components;
 using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Utility;
@@ -156,8 +153,11 @@ public sealed partial class GunSystem : SharedGunSystem
                         }
                         if (cartridge.Count > 1)
                         {
-                            var angles = LinearSpread(mapAngle - cartridge.Spread / 2,
-                                mapAngle + cartridge.Spread / 2, cartridge.Count);
+                            var ev = new GunGetAmmoSpreadEvent(cartridge.Spread);
+                            RaiseLocalEvent(gunUid, ref ev);
+
+                            var angles = LinearSpread(mapAngle - ev.Spread / 2,
+                                mapAngle + ev.Spread / 2, cartridge.Count);
 
                             for (var i = 0; i < cartridge.Count; i++)
                             {
@@ -180,7 +180,7 @@ public sealed partial class GunSystem : SharedGunSystem
 
                         SetCartridgeSpent(ent.Value, cartridge, true);
                         MuzzleFlash(gunUid, cartridge, user);
-                        Audio.PlayPredicted(gun.SoundGunshot, gunUid, user);
+                        Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
 
                         if (cartridge.DeleteOnSpawn)
                             Del(ent.Value);
@@ -201,7 +201,7 @@ public sealed partial class GunSystem : SharedGunSystem
                 case AmmoComponent newAmmo:
                     shotProjectiles.Add(ent!.Value);
                     MuzzleFlash(gunUid, newAmmo, user);
-                    Audio.PlayPredicted(gun.SoundGunshot, gunUid, user);
+                    Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
                     ShootOrThrow(ent.Value, mapDirection, gunVelocity, gun, gunUid, user);
                     break;
                 case HitscanPrototype hitscan:
@@ -288,7 +288,7 @@ public sealed partial class GunSystem : SharedGunSystem
                         FireEffects(fromEffect, hitscan.MaxLength, dir.ToAngle(), hitscan);
                     }
 
-                    Audio.PlayPredicted(gun.SoundGunshot, gunUid, user);
+                    Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
                     break;
                 default:
                     throw new ArgumentOutOfRangeException();
@@ -308,11 +308,11 @@ public sealed partial class GunSystem : SharedGunSystem
         {
             RemoveShootable(uid);
             // TODO: Someone can probably yeet this a billion miles so need to pre-validate input somewhere up the call stack.
-            ThrowingSystem.TryThrow(uid, mapDirection, gun.ProjectileSpeed, user);
+            ThrowingSystem.TryThrow(uid, mapDirection, gun.ProjectileSpeedModified, user);
             return;
         }
 
-        ShootProjectile(uid, mapDirection, gunVelocity, gunUid, user, gun.ProjectileSpeed);
+        ShootProjectile(uid, mapDirection, gunVelocity, gunUid, user, gun.ProjectileSpeedModified);
     }
 
     /// <summary>
@@ -337,7 +337,7 @@ public sealed partial class GunSystem : SharedGunSystem
     private Angle GetRecoilAngle(TimeSpan curTime, GunComponent component, Angle direction)
     {
         var timeSinceLastFire = (curTime - component.LastFire).TotalSeconds;
-        var newTheta = MathHelper.Clamp(component.CurrentAngle.Theta + component.AngleIncrease.Theta - component.AngleDecay.Theta * timeSinceLastFire, component.MinAngle.Theta, component.MaxAngle.Theta);
+        var newTheta = MathHelper.Clamp(component.CurrentAngle.Theta + component.AngleIncreaseModified.Theta - component.AngleDecayModified.Theta * timeSinceLastFire, component.MinAngleModified.Theta, component.MaxAngleModified.Theta);
         component.CurrentAngle = new Angle(newTheta);
         component.LastFire = component.NextFire;
 
@@ -345,7 +345,7 @@ public sealed partial class GunSystem : SharedGunSystem
         var random = Random.NextFloat(-0.5f, 0.5f);
         var spread = component.CurrentAngle.Theta * random;
         var angle = new Angle(direction.Theta + component.CurrentAngle.Theta * random);
-        DebugTools.Assert(spread <= component.MaxAngle.Theta);
+        DebugTools.Assert(spread <= component.MaxAngleModified.Theta);
         return angle;
     }
 
index 8c760abe58a295431d057321c40cedf20f2a2f91..f3726c802e882665ec1c53f157a9dfd1423d954c 100644 (file)
@@ -38,6 +38,12 @@ public sealed partial class PneumaticCannonComponent : Component
     [DataField("baseProjectileSpeed")]
     public float BaseProjectileSpeed = 20f;
 
+    /// <summary>
+    ///     The current projectile speed setting.
+    /// </summary>
+    [DataField]
+    public float? ProjectileSpeed;
+
     /// <summary>
     /// If true, will throw ammo rather than shoot it.
     /// </summary>
index 95853bbd2ea8c2d60e58dad725292e9cc64376d6..c3335a1ad061da86bf6d5c71b8fe3775264e4832 100644 (file)
@@ -1,5 +1,7 @@
 using Content.Shared.Damage;
 using Content.Shared.Tag;
+using Content.Shared.Weapons.Ranged.Events;
+using Content.Shared.Weapons.Ranged.Systems;
 using Robust.Shared.Audio;
 using Robust.Shared.GameStates;
 using Robust.Shared.Map;
@@ -8,23 +10,33 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 namespace Content.Shared.Weapons.Ranged.Components;
 
-[RegisterComponent, NetworkedComponent, Virtual]
-[AutoGenerateComponentState]
-public partial class GunComponent : Component
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedGunSystem))]
+public sealed partial class GunComponent : Component
 {
     #region Sound
 
-    [ViewVariables(VVAccess.ReadWrite), DataField("soundGunshot")]
+    /// <summary>
+    /// The base sound to use when the gun is fired.
+    /// </summary>
+    [DataField]
     public SoundSpecifier? SoundGunshot = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/smg.ogg");
 
-    [ViewVariables(VVAccess.ReadWrite), DataField("soundEmpty")]
+    /// <summary>
+    /// The sound to use when the gun is fired.
+    /// <seealso cref="GunRefreshModifiersEvent"/>
+    /// </summary>
+    [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
+    public SoundSpecifier? SoundGunshotModified;
+
+    [DataField]
     public SoundSpecifier? SoundEmpty = new SoundPathSpecifier("/Audio/Weapons/Guns/Empty/empty.ogg");
 
     /// <summary>
     /// Sound played when toggling the <see cref="SelectedMode"/> for this gun.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("soundMode")]
-    public SoundSpecifier? SoundModeToggle = new SoundPathSpecifier("/Audio/Weapons/Guns/Misc/selector.ogg");
+    [DataField]
+    public SoundSpecifier? SoundMode = new SoundPathSpecifier("/Audio/Weapons/Guns/Misc/selector.ogg");
 
     #endregion
 
@@ -32,59 +44,94 @@ public partial class GunComponent : Component
 
     // These values are very small for now until we get a debug overlay and fine tune it
 
+    /// <summary>
+    /// The base scalar value applied to the vector governing camera recoil.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float CameraRecoilScalar = 1f;
+
     /// <summary>
     /// A scalar value applied to the vector governing camera recoil.
     /// If 0, there will be no camera recoil.
+    /// <seealso cref="GunRefreshModifiersEvent"/>
     /// </summary>
-    [DataField("cameraRecoilScalar"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
-    public float CameraRecoilScalar = 1f;
+    [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
+    public float CameraRecoilScalarModified = 1f;
 
     /// <summary>
     /// Last time the gun fired.
     /// Used for recoil purposes.
     /// </summary>
-    [DataField("lastFire")]
+    [DataField]
     public TimeSpan LastFire = TimeSpan.Zero;
 
     /// <summary>
     /// What the current spread is for shooting. This gets changed every time the gun fires.
     /// </summary>
-    [DataField("currentAngle")]
+    [DataField]
     [AutoNetworkedField]
     public Angle CurrentAngle;
 
     /// <summary>
-    /// How much the spread increases every time the gun fires.
+    /// The base value for how much the spread increases every time the gun fires.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("angleIncrease")]
+    [DataField]
     public Angle AngleIncrease = Angle.FromDegrees(0.5);
 
     /// <summary>
-    /// How much the <see cref="CurrentAngle"/> decreases per second.
+    /// How much the spread increases every time the gun fires.
+    /// <seealso cref="GunRefreshModifiersEvent"/>
+    /// </summary>
+    [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
+    public Angle AngleIncreaseModified;
+
+    /// <summary>
+    /// The base value for how much the <see cref="CurrentAngle"/> decreases per second.
     /// </summary>
-    [DataField("angleDecay")]
+    [DataField]
     public Angle AngleDecay = Angle.FromDegrees(4);
 
     /// <summary>
-    /// The maximum angle allowed for <see cref="CurrentAngle"/>
+    /// How much the <see cref="CurrentAngle"/> decreases per second.
+    /// <seealso cref="GunRefreshModifiersEvent"/>
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("maxAngle")]
+    [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
+    public Angle AngleDecayModified;
+
+    /// <summary>
+    /// The base value for the maximum angle allowed for <see cref="CurrentAngle"/>
+    /// </summary>
+    [DataField]
     [AutoNetworkedField]
     public Angle MaxAngle = Angle.FromDegrees(2);
 
     /// <summary>
-    /// The minimum angle allowed for <see cref="CurrentAngle"/>
+    /// The maximum angle allowed for <see cref="CurrentAngle"/>
+    /// <seealso cref="GunRefreshModifiersEvent"/>
+    /// </summary>
+    [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
+    public Angle MaxAngleModified;
+
+    /// <summary>
+    /// The base value for the minimum angle allowed for <see cref="CurrentAngle"/>
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("minAngle")]
+    [DataField]
     [AutoNetworkedField]
     public Angle MinAngle = Angle.FromDegrees(1);
 
+    /// <summary>
+    ///  The minimum angle allowed for <see cref="CurrentAngle"/>.
+    /// <seealso cref="GunRefreshModifiersEvent"/>
+    /// </summary>
+    [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
+    public Angle MinAngleModified;
+
     #endregion
 
     /// <summary>
     /// Whether this gun is shot via the use key or the alt-use key.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("useKey"), AutoNetworkedField]
+    [DataField, AutoNetworkedField]
     public bool UseKey = true;
 
     /// <summary>
@@ -93,6 +140,19 @@ public partial class GunComponent : Component
     [ViewVariables]
     public EntityCoordinates? ShootCoordinates = null;
 
+    /// <summary>
+    ///     The base value for how many shots to fire per burst.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public int ShotsPerBurst = 3;
+
+    /// <summary>
+    ///     How many shots to fire per burst.
+    /// <seealso cref="GunRefreshModifiersEvent"/>
+    /// </summary>
+    [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
+    public int ShotsPerBurstModified = 3;
+
     /// <summary>
     /// Used for tracking semi-auto / burst
     /// </summary>
@@ -101,55 +161,69 @@ public partial class GunComponent : Component
     public int ShotCounter = 0;
 
     /// <summary>
-    /// How many times it shoots per second.
+    /// The base value for how many times it shoots per second.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("fireRate")]
+    [DataField]
     [AutoNetworkedField]
     public float FireRate = 8f;
 
+    /// <summary>
+    /// How many times it shoots per second.
+    /// <seealso cref="GunRefreshModifiersEvent"/>
+    /// </summary>
+    [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
+    public float FireRateModified;
+
     /// <summary>
     /// Starts fire cooldown when equipped if true.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("resetOnHandSelected")]
+    [DataField]
     public bool ResetOnHandSelected = true;
 
     /// <summary>
     /// Type of ammo the gun can work with
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("compatibleAmmo")]
+    [DataField]
     public List<ProtoId<TagPrototype>>? CompatibleAmmo;
 
     /// <summary>
     /// Damage the gun deals when used with wrong ammo
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("damageOnWrongAmmo")]
+    [DataField]
     public DamageSpecifier? DamageOnWrongAmmo = null;
 
     /// <summary>
-    /// How fast the projectile moves.
+    /// The base value for how fast the projectile moves.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("projectileSpeed")]
+    [DataField]
     public float ProjectileSpeed = 25f;
 
+    /// <summary>
+    /// How fast the projectile moves.
+    /// <seealso cref="GunRefreshModifiersEvent"/>
+    /// </summary>
+    [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
+    public float ProjectileSpeedModified;
+
     /// <summary>
     /// When the gun is next available to be shot.
     /// Can be set multiple times in a single tick due to guns firing faster than a single tick time.
     /// </summary>
-    [DataField("nextFire", customTypeSerializer:typeof(TimeOffsetSerializer))]
+    [DataField(customTypeSerializer:typeof(TimeOffsetSerializer))]
     [AutoNetworkedField]
     public TimeSpan NextFire = TimeSpan.Zero;
 
     /// <summary>
     /// What firemodes can be selected.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("availableModes")]
+    [DataField]
     [AutoNetworkedField]
     public SelectiveFire AvailableModes = SelectiveFire.SemiAuto;
 
     /// <summary>
     /// What firemode is currently selected.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("selectedMode")]
+    [DataField]
     [AutoNetworkedField]
     public SelectiveFire SelectedMode = SelectiveFire.SemiAuto;
 
@@ -157,14 +231,14 @@ public partial class GunComponent : Component
     /// Whether or not information about
     /// the gun will be shown on examine.
     /// </summary>
-    [DataField("showExamineText")]
+    [DataField]
     public bool ShowExamineText = true;
 
     /// <summary>
     /// Whether or not someone with the
     /// clumsy trait can shoot this
     /// </summary>
-    [DataField("clumsyProof"), ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public bool ClumsyProof = false;
 }
 
diff --git a/Content.Shared/Weapons/Ranged/Events/GunGetAmmoSpreadEvent.cs b/Content.Shared/Weapons/Ranged/Events/GunGetAmmoSpreadEvent.cs
new file mode 100644 (file)
index 0000000..63419ab
--- /dev/null
@@ -0,0 +1,8 @@
+namespace Content.Shared.Weapons.Ranged.Events;
+
+/// <summary>
+///     Raised directed on the gun entity when ammo is shot to calculate its spread.
+/// </summary>
+/// <param name="Spread">The spread of the ammo, can be changed by handlers.</param>
+[ByRefEvent]
+public record struct GunGetAmmoSpreadEvent(Angle Spread);
diff --git a/Content.Shared/Weapons/Ranged/Events/GunMuzzleFlashAttemptEvent.cs b/Content.Shared/Weapons/Ranged/Events/GunMuzzleFlashAttemptEvent.cs
new file mode 100644 (file)
index 0000000..d6a2475
--- /dev/null
@@ -0,0 +1,8 @@
+namespace Content.Shared.Weapons.Ranged.Events;
+
+/// <summary>
+///     Raised directed on the gun entity when a muzzle flash is about to happen.
+/// </summary>
+/// <param name="Cancelled">If set to true, the muzzle flash will not be shown.</param>
+[ByRefEvent]
+public record struct GunMuzzleFlashAttemptEvent(bool Cancelled);
diff --git a/Content.Shared/Weapons/Ranged/Events/GunRefreshModifiersEvent.cs b/Content.Shared/Weapons/Ranged/Events/GunRefreshModifiersEvent.cs
new file mode 100644 (file)
index 0000000..0ad79bd
--- /dev/null
@@ -0,0 +1,23 @@
+using Content.Shared.Weapons.Ranged.Components;
+using Content.Shared.Weapons.Ranged.Systems;
+using Robust.Shared.Audio;
+
+namespace Content.Shared.Weapons.Ranged.Events;
+
+/// <summary>
+///     Raised directed on the gun entity when <see cref="SharedGunSystem.RefreshModifiers"/>
+///     is called, to update the values of <see cref="GunComponent"/> from other systems.
+/// </summary>
+[ByRefEvent]
+public record struct GunRefreshModifiersEvent(
+    Entity<GunComponent> Gun,
+    SoundSpecifier? SoundGunshot,
+    float CameraRecoilScalar,
+    Angle AngleIncrease,
+    Angle AngleDecay,
+    Angle MaxAngle,
+    Angle MinAngle,
+    int ShotsPerBurst,
+    float FireRate,
+    float ProjectileSpeed
+);
index 501d0069a75b76644c6a9841a445184c344dea56..a8f7ee23956c41f36ac9830b6b90db4e4eb8f909 100644 (file)
@@ -183,10 +183,10 @@ public abstract partial class SharedGunSystem
 
         // Reset shotting for cycling
         if (Resolve(uid, ref gunComp, false) &&
-            gunComp is { FireRate: > 0f } &&
+            gunComp is { FireRateModified: > 0f } &&
             !Paused(uid))
         {
-            gunComp.NextFire = Timing.CurTime + TimeSpan.FromSeconds(1 / gunComp.FireRate);
+            gunComp.NextFire = Timing.CurTime + TimeSpan.FromSeconds(1 / gunComp.FireRateModified);
         }
 
         Dirty(uid, component);
index 852096a386483d30e137a544021cde6d13c5b5da..d47d024de5e1b93d4b49f394e08bfa6004f55ea8 100644 (file)
@@ -19,7 +19,7 @@ public abstract partial class SharedGunSystem
             args.PushMarkup(Loc.GetString("gun-selected-mode-examine", ("color", ModeExamineColor),
                 ("mode", GetLocSelector(component.SelectedMode))));
             args.PushMarkup(Loc.GetString("gun-fire-rate-examine", ("color", FireRateExamineColor),
-                ("fireRate", $"{component.FireRate:0.0}")));
+                ("fireRate", $"{component.FireRateModified:0.0}")));
         }
     }
 
@@ -80,7 +80,7 @@ public abstract partial class SharedGunSystem
                 component.NextFire += cooldown;
         }
 
-        Audio.PlayPredicted(component.SoundModeToggle, uid, user);
+        Audio.PlayPredicted(component.SoundMode, uid, user);
         Popup(Loc.GetString("gun-selected-mode", ("mode", GetLocSelector(fire))), uid, user);
         Dirty(uid, component);
     }
@@ -112,7 +112,7 @@ public abstract partial class SharedGunSystem
 
     private void OnGunSelected(EntityUid uid, GunComponent component, HandSelectedEvent args)
     {
-        var fireDelay = 1f / component.FireRate;
+        var fireDelay = 1f / component.FireRateModified;
         if (fireDelay.Equals(0f))
             return;
 
index 9047ef7a8d10717f80e1f3ad93482d4138c3b026..07af02e509a7335e54cdfd86076f8d1c2fed48f8 100644 (file)
@@ -95,18 +95,19 @@ public abstract partial class SharedGunSystem : EntitySystem
         SubscribeLocalEvent<GunComponent, CycleModeEvent>(OnCycleMode);
         SubscribeLocalEvent<GunComponent, HandSelectedEvent>(OnGunSelected);
         SubscribeLocalEvent<GunComponent, EntityUnpausedEvent>(OnGunUnpaused);
-
-#if DEBUG
         SubscribeLocalEvent<GunComponent, MapInitEvent>(OnMapInit);
     }
 
-    private void OnMapInit(EntityUid uid, GunComponent component, MapInitEvent args)
+    private void OnMapInit(Entity<GunComponent> gun, ref MapInitEvent args)
     {
-        if (component.NextFire > Timing.CurTime)
-            Log.Warning($"Initializing a map that contains an entity that is on cooldown. Entity: {ToPrettyString(uid)}");
+#if DEBUG
+        if (gun.Comp.NextFire > Timing.CurTime)
+            Log.Warning($"Initializing a map that contains an entity that is on cooldown. Entity: {ToPrettyString(gun)}");
 
-        DebugTools.Assert((component.AvailableModes & component.SelectedMode) != 0x0);
+        DebugTools.Assert((gun.Comp.AvailableModes & gun.Comp.SelectedMode) != 0x0);
 #endif
+
+        RefreshModifiers((gun, gun));
     }
 
     private void OnGunMelee(EntityUid uid, GunComponent component, MeleeHitEvent args)
@@ -229,7 +230,7 @@ public abstract partial class SharedGunSystem : EntitySystem
 
     private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
     {
-        if (gun.FireRate <= 0f ||
+        if (gun.FireRateModified <= 0f ||
             !_actionBlockerSystem.CanAttack(user))
             return;
 
@@ -259,7 +260,7 @@ public abstract partial class SharedGunSystem : EntitySystem
         if (gun.NextFire > curTime)
             return;
 
-        var fireRate = TimeSpan.FromSeconds(1f / gun.FireRate);
+        var fireRate = TimeSpan.FromSeconds(1f / gun.FireRateModified);
 
         // First shot
         // Previously we checked shotcounter but in some cases all the bullets got dumped at once
@@ -287,7 +288,7 @@ public abstract partial class SharedGunSystem : EntitySystem
                 shots = Math.Min(shots, 1 - gun.ShotCounter);
                 break;
             case SelectiveFire.Burst:
-                shots = Math.Min(shots, 3 - gun.ShotCounter);
+                shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter);
                 break;
             case SelectiveFire.FullAuto:
                 break;
@@ -468,6 +469,11 @@ public abstract partial class SharedGunSystem : EntitySystem
 
     protected void MuzzleFlash(EntityUid gun, AmmoComponent component, EntityUid? user = null)
     {
+        var attemptEv = new GunMuzzleFlashAttemptEvent();
+        RaiseLocalEvent(gun, ref attemptEv);
+        if (attemptEv.Cancelled)
+            return;
+
         var sprite = component.MuzzleFlash;
 
         if (sprite == null)
@@ -487,6 +493,41 @@ public abstract partial class SharedGunSystem : EntitySystem
         var impulseVector =  shotDirection * impulseStrength;
         Physics.ApplyLinearImpulse(user, -impulseVector, body: userPhysics);
     }
+
+    public void RefreshModifiers(Entity<GunComponent?> gun)
+    {
+        if (!Resolve(gun, ref gun.Comp))
+            return;
+
+        var comp = gun.Comp;
+        var ev = new GunRefreshModifiersEvent(
+            (gun, comp),
+            comp.SoundGunshot,
+            comp.CameraRecoilScalar,
+            comp.AngleIncrease,
+            comp.AngleDecay,
+            comp.MaxAngle,
+            comp.MinAngle,
+            comp.ShotsPerBurst,
+            comp.FireRate,
+            comp.ProjectileSpeed
+        );
+
+        RaiseLocalEvent(gun, ref ev);
+
+        comp.SoundGunshotModified = ev.SoundGunshot;
+        comp.CameraRecoilScalarModified = ev.CameraRecoilScalar;
+        comp.AngleIncreaseModified = ev.AngleIncrease;
+        comp.AngleDecayModified = ev.AngleDecay;
+        comp.MaxAngleModified = ev.MaxAngle;
+        comp.MinAngleModified = ev.MinAngle;
+        comp.ShotsPerBurstModified = ev.ShotsPerBurst;
+        comp.FireRateModified = ev.FireRate;
+        comp.ProjectileSpeedModified = ev.ProjectileSpeed;
+
+        Dirty(gun);
+    }
+
     protected abstract void CreateEffect(EntityUid uid, MuzzleFlashEvent message, EntityUid? user = null);
 
     /// <summary>
index 54b030b9eb21df5bb6fd85e182b92071af82053e..b7529328793a651bc37d1155dcfa192f0e0a3c01 100644 (file)
@@ -11,6 +11,7 @@ using Content.Shared.Weapons.Melee;
 using Content.Shared.Weapons.Melee.Components;
 using Content.Shared.Weapons.Melee.Events;
 using Content.Shared.Weapons.Ranged.Components;
+using Content.Shared.Weapons.Ranged.Events;
 using Content.Shared.Weapons.Ranged.Systems;
 using Content.Shared.Wieldable.Components;
 using Robust.Shared.Audio.Systems;
@@ -27,6 +28,7 @@ public sealed class WieldableSystem : EntitySystem
     [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
     [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
     [Dependency] private readonly UseDelaySystem _delay = default!;
+    [Dependency] private readonly SharedGunSystem _gun = default!;
 
     public override void Initialize()
     {
@@ -42,6 +44,7 @@ public sealed class WieldableSystem : EntitySystem
         SubscribeLocalEvent<GunRequiresWieldComponent, AttemptShootEvent>(OnShootAttempt);
         SubscribeLocalEvent<GunWieldBonusComponent, ItemWieldedEvent>(OnGunWielded);
         SubscribeLocalEvent<GunWieldBonusComponent, ItemUnwieldedEvent>(OnGunUnwielded);
+        SubscribeLocalEvent<GunWieldBonusComponent, GunRefreshModifiersEvent>(OnGunRefreshModifiers);
 
         SubscribeLocalEvent<IncreaseDamageOnWieldComponent, GetMeleeDamageEvent>(OnGetMeleeDamage);
     }
@@ -72,22 +75,22 @@ public sealed class WieldableSystem : EntitySystem
 
     private void OnGunUnwielded(EntityUid uid, GunWieldBonusComponent component, ItemUnwieldedEvent args)
     {
-        if (!TryComp<GunComponent>(uid, out var gun))
-            return;
-
-        gun.MinAngle -= component.MinAngle;
-        gun.MaxAngle -= component.MaxAngle;
-        Dirty(uid, gun);
+        _gun.RefreshModifiers(uid);
     }
 
     private void OnGunWielded(EntityUid uid, GunWieldBonusComponent component, ref ItemWieldedEvent args)
     {
-        if (!TryComp<GunComponent>(uid, out var gun))
-            return;
+        _gun.RefreshModifiers(uid);
+    }
 
-        gun.MinAngle += component.MinAngle;
-        gun.MaxAngle += component.MaxAngle;
-        Dirty(uid, gun);
+    private void OnGunRefreshModifiers(Entity<GunWieldBonusComponent> bonus, ref GunRefreshModifiersEvent args)
+    {
+        if (TryComp(bonus, out WieldableComponent? wield) &&
+            wield.Wielded)
+        {
+            args.MinAngle += bonus.Comp.MinAngle;
+            args.MaxAngle += bonus.Comp.MaxAngle;
+        }
     }
 
     private void AddToggleWieldVerb(EntityUid uid, WieldableComponent component, GetVerbsEvent<InteractionVerb> args)
@@ -214,6 +217,7 @@ public sealed class WieldableSystem : EntitySystem
         if (ev.Cancelled)
             return false;
 
+        component.Wielded = false;
         var targEv = new ItemUnwieldedEvent(user);
 
         RaiseLocalEvent(used, targEv);
@@ -225,16 +229,11 @@ public sealed class WieldableSystem : EntitySystem
         if (args.User == null)
             return;
 
-        if (!component.Wielded)
-            return;
-
         if (TryComp<ItemComponent>(uid, out var item))
         {
             _itemSystem.SetHeldPrefix(uid, component.OldInhandPrefix, component: item);
         }
 
-        component.Wielded = false;
-
         if (!args.Force) // don't play sound/popup if this was a forced unwield
         {
             if (component.UnwieldSound != null)