Add events for GunComponent values, muzzle flashes and cartridge spread (#24077)

* 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
This commit is contained in:
DrSmugleaf
2024-01-28 15:32:42 -08:00
committed by GitHub
parent 556545e324
commit 4e8b1fb0d3
16 changed files with 284 additions and 90 deletions

View File

@@ -56,11 +56,11 @@ public sealed class GunSpreadOverlay : Overlay
return; return;
// (☞゚ヮ゚)☞ // (☞゚ヮ゚)☞
var maxSpread = gun.MaxAngle; var maxSpread = gun.MaxAngleModified;
var minSpread = gun.MinAngle; var minSpread = gun.MinAngleModified;
var timeSinceLastFire = (_timing.CurTime - gun.NextFire).TotalSeconds; var timeSinceLastFire = (_timing.CurTime - gun.NextFire).TotalSeconds;
var currentAngle = new Angle(MathHelper.Clamp(gun.CurrentAngle.Theta - gun.AngleDecay.Theta * timeSinceLastFire, var currentAngle = new Angle(MathHelper.Clamp(gun.CurrentAngle.Theta - gun.AngleDecayModified.Theta * timeSinceLastFire,
gun.MinAngle.Theta, gun.MaxAngle.Theta)); gun.MinAngleModified.Theta, gun.MaxAngleModified.Theta));
var direction = (mousePos.Position - mapPos.Position); var direction = (mousePos.Position - mapPos.Position);
worldHandle.DrawLine(mapPos.Position, mousePos.Position + direction, Color.Orange); worldHandle.DrawLine(mapPos.Position, mousePos.Position + direction, Color.Orange);

View File

@@ -3,7 +3,6 @@ using Content.Client.Items;
using Content.Client.Weapons.Ranged.Components; using Content.Client.Weapons.Ranged.Components;
using Content.Shared.Camera; using Content.Shared.Camera;
using Content.Shared.CombatMode; using Content.Shared.CombatMode;
using Robust.Shared.Spawners;
using Content.Shared.Weapons.Ranged; using Content.Shared.Weapons.Ranged;
using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Events;
@@ -195,7 +194,7 @@ public sealed partial class GunSystem : SharedGunSystem
{ {
if (throwItems) if (throwItems)
{ {
Recoil(user, direction, gun.CameraRecoilScalar); Recoil(user, direction, gun.CameraRecoilScalarModified);
if (IsClientSide(ent!.Value)) if (IsClientSide(ent!.Value))
Del(ent.Value); Del(ent.Value);
else else
@@ -210,8 +209,8 @@ public sealed partial class GunSystem : SharedGunSystem
{ {
SetCartridgeSpent(ent!.Value, cartridge, true); SetCartridgeSpent(ent!.Value, cartridge, true);
MuzzleFlash(gunUid, cartridge, user); MuzzleFlash(gunUid, cartridge, user);
Audio.PlayPredicted(gun.SoundGunshot, gunUid, user); Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
Recoil(user, direction, gun.CameraRecoilScalar); Recoil(user, direction, gun.CameraRecoilScalarModified);
// TODO: Can't predict entity deletions. // TODO: Can't predict entity deletions.
//if (cartridge.DeleteOnSpawn) //if (cartridge.DeleteOnSpawn)
// Del(cartridge.Owner); // Del(cartridge.Owner);
@@ -228,16 +227,16 @@ public sealed partial class GunSystem : SharedGunSystem
break; break;
case AmmoComponent newAmmo: case AmmoComponent newAmmo:
MuzzleFlash(gunUid, newAmmo, user); MuzzleFlash(gunUid, newAmmo, user);
Audio.PlayPredicted(gun.SoundGunshot, gunUid, user); Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
Recoil(user, direction, gun.CameraRecoilScalar); Recoil(user, direction, gun.CameraRecoilScalarModified);
if (IsClientSide(ent!.Value)) if (IsClientSide(ent!.Value))
Del(ent.Value); Del(ent.Value);
else else
RemoveShootable(ent.Value); RemoveShootable(ent.Value);
break; break;
case HitscanPrototype: case HitscanPrototype:
Audio.PlayPredicted(gun.SoundGunshot, gunUid, user); Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
Recoil(user, direction, gun.CameraRecoilScalar); Recoil(user, direction, gun.CameraRecoilScalarModified);
break; break;
} }
} }

View File

@@ -0,0 +1,7 @@
namespace Content.Server.Administration.Components;
[RegisterComponent]
public sealed partial class AdminMinigunComponent : Component
{
}

View File

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

View File

@@ -12,6 +12,7 @@ using Content.Server.Power.EntitySystems;
using Content.Server.Stack; using Content.Server.Stack;
using Content.Server.Station.Components; using Content.Server.Station.Components;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;
using Content.Server.Weapons.Ranged.Systems;
using Content.Shared.Access; using Content.Shared.Access;
using Content.Shared.Access.Components; using Content.Shared.Access.Components;
using Content.Shared.Access.Systems; using Content.Shared.Access.Systems;
@@ -52,6 +53,7 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly BatterySystem _batterySystem = default!; [Dependency] private readonly BatterySystem _batterySystem = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!; [Dependency] private readonly SharedTransformSystem _xformSystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!; [Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly GunSystem _gun = default!;
private void AddTricksVerbs(GetVerbsEvent<Verb> args) 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"), Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Weapons/Guns/HMGs/minigun.rsi"), "icon"),
Act = () => Act = () =>
{ {
gun.FireRate = 15; EnsureComp<AdminMinigunComponent>(args.Target);
_gun.RefreshModifiers((args.Target, gun));
}, },
Impact = LogImpact.Medium, Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-minigun-fire-description"), Message = Loc.GetString("admin-trick-minigun-fire-description"),

View File

@@ -2,12 +2,14 @@ using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Storage.EntitySystems; using Content.Server.Storage.EntitySystems;
using Content.Server.Stunnable; using Content.Server.Stunnable;
using Content.Server.Weapons.Ranged.Systems;
using Content.Shared.Containers.ItemSlots; using Content.Shared.Containers.ItemSlots;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.PneumaticCannon; using Content.Shared.PneumaticCannon;
using Content.Shared.StatusEffect; using Content.Shared.StatusEffect;
using Content.Shared.Tools.Components; using Content.Shared.Tools.Components;
using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Weapons.Ranged.Systems; using Content.Shared.Weapons.Ranged.Systems;
using Robust.Shared.Containers; using Robust.Shared.Containers;
@@ -17,6 +19,7 @@ public sealed class PneumaticCannonSystem : SharedPneumaticCannonSystem
{ {
[Dependency] private readonly AtmosphereSystem _atmos = default!; [Dependency] private readonly AtmosphereSystem _atmos = default!;
[Dependency] private readonly GasTankSystem _gasTank = default!; [Dependency] private readonly GasTankSystem _gasTank = default!;
[Dependency] private readonly GunSystem _gun = default!;
[Dependency] private readonly StunSystem _stun = default!; [Dependency] private readonly StunSystem _stun = default!;
[Dependency] private readonly ItemSlotsSystem _slots = 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, InteractUsingEvent>(OnInteractUsing, before: new []{ typeof(StorageSystem) });
SubscribeLocalEvent<PneumaticCannonComponent, GunShotEvent>(OnShoot); SubscribeLocalEvent<PneumaticCannonComponent, GunShotEvent>(OnShoot);
SubscribeLocalEvent<PneumaticCannonComponent, ContainerIsInsertingAttemptEvent>(OnContainerInserting); SubscribeLocalEvent<PneumaticCannonComponent, ContainerIsInsertingAttemptEvent>(OnContainerInserting);
SubscribeLocalEvent<PneumaticCannonComponent, GunRefreshModifiersEvent>(OnGunRefreshModifiers);
} }
private void OnInteractUsing(EntityUid uid, PneumaticCannonComponent component, InteractUsingEvent args) 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", Popup.PopupEntity(Loc.GetString("pneumatic-cannon-component-change-power",
("power", component.Power.ToString())), uid, args.User); ("power", component.Power.ToString())), uid, args.User);
component.ProjectileSpeed = GetProjectileSpeedFromPower(component);
if (TryComp<GunComponent>(uid, out var gun)) if (TryComp<GunComponent>(uid, out var gun))
{ _gun.RefreshModifiers((uid, gun));
gun.ProjectileSpeed = GetProjectileSpeedFromPower(component);
}
args.Handled = true; args.Handled = true;
} }
@@ -105,6 +108,12 @@ public sealed class PneumaticCannonSystem : SharedPneumaticCannonSystem
_slots.TryEject(uid, PneumaticCannonComponent.TankSlotId, args.User, out _); _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> /// <summary>
/// Returns whether the pneumatic cannon has enough gas to shoot an item, as well as the tank itself. /// Returns whether the pneumatic cannon has enough gas to shoot an item, as well as the tank itself.
/// </summary> /// </summary>

