diff --git a/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs b/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs index 559c465e6d..84b6ce4048 100644 --- a/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs +++ b/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs @@ -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); diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs index e4a95ac2c5..ce43057d63 100644 --- a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs @@ -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 index 0000000000..368d3d3ba7 --- /dev/null +++ b/Content.Server/Administration/Components/AdminMinigunComponent.cs @@ -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 index 0000000000..6270481a3c --- /dev/null +++ b/Content.Server/Administration/Systems/AdminGunSystem.cs @@ -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(OnGunRefreshModifiers); + } + + private void OnGunRefreshModifiers(Entity ent, ref GunRefreshModifiersEvent args) + { + args.FireRate = 15; + } +} diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs index 7dbbacb340..296e48274c 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs @@ -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 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(args.Target); + _gun.RefreshModifiers((args.Target, gun)); }, Impact = LogImpact.Medium, Message = Loc.GetString("admin-trick-minigun-fire-description"), diff --git a/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs b/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs index 9db9aa296d..60f0603074 100644 --- a/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs +++ b/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs @@ -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(OnInteractUsing, before: new []{ typeof(StorageSystem) }); SubscribeLocalEvent(OnShoot); SubscribeLocalEvent(OnContainerInserting); + SubscribeLocalEvent(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(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 ent, ref GunRefreshModifiersEvent args) + { + if (ent.Comp.ProjectileSpeed is { } speed) + args.ProjectileSpeed = speed; + } + /// /// Returns whether the pneumatic cannon has enough gas to shoot an item, as well as the tank itself. /// diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs index 4e4bdf78b1..007f30a284 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs @@ -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); } /// @@ -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; } diff --git a/Content.Shared/PneumaticCannon/PneumaticCannonComponent.cs b/Content.Shared/PneumaticCannon/PneumaticCannonComponent.cs index 8c760abe58..f3726c802e 100644 --- a/Content.Shared/PneumaticCannon/PneumaticCannonComponent.cs +++ b/Content.Shared/PneumaticCannon/PneumaticCannonComponent.cs @@ -38,6 +38,12 @@ public sealed partial class PneumaticCannonComponent : Component [DataField("baseProjectileSpeed")] public float BaseProjectileSpeed = 20f; + /// + /// The current projectile speed setting. + /// + [DataField] + public float? ProjectileSpeed; + /// /// If true, will throw ammo rather than shoot it. /// diff --git a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs index 95853bbd2e..c3335a1ad0 100644 --- a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs @@ -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")] + /// + /// The base sound to use when the gun is fired. + /// + [DataField] public SoundSpecifier? SoundGunshot = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/smg.ogg"); - [ViewVariables(VVAccess.ReadWrite), DataField("soundEmpty")] + /// + /// The sound to use when the gun is fired. + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public SoundSpecifier? SoundGunshotModified; + + [DataField] public SoundSpecifier? SoundEmpty = new SoundPathSpecifier("/Audio/Weapons/Guns/Empty/empty.ogg"); /// /// Sound played when toggling the for this gun. /// - [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 + /// + /// The base scalar value applied to the vector governing camera recoil. + /// + [DataField, AutoNetworkedField] + public float CameraRecoilScalar = 1f; + /// /// A scalar value applied to the vector governing camera recoil. /// If 0, there will be no camera recoil. + /// /// - [DataField("cameraRecoilScalar"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] - public float CameraRecoilScalar = 1f; + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public float CameraRecoilScalarModified = 1f; /// /// Last time the gun fired. /// Used for recoil purposes. /// - [DataField("lastFire")] + [DataField] public TimeSpan LastFire = TimeSpan.Zero; /// /// What the current spread is for shooting. This gets changed every time the gun fires. /// - [DataField("currentAngle")] + [DataField] [AutoNetworkedField] public Angle CurrentAngle; /// - /// How much the spread increases every time the gun fires. + /// The base value for how much the spread increases every time the gun fires. /// - [ViewVariables(VVAccess.ReadWrite), DataField("angleIncrease")] + [DataField] public Angle AngleIncrease = Angle.FromDegrees(0.5); /// - /// How much the decreases per second. + /// How much the spread increases every time the gun fires. + /// /// - [DataField("angleDecay")] + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public Angle AngleIncreaseModified; + + /// + /// The base value for how much the decreases per second. + /// + [DataField] public Angle AngleDecay = Angle.FromDegrees(4); /// - /// The maximum angle allowed for + /// How much the decreases per second. + /// /// - [ViewVariables(VVAccess.ReadWrite), DataField("maxAngle")] + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public Angle AngleDecayModified; + + /// + /// The base value for the maximum angle allowed for + /// + [DataField] [AutoNetworkedField] public Angle MaxAngle = Angle.FromDegrees(2); /// - /// The minimum angle allowed for + /// The maximum angle allowed for + /// /// - [ViewVariables(VVAccess.ReadWrite), DataField("minAngle")] + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public Angle MaxAngleModified; + + /// + /// The base value for the minimum angle allowed for + /// + [DataField] [AutoNetworkedField] public Angle MinAngle = Angle.FromDegrees(1); + /// + /// The minimum angle allowed for . + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public Angle MinAngleModified; + #endregion /// /// Whether this gun is shot via the use key or the alt-use key. /// - [ViewVariables(VVAccess.ReadWrite), DataField("useKey"), AutoNetworkedField] + [DataField, AutoNetworkedField] public bool UseKey = true; /// @@ -93,6 +140,19 @@ public partial class GunComponent : Component [ViewVariables] public EntityCoordinates? ShootCoordinates = null; + /// + /// The base value for how many shots to fire per burst. + /// + [DataField, AutoNetworkedField] + public int ShotsPerBurst = 3; + + /// + /// How many shots to fire per burst. + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public int ShotsPerBurstModified = 3; + /// /// Used for tracking semi-auto / burst /// @@ -101,55 +161,69 @@ public partial class GunComponent : Component public int ShotCounter = 0; /// - /// How many times it shoots per second. + /// The base value for how many times it shoots per second. /// - [ViewVariables(VVAccess.ReadWrite), DataField("fireRate")] + [DataField] [AutoNetworkedField] public float FireRate = 8f; + /// + /// How many times it shoots per second. + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public float FireRateModified; + /// /// Starts fire cooldown when equipped if true. /// - [ViewVariables(VVAccess.ReadWrite), DataField("resetOnHandSelected")] + [DataField] public bool ResetOnHandSelected = true; /// /// Type of ammo the gun can work with /// - [ViewVariables(VVAccess.ReadWrite), DataField("compatibleAmmo")] + [DataField] public List>? CompatibleAmmo; /// /// Damage the gun deals when used with wrong ammo /// - [ViewVariables(VVAccess.ReadWrite), DataField("damageOnWrongAmmo")] + [DataField] public DamageSpecifier? DamageOnWrongAmmo = null; /// - /// How fast the projectile moves. + /// The base value for how fast the projectile moves. /// - [ViewVariables(VVAccess.ReadWrite), DataField("projectileSpeed")] + [DataField] public float ProjectileSpeed = 25f; + /// + /// How fast the projectile moves. + /// + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public float ProjectileSpeedModified; + /// /// 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. /// - [DataField("nextFire", customTypeSerializer:typeof(TimeOffsetSerializer))] + [DataField(customTypeSerializer:typeof(TimeOffsetSerializer))] [AutoNetworkedField] public TimeSpan NextFire = TimeSpan.Zero; /// /// What firemodes can be selected. /// - [ViewVariables(VVAccess.ReadWrite), DataField("availableModes")] + [DataField] [AutoNetworkedField] public SelectiveFire AvailableModes = SelectiveFire.SemiAuto; /// /// What firemode is currently selected. /// - [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. /// - [DataField("showExamineText")] + [DataField] public bool ShowExamineText = true; /// /// Whether or not someone with the /// clumsy trait can shoot this /// - [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 index 0000000000..63419ab0f5 --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Events/GunGetAmmoSpreadEvent.cs @@ -0,0 +1,8 @@ +namespace Content.Shared.Weapons.Ranged.Events; + +/// +/// Raised directed on the gun entity when ammo is shot to calculate its spread. +/// +/// The spread of the ammo, can be changed by handlers. +[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 index 0000000000..d6a2475308 --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Events/GunMuzzleFlashAttemptEvent.cs @@ -0,0 +1,8 @@ +namespace Content.Shared.Weapons.Ranged.Events; + +/// +/// Raised directed on the gun entity when a muzzle flash is about to happen. +/// +/// If set to true, the muzzle flash will not be shown. +[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 index 0000000000..0ad79bd74a --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Events/GunRefreshModifiersEvent.cs @@ -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; + +/// +/// Raised directed on the gun entity when +/// is called, to update the values of from other systems. +/// +[ByRefEvent] +public record struct GunRefreshModifiersEvent( + Entity Gun, + SoundSpecifier? SoundGunshot, + float CameraRecoilScalar, + Angle AngleIncrease, + Angle AngleDecay, + Angle MaxAngle, + Angle MinAngle, + int ShotsPerBurst, + float FireRate, + float ProjectileSpeed +); diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs index 501d0069a7..a8f7ee2395 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs @@ -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); diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Interactions.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Interactions.cs index 852096a386..d47d024de5 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Interactions.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Interactions.cs @@ -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; diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs index 9047ef7a8d..07af02e509 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs @@ -95,18 +95,19 @@ public abstract partial class SharedGunSystem : EntitySystem SubscribeLocalEvent(OnCycleMode); SubscribeLocalEvent(OnGunSelected); SubscribeLocalEvent(OnGunUnpaused); - -#if DEBUG SubscribeLocalEvent(OnMapInit); } - private void OnMapInit(EntityUid uid, GunComponent component, MapInitEvent args) + private void OnMapInit(Entity 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 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); /// diff --git a/Content.Shared/Wieldable/WieldableSystem.cs b/Content.Shared/Wieldable/WieldableSystem.cs index 54b030b9eb..b752932879 100644 --- a/Content.Shared/Wieldable/WieldableSystem.cs +++ b/Content.Shared/Wieldable/WieldableSystem.cs @@ -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(OnShootAttempt); SubscribeLocalEvent(OnGunWielded); SubscribeLocalEvent(OnGunUnwielded); + SubscribeLocalEvent(OnGunRefreshModifiers); SubscribeLocalEvent(OnGetMeleeDamage); } @@ -72,22 +75,22 @@ public sealed class WieldableSystem : EntitySystem private void OnGunUnwielded(EntityUid uid, GunWieldBonusComponent component, ItemUnwieldedEvent args) { - if (!TryComp(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(uid, out var gun)) - return; + _gun.RefreshModifiers(uid); + } - gun.MinAngle += component.MinAngle; - gun.MaxAngle += component.MaxAngle; - Dirty(uid, gun); + private void OnGunRefreshModifiers(Entity 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 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(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)