View File

@@ -10,7 +10,6 @@ using Content.Shared.Damage;
using Content.Shared.Damage.Systems; using Content.Shared.Damage.Systems;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Effects; using Content.Shared.Effects;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction.Components; using Content.Shared.Interaction.Components;
using Content.Shared.Projectiles; using Content.Shared.Projectiles;
using Content.Shared.Weapons.Melee; 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.Events;
using Content.Shared.Weapons.Ranged.Systems; using Content.Shared.Weapons.Ranged.Systems;
using Content.Shared.Weapons.Reflect; using Content.Shared.Weapons.Reflect;
using Robust.Server.GameObjects;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Physics; using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -156,8 +153,11 @@ public sealed partial class GunSystem : SharedGunSystem
} }
if (cartridge.Count > 1) if (cartridge.Count > 1)
{ {
var angles = LinearSpread(mapAngle - cartridge.Spread / 2, var ev = new GunGetAmmoSpreadEvent(cartridge.Spread);
mapAngle + cartridge.Spread / 2, cartridge.Count); 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++) for (var i = 0; i < cartridge.Count; i++)
{ {
@@ -180,7 +180,7 @@ public sealed partial class GunSystem : SharedGunSystem
SetCartridgeSpent(ent.Value, cartridge, true); SetCartridgeSpent(ent.Value, cartridge, true);
MuzzleFlash(gunUid, cartridge, user); MuzzleFlash(gunUid, cartridge, user);
Audio.PlayPredicted(gun.SoundGunshot, gunUid, user); Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
if (cartridge.DeleteOnSpawn) if (cartridge.DeleteOnSpawn)
Del(ent.Value); Del(ent.Value);
@@ -201,7 +201,7 @@ public sealed partial class GunSystem : SharedGunSystem
case AmmoComponent newAmmo: case AmmoComponent newAmmo:
shotProjectiles.Add(ent!.Value); shotProjectiles.Add(ent!.Value);
MuzzleFlash(gunUid, newAmmo, user); MuzzleFlash(gunUid, newAmmo, user);
Audio.PlayPredicted(gun.SoundGunshot, gunUid, user); Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
ShootOrThrow(ent.Value, mapDirection, gunVelocity, gun, gunUid, user); ShootOrThrow(ent.Value, mapDirection, gunVelocity, gun, gunUid, user);
break; break;
case HitscanPrototype hitscan: case HitscanPrototype hitscan:
@@ -288,7 +288,7 @@ public sealed partial class GunSystem : SharedGunSystem
FireEffects(fromEffect, hitscan.MaxLength, dir.ToAngle(), hitscan); FireEffects(fromEffect, hitscan.MaxLength, dir.ToAngle(), hitscan);
} }
Audio.PlayPredicted(gun.SoundGunshot, gunUid, user); Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
break; break;
default: default:
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
@@ -308,11 +308,11 @@ public sealed partial class GunSystem : SharedGunSystem
{ {
RemoveShootable(uid); RemoveShootable(uid);
// TODO: Someone can probably yeet this a billion miles so need to pre-validate input somewhere up the call stack. // 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; return;
} }
ShootProjectile(uid, mapDirection, gunVelocity, gunUid, user, gun.ProjectileSpeed); ShootProjectile(uid, mapDirection, gunVelocity, gunUid, user, gun.ProjectileSpeedModified);
} }
/// <summary> /// <summary>
@@ -337,7 +337,7 @@ public sealed partial class GunSystem : SharedGunSystem
private Angle GetRecoilAngle(TimeSpan curTime, GunComponent component, Angle direction) private Angle GetRecoilAngle(TimeSpan curTime, GunComponent component, Angle direction)
{ {
var timeSinceLastFire = (curTime - component.LastFire).TotalSeconds; 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.CurrentAngle = new Angle(newTheta);
component.LastFire = component.NextFire; component.LastFire = component.NextFire;
@@ -345,7 +345,7 @@ public sealed partial class GunSystem : SharedGunSystem
var random = Random.NextFloat(-0.5f, 0.5f); var random = Random.NextFloat(-0.5f, 0.5f);
var spread = component.CurrentAngle.Theta * random; var spread = component.CurrentAngle.Theta * random;
var angle = new Angle(direction.Theta + 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; return angle;
} }

View File

@@ -38,6 +38,12 @@ public sealed partial class PneumaticCannonComponent : Component
[DataField("baseProjectileSpeed")] [DataField("baseProjectileSpeed")]
public float BaseProjectileSpeed = 20f; public float BaseProjectileSpeed = 20f;
/// <summary>
/// The current projectile speed setting.
/// </summary>
[DataField]
public float? ProjectileSpeed;
/// <summary> /// <summary>
/// If true, will throw ammo rather than shoot it. /// If true, will throw ammo rather than shoot it.
/// </summary> /// </summary>

View File

@@ -1,5 +1,7 @@
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Tag; using Content.Shared.Tag;
using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Map; using Robust.Shared.Map;
@@ -8,23 +10,33 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Weapons.Ranged.Components; namespace Content.Shared.Weapons.Ranged.Components;
[RegisterComponent, NetworkedComponent, Virtual] [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[AutoGenerateComponentState] [Access(typeof(SharedGunSystem))]
public partial class GunComponent : Component public sealed partial class GunComponent : Component
{ {
#region Sound #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"); 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"); public SoundSpecifier? SoundEmpty = new SoundPathSpecifier("/Audio/Weapons/Guns/Empty/empty.ogg");
/// <summary> /// <summary>
/// Sound played when toggling the <see cref="SelectedMode"/> for this gun. /// Sound played when toggling the <see cref="SelectedMode"/> for this gun.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("soundMode")] [DataField]
public SoundSpecifier? SoundModeToggle = new SoundPathSpecifier("/Audio/Weapons/Guns/Misc/selector.ogg"); public SoundSpecifier? SoundMode = new SoundPathSpecifier("/Audio/Weapons/Guns/Misc/selector.ogg");
#endregion #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 // 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> /// <summary>
/// A scalar value applied to the vector governing camera recoil. /// A scalar value applied to the vector governing camera recoil.
/// If 0, there will be no camera recoil. /// If 0, there will be no camera recoil.
/// <seealso cref="GunRefreshModifiersEvent"/>
/// </summary> /// </summary>
[DataField("cameraRecoilScalar"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
public float CameraRecoilScalar = 1f; public float CameraRecoilScalarModified = 1f;
/// <summary> /// <summary>
/// Last time the gun fired. /// Last time the gun fired.
/// Used for recoil purposes. /// Used for recoil purposes.
/// </summary> /// </summary>
[DataField("lastFire")] [DataField]
public TimeSpan LastFire = TimeSpan.Zero; public TimeSpan LastFire = TimeSpan.Zero;
/// <summary> /// <summary>
/// What the current spread is for shooting. This gets changed every time the gun fires. /// What the current spread is for shooting. This gets changed every time the gun fires.
/// </summary> /// </summary>
[DataField("currentAngle")] [DataField]
[AutoNetworkedField] [AutoNetworkedField]
public Angle CurrentAngle; public Angle CurrentAngle;
/// <summary> /// <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> /// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("angleIncrease")] [DataField]
public Angle AngleIncrease = Angle.FromDegrees(0.5); public Angle AngleIncrease = Angle.FromDegrees(0.5);
/// <summary> /// <summary>
/// How much the <see cref="CurrentAngle"/> decreases per second. /// How much the spread increases every time the gun fires.
/// <seealso cref="GunRefreshModifiersEvent"/>
/// </summary> /// </summary>
[DataField("angleDecay")] [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
public Angle AngleIncreaseModified;
/// <summary>
/// The base value for how much the <see cref="CurrentAngle"/> decreases per second.
/// </summary>
[DataField]
public Angle AngleDecay = Angle.FromDegrees(4); public Angle AngleDecay = Angle.FromDegrees(4);
/// <summary> /// <summary>
/// The maximum angle allowed for <see cref="CurrentAngle"/> /// How much the <see cref="CurrentAngle"/> decreases per second.
/// <seealso cref="GunRefreshModifiersEvent"/>
/// </summary> /// </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] [AutoNetworkedField]
public Angle MaxAngle = Angle.FromDegrees(2); public Angle MaxAngle = Angle.FromDegrees(2);
/// <summary> /// <summary>
/// The minimum angle allowed for <see cref="CurrentAngle"/> /// The maximum angle allowed for <see cref="CurrentAngle"/>
/// <seealso cref="GunRefreshModifiersEvent"/>
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("minAngle")] [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
public Angle MaxAngleModified;
/// <summary>
/// The base value for the minimum angle allowed for <see cref="CurrentAngle"/>
/// </summary>
[DataField]
[AutoNetworkedField] [AutoNetworkedField]
public Angle MinAngle = Angle.FromDegrees(1); 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 #endregion
/// <summary> /// <summary>
/// Whether this gun is shot via the use key or the alt-use key. /// Whether this gun is shot via the use key or the alt-use key.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("useKey"), AutoNetworkedField] [DataField, AutoNetworkedField]
public bool UseKey = true; public bool UseKey = true;
/// <summary> /// <summary>
@@ -93,6 +140,19 @@ public partial class GunComponent : Component
[ViewVariables] [ViewVariables]
public EntityCoordinates? ShootCoordinates = null; 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> /// <summary>
/// Used for tracking semi-auto / burst /// Used for tracking semi-auto / burst
/// </summary> /// </summary>
@@ -101,55 +161,69 @@ public partial class GunComponent : Component
public int ShotCounter = 0; public int ShotCounter = 0;
/// <summary> /// <summary>
/// How many times it shoots per second. /// The base value for how many times it shoots per second.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("fireRate")] [DataField]
[AutoNetworkedField] [AutoNetworkedField]
public float FireRate = 8f; public float FireRate = 8f;
/// <summary>
/// How many times it shoots per second.
/// <seealso cref="GunRefreshModifiersEvent"/>
/// </summary>
[AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
public float FireRateModified;
/// <summary> /// <summary>
/// Starts fire cooldown when equipped if true. /// Starts fire cooldown when equipped if true.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("resetOnHandSelected")] [DataField]
public bool ResetOnHandSelected = true; public bool ResetOnHandSelected = true;
/// <summary> /// <summary>
/// Type of ammo the gun can work with /// Type of ammo the gun can work with
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("compatibleAmmo")] [DataField]
public List<ProtoId<TagPrototype>>? CompatibleAmmo; public List<ProtoId<TagPrototype>>? CompatibleAmmo;
/// <summary> /// <summary>
/// Damage the gun deals when used with wrong ammo /// Damage the gun deals when used with wrong ammo
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("damageOnWrongAmmo")] [DataField]
public DamageSpecifier? DamageOnWrongAmmo = null; public DamageSpecifier? DamageOnWrongAmmo = null;
/// <summary> /// <summary>
/// How fast the projectile moves. /// The base value for how fast the projectile moves.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("projectileSpeed")] [DataField]
public float ProjectileSpeed = 25f; public float ProjectileSpeed = 25f;
/// <summary>
/// How fast the projectile moves.
/// <seealso cref="GunRefreshModifiersEvent"/>
/// </summary>
[AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
public float ProjectileSpeedModified;
/// <summary> /// <summary>
/// When the gun is next available to be shot. /// 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. /// Can be set multiple times in a single tick due to guns firing faster than a single tick time.
/// </summary> /// </summary>
[DataField("nextFire", customTypeSerializer:typeof(TimeOffsetSerializer))] [DataField(customTypeSerializer:typeof(TimeOffsetSerializer))]
[AutoNetworkedField] [AutoNetworkedField]
public TimeSpan NextFire = TimeSpan.Zero; public TimeSpan NextFire = TimeSpan.Zero;
/// <summary> /// <summary>
/// What firemodes can be selected. /// What firemodes can be selected.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("availableModes")] [DataField]
[AutoNetworkedField] [AutoNetworkedField]
public SelectiveFire AvailableModes = SelectiveFire.SemiAuto; public SelectiveFire AvailableModes = SelectiveFire.SemiAuto;
/// <summary> /// <summary>
/// What firemode is currently selected. /// What firemode is currently selected.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("selectedMode")] [DataField]
[AutoNetworkedField] [AutoNetworkedField]
public SelectiveFire SelectedMode = SelectiveFire.SemiAuto; public SelectiveFire SelectedMode = SelectiveFire.SemiAuto;
@@ -157,14 +231,14 @@ public partial class GunComponent : Component
/// Whether or not information about /// Whether or not information about
/// the gun will be shown on examine. /// the gun will be shown on examine.
/// </summary> /// </summary>
[DataField("showExamineText")] [DataField]
public bool ShowExamineText = true; public bool ShowExamineText = true;
/// <summary> /// <summary>
/// Whether or not someone with the /// Whether or not someone with the
/// clumsy trait can shoot this /// clumsy trait can shoot this
/// </summary> /// </summary>
[DataField("clumsyProof"), ViewVariables(VVAccess.ReadWrite)] [DataField]
public bool ClumsyProof = false; public bool ClumsyProof = false;
} }

View File

@@ -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);

View File

@@ -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);

View File

@@ -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
);

View File

@@ -183,10 +183,10 @@ public abstract partial class SharedGunSystem
// Reset shotting for cycling // Reset shotting for cycling
if (Resolve(uid, ref gunComp, false) && if (Resolve(uid, ref gunComp, false) &&
gunComp is { FireRate: > 0f } && gunComp is { FireRateModified: > 0f } &&
!Paused(uid)) !Paused(uid))
{ {
gunComp.NextFire = Timing.CurTime + TimeSpan.FromSeconds(1 / gunComp.FireRate); gunComp.NextFire = Timing.CurTime + TimeSpan.FromSeconds(1 / gunComp.FireRateModified);
} }
Dirty(uid, component); Dirty(uid, component);

View File

@@ -19,7 +19,7 @@ public abstract partial class SharedGunSystem
args.PushMarkup(Loc.GetString("gun-selected-mode-examine", ("color", ModeExamineColor), args.PushMarkup(Loc.GetString("gun-selected-mode-examine", ("color", ModeExamineColor),
("mode", GetLocSelector(component.SelectedMode)))); ("mode", GetLocSelector(component.SelectedMode))));
args.PushMarkup(Loc.GetString("gun-fire-rate-examine", ("color", FireRateExamineColor), 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; 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); Popup(Loc.GetString("gun-selected-mode", ("mode", GetLocSelector(fire))), uid, user);
Dirty(uid, component); Dirty(uid, component);
} }
@@ -112,7 +112,7 @@ public abstract partial class SharedGunSystem
private void OnGunSelected(EntityUid uid, GunComponent component, HandSelectedEvent args) private void OnGunSelected(EntityUid uid, GunComponent component, HandSelectedEvent args)
{ {
var fireDelay = 1f / component.FireRate; var fireDelay = 1f / component.FireRateModified;
if (fireDelay.Equals(0f)) if (fireDelay.Equals(0f))
return; return;

View File

@@ -95,18 +95,19 @@ public abstract partial class SharedGunSystem : EntitySystem
SubscribeLocalEvent<GunComponent, CycleModeEvent>(OnCycleMode); SubscribeLocalEvent<GunComponent, CycleModeEvent>(OnCycleMode);
SubscribeLocalEvent<GunComponent, HandSelectedEvent>(OnGunSelected); SubscribeLocalEvent<GunComponent, HandSelectedEvent>(OnGunSelected);
SubscribeLocalEvent<GunComponent, EntityUnpausedEvent>(OnGunUnpaused); SubscribeLocalEvent<GunComponent, EntityUnpausedEvent>(OnGunUnpaused);
#if DEBUG
SubscribeLocalEvent<GunComponent, MapInitEvent>(OnMapInit); 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) #if DEBUG
Log.Warning($"Initializing a map that contains an entity that is on cooldown. Entity: {ToPrettyString(uid)}"); 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 #endif
RefreshModifiers((gun, gun));
} }
private void OnGunMelee(EntityUid uid, GunComponent component, MeleeHitEvent args) 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) private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
{ {
if (gun.FireRate <= 0f || if (gun.FireRateModified <= 0f ||
!_actionBlockerSystem.CanAttack(user)) !_actionBlockerSystem.CanAttack(user))
return; return;
@@ -259,7 +260,7 @@ public abstract partial class SharedGunSystem : EntitySystem
if (gun.NextFire > curTime) if (gun.NextFire > curTime)
return; return;
var fireRate = TimeSpan.FromSeconds(1f / gun.FireRate); var fireRate = TimeSpan.FromSeconds(1f / gun.FireRateModified);
// First shot // First shot
// Previously we checked shotcounter but in some cases all the bullets got dumped at once // 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); shots = Math.Min(shots, 1 - gun.ShotCounter);
break; break;
case SelectiveFire.Burst: case SelectiveFire.Burst:
shots = Math.Min(shots, 3 - gun.ShotCounter); shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter);
break; break;
case SelectiveFire.FullAuto: case SelectiveFire.FullAuto:
break; break;
@@ -468,6 +469,11 @@ public abstract partial class SharedGunSystem : EntitySystem
protected void MuzzleFlash(EntityUid gun, AmmoComponent component, EntityUid? user = null) 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; var sprite = component.MuzzleFlash;
if (sprite == null) if (sprite == null)
@@ -487,6 +493,41 @@ public abstract partial class SharedGunSystem : EntitySystem
var impulseVector = shotDirection * impulseStrength; var impulseVector = shotDirection * impulseStrength;
Physics.ApplyLinearImpulse(user, -impulseVector, body: userPhysics); 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); protected abstract void CreateEffect(EntityUid uid, MuzzleFlashEvent message, EntityUid? user = null);
/// <summary> /// <summary>

View File

@@ -11,6 +11,7 @@ using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Components; using Content.Shared.Weapons.Melee.Components;
using Content.Shared.Weapons.Melee.Events; using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Weapons.Ranged.Systems; using Content.Shared.Weapons.Ranged.Systems;
using Content.Shared.Wieldable.Components; using Content.Shared.Wieldable.Components;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
@@ -27,6 +28,7 @@ public sealed class WieldableSystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audioSystem = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly UseDelaySystem _delay = default!; [Dependency] private readonly UseDelaySystem _delay = default!;
[Dependency] private readonly SharedGunSystem _gun = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -42,6 +44,7 @@ public sealed class WieldableSystem : EntitySystem
SubscribeLocalEvent<GunRequiresWieldComponent, AttemptShootEvent>(OnShootAttempt); SubscribeLocalEvent<GunRequiresWieldComponent, AttemptShootEvent>(OnShootAttempt);
SubscribeLocalEvent<GunWieldBonusComponent, ItemWieldedEvent>(OnGunWielded); SubscribeLocalEvent<GunWieldBonusComponent, ItemWieldedEvent>(OnGunWielded);
SubscribeLocalEvent<GunWieldBonusComponent, ItemUnwieldedEvent>(OnGunUnwielded); SubscribeLocalEvent<GunWieldBonusComponent, ItemUnwieldedEvent>(OnGunUnwielded);
SubscribeLocalEvent<GunWieldBonusComponent, GunRefreshModifiersEvent>(OnGunRefreshModifiers);
SubscribeLocalEvent<IncreaseDamageOnWieldComponent, GetMeleeDamageEvent>(OnGetMeleeDamage); SubscribeLocalEvent<IncreaseDamageOnWieldComponent, GetMeleeDamageEvent>(OnGetMeleeDamage);
} }
@@ -72,22 +75,22 @@ public sealed class WieldableSystem : EntitySystem
private void OnGunUnwielded(EntityUid uid, GunWieldBonusComponent component, ItemUnwieldedEvent args) private void OnGunUnwielded(EntityUid uid, GunWieldBonusComponent component, ItemUnwieldedEvent args)
{ {
if (!TryComp<GunComponent>(uid, out var gun)) _gun.RefreshModifiers(uid);
return;
gun.MinAngle -= component.MinAngle;
gun.MaxAngle -= component.MaxAngle;
Dirty(uid, gun);
} }
private void OnGunWielded(EntityUid uid, GunWieldBonusComponent component, ref ItemWieldedEvent args) private void OnGunWielded(EntityUid uid, GunWieldBonusComponent component, ref ItemWieldedEvent args)
{ {
if (!TryComp<GunComponent>(uid, out var gun)) _gun.RefreshModifiers(uid);
return; }
gun.MinAngle += component.MinAngle; private void OnGunRefreshModifiers(Entity<GunWieldBonusComponent> bonus, ref GunRefreshModifiersEvent args)
gun.MaxAngle += component.MaxAngle; {
Dirty(uid, gun); 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) private void AddToggleWieldVerb(EntityUid uid, WieldableComponent component, GetVerbsEvent<InteractionVerb> args)
@@ -214,6 +217,7 @@ public sealed class WieldableSystem : EntitySystem
if (ev.Cancelled) if (ev.Cancelled)
return false; return false;
component.Wielded = false;
var targEv = new ItemUnwieldedEvent(user); var targEv = new ItemUnwieldedEvent(user);
RaiseLocalEvent(used, targEv); RaiseLocalEvent(used, targEv);
@@ -225,16 +229,11 @@ public sealed class WieldableSystem : EntitySystem
if (args.User == null) if (args.User == null)
return; return;
if (!component.Wielded)
return;
if (TryComp<ItemComponent>(uid, out var item)) if (TryComp<ItemComponent>(uid, out var item))
{ {
_itemSystem.SetHeldPrefix(uid, component.OldInhandPrefix, component: 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 (!args.Force) // don't play sound/popup if this was a forced unwield
{ {
if (component.UnwieldSound != null) if (component.UnwieldSound != null)