diff --git a/Content.Client/Content.Client.csproj b/Content.Client/Content.Client.csproj
index 0ac62cc4ad..92b0d11497 100644
--- a/Content.Client/Content.Client.csproj
+++ b/Content.Client/Content.Client.csproj
@@ -23,7 +23,6 @@
-
diff --git a/Content.Client/Effects/EffectVisualizerSystem.cs b/Content.Client/Effects/EffectVisualizerSystem.cs
new file mode 100644
index 0000000000..05489bf82d
--- /dev/null
+++ b/Content.Client/Effects/EffectVisualizerSystem.cs
@@ -0,0 +1,17 @@
+using Robust.Client.Animations;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Effects;
+
+public sealed class EffectVisualizerSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnEffectAnimComplete);
+ }
+
+ private void OnEffectAnimComplete(EntityUid uid, EffectVisualsComponent component, AnimationCompletedEvent args)
+ {
+ QueueDel(uid);
+ }
+}
diff --git a/Content.Client/Effects/EffectVisualsComponent.cs b/Content.Client/Effects/EffectVisualsComponent.cs
new file mode 100644
index 0000000000..05a1231f28
--- /dev/null
+++ b/Content.Client/Effects/EffectVisualsComponent.cs
@@ -0,0 +1,8 @@
+namespace Content.Client.Effects;
+
+[RegisterComponent]
+public sealed class EffectVisualsComponent : Component
+{
+ public float Length;
+ public float Accumulator = 0f;
+}
diff --git a/Content.Client/Items/ItemStatusMessages.cs b/Content.Client/Items/ItemStatusMessages.cs
index 93ebaeabe0..58a302e882 100644
--- a/Content.Client/Items/ItemStatusMessages.cs
+++ b/Content.Client/Items/ItemStatusMessages.cs
@@ -1,7 +1,4 @@
-using System;
-using System.Collections.Generic;
-using Robust.Client.UserInterface;
-using Robust.Shared.GameObjects;
+using Robust.Client.UserInterface;
namespace Content.Client.Items
{
diff --git a/Content.Client/Weapons/Ranged/Barrels/Components/ClientBatteryBarrelComponent.cs b/Content.Client/Weapons/Ranged/Barrels/Components/ClientBatteryBarrelComponent.cs
deleted file mode 100644
index bc8720faf1..0000000000
--- a/Content.Client/Weapons/Ranged/Barrels/Components/ClientBatteryBarrelComponent.cs
+++ /dev/null
@@ -1,140 +0,0 @@
-using Content.Client.Items.Components;
-using Content.Client.Stylesheets;
-using Content.Shared.Weapons.Ranged.Barrels.Components;
-using Robust.Client.Graphics;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Shared.GameStates;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
-
-namespace Content.Client.Weapons.Ranged.Barrels.Components
-{
- [RegisterComponent]
- [NetworkedComponent()]
- public sealed class ClientBatteryBarrelComponent : Component, IItemStatus
- {
- public StatusControl? ItemStatus;
-
- public Control MakeControl()
- {
- ItemStatus = new StatusControl(this);
-
- if (IoCManager.Resolve().TryGetComponent(Owner, out AppearanceComponent appearance))
- ItemStatus.Update(appearance);
-
- return ItemStatus;
- }
-
- public void DestroyControl(Control control)
- {
- if (ItemStatus == control)
- {
- ItemStatus = null;
- }
- }
-
- public sealed class StatusControl : Control
- {
- private readonly ClientBatteryBarrelComponent _parent;
- private readonly BoxContainer _bulletsList;
- private readonly Label _noBatteryLabel;
- private readonly Label _ammoCount;
-
- public StatusControl(ClientBatteryBarrelComponent parent)
- {
- MinHeight = 15;
- _parent = parent;
- HorizontalExpand = true;
- VerticalAlignment = VAlignment.Center;
-
- AddChild(new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- HorizontalExpand = true,
- Children =
- {
- new Control
- {
- HorizontalExpand = true,
- Children =
- {
- (_bulletsList = new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- VerticalAlignment = VAlignment.Center,
- SeparationOverride = 4
- }),
- (_noBatteryLabel = new Label
- {
- Text = "No Battery!",
- StyleClasses = {StyleNano.StyleClassItemStatus}
- })
- }
- },
- new Control() { MinSize = (5,0) },
- (_ammoCount = new Label
- {
- StyleClasses = {StyleNano.StyleClassItemStatus},
- HorizontalAlignment = HAlignment.Right,
- }),
- }
- });
- }
-
- public void Update(AppearanceComponent appearance)
- {
- _bulletsList.RemoveAllChildren();
-
- if (!appearance.TryGetData(MagazineBarrelVisuals.MagLoaded, out bool loaded) || !loaded)
- {
- _noBatteryLabel.Visible = true;
- _ammoCount.Visible = false;
- return;
- }
-
- appearance.TryGetData(AmmoVisuals.AmmoCount, out int count);
- appearance.TryGetData(AmmoVisuals.AmmoMax, out int capacity);
-
- _noBatteryLabel.Visible = false;
- _ammoCount.Visible = true;
-
- _ammoCount.Text = $"x{count:00}";
- capacity = Math.Min(capacity, 8);
- FillBulletRow(_bulletsList, count, capacity);
- }
-
- private static void FillBulletRow(Control container, int count, int capacity)
- {
- var colorGone = Color.FromHex("#000000");
- var color = Color.FromHex("#E00000");
-
- // Draw the empty ones
- for (var i = count; i < capacity; i++)
- {
- container.AddChild(new PanelContainer
- {
- PanelOverride = new StyleBoxFlat()
- {
- BackgroundColor = colorGone,
- },
- MinSize = (10, 15),
- });
- }
-
- // Draw the full ones, but limit the count to the capacity
- count = Math.Min(count, capacity);
- for (var i = 0; i < count; i++)
- {
- container.AddChild(new PanelContainer
- {
- PanelOverride = new StyleBoxFlat()
- {
- BackgroundColor = color,
- },
- MinSize = (10, 15),
- });
- }
- }
- }
- }
-}
diff --git a/Content.Client/Weapons/Ranged/Barrels/Components/ClientBoltActionBarrelComponent.cs b/Content.Client/Weapons/Ranged/Barrels/Components/ClientBoltActionBarrelComponent.cs
deleted file mode 100644
index f50754abb2..0000000000
--- a/Content.Client/Weapons/Ranged/Barrels/Components/ClientBoltActionBarrelComponent.cs
+++ /dev/null
@@ -1,212 +0,0 @@
-using System;
-using Content.Client.IoC;
-using Content.Client.Items.Components;
-using Content.Client.Resources;
-using Content.Client.Stylesheets;
-using Content.Shared.Weapons.Ranged.Barrels.Components;
-using Robust.Client.Graphics;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Shared.GameObjects;
-using Robust.Shared.GameStates;
-using Robust.Shared.Maths;
-using Robust.Shared.ViewVariables;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
-
-namespace Content.Client.Weapons.Ranged.Barrels.Components
-{
- [RegisterComponent]
- [NetworkedComponent()]
- public sealed class ClientBoltActionBarrelComponent : Component, IItemStatus
- {
- private StatusControl? _statusControl;
-
- ///
- /// chambered is true when a bullet is chambered
- /// spent is true when the chambered bullet is spent
- ///
- [ViewVariables]
- public (bool chambered, bool spent) Chamber { get; private set; }
-
- ///
- /// Count of bullets in the magazine.
- ///
- ///
- /// Null if no magazine is inserted.
- ///
- [ViewVariables]
- public (int count, int max)? MagazineCount { get; private set; }
-
- public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
- {
- base.HandleComponentState(curState, nextState);
-
- if (curState is not BoltActionBarrelComponentState cast)
- return;
-
- Chamber = cast.Chamber;
- MagazineCount = cast.Magazine;
- _statusControl?.Update();
- }
-
- public Control MakeControl()
- {
- _statusControl = new StatusControl(this);
- _statusControl.Update();
- return _statusControl;
- }
-
- public void DestroyControl(Control control)
- {
- if (_statusControl == control)
- {
- _statusControl = null;
- }
- }
-
- private sealed class StatusControl : Control
- {
- private readonly ClientBoltActionBarrelComponent _parent;
- private readonly BoxContainer _bulletsListTop;
- private readonly BoxContainer _bulletsListBottom;
- private readonly TextureRect _chamberedBullet;
- private readonly Label _noMagazineLabel;
-
- public StatusControl(ClientBoltActionBarrelComponent parent)
- {
- MinHeight = 15;
- _parent = parent;
- HorizontalExpand = true;
- VerticalAlignment = VAlignment.Center;
- AddChild(new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical,
- HorizontalExpand = true,
- VerticalAlignment = VAlignment.Center,
- SeparationOverride = 0,
- Children =
- {
- (_bulletsListTop = new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- SeparationOverride = 0
- }),
- new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- HorizontalExpand = true,
- Children =
- {
- new Control
- {
- HorizontalExpand = true,
- Children =
- {
- (_bulletsListBottom = new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- VerticalAlignment = VAlignment.Center,
- SeparationOverride = 0
- }),
- (_noMagazineLabel = new Label
- {
- Text = "No Magazine!",
- StyleClasses = {StyleNano.StyleClassItemStatus}
- })
- }
- },
- (_chamberedBullet = new TextureRect
- {
- Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/chambered.png"),
- VerticalAlignment = VAlignment.Center,
- HorizontalAlignment = HAlignment.Right,
- })
- }
- }
- }
- });
- }
-
- public void Update()
- {
- _chamberedBullet.ModulateSelfOverride =
- _parent.Chamber.chambered ?
- _parent.Chamber.spent ? Color.Red : Color.FromHex("#d7df60")
- : Color.Black;
-
- _bulletsListTop.RemoveAllChildren();
- _bulletsListBottom.RemoveAllChildren();
-
- if (_parent.MagazineCount == null)
- {
- _noMagazineLabel.Visible = true;
- return;
- }
-
- var (count, capacity) = _parent.MagazineCount.Value;
-
- _noMagazineLabel.Visible = false;
-
- string texturePath;
- if (capacity <= 20)
- {
- texturePath = "/Textures/Interface/ItemStatus/Bullets/normal.png";
- }
- else if (capacity <= 30)
- {
- texturePath = "/Textures/Interface/ItemStatus/Bullets/small.png";
- }
- else
- {
- texturePath = "/Textures/Interface/ItemStatus/Bullets/tiny.png";
- }
-
- var texture = StaticIoC.ResC.GetTexture(texturePath);
-
- const int tinyMaxRow = 60;
-
- if (capacity > tinyMaxRow)
- {
- FillBulletRow(_bulletsListBottom, Math.Min(tinyMaxRow, count), tinyMaxRow, texture);
- FillBulletRow(_bulletsListTop, Math.Max(0, count - tinyMaxRow), capacity - tinyMaxRow, texture);
- }
- else
- {
- FillBulletRow(_bulletsListBottom, count, capacity, texture);
- }
- }
-
- private static void FillBulletRow(Control container, int count, int capacity, Texture texture)
- {
- var colorA = Color.FromHex("#b68f0e");
- var colorB = Color.FromHex("#d7df60");
- var colorGoneA = Color.FromHex("#000000");
- var colorGoneB = Color.FromHex("#222222");
-
- var altColor = false;
-
- for (var i = count; i < capacity; i++)
- {
- container.AddChild(new TextureRect
- {
- Texture = texture,
- ModulateSelfOverride = altColor ? colorGoneA : colorGoneB
- });
-
- altColor ^= true;
- }
-
- for (var i = 0; i < count; i++)
- {
- container.AddChild(new TextureRect
- {
- Texture = texture,
- ModulateSelfOverride = altColor ? colorA : colorB
- });
-
- altColor ^= true;
- }
- }
- }
- }
-}
diff --git a/Content.Client/Weapons/Ranged/Barrels/Components/ClientMagazineBarrelComponent.cs b/Content.Client/Weapons/Ranged/Barrels/Components/ClientMagazineBarrelComponent.cs
deleted file mode 100644
index f11adce7b5..0000000000
--- a/Content.Client/Weapons/Ranged/Barrels/Components/ClientMagazineBarrelComponent.cs
+++ /dev/null
@@ -1,248 +0,0 @@
-using System;
-using Content.Client.IoC;
-using Content.Client.Items.Components;
-using Content.Client.Resources;
-using Content.Client.Stylesheets;
-using Content.Shared.Weapons.Ranged.Barrels.Components;
-using Robust.Client.Animations;
-using Robust.Client.Graphics;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Shared.Animations;
-using Robust.Shared.GameObjects;
-using Robust.Shared.GameStates;
-using Robust.Shared.Maths;
-using Robust.Shared.Serialization.Manager.Attributes;
-using Robust.Shared.ViewVariables;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
-
-namespace Content.Client.Weapons.Ranged.Barrels.Components
-{
- [RegisterComponent]
- [NetworkedComponent()]
- public sealed class ClientMagazineBarrelComponent : Component, IItemStatus
- {
- private static readonly Animation AlarmAnimationSmg = new()
- {
- Length = TimeSpan.FromSeconds(1.4),
- AnimationTracks =
- {
- new AnimationTrackControlProperty
- {
- // These timings match the SMG audio file.
- Property = nameof(Label.FontColorOverride),
- InterpolationMode = AnimationInterpolationMode.Previous,
- KeyFrames =
- {
- new AnimationTrackProperty.KeyFrame(Color.Red, 0.1f),
- new AnimationTrackProperty.KeyFrame(null!, 0.3f),
- new AnimationTrackProperty.KeyFrame(Color.Red, 0.2f),
- new AnimationTrackProperty.KeyFrame(null!, 0.3f),
- new AnimationTrackProperty.KeyFrame(Color.Red, 0.2f),
- new AnimationTrackProperty.KeyFrame(null!, 0.3f),
- }
- }
- }
- };
-
- private static readonly Animation AlarmAnimationLmg = new()
- {
- Length = TimeSpan.FromSeconds(0.75),
- AnimationTracks =
- {
- new AnimationTrackControlProperty
- {
- // These timings match the SMG audio file.
- Property = nameof(Label.FontColorOverride),
- InterpolationMode = AnimationInterpolationMode.Previous,
- KeyFrames =
- {
- new AnimationTrackProperty.KeyFrame(Color.Red, 0.0f),
- new AnimationTrackProperty.KeyFrame(null!, 0.15f),
- new AnimationTrackProperty.KeyFrame(Color.Red, 0.15f),
- new AnimationTrackProperty.KeyFrame(null!, 0.15f),
- new AnimationTrackProperty.KeyFrame(Color.Red, 0.15f),
- new AnimationTrackProperty.KeyFrame(null!, 0.15f),
- }
- }
- }
- };
- private StatusControl? _statusControl;
-
- ///
- /// True if a bullet is chambered.
- ///
- [ViewVariables]
- public bool Chambered { get; private set; }
-
- ///
- /// Count of bullets in the magazine.
- ///
- ///
- /// Null if no magazine is inserted.
- ///
- [ViewVariables]
- public (int count, int max)? MagazineCount { get; private set; }
-
- [ViewVariables(VVAccess.ReadWrite)] [DataField("lmg_alarm_animation")] private bool _isLmgAlarmAnimation = default;
-
- public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
- {
- base.HandleComponentState(curState, nextState);
-
- if (curState is not MagazineBarrelComponentState cast)
- return;
-
- Chambered = cast.Chambered;
- MagazineCount = cast.Magazine;
- _statusControl?.Update();
- }
-
- public void PlayAlarmAnimation()
- {
- _statusControl?.PlayAlarmAnimation();
- }
-
- public Control MakeControl()
- {
- _statusControl = new StatusControl(this);
- _statusControl.Update();
- return _statusControl;
- }
-
- public void DestroyControl(Control control)
- {
- if (_statusControl == control)
- {
- _statusControl = null;
- }
- }
-
- private sealed class StatusControl : Control
- {
- private readonly ClientMagazineBarrelComponent _parent;
- private readonly BoxContainer _bulletsList;
- private readonly TextureRect _chamberedBullet;
- private readonly Label _noMagazineLabel;
- private readonly Label _ammoCount;
-
- public StatusControl(ClientMagazineBarrelComponent parent)
- {
- MinHeight = 15;
- _parent = parent;
- HorizontalExpand = true;
- VerticalAlignment = VAlignment.Center;
-
- AddChild(new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- HorizontalExpand = true,
- Children =
- {
- (_chamberedBullet = new TextureRect
- {
- Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/chambered_rotated.png"),
- VerticalAlignment = VAlignment.Center,
- HorizontalAlignment = HAlignment.Right,
- }),
- new Control() { MinSize = (5,0) },
- new Control
- {
- HorizontalExpand = true,
- Children =
- {
- (_bulletsList = new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- VerticalAlignment = VAlignment.Center,
- SeparationOverride = 0
- }),
- (_noMagazineLabel = new Label
- {
- Text = "No Magazine!",
- StyleClasses = {StyleNano.StyleClassItemStatus}
- })
- }
- },
- new Control() { MinSize = (5,0) },
- (_ammoCount = new Label
- {
- StyleClasses = {StyleNano.StyleClassItemStatus},
- HorizontalAlignment = HAlignment.Right,
- }),
- }
- });
- }
-
- public void Update()
- {
- _chamberedBullet.ModulateSelfOverride =
- _parent.Chambered ? Color.FromHex("#d7df60") : Color.Black;
-
- _bulletsList.RemoveAllChildren();
-
- if (_parent.MagazineCount == null)
- {
- _noMagazineLabel.Visible = true;
- _ammoCount.Visible = false;
- return;
- }
-
- var (count, capacity) = _parent.MagazineCount.Value;
-
- _noMagazineLabel.Visible = false;
- _ammoCount.Visible = true;
-
- var texturePath = "/Textures/Interface/ItemStatus/Bullets/normal.png";
- var texture = StaticIoC.ResC.GetTexture(texturePath);
-
- _ammoCount.Text = $"x{count:00}";
- capacity = Math.Min(capacity, 20);
- FillBulletRow(_bulletsList, count, capacity, texture);
- }
-
- private static void FillBulletRow(Control container, int count, int capacity, Texture texture)
- {
- var colorA = Color.FromHex("#b68f0e");
- var colorB = Color.FromHex("#d7df60");
- var colorGoneA = Color.FromHex("#000000");
- var colorGoneB = Color.FromHex("#222222");
-
- var altColor = false;
-
- // Draw the empty ones
- for (var i = count; i < capacity; i++)
- {
- container.AddChild(new TextureRect
- {
- Texture = texture,
- ModulateSelfOverride = altColor ? colorGoneA : colorGoneB,
- Stretch = TextureRect.StretchMode.KeepCentered
- });
-
- altColor ^= true;
- }
-
- // Draw the full ones, but limit the count to the capacity
- count = Math.Min(count, capacity);
- for (var i = 0; i < count; i++)
- {
- container.AddChild(new TextureRect
- {
- Texture = texture,
- ModulateSelfOverride = altColor ? colorA : colorB,
- Stretch = TextureRect.StretchMode.KeepCentered
- });
-
- altColor ^= true;
- }
- }
-
- public void PlayAlarmAnimation()
- {
- var animation = _parent._isLmgAlarmAnimation ? AlarmAnimationLmg : AlarmAnimationSmg;
- _noMagazineLabel.PlayAnimation(animation, "alarm");
- }
- }
- }
-}
diff --git a/Content.Client/Weapons/Ranged/Barrels/Components/ClientPumpBarrelComponent.cs b/Content.Client/Weapons/Ranged/Barrels/Components/ClientPumpBarrelComponent.cs
deleted file mode 100644
index 5d103819f7..0000000000
--- a/Content.Client/Weapons/Ranged/Barrels/Components/ClientPumpBarrelComponent.cs
+++ /dev/null
@@ -1,212 +0,0 @@
-using System;
-using Content.Client.IoC;
-using Content.Client.Items.Components;
-using Content.Client.Resources;
-using Content.Client.Stylesheets;
-using Content.Shared.Weapons.Ranged.Barrels.Components;
-using Robust.Client.Graphics;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Shared.GameObjects;
-using Robust.Shared.GameStates;
-using Robust.Shared.Maths;
-using Robust.Shared.ViewVariables;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
-
-namespace Content.Client.Weapons.Ranged.Barrels.Components
-{
- [RegisterComponent]
- [NetworkedComponent()]
- public sealed class ClientPumpBarrelComponent : Component, IItemStatus
- {
- private StatusControl? _statusControl;
-
- ///
- /// chambered is true when a bullet is chambered
- /// spent is true when the chambered bullet is spent
- ///
- [ViewVariables]
- public (bool chambered, bool spent) Chamber { get; private set; }
-
- ///
- /// Count of bullets in the magazine.
- ///
- ///
- /// Null if no magazine is inserted.
- ///
- [ViewVariables]
- public (int count, int max)? MagazineCount { get; private set; }
-
- public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
- {
- base.HandleComponentState(curState, nextState);
-
- if (curState is not PumpBarrelComponentState cast)
- return;
-
- Chamber = cast.Chamber;
- MagazineCount = cast.Magazine;
- _statusControl?.Update();
- }
-
- public Control MakeControl()
- {
- _statusControl = new StatusControl(this);
- _statusControl.Update();
- return _statusControl;
- }
-
- public void DestroyControl(Control control)
- {
- if (_statusControl == control)
- {
- _statusControl = null;
- }
- }
-
- private sealed class StatusControl : Control
- {
- private readonly ClientPumpBarrelComponent _parent;
- private readonly BoxContainer _bulletsListTop;
- private readonly BoxContainer _bulletsListBottom;
- private readonly TextureRect _chamberedBullet;
- private readonly Label _noMagazineLabel;
-
- public StatusControl(ClientPumpBarrelComponent parent)
- {
- MinHeight = 15;
- _parent = parent;
- HorizontalExpand = true;
- VerticalAlignment = VAlignment.Center;
- AddChild(new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical,
- HorizontalExpand = true,
- VerticalAlignment = VAlignment.Center,
- SeparationOverride = 0,
- Children =
- {
- (_bulletsListTop = new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- SeparationOverride = 0
- }),
- new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- HorizontalExpand = true,
- Children =
- {
- new Control
- {
- HorizontalExpand = true,
- Children =
- {
- (_bulletsListBottom = new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- VerticalAlignment = VAlignment.Center,
- SeparationOverride = 0
- }),
- (_noMagazineLabel = new Label
- {
- Text = "No Magazine!",
- StyleClasses = {StyleNano.StyleClassItemStatus}
- })
- }
- },
- (_chamberedBullet = new TextureRect
- {
- Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/chambered.png"),
- VerticalAlignment = VAlignment.Center,
- HorizontalAlignment = HAlignment.Right,
- })
- }
- }
- }
- });
- }
-
- public void Update()
- {
- _chamberedBullet.ModulateSelfOverride =
- _parent.Chamber.chambered ?
- _parent.Chamber.spent ? Color.Red : Color.FromHex("#d7df60")
- : Color.Black;
-
- _bulletsListTop.RemoveAllChildren();
- _bulletsListBottom.RemoveAllChildren();
-
- if (_parent.MagazineCount == null)
- {
- _noMagazineLabel.Visible = true;
- return;
- }
-
- var (count, capacity) = _parent.MagazineCount.Value;
-
- _noMagazineLabel.Visible = false;
-
- string texturePath;
- if (capacity <= 20)
- {
- texturePath = "/Textures/Interface/ItemStatus/Bullets/normal.png";
- }
- else if (capacity <= 30)
- {
- texturePath = "/Textures/Interface/ItemStatus/Bullets/small.png";
- }
- else
- {
- texturePath = "/Textures/Interface/ItemStatus/Bullets/tiny.png";
- }
-
- var texture = StaticIoC.ResC.GetTexture(texturePath);
-
- const int tinyMaxRow = 60;
-
- if (capacity > tinyMaxRow)
- {
- FillBulletRow(_bulletsListBottom, Math.Min(tinyMaxRow, count), tinyMaxRow, texture);
- FillBulletRow(_bulletsListTop, Math.Max(0, count - tinyMaxRow), capacity - tinyMaxRow, texture);
- }
- else
- {
- FillBulletRow(_bulletsListBottom, count, capacity, texture);
- }
- }
-
- private static void FillBulletRow(Control container, int count, int capacity, Texture texture)
- {
- var colorA = Color.FromHex("#b68f0e");
- var colorB = Color.FromHex("#d7df60");
- var colorGoneA = Color.FromHex("#000000");
- var colorGoneB = Color.FromHex("#222222");
-
- var altColor = false;
-
- for (var i = count; i < capacity; i++)
- {
- container.AddChild(new TextureRect
- {
- Texture = texture,
- ModulateSelfOverride = altColor ? colorGoneA : colorGoneB
- });
-
- altColor ^= true;
- }
-
- for (var i = 0; i < count; i++)
- {
- container.AddChild(new TextureRect
- {
- Texture = texture,
- ModulateSelfOverride = altColor ? colorA : colorB
- });
-
- altColor ^= true;
- }
- }
- }
- }
-}
diff --git a/Content.Client/Weapons/Ranged/Barrels/Components/ClientRevolverBarrelComponent.cs b/Content.Client/Weapons/Ranged/Barrels/Components/ClientRevolverBarrelComponent.cs
deleted file mode 100644
index 8ddd3999bc..0000000000
--- a/Content.Client/Weapons/Ranged/Barrels/Components/ClientRevolverBarrelComponent.cs
+++ /dev/null
@@ -1,168 +0,0 @@
-using Content.Client.IoC;
-using Content.Client.Items.Components;
-using Content.Client.Resources;
-using Content.Shared.Weapons.Ranged.Barrels.Components;
-using Robust.Client.Graphics;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Shared.GameObjects;
-using Robust.Shared.GameStates;
-using Robust.Shared.Maths;
-using Robust.Shared.ViewVariables;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
-
-namespace Content.Client.Weapons.Ranged.Barrels.Components
-{
- [RegisterComponent]
- [NetworkedComponent()]
- public sealed class ClientRevolverBarrelComponent : Component, IItemStatus
- {
- private StatusControl? _statusControl;
-
- ///
- /// A array that lists the bullet states
- /// true means a spent bullet
- /// false means a "shootable" bullet
- /// null means no bullet
- ///
- [ViewVariables]
- public bool?[] Bullets { get; private set; } = new bool?[0];
-
- [ViewVariables]
- public int CurrentSlot { get; private set; }
-
- public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
- {
- base.HandleComponentState(curState, nextState);
-
- if (curState is not RevolverBarrelComponentState cast)
- return;
-
- CurrentSlot = cast.CurrentSlot;
- Bullets = cast.Bullets;
- _statusControl?.Update();
- }
-
- public Control MakeControl()
- {
- _statusControl = new StatusControl(this);
- _statusControl.Update();
- return _statusControl;
- }
-
- public void DestroyControl(Control control)
- {
- if (_statusControl == control)
- {
- _statusControl = null;
- }
- }
-
- private sealed class StatusControl : Control
- {
- private readonly ClientRevolverBarrelComponent _parent;
- private readonly BoxContainer _bulletsList;
-
- public StatusControl(ClientRevolverBarrelComponent parent)
- {
- MinHeight = 15;
- _parent = parent;
- HorizontalExpand = true;
- VerticalAlignment = VAlignment.Center;
- AddChild((_bulletsList = new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- HorizontalExpand = true,
- VerticalAlignment = VAlignment.Center,
- SeparationOverride = 0
- }));
- }
-
- public void Update()
- {
- _bulletsList.RemoveAllChildren();
-
- var capacity = _parent.Bullets.Length;
-
- string texturePath;
- if (capacity <= 20)
- {
- texturePath = "/Textures/Interface/ItemStatus/Bullets/normal.png";
- }
- else if (capacity <= 30)
- {
- texturePath = "/Textures/Interface/ItemStatus/Bullets/small.png";
- }
- else
- {
- texturePath = "/Textures/Interface/ItemStatus/Bullets/tiny.png";
- }
-
- var texture = StaticIoC.ResC.GetTexture(texturePath);
- var spentTexture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/empty.png");
-
- FillBulletRow(_bulletsList, texture, spentTexture);
- }
-
- private void FillBulletRow(Control container, Texture texture, Texture emptyTexture)
- {
- var colorA = Color.FromHex("#b68f0e");
- var colorB = Color.FromHex("#d7df60");
- var colorSpentA = Color.FromHex("#b50e25");
- var colorSpentB = Color.FromHex("#d3745f");
- var colorGoneA = Color.FromHex("#000000");
- var colorGoneB = Color.FromHex("#222222");
-
- var altColor = false;
- var scale = 1.3f;
-
- for (var i = 0; i < _parent.Bullets.Length; i++)
- {
- var bulletSpent = _parent.Bullets[i];
- // Add a outline
- var box = new Control()
- {
- MinSize = texture.Size * scale,
- };
- if (i == _parent.CurrentSlot)
- {
- box.AddChild(new TextureRect
- {
- Texture = texture,
- TextureScale = (scale, scale),
- ModulateSelfOverride = Color.LimeGreen,
- });
- }
- Color color;
- Texture bulletTexture = texture;
-
- if (bulletSpent.HasValue)
- {
- if (bulletSpent.Value)
- {
- color = altColor ? colorSpentA : colorSpentB;
- bulletTexture = emptyTexture;
- }
- else
- {
- color = altColor ? colorA : colorB;
- }
- }
- else
- {
- color = altColor ? colorGoneA : colorGoneB;
- }
-
- box.AddChild(new TextureRect
- {
- Stretch = TextureRect.StretchMode.KeepCentered,
- Texture = bulletTexture,
- ModulateSelfOverride = color,
- });
- altColor ^= true;
- container.AddChild(box);
- }
- }
- }
- }
-}
diff --git a/Content.Client/Weapons/Ranged/Barrels/EntitySystems/ClientBatteryBarrelSystem.cs b/Content.Client/Weapons/Ranged/Barrels/EntitySystems/ClientBatteryBarrelSystem.cs
deleted file mode 100644
index 19523712ab..0000000000
--- a/Content.Client/Weapons/Ranged/Barrels/EntitySystems/ClientBatteryBarrelSystem.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using Content.Client.Weapons.Ranged.Barrels.Components;
-using Robust.Client.GameObjects;
-
-namespace Content.Client.Weapons.Ranged.Barrels.EntitySystems;
-
-public sealed class ClientBatteryBarrelSystem : EntitySystem
-{
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnAppearanceChange);
- }
-
- private void OnAppearanceChange(EntityUid uid, ClientBatteryBarrelComponent component, ref AppearanceChangeEvent args)
- {
- component.ItemStatus?.Update(args.Component);
- }
-}
diff --git a/Content.Client/Weapons/Ranged/Barrels/Visualizers/BarrelBoltVisualizer.cs b/Content.Client/Weapons/Ranged/Barrels/Visualizers/BarrelBoltVisualizer.cs
deleted file mode 100644
index cce9bbdac8..0000000000
--- a/Content.Client/Weapons/Ranged/Barrels/Visualizers/BarrelBoltVisualizer.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using Content.Shared.Weapons.Ranged.Barrels.Components;
-using JetBrains.Annotations;
-using Robust.Client.GameObjects;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-
-namespace Content.Client.Weapons.Ranged.Barrels.Visualizers
-{
- [UsedImplicitly]
- public sealed class BarrelBoltVisualizer : AppearanceVisualizer
- {
- public override void InitializeEntity(EntityUid entity)
- {
- base.InitializeEntity(entity);
- var sprite = IoCManager.Resolve().GetComponent(entity);
- sprite.LayerSetState(RangedBarrelVisualLayers.Bolt, "bolt-open");
- }
-
- public override void OnChangeData(AppearanceComponent component)
- {
- base.OnChangeData(component);
- var sprite = IoCManager.Resolve().GetComponent(component.Owner);
-
- if (!component.TryGetData(BarrelBoltVisuals.BoltOpen, out bool boltOpen))
- {
- return;
- }
-
- if (boltOpen)
- {
- sprite.LayerSetState(RangedBarrelVisualLayers.Bolt, "bolt-open");
- }
- else
- {
- sprite.LayerSetState(RangedBarrelVisualLayers.Bolt, "bolt-closed");
- }
- }
- }
-}
diff --git a/Content.Client/Weapons/Ranged/Barrels/Visualizers/MagVisualizer.cs b/Content.Client/Weapons/Ranged/Barrels/Visualizers/MagVisualizer.cs
deleted file mode 100644
index 948c1aeff3..0000000000
--- a/Content.Client/Weapons/Ranged/Barrels/Visualizers/MagVisualizer.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-using Content.Shared.Rounding;
-using Content.Shared.Weapons.Ranged.Barrels.Components;
-using JetBrains.Annotations;
-using Robust.Client.GameObjects;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Serialization.Manager.Attributes;
-
-namespace Content.Client.Weapons.Ranged.Barrels.Visualizers
-{
- [UsedImplicitly]
- public sealed class MagVisualizer : AppearanceVisualizer
- {
- private bool _magLoaded;
- [DataField("magState")]
- private string? _magState;
- [DataField("steps")]
- private int _magSteps;
- [DataField("zeroVisible")]
- private bool _zeroVisible;
-
- public override void InitializeEntity(EntityUid entity)
- {
- base.InitializeEntity(entity);
- var sprite = IoCManager.Resolve().GetComponent(entity);
-
- if (sprite.LayerMapTryGet(RangedBarrelVisualLayers.Mag, out _))
- {
- sprite.LayerSetState(RangedBarrelVisualLayers.Mag, $"{_magState}-{_magSteps-1}");
- sprite.LayerSetVisible(RangedBarrelVisualLayers.Mag, false);
- }
-
- if (sprite.LayerMapTryGet(RangedBarrelVisualLayers.MagUnshaded, out _))
- {
- sprite.LayerSetState(RangedBarrelVisualLayers.MagUnshaded, $"{_magState}-unshaded-{_magSteps-1}");
- sprite.LayerSetVisible(RangedBarrelVisualLayers.MagUnshaded, false);
- }
- }
-
- public override void OnChangeData(AppearanceComponent component)
- {
- base.OnChangeData(component);
-
- // tl;dr
- // 1.If no mag then hide it OR
- // 2. If step 0 isn't visible then hide it (mag or unshaded)
- // 3. Otherwise just do mag / unshaded as is
- var sprite = IoCManager.Resolve().GetComponent(component.Owner);
-
- component.TryGetData(MagazineBarrelVisuals.MagLoaded, out _magLoaded);
-
- if (_magLoaded)
- {
- if (!component.TryGetData(AmmoVisuals.AmmoMax, out int capacity))
- {
- return;
- }
- if (!component.TryGetData(AmmoVisuals.AmmoCount, out int current))
- {
- return;
- }
-
- var step = ContentHelpers.RoundToLevels(current, capacity, _magSteps);
-
- if (step == 0 && !_zeroVisible)
- {
- if (sprite.LayerMapTryGet(RangedBarrelVisualLayers.Mag, out _))
- {
- sprite.LayerSetVisible(RangedBarrelVisualLayers.Mag, false);
- }
-
- if (sprite.LayerMapTryGet(RangedBarrelVisualLayers.MagUnshaded, out _))
- {
- sprite.LayerSetVisible(RangedBarrelVisualLayers.MagUnshaded, false);
- }
-
- return;
- }
-
- if (sprite.LayerMapTryGet(RangedBarrelVisualLayers.Mag, out _))
- {
- sprite.LayerSetVisible(RangedBarrelVisualLayers.Mag, true);
- sprite.LayerSetState(RangedBarrelVisualLayers.Mag, $"{_magState}-{step}");
- }
-
- if (sprite.LayerMapTryGet(RangedBarrelVisualLayers.MagUnshaded, out _))
- {
- sprite.LayerSetVisible(RangedBarrelVisualLayers.MagUnshaded, true);
- sprite.LayerSetState(RangedBarrelVisualLayers.MagUnshaded, $"{_magState}-unshaded-{step}");
- }
- }
- else
- {
- if (sprite.LayerMapTryGet(RangedBarrelVisualLayers.Mag, out _))
- {
- sprite.LayerSetVisible(RangedBarrelVisualLayers.Mag, false);
- }
-
- if (sprite.LayerMapTryGet(RangedBarrelVisualLayers.MagUnshaded, out _))
- {
- sprite.LayerSetVisible(RangedBarrelVisualLayers.MagUnshaded, false);
- }
- }
- }
- }
-}
diff --git a/Content.Client/Weapons/Ranged/Barrels/Visualizers/SpentAmmoVisualizer.cs b/Content.Client/Weapons/Ranged/Barrels/Visualizers/SpentAmmoVisualizer.cs
deleted file mode 100644
index 10602a6644..0000000000
--- a/Content.Client/Weapons/Ranged/Barrels/Visualizers/SpentAmmoVisualizer.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using Content.Shared.Weapons.Ranged.Barrels.Components;
-using JetBrains.Annotations;
-using Robust.Client.GameObjects;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-
-namespace Content.Client.Weapons.Ranged.Barrels.Visualizers
-{
- [UsedImplicitly]
- public sealed class SpentAmmoVisualizer : AppearanceVisualizer
- {
- public override void OnChangeData(AppearanceComponent component)
- {
- base.OnChangeData(component);
- var sprite = IoCManager.Resolve().GetComponent(component.Owner);
-
- if (!component.TryGetData(AmmoVisuals.Spent, out bool spent))
- {
- return;
- }
-
- sprite.LayerSetState(AmmoVisualLayers.Base, spent ? "spent" : "base");
- }
- }
-
- public enum AmmoVisualLayers : byte
- {
- Base,
- }
-}
diff --git a/Content.Client/Weapons/Ranged/ClientRangedWeaponComponent.cs b/Content.Client/Weapons/Ranged/ClientRangedWeaponComponent.cs
deleted file mode 100644
index 8ce2c638ec..0000000000
--- a/Content.Client/Weapons/Ranged/ClientRangedWeaponComponent.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using Content.Shared.Weapons.Ranged.Components;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Map;
-using Robust.Shared.Maths;
-
-namespace Content.Client.Weapons.Ranged
-{
- // Yeah I put it all in the same enum, don't judge me
- public enum RangedBarrelVisualLayers : byte
- {
- Base,
- BaseUnshaded,
- Bolt,
- Mag,
- MagUnshaded,
- }
-
- [RegisterComponent]
- public sealed class ClientRangedWeaponComponent : SharedRangedWeaponComponent
- {
- public FireRateSelector FireRateSelector { get; private set; } = FireRateSelector.Automatic;
-
- public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
- {
- base.HandleComponentState(curState, nextState);
- if (curState is not RangedWeaponComponentState rangedState)
- {
- return;
- }
-
- FireRateSelector = rangedState.FireRateSelector;
- }
- }
-}
diff --git a/Content.Client/Weapons/Ranged/TetherGunCommand.cs b/Content.Client/Weapons/Ranged/Commands/TetherGunCommand.cs
similarity index 93%
rename from Content.Client/Weapons/Ranged/TetherGunCommand.cs
rename to Content.Client/Weapons/Ranged/Commands/TetherGunCommand.cs
index 4a9609addf..f932eeb8a0 100644
--- a/Content.Client/Weapons/Ranged/TetherGunCommand.cs
+++ b/Content.Client/Weapons/Ranged/Commands/TetherGunCommand.cs
@@ -1,3 +1,4 @@
+using Content.Client.Weapons.Ranged.Systems;
using Robust.Shared.Console;
namespace Content.Client.Weapons.Ranged;
diff --git a/Content.Client/Weapons/Ranged/Components/AmmoCounterComponent.cs b/Content.Client/Weapons/Ranged/Components/AmmoCounterComponent.cs
new file mode 100644
index 0000000000..22742219c0
--- /dev/null
+++ b/Content.Client/Weapons/Ranged/Components/AmmoCounterComponent.cs
@@ -0,0 +1,10 @@
+using Content.Shared.Weapons.Ranged.Components;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Weapons.Ranged.Components;
+
+[RegisterComponent]
+public sealed class AmmoCounterComponent : SharedAmmoCounterComponent
+{
+ public Control? Control;
+}
diff --git a/Content.Client/Weapons/Ranged/Components/MagVisualizer.cs b/Content.Client/Weapons/Ranged/Components/MagVisualizer.cs
new file mode 100644
index 0000000000..00f04a6f87
--- /dev/null
+++ b/Content.Client/Weapons/Ranged/Components/MagVisualizer.cs
@@ -0,0 +1,107 @@
+using Content.Shared.Rounding;
+using Content.Shared.Weapons.Ranged.Systems;
+using JetBrains.Annotations;
+using Robust.Client.GameObjects;
+using SharedGunSystem = Content.Shared.Weapons.Ranged.Systems.SharedGunSystem;
+
+namespace Content.Client.Weapons.Ranged.Components;
+
+[UsedImplicitly]
+public sealed class MagVisualizer : AppearanceVisualizer
+{
+ [DataField("magState")] private string? _magState;
+ [DataField("steps")] private int _magSteps;
+ [DataField("zeroVisible")] private bool _zeroVisible;
+
+ public override void InitializeEntity(EntityUid entity)
+ {
+ base.InitializeEntity(entity);
+ var sprite = IoCManager.Resolve().GetComponent(entity);
+
+ if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out _))
+ {
+ sprite.LayerSetState(GunVisualLayers.Mag, $"{_magState}-{_magSteps - 1}");
+ sprite.LayerSetVisible(GunVisualLayers.Mag, false);
+ }
+
+ if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out _))
+ {
+ sprite.LayerSetState(GunVisualLayers.MagUnshaded, $"{_magState}-unshaded-{_magSteps - 1}");
+ sprite.LayerSetVisible(GunVisualLayers.MagUnshaded, false);
+ }
+ }
+
+ public override void OnChangeData(AppearanceComponent component)
+ {
+ base.OnChangeData(component);
+
+ // tl;dr
+ // 1.If no mag then hide it OR
+ // 2. If step 0 isn't visible then hide it (mag or unshaded)
+ // 3. Otherwise just do mag / unshaded as is
+ var sprite = IoCManager.Resolve().GetComponent(component.Owner);
+
+ if (!component.TryGetData(AmmoVisuals.MagLoaded, out bool magloaded) ||
+ magloaded)
+ {
+ if (!component.TryGetData(AmmoVisuals.AmmoMax, out int capacity))
+ {
+ capacity = _magSteps;
+ }
+
+ if (!component.TryGetData(AmmoVisuals.AmmoCount, out int current))
+ {
+ current = _magSteps;
+ }
+
+ var step = ContentHelpers.RoundToLevels(current, capacity, _magSteps);
+
+ if (step == 0 && !_zeroVisible)
+ {
+ if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out _))
+ {
+ sprite.LayerSetVisible(GunVisualLayers.Mag, false);
+ }
+
+ if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out _))
+ {
+ sprite.LayerSetVisible(GunVisualLayers.MagUnshaded, false);
+ }
+
+ return;
+ }
+
+ if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out _))
+ {
+ sprite.LayerSetVisible(GunVisualLayers.Mag, true);
+ sprite.LayerSetState(GunVisualLayers.Mag, $"{_magState}-{step}");
+ }
+
+ if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out _))
+ {
+ sprite.LayerSetVisible(GunVisualLayers.MagUnshaded, true);
+ sprite.LayerSetState(GunVisualLayers.MagUnshaded, $"{_magState}-unshaded-{step}");
+ }
+ }
+ else
+ {
+ if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out _))
+ {
+ sprite.LayerSetVisible(GunVisualLayers.Mag, false);
+ }
+
+ if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out _))
+ {
+ sprite.LayerSetVisible(GunVisualLayers.MagUnshaded, false);
+ }
+ }
+ }
+}
+
+public enum GunVisualLayers : byte
+{
+ Base,
+ BaseUnshaded,
+ Mag,
+ MagUnshaded,
+}
diff --git a/Content.Client/Weapons/Ranged/Components/SpentAmmoVisualsComponent.cs b/Content.Client/Weapons/Ranged/Components/SpentAmmoVisualsComponent.cs
new file mode 100644
index 0000000000..e14263cf88
--- /dev/null
+++ b/Content.Client/Weapons/Ranged/Components/SpentAmmoVisualsComponent.cs
@@ -0,0 +1,20 @@
+using Content.Client.Weapons.Ranged.Systems;
+
+namespace Content.Client.Weapons.Ranged.Components;
+
+[RegisterComponent, Friend(typeof(GunSystem))]
+public sealed class SpentAmmoVisualsComponent : Component
+{
+ ///
+ /// Should we do "{_state}-spent" or just "spent"
+ ///
+ [DataField("suffix")] public bool Suffix = true;
+
+ [DataField("state")]
+ public string State = "base";
+}
+
+public enum AmmoVisualLayers : byte
+{
+ Base,
+}
diff --git a/Content.Client/Weapons/Ranged/GunSystem.AmmoCounter.cs b/Content.Client/Weapons/Ranged/GunSystem.AmmoCounter.cs
deleted file mode 100644
index 8515d06a47..0000000000
--- a/Content.Client/Weapons/Ranged/GunSystem.AmmoCounter.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using Content.Client.Weapons.Ranged.Barrels.Components;
-using Content.Shared.Weapons.Ranged;
-using Robust.Client.Player;
-using Robust.Shared.Containers;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-
-namespace Content.Client.Weapons.Ranged;
-
-public sealed class GunSystem : EntitySystem
-{
- [Dependency] private readonly IPlayerManager _playerManager = default!;
- [Dependency] private readonly SharedContainerSystem _container = default!;
-
- public override void Initialize()
- {
- base.Initialize();
- SubscribeNetworkEvent(OnMagAutoEject);
- }
-
- private void OnMagAutoEject(MagazineAutoEjectEvent ev)
- {
- var player = _playerManager.LocalPlayer?.ControlledEntity;
-
- if (!TryComp(ev.Uid, out ClientMagazineBarrelComponent? mag) ||
- !_container.TryGetContainingContainer(ev.Uid, out var container) ||
- container.Owner != player) return;
-
- mag.PlayAlarmAnimation();
- }
-}
diff --git a/Content.Client/Weapons/Ranged/RangedWeaponSystem.cs b/Content.Client/Weapons/Ranged/RangedWeaponSystem.cs
deleted file mode 100644
index 1484aa7872..0000000000
--- a/Content.Client/Weapons/Ranged/RangedWeaponSystem.cs
+++ /dev/null
@@ -1,110 +0,0 @@
-using System;
-using Content.Client.CombatMode;
-using Content.Shared.Hands.Components;
-using Content.Shared.Weapons.Ranged.Components;
-using JetBrains.Annotations;
-using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
-using Robust.Client.Input;
-using Robust.Client.Player;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Input;
-using Robust.Shared.IoC;
-using Robust.Shared.Map;
-using Robust.Shared.Timing;
-
-namespace Content.Client.Weapons.Ranged
-{
- [UsedImplicitly]
- public sealed class RangedWeaponSystem : EntitySystem
- {
- [Dependency] private readonly IPlayerManager _playerManager = default!;
- [Dependency] private readonly IEyeManager _eyeManager = default!;
- [Dependency] private readonly IMapManager _mapManager = default!;
- [Dependency] private readonly IInputManager _inputManager = default!;
- [Dependency] private readonly IGameTiming _gameTiming = default!;
- [Dependency] private readonly InputSystem _inputSystem = default!;
- [Dependency] private readonly CombatModeSystem _combatModeSystem = default!;
-
- private bool _blocked;
- private int _shotCounter;
-
- public override void Initialize()
- {
- base.Initialize();
-
- UpdatesOutsidePrediction = true;
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- if (!_gameTiming.IsFirstTimePredicted)
- {
- return;
- }
-
- var state = _inputSystem.CmdStates.GetState(EngineKeyFunctions.Use);
- if (!_combatModeSystem.IsInCombatMode() || state != BoundKeyState.Down)
- {
- _shotCounter = 0;
- _blocked = false;
- return;
- }
-
- var entity = _playerManager.LocalPlayer?.ControlledEntity;
- if (!EntityManager.TryGetComponent(entity, out SharedHandsComponent? hands))
- {
- return;
- }
-
- if (hands.ActiveHandEntity is not EntityUid held || !EntityManager.TryGetComponent(held, out ClientRangedWeaponComponent? weapon))
- {
- _blocked = true;
- return;
- }
-
- switch (weapon.FireRateSelector)
- {
- case FireRateSelector.Safety:
- _blocked = true;
- return;
- case FireRateSelector.Single:
- if (_shotCounter >= 1)
- {
- _blocked = true;
- return;
- }
-
- break;
- case FireRateSelector.Automatic:
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
-
- if (_blocked)
- return;
-
- var mapCoordinates = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition);
- EntityCoordinates coordinates;
-
- if (_mapManager.TryFindGridAt(mapCoordinates, out var grid))
- {
- coordinates = EntityCoordinates.FromMap(grid.GridEntityId, mapCoordinates);
- }
- else
- {
- coordinates = EntityCoordinates.FromMap(_mapManager.GetMapEntityId(mapCoordinates.MapId), mapCoordinates);
- }
-
- SyncFirePos(coordinates);
- }
-
- private void SyncFirePos(EntityCoordinates coordinates)
- {
- RaiseNetworkEvent(new FirePosEvent(coordinates));
- }
- }
-}
diff --git a/Content.Client/Weapons/Ranged/FlyBySoundSystem.cs b/Content.Client/Weapons/Ranged/Systems/FlyBySoundSystem.cs
similarity index 89%
rename from Content.Client/Weapons/Ranged/FlyBySoundSystem.cs
rename to Content.Client/Weapons/Ranged/Systems/FlyBySoundSystem.cs
index 0a0beb08a8..da9184a3c3 100644
--- a/Content.Client/Weapons/Ranged/FlyBySoundSystem.cs
+++ b/Content.Client/Weapons/Ranged/Systems/FlyBySoundSystem.cs
@@ -1,12 +1,13 @@
using Content.Client.Projectiles;
-using Content.Shared.Weapons.Ranged;
+using Content.Shared.Weapons.Ranged.Components;
+using Content.Shared.Weapons.Ranged.Systems;
using Robust.Client.Player;
using Robust.Shared.Audio;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Player;
using Robust.Shared.Random;
-namespace Content.Client.Weapons.Ranged;
+namespace Content.Client.Weapons.Ranged.Systems;
public sealed class FlyBySoundSystem : SharedFlyBySoundSystem
{
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
new file mode 100644
index 0000000000..afb4229bb7
--- /dev/null
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
@@ -0,0 +1,513 @@
+using Content.Client.IoC;
+using Content.Client.Items;
+using Content.Client.Resources;
+using Content.Client.Stylesheets;
+using Content.Client.Weapons.Ranged.Components;
+using Robust.Client.Animations;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+
+namespace Content.Client.Weapons.Ranged.Systems;
+
+public sealed partial class GunSystem
+{
+ private void OnAmmoCounterCollect(EntityUid uid, AmmoCounterComponent component, ItemStatusCollectMessage args)
+ {
+ RefreshControl(uid, component);
+
+ if (component.Control != null)
+ args.Controls.Add(component.Control);
+ }
+
+ ///
+ /// Refreshes the control being used to show ammo. Useful if you change the AmmoProvider.
+ ///
+ ///
+ ///
+ private void RefreshControl(EntityUid uid, AmmoCounterComponent? component = null)
+ {
+ if (!Resolve(uid, ref component, false)) return;
+
+ component.Control?.Dispose();
+ component.Control = null;
+
+ var ev = new AmmoCounterControlEvent();
+ RaiseLocalEvent(uid, ev, false);
+
+ // Fallback to default if none specified
+ ev.Control ??= new DefaultStatusControl();
+
+ component.Control = ev.Control;
+ UpdateAmmoCount(uid, component);
+ }
+
+ private void UpdateAmmoCount(EntityUid uid, AmmoCounterComponent component)
+ {
+ if (component.Control == null) return;
+
+ var ev = new UpdateAmmoCounterEvent()
+ {
+ Control = component.Control
+ };
+
+ RaiseLocalEvent(uid, ev, false);
+ }
+
+ protected override void UpdateAmmoCount(EntityUid uid)
+ {
+ // Don't use resolves because the method is shared and there's no compref and I'm trying to
+ // share as much code as possible
+ if (!Timing.IsFirstTimePredicted ||
+ !TryComp(uid, out var clientComp)) return;
+
+ UpdateAmmoCount(uid, clientComp);
+ }
+
+ ///
+ /// Raised when an ammocounter is requesting a control.
+ ///
+ public sealed class AmmoCounterControlEvent : EntityEventArgs
+ {
+ public Control? Control;
+ }
+
+ ///
+ /// Raised whenever the ammo count / magazine for a control needs updating.
+ ///
+ public sealed class UpdateAmmoCounterEvent : HandledEntityEventArgs
+ {
+ public Control Control = default!;
+ }
+
+ #region Controls
+
+ private sealed class DefaultStatusControl : Control
+ {
+ private readonly BoxContainer _bulletsListTop;
+ private readonly BoxContainer _bulletsListBottom;
+
+ public DefaultStatusControl()
+ {
+ MinHeight = 15;
+ HorizontalExpand = true;
+ VerticalAlignment = VAlignment.Center;
+ AddChild(new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Vertical,
+ HorizontalExpand = true,
+ VerticalAlignment = VAlignment.Center,
+ SeparationOverride = 0,
+ Children =
+ {
+ (_bulletsListTop = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ SeparationOverride = 0
+ }),
+ new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ HorizontalExpand = true,
+ Children =
+ {
+ new Control
+ {
+ HorizontalExpand = true,
+ Children =
+ {
+ (_bulletsListBottom = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ VerticalAlignment = VAlignment.Center,
+ SeparationOverride = 0
+ }),
+ }
+ },
+ }
+ }
+ }
+ });
+ }
+
+ public void Update(int count, int capacity)
+ {
+ _bulletsListTop.RemoveAllChildren();
+ _bulletsListBottom.RemoveAllChildren();
+
+ string texturePath;
+ if (capacity <= 20)
+ {
+ texturePath = "/Textures/Interface/ItemStatus/Bullets/normal.png";
+ }
+ else if (capacity <= 30)
+ {
+ texturePath = "/Textures/Interface/ItemStatus/Bullets/small.png";
+ }
+ else
+ {
+ texturePath = "/Textures/Interface/ItemStatus/Bullets/tiny.png";
+ }
+
+ var texture = StaticIoC.ResC.GetTexture(texturePath);
+
+ const int tinyMaxRow = 60;
+
+ if (capacity > tinyMaxRow)
+ {
+ FillBulletRow(_bulletsListBottom, Math.Min(tinyMaxRow, count), tinyMaxRow, texture);
+ FillBulletRow(_bulletsListTop, Math.Max(0, count - tinyMaxRow), capacity - tinyMaxRow, texture);
+ }
+ else
+ {
+ FillBulletRow(_bulletsListBottom, count, capacity, texture);
+ }
+ }
+
+ private static void FillBulletRow(Control container, int count, int capacity, Texture texture)
+ {
+ var colorA = Color.FromHex("#b68f0e");
+ var colorB = Color.FromHex("#d7df60");
+ var colorGoneA = Color.FromHex("#000000");
+ var colorGoneB = Color.FromHex("#222222");
+
+ var altColor = false;
+
+ for (var i = count; i < capacity; i++)
+ {
+ container.AddChild(new TextureRect
+ {
+ Texture = texture,
+ ModulateSelfOverride = altColor ? colorGoneA : colorGoneB
+ });
+
+ altColor ^= true;
+ }
+
+ for (var i = 0; i < count; i++)
+ {
+ container.AddChild(new TextureRect
+ {
+ Texture = texture,
+ ModulateSelfOverride = altColor ? colorA : colorB
+ });
+
+ altColor ^= true;
+ }
+ }
+ }
+
+ public sealed class BoxesStatusControl : Control
+ {
+ private readonly BoxContainer _bulletsList;
+ private readonly Label _ammoCount;
+
+ public BoxesStatusControl()
+ {
+ MinHeight = 15;
+ HorizontalExpand = true;
+ VerticalAlignment = VAlignment.Center;
+
+ AddChild(new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ HorizontalExpand = true,
+ Children =
+ {
+ new Control
+ {
+ HorizontalExpand = true,
+ Children =
+ {
+ (_bulletsList = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ VerticalAlignment = VAlignment.Center,
+ SeparationOverride = 4
+ }),
+ }
+ },
+ new Control() { MinSize = (5, 0) },
+ (_ammoCount = new Label
+ {
+ StyleClasses = { StyleNano.StyleClassItemStatus },
+ HorizontalAlignment = HAlignment.Right,
+ }),
+ }
+ });
+ }
+
+ public void Update(int count, int max)
+ {
+ _bulletsList.RemoveAllChildren();
+
+ _ammoCount.Visible = true;
+
+ _ammoCount.Text = $"x{count:00}";
+ max = Math.Min(max, 8);
+ FillBulletRow(_bulletsList, count, max);
+ }
+
+ private static void FillBulletRow(Control container, int count, int capacity)
+ {
+ var colorGone = Color.FromHex("#000000");
+ var color = Color.FromHex("#E00000");
+
+ // Draw the empty ones
+ for (var i = count; i < capacity; i++)
+ {
+ container.AddChild(new PanelContainer
+ {
+ PanelOverride = new StyleBoxFlat()
+ {
+ BackgroundColor = colorGone,
+ },
+ MinSize = (10, 15),
+ });
+ }
+
+ // Draw the full ones, but limit the count to the capacity
+ count = Math.Min(count, capacity);
+ for (var i = 0; i < count; i++)
+ {
+ container.AddChild(new PanelContainer
+ {
+ PanelOverride = new StyleBoxFlat()
+ {
+ BackgroundColor = color,
+ },
+ MinSize = (10, 15),
+ });
+ }
+ }
+ }
+
+ private sealed class ChamberMagazineStatusControl : Control
+ {
+ private readonly BoxContainer _bulletsList;
+ private readonly TextureRect _chamberedBullet;
+ private readonly Label _noMagazineLabel;
+ private readonly Label _ammoCount;
+
+ public ChamberMagazineStatusControl()
+ {
+ MinHeight = 15;
+ HorizontalExpand = true;
+ VerticalAlignment = VAlignment.Center;
+
+ AddChild(new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ HorizontalExpand = true,
+ Children =
+ {
+ (_chamberedBullet = new TextureRect
+ {
+ Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/chambered_rotated.png"),
+ VerticalAlignment = VAlignment.Center,
+ HorizontalAlignment = HAlignment.Right,
+ }),
+ new Control() { MinSize = (5,0) },
+ new Control
+ {
+ HorizontalExpand = true,
+ Children =
+ {
+ (_bulletsList = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ VerticalAlignment = VAlignment.Center,
+ SeparationOverride = 0
+ }),
+ (_noMagazineLabel = new Label
+ {
+ Text = "No Magazine!",
+ StyleClasses = {StyleNano.StyleClassItemStatus}
+ })
+ }
+ },
+ new Control() { MinSize = (5,0) },
+ (_ammoCount = new Label
+ {
+ StyleClasses = {StyleNano.StyleClassItemStatus},
+ HorizontalAlignment = HAlignment.Right,
+ }),
+ }
+ });
+ }
+
+ public void Update(bool chambered, bool magazine, int count, int capacity)
+ {
+ _chamberedBullet.ModulateSelfOverride =
+ chambered ? Color.FromHex("#d7df60") : Color.Black;
+
+ _bulletsList.RemoveAllChildren();
+
+ if (!magazine)
+ {
+ _noMagazineLabel.Visible = true;
+ _ammoCount.Visible = false;
+ return;
+ }
+
+ _noMagazineLabel.Visible = false;
+ _ammoCount.Visible = true;
+
+ var texturePath = "/Textures/Interface/ItemStatus/Bullets/normal.png";
+ var texture = StaticIoC.ResC.GetTexture(texturePath);
+
+ _ammoCount.Text = $"x{count:00}";
+ capacity = Math.Min(capacity, 20);
+ FillBulletRow(_bulletsList, count, capacity, texture);
+ }
+
+ private static void FillBulletRow(Control container, int count, int capacity, Texture texture)
+ {
+ var colorA = Color.FromHex("#b68f0e");
+ var colorB = Color.FromHex("#d7df60");
+ var colorGoneA = Color.FromHex("#000000");
+ var colorGoneB = Color.FromHex("#222222");
+
+ var altColor = false;
+
+ // Draw the empty ones
+ for (var i = count; i < capacity; i++)
+ {
+ container.AddChild(new TextureRect
+ {
+ Texture = texture,
+ ModulateSelfOverride = altColor ? colorGoneA : colorGoneB,
+ Stretch = TextureRect.StretchMode.KeepCentered
+ });
+
+ altColor ^= true;
+ }
+
+ // Draw the full ones, but limit the count to the capacity
+ count = Math.Min(count, capacity);
+ for (var i = 0; i < count; i++)
+ {
+ container.AddChild(new TextureRect
+ {
+ Texture = texture,
+ ModulateSelfOverride = altColor ? colorA : colorB,
+ Stretch = TextureRect.StretchMode.KeepCentered
+ });
+
+ altColor ^= true;
+ }
+ }
+
+ public void PlayAlarmAnimation(Animation animation)
+ {
+ _noMagazineLabel.PlayAnimation(animation, "alarm");
+ }
+ }
+
+ private sealed class RevolverStatusControl : Control
+ {
+ private readonly BoxContainer _bulletsList;
+
+ public RevolverStatusControl()
+ {
+ MinHeight = 15;
+ HorizontalExpand = true;
+ VerticalAlignment = VAlignment.Center;
+ AddChild((_bulletsList = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ HorizontalExpand = true,
+ VerticalAlignment = VAlignment.Center,
+ SeparationOverride = 0
+ }));
+ }
+
+ public void Update(int currentIndex, bool?[] bullets)
+ {
+ _bulletsList.RemoveAllChildren();
+ var capacity = bullets.Length;
+
+ string texturePath;
+ if (capacity <= 20)
+ {
+ texturePath = "/Textures/Interface/ItemStatus/Bullets/normal.png";
+ }
+ else if (capacity <= 30)
+ {
+ texturePath = "/Textures/Interface/ItemStatus/Bullets/small.png";
+ }
+ else
+ {
+ texturePath = "/Textures/Interface/ItemStatus/Bullets/tiny.png";
+ }
+
+ var texture = StaticIoC.ResC.GetTexture(texturePath);
+ var spentTexture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/empty.png");
+
+ FillBulletRow(currentIndex, bullets, _bulletsList, texture, spentTexture);
+ }
+
+ private void FillBulletRow(int currentIndex, bool?[] bullets, Control container, Texture texture, Texture emptyTexture)
+ {
+ var capacity = bullets.Length;
+ var colorA = Color.FromHex("#b68f0e");
+ var colorB = Color.FromHex("#d7df60");
+ var colorSpentA = Color.FromHex("#b50e25");
+ var colorSpentB = Color.FromHex("#d3745f");
+ var colorGoneA = Color.FromHex("#000000");
+ var colorGoneB = Color.FromHex("#222222");
+
+ var altColor = false;
+ var scale = 1.3f;
+
+ for (var i = 0; i < capacity; i++)
+ {
+ var bulletFree = bullets[i];
+ // Add a outline
+ var box = new Control()
+ {
+ MinSize = texture.Size * scale,
+ };
+ if (i == currentIndex)
+ {
+ box.AddChild(new TextureRect
+ {
+ Texture = texture,
+ TextureScale = (scale, scale),
+ ModulateSelfOverride = Color.LimeGreen,
+ });
+ }
+ Color color;
+ Texture bulletTexture = texture;
+
+ if (bulletFree.HasValue)
+ {
+ if (bulletFree.Value)
+ {
+ color = altColor ? colorA : colorB;
+ }
+ else
+ {
+ color = altColor ? colorSpentA : colorSpentB;
+ bulletTexture = emptyTexture;
+ }
+ }
+ else
+ {
+ color = altColor ? colorGoneA : colorGoneB;
+ }
+
+ box.AddChild(new TextureRect
+ {
+ Stretch = TextureRect.StretchMode.KeepCentered,
+ Texture = bulletTexture,
+ ModulateSelfOverride = color,
+ });
+ altColor ^= true;
+ container.AddChild(box);
+ }
+ }
+ }
+
+ #endregion
+}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.Ballistic.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.Ballistic.cs
new file mode 100644
index 0000000000..4f0cce523c
--- /dev/null
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.Ballistic.cs
@@ -0,0 +1,48 @@
+using Content.Shared.Weapons.Ranged.Components;
+using Robust.Shared.Map;
+
+namespace Content.Client.Weapons.Ranged.Systems;
+
+public sealed partial class GunSystem
+{
+ protected override void InitializeBallistic()
+ {
+ base.InitializeBallistic();
+ SubscribeLocalEvent(OnBallisticAmmoCount);
+ }
+
+ private void OnBallisticAmmoCount(EntityUid uid, BallisticAmmoProviderComponent component, UpdateAmmoCounterEvent args)
+ {
+ if (args.Control is DefaultStatusControl control)
+ {
+ control.Update(GetBallisticShots(component), component.Capacity);
+ return;
+ }
+ }
+
+ protected override void Cycle(BallisticAmmoProviderComponent component, MapCoordinates coordinates)
+ {
+ if (!Timing.IsFirstTimePredicted) return;
+
+ EntityUid? ent = null;
+
+ // TODO: Combine with TakeAmmo
+ if (component.Entities.Count > 0)
+ {
+ var existing = component.Entities[^1];
+ component.Entities.RemoveAt(component.Entities.Count - 1);
+
+ component.Container.Remove(existing);
+ EnsureComp(existing);
+ }
+ else if (component.UnspawnedCount > 0)
+ {
+ component.UnspawnedCount--;
+ ent = Spawn(component.FillProto, coordinates);
+ EnsureComp(ent.Value);
+ }
+
+ if (ent != null && ent.Value.IsClientSide())
+ Del(ent.Value);
+ }
+}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.Battery.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.Battery.cs
new file mode 100644
index 0000000000..122244e7f2
--- /dev/null
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.Battery.cs
@@ -0,0 +1,30 @@
+using Content.Shared.Weapons.Ranged.Components;
+
+namespace Content.Client.Weapons.Ranged.Systems;
+
+public sealed partial class GunSystem
+{
+ protected override void InitializeBattery()
+ {
+ base.InitializeBattery();
+ // Hitscan
+ SubscribeLocalEvent(OnControl);
+ SubscribeLocalEvent(OnAmmoCountUpdate);
+
+ // Projectile
+ SubscribeLocalEvent(OnControl);
+ SubscribeLocalEvent(OnAmmoCountUpdate);
+ }
+
+ private void OnAmmoCountUpdate(EntityUid uid, BatteryAmmoProviderComponent component, UpdateAmmoCounterEvent args)
+ {
+ if (args.Control is not BoxesStatusControl boxes) return;
+
+ boxes.Update(component.Shots, component.Capacity);
+ }
+
+ private void OnControl(EntityUid uid, BatteryAmmoProviderComponent component, AmmoCounterControlEvent args)
+ {
+ args.Control = new BoxesStatusControl();
+ }
+}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.ChamberMagazine.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.ChamberMagazine.cs
new file mode 100644
index 0000000000..76cf47cb0c
--- /dev/null
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.ChamberMagazine.cs
@@ -0,0 +1,50 @@
+using Content.Shared.Examine;
+using Content.Shared.Weapons.Ranged.Components;
+using Content.Shared.Weapons.Ranged.Events;
+using Robust.Shared.Containers;
+
+namespace Content.Client.Weapons.Ranged.Systems;
+
+public sealed partial class GunSystem
+{
+ protected override void InitializeChamberMagazine()
+ {
+ base.InitializeChamberMagazine();
+ SubscribeLocalEvent(OnChamberMagazineCounter);
+ SubscribeLocalEvent(OnChamberMagazineAmmoUpdate);
+ SubscribeLocalEvent(OnChamberEntRemove);
+ }
+
+ private void OnChamberEntRemove(EntityUid uid, ChamberMagazineAmmoProviderComponent component, EntRemovedFromContainerMessage args)
+ {
+ if (args.Container.ID != ChamberSlot) return;
+
+ // This is dirty af. Prediction moment.
+ // We may be predicting spawning entities and the engine just removes them from the container so we'll just delete them.
+ if (args.Entity.IsClientSide())
+ QueueDel(args.Entity);
+
+ // AFAIK the only main alternative is having some client-specific handling via a bool or otherwise for the state.
+ // which is much larger and I'm not sure how much better it is. It's bad enough we have to do it with revolvers
+ // to avoid 6-7 additional entity spawns.
+ }
+
+ private void OnChamberMagazineCounter(EntityUid uid, ChamberMagazineAmmoProviderComponent component, AmmoCounterControlEvent args)
+ {
+ args.Control = new ChamberMagazineStatusControl();
+ }
+
+ private void OnChamberMagazineAmmoUpdate(EntityUid uid, ChamberMagazineAmmoProviderComponent component, UpdateAmmoCounterEvent args)
+ {
+ if (args.Control is not ChamberMagazineStatusControl control) return;
+
+ var chambered = GetChamberEntity(uid);
+ var magEntity = GetMagazineEntity(uid);
+ var ammoCountEv = new GetAmmoCountEvent();
+
+ if (magEntity != null)
+ RaiseLocalEvent(magEntity.Value, ref ammoCountEv, false);
+
+ control.Update(chambered != null, magEntity != null, ammoCountEv.Count, ammoCountEv.Capacity);
+ }
+}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.Magazine.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.Magazine.cs
new file mode 100644
index 0000000000..eaab8401bc
--- /dev/null
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.Magazine.cs
@@ -0,0 +1,29 @@
+using Content.Shared.Weapons.Ranged;
+
+namespace Content.Client.Weapons.Ranged.Systems;
+
+public sealed partial class GunSystem
+{
+ protected override void InitializeMagazine()
+ {
+ base.InitializeMagazine();
+ SubscribeLocalEvent(OnMagazineAmmoUpdate);
+ }
+
+ private void OnMagazineAmmoUpdate(EntityUid uid, MagazineAmmoProviderComponent component, UpdateAmmoCounterEvent args)
+ {
+ var ent = GetMagazineEntity(uid);
+
+ if (ent == null)
+ {
+ if (args.Control is DefaultStatusControl control)
+ {
+ control.Update(0, 0);
+ }
+
+ return;
+ }
+
+ RaiseLocalEvent(ent.Value, args, false);
+ }
+}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.Revolver.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.Revolver.cs
new file mode 100644
index 0000000000..93e8266169
--- /dev/null
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.Revolver.cs
@@ -0,0 +1,37 @@
+using Content.Shared.Weapons.Ranged.Components;
+using Robust.Shared.Containers;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Weapons.Ranged.Systems;
+
+public sealed partial class GunSystem
+{
+ protected override void InitializeRevolver()
+ {
+ base.InitializeRevolver();
+ SubscribeLocalEvent(OnRevolverCounter);
+ SubscribeLocalEvent(OnRevolverAmmoUpdate);
+ SubscribeLocalEvent(OnRevolverEntRemove);
+ }
+
+ private void OnRevolverEntRemove(EntityUid uid, RevolverAmmoProviderComponent component, EntRemovedFromContainerMessage args)
+ {
+ if (args.Container.ID != RevolverContainer) return;
+
+ // See ChamberMagazineAmmoProvider
+ if (!args.Entity.IsClientSide()) return;
+
+ QueueDel(args.Entity);
+ }
+
+ private void OnRevolverAmmoUpdate(EntityUid uid, RevolverAmmoProviderComponent component, UpdateAmmoCounterEvent args)
+ {
+ if (args.Control is not RevolverStatusControl control) return;
+ control.Update(component.CurrentIndex, component.Chambers);
+ }
+
+ private void OnRevolverCounter(EntityUid uid, RevolverAmmoProviderComponent component, AmmoCounterControlEvent args)
+ {
+ args.Control = new RevolverStatusControl();
+ }
+}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.SpentAmmo.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.SpentAmmo.cs
new file mode 100644
index 0000000000..7227f333c0
--- /dev/null
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.SpentAmmo.cs
@@ -0,0 +1,34 @@
+using Content.Client.Weapons.Ranged.Components;
+using Content.Shared.Weapons.Ranged.Systems;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Weapons.Ranged.Systems;
+
+public sealed partial class GunSystem
+{
+ private void InitializeSpentAmmo()
+ {
+ SubscribeLocalEvent(OnSpentAmmoAppearance);
+ }
+
+ private void OnSpentAmmoAppearance(EntityUid uid, SpentAmmoVisualsComponent component, ref AppearanceChangeEvent args)
+ {
+ var sprite = args.Sprite;
+ if (sprite == null) return;
+
+ if (!args.AppearanceData.TryGetValue(AmmoVisuals.Spent, out var varSpent))
+ {
+ return;
+ }
+
+ var spent = (bool) varSpent;
+ string state;
+
+ if (spent)
+ state = component.Suffix ? $"{component.State}-spent" : "spent";
+ else
+ state = component.State;
+
+ sprite.LayerSetState(AmmoVisualLayers.Base, state);
+ }
+}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs
new file mode 100644
index 0000000000..8f84040bbd
--- /dev/null
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs
@@ -0,0 +1,187 @@
+using Content.Client.Items;
+using Content.Client.Weapons.Ranged.Components;
+using Content.Shared.Weapons.Ranged;
+using Content.Shared.Weapons.Ranged.Components;
+using Content.Shared.Weapons.Ranged.Events;
+using Content.Shared.Weapons.Ranged.Systems;
+using Robust.Client.Animations;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.Player;
+using Robust.Shared.Audio;
+using Robust.Shared.Input;
+using Robust.Shared.Map;
+using Robust.Shared.Player;
+using Robust.Shared.Utility;
+using SharedGunSystem = Content.Shared.Weapons.Ranged.Systems.SharedGunSystem;
+
+namespace Content.Client.Weapons.Ranged.Systems;
+
+public sealed partial class GunSystem : SharedGunSystem
+{
+ [Dependency] private readonly IEyeManager _eyeManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly AnimationPlayerSystem _animPlayer = default!;
+ [Dependency] private readonly EffectSystem _effects = default!;
+ [Dependency] private readonly InputSystem _inputSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ UpdatesOutsidePrediction = true;
+ SubscribeLocalEvent(OnAmmoCounterCollect);
+
+ // Plays animated effects on the client.
+ SubscribeNetworkEvent(OnHitscan);
+
+ InitializeSpentAmmo();
+ }
+
+ private void OnHitscan(HitscanEvent ev)
+ {
+ // ALL I WANT IS AN ANIMATED EFFECT
+ foreach (var a in ev.Sprites)
+ {
+ if (a.Sprite is not SpriteSpecifier.Rsi rsi) continue;
+
+ var ent = Spawn("HitscanEffect", a.coordinates);
+ var sprite = Comp(ent);
+ var xform = Transform(ent);
+ xform.LocalRotation = a.angle;
+ sprite[EffectLayers.Unshaded].AutoAnimated = false;
+ sprite.LayerSetSprite(EffectLayers.Unshaded, rsi);
+ sprite.LayerSetState(EffectLayers.Unshaded, rsi.RsiState);
+ sprite.Scale = new Vector2(a.Distance, 1f);
+ sprite[EffectLayers.Unshaded].Visible = true;
+
+ var anim = new Animation()
+ {
+ Length = TimeSpan.FromSeconds(0.48f),
+ AnimationTracks =
+ {
+ new AnimationTrackSpriteFlick()
+ {
+ LayerKey = EffectLayers.Unshaded,
+ KeyFrames =
+ {
+ new AnimationTrackSpriteFlick.KeyFrame(rsi.RsiState, 0f),
+ }
+ }
+ }
+ };
+
+ _animPlayer.Play(ent, null, anim, "hitscan-effect");
+ }
+ }
+
+ public override void Update(float frameTime)
+ {
+ if (!Timing.IsFirstTimePredicted)
+ return;
+
+ var entityNull = _player.LocalPlayer?.ControlledEntity;
+
+ if (entityNull == null)
+ {
+ return;
+ }
+
+ var entity = entityNull.Value;
+ var gun = GetGun(entity);
+
+ if (gun == null)
+ {
+ return;
+ }
+
+ if (_inputSystem.CmdStates.GetState(EngineKeyFunctions.Use) != BoundKeyState.Down)
+ {
+ if (gun.ShotCounter != 0)
+ EntityManager.RaisePredictiveEvent(new RequestStopShootEvent { Gun = gun.Owner });
+ return;
+ }
+
+ if (gun.NextFire > Timing.CurTime)
+ return;
+
+ var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition);
+ EntityCoordinates coordinates;
+
+ // Bro why would I want a ternary here
+ // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
+ if (MapManager.TryFindGridAt(mousePos, out var grid))
+ {
+ coordinates = EntityCoordinates.FromMap(grid.GridEntityId, mousePos, EntityManager);
+ }
+ else
+ {
+ coordinates = EntityCoordinates.FromMap(MapManager.GetMapEntityId(mousePos.MapId), mousePos, EntityManager);
+ }
+
+ Sawmill.Debug($"Sending shoot request tick {Timing.CurTick} / {Timing.CurTime}");
+
+ EntityManager.RaisePredictiveEvent(new RequestShootEvent
+ {
+ Coordinates = coordinates,
+ Gun = gun.Owner,
+ });
+ }
+
+ public override void Shoot(GunComponent gun, List ammo, EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, EntityUid? user = null)
+ {
+ // Rather than splitting client / server for every ammo provider it's easier
+ // to just delete the spawned entities. This is for programmer sanity despite the wasted perf.
+ // This also means any ammo specific stuff can be grabbed as necessary.
+ foreach (var ent in ammo)
+ {
+ switch (ent)
+ {
+ case CartridgeAmmoComponent cartridge:
+ if (!cartridge.Spent)
+ {
+ SetCartridgeSpent(cartridge, true);
+ MuzzleFlash(gun.Owner, cartridge, user);
+
+ // TODO: Can't predict entity deletions.
+ //if (cartridge.DeleteOnSpawn)
+ // Del(cartridge.Owner);
+ }
+ else
+ {
+ PlaySound(gun.Owner, gun.SoundEmpty?.GetSound(Random, ProtoManager), user);
+ }
+
+ if (cartridge.Owner.IsClientSide())
+ Del(cartridge.Owner);
+
+ break;
+ case AmmoComponent newAmmo:
+ MuzzleFlash(gun.Owner, newAmmo, user);
+ if (newAmmo.Owner.IsClientSide())
+ Del(newAmmo.Owner);
+ else
+ RemComp(newAmmo.Owner);
+ break;
+ }
+ }
+ }
+
+ protected override void PlaySound(EntityUid gun, string? sound, EntityUid? user = null)
+ {
+ if (sound == null || user == null || !Timing.IsFirstTimePredicted) return;
+ SoundSystem.Play(Filter.Local(), sound, gun);
+ }
+
+ protected override void Popup(string message, EntityUid? uid, EntityUid? user)
+ {
+ if (uid == null || user == null || !Timing.IsFirstTimePredicted) return;
+ PopupSystem.PopupEntity(message, uid.Value, Filter.Entities(user.Value));
+ }
+
+ protected override void CreateEffect(EffectSystemMessage message, EntityUid? user = null)
+ {
+ _effects.CreateEffect(message);
+ }
+}
diff --git a/Content.Client/Weapons/Ranged/TetherGunSystem.cs b/Content.Client/Weapons/Ranged/Systems/TetherGunSystem.cs
similarity index 97%
rename from Content.Client/Weapons/Ranged/TetherGunSystem.cs
rename to Content.Client/Weapons/Ranged/Systems/TetherGunSystem.cs
index 67a0e1a2d8..5a6117d0b1 100644
--- a/Content.Client/Weapons/Ranged/TetherGunSystem.cs
+++ b/Content.Client/Weapons/Ranged/Systems/TetherGunSystem.cs
@@ -1,5 +1,5 @@
using Content.Client.Clickable;
-using Content.Shared.Weapons.Ranged;
+using Content.Shared.Weapons.Ranged.Systems;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
@@ -7,7 +7,7 @@ using Robust.Shared.Input;
using Robust.Shared.Map;
using Robust.Shared.Timing;
-namespace Content.Client.Weapons.Ranged;
+namespace Content.Client.Weapons.Ranged.Systems;
public sealed class TetherGunSystem : SharedTetherGunSystem
{
diff --git a/Content.Server/Entry/IgnoredComponents.cs b/Content.Server/Entry/IgnoredComponents.cs
index acab9fa497..9f8b773cd5 100644
--- a/Content.Server/Entry/IgnoredComponents.cs
+++ b/Content.Server/Entry/IgnoredComponents.cs
@@ -9,6 +9,7 @@ namespace Content.Server.Entry
"StasisBedVisuals",
"InteractionOutline",
"MeleeWeaponArcAnimation",
+ "EffectVisuals",
"AnimationsTest",
"ItemStatus",
"VehicleVisuals",
@@ -21,11 +22,12 @@ namespace Content.Server.Entry
"LatheVisuals",
"DiseaseMachineVisuals",
"HandheldGPS",
+ "SpentAmmoVisuals",
"ToggleableLightVisuals",
"CableVisualizer",
"PotencyVisuals",
"PaperVisuals",
- "SurveillanceCameraVisuals"
+ "SurveillanceCameraVisuals",
};
}
}
diff --git a/Content.Server/Interaction/InteractionSystem.cs b/Content.Server/Interaction/InteractionSystem.cs
index 67a54f9c6c..4cbd608afa 100644
--- a/Content.Server/Interaction/InteractionSystem.cs
+++ b/Content.Server/Interaction/InteractionSystem.cs
@@ -3,7 +3,6 @@ using Content.Server.CombatMode;
using Content.Server.Hands.Components;
using Content.Server.Pulling;
using Content.Server.Storage.Components;
-using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Database;
using Content.Shared.DragDrop;
@@ -13,7 +12,6 @@ using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Pulling.Components;
using Content.Shared.Weapons.Melee;
-using Content.Shared.Weapons.Ranged.Components;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
diff --git a/Content.Server/Power/Components/ChargerComponent.cs b/Content.Server/Power/Components/ChargerComponent.cs
index d39f10ef46..f9e7dbddb3 100644
--- a/Content.Server/Power/Components/ChargerComponent.cs
+++ b/Content.Server/Power/Components/ChargerComponent.cs
@@ -15,29 +15,26 @@ namespace Content.Server.Power.Components
private CellChargerStatus _status;
[DataField("chargeRate")]
- private int _chargeRate = 100;
-
- [DataField("transferEfficiency")]
- private float _transferEfficiency = 0.85f;
+ public int ChargeRate = 20;
[DataField("chargerSlot", required: true)]
public ItemSlot ChargerSlot = new();
private CellChargerStatus GetStatus()
{
- if (_entMan.TryGetComponent(Owner, out ApcPowerReceiverComponent? receiver) &&
- !receiver.Powered)
+ if (!_entMan.TryGetComponent(Owner, out var xform) ||
+ !xform.Anchored ||
+ _entMan.TryGetComponent(Owner, out ApcPowerReceiverComponent? receiver) && !receiver.Powered)
{
return CellChargerStatus.Off;
}
+
if (!ChargerSlot.HasItem)
- {
return CellChargerStatus.Empty;
- }
+
if (HeldBattery != null && Math.Abs(HeldBattery.MaxCharge - HeldBattery.CurrentCharge) < 0.01)
- {
return CellChargerStatus.Charged;
- }
+
return CellChargerStatus.Charging;
}
@@ -66,7 +63,7 @@ namespace Content.Server.Power.Components
appearance?.SetData(CellVisual.Light, CellChargerStatus.Empty);
break;
case CellChargerStatus.Charging:
- receiver.Load = (int) (_chargeRate / _transferEfficiency);
+ receiver.Load = ChargeRate;
appearance?.SetData(CellVisual.Light, CellChargerStatus.Charging);
break;
case CellChargerStatus.Charged:
@@ -83,9 +80,8 @@ namespace Content.Server.Power.Components
public void OnUpdate(float frameTime) //todo: make single system for this
{
if (_status == CellChargerStatus.Empty || _status == CellChargerStatus.Charged || !ChargerSlot.HasItem)
- {
return;
- }
+
TransferPower(frameTime);
}
@@ -98,16 +94,15 @@ namespace Content.Server.Power.Components
}
if (HeldBattery == null)
- {
return;
- }
- HeldBattery.CurrentCharge += _chargeRate * frameTime;
+ HeldBattery.CurrentCharge += ChargeRate * frameTime;
// Just so the sprite won't be set to 99.99999% visibility
if (HeldBattery.MaxCharge - HeldBattery.CurrentCharge < 0.01)
{
HeldBattery.CurrentCharge = HeldBattery.MaxCharge;
}
+
UpdateStatus();
}
}
diff --git a/Content.Server/Power/EntitySystems/ChargerSystem.cs b/Content.Server/Power/EntitySystems/ChargerSystem.cs
index d3278871bd..b259d98bb3 100644
--- a/Content.Server/Power/EntitySystems/ChargerSystem.cs
+++ b/Content.Server/Power/EntitySystems/ChargerSystem.cs
@@ -1,6 +1,7 @@
using Content.Server.Power.Components;
using Content.Server.PowerCell;
using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Examine;
using Content.Shared.PowerCell.Components;
using JetBrains.Annotations;
using Robust.Shared.Containers;
@@ -15,16 +16,18 @@ internal sealed class ChargerSystem : EntitySystem
public override void Initialize()
{
- base.Initialize();
-
SubscribeLocalEvent(OnChargerInit);
SubscribeLocalEvent(OnChargerRemove);
-
SubscribeLocalEvent(OnPowerChanged);
-
SubscribeLocalEvent(OnInserted);
SubscribeLocalEvent(OnRemoved);
SubscribeLocalEvent(OnInsertAttempt);
+ SubscribeLocalEvent(OnChargerExamine);
+ }
+
+ private void OnChargerExamine(EntityUid uid, ChargerComponent component, ExaminedEvent args)
+ {
+ args.PushMarkup(Loc.GetString("charger-examine", ("color", "yellow"), ("chargeRate", component.ChargeRate)));
}
public override void Update(float frameTime)
@@ -63,7 +66,7 @@ internal sealed class ChargerSystem : EntitySystem
// or by checking for a power cell slot on the inserted entity
_cellSystem.TryGetBatteryFromSlot(args.Entity, out component.HeldBattery);
}
-
+
component.UpdateStatus();
}
diff --git a/Content.Server/Projectiles/Components/HitscanComponent.cs b/Content.Server/Projectiles/Components/HitscanComponent.cs
deleted file mode 100644
index a16fd6ca1b..0000000000
--- a/Content.Server/Projectiles/Components/HitscanComponent.cs
+++ /dev/null
@@ -1,166 +0,0 @@
-using Content.Server.Weapon.Ranged;
-using Content.Shared.Damage;
-using Content.Shared.Physics;
-using Content.Shared.Sound;
-using Robust.Server.GameObjects;
-using Robust.Shared.Audio;
-using Robust.Shared.Map;
-using Robust.Shared.Player;
-using Robust.Shared.Timing;
-
-namespace Content.Server.Projectiles.Components
-{
- ///
- /// Lasers etc.
- ///
- [RegisterComponent]
- public sealed class HitscanComponent : Component
- {
- [Dependency] private readonly IEntityManager _entMan = default!;
- [Dependency] private readonly IGameTiming _gameTiming = default!;
-
- public CollisionGroup CollisionMask => (CollisionGroup) _collisionMask;
-
- [DataField("layers")] //todo WithFormat.Flags()
- private int _collisionMask = (int) CollisionGroup.Opaque;
-
- [DataField("damage", required: true)]
- [ViewVariables(VVAccess.ReadWrite)]
- public DamageSpecifier Damage = default!;
-
- public float MaxLength => 20.0f;
- private TimeSpan _startTime;
- private TimeSpan _deathTime;
-
- public float ColorModifier { get; set; } = 1.0f;
- [DataField("spriteName")]
- private string _spriteName = "Objects/Weapons/Guns/Projectiles/laser.png";
- [DataField("muzzleFlash")]
- private string? _muzzleFlash;
- [DataField("impactFlash")]
- private string? _impactFlash;
-
- [DataField("soundHit")]
- public SoundSpecifier? SoundHit;
-
- [DataField("soundForce")]
- public bool ForceSound = false;
-
- public void FireEffects(EntityUid user, float distance, Angle angle, EntityUid? hitEntity = null)
- {
- var effectSystem = EntitySystem.Get();
- _startTime = _gameTiming.CurTime;
- _deathTime = _startTime + TimeSpan.FromSeconds(1);
-
- var mapManager = IoCManager.Resolve();
-
- // We'll get the effects relative to the grid / map of the firer
- var gridOrMap = _entMan.GetComponent(user).GridID == GridId.Invalid ? mapManager.GetMapEntityId(_entMan.GetComponent(user).MapID) :
- mapManager.GetGrid(_entMan.GetComponent(user).GridID).GridEntityId;
-
- var parentXform = _entMan.GetComponent(gridOrMap);
-
- var localCoordinates = new EntityCoordinates(gridOrMap, parentXform.InvWorldMatrix.Transform(_entMan.GetComponent(user).WorldPosition));
- var localAngle = angle - parentXform.WorldRotation;
-
- var afterEffect = AfterEffects(localCoordinates, localAngle, distance, 1.0f);
- if (afterEffect != null)
- {
- effectSystem.CreateParticle(afterEffect);
- }
-
- // if we're too close we'll stop the impact and muzzle / impact sprites from clipping
- if (distance > 1.0f)
- {
- var impactEffect = ImpactFlash(distance, localAngle);
- if (impactEffect != null)
- {
- effectSystem.CreateParticle(impactEffect);
- }
-
- var muzzleEffect = MuzzleFlash(localCoordinates, localAngle);
- if (muzzleEffect != null)
- {
- effectSystem.CreateParticle(muzzleEffect);
- }
- }
-
- Owner.SpawnTimer((int) _deathTime.TotalMilliseconds, () =>
- {
- if (!_entMan.Deleted(Owner))
- {
- _entMan.DeleteEntity(Owner);
- }
- });
- }
-
- private EffectSystemMessage? MuzzleFlash(EntityCoordinates grid, Angle angle)
- {
- if (_muzzleFlash == null)
- {
- return null;
- }
-
- var offset = angle.ToVec().Normalized / 2;
-
- var message = new EffectSystemMessage
- {
- EffectSprite = _muzzleFlash,
- Born = _startTime,
- DeathTime = _deathTime,
- Coordinates = grid.Offset(offset),
- //Rotated from east facing
- Rotation = (float) angle.Theta,
- Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), ColorModifier),
- ColorDelta = new Vector4(0, 0, 0, -1500f),
- Shaded = false
- };
-
- return message;
- }
-
- private EffectSystemMessage AfterEffects(EntityCoordinates origin, Angle angle, float distance, float offset = 0.0f)
- {
- var midPointOffset = angle.ToVec() * distance / 2;
- var message = new EffectSystemMessage
- {
- EffectSprite = _spriteName,
- Born = _startTime,
- DeathTime = _deathTime,
- Size = new Vector2(distance - offset, 1f),
- Coordinates = origin.Offset(midPointOffset),
- //Rotated from east facing
- Rotation = (float) angle.Theta,
- Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), ColorModifier),
- ColorDelta = new Vector4(0, 0, 0, -1500f),
-
- Shaded = false
- };
-
- return message;
- }
-
- private EffectSystemMessage? ImpactFlash(float distance, Angle angle)
- {
- if (_impactFlash == null)
- {
- return null;
- }
-
- var message = new EffectSystemMessage
- {
- EffectSprite = _impactFlash,
- Born = _startTime,
- DeathTime = _deathTime,
- Coordinates = _entMan.GetComponent(Owner).Coordinates.Offset(angle.ToVec() * distance),
- //Rotated from east facing
- Rotation = (float) angle.FlipPositive(),
- Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), ColorModifier),
- ColorDelta = new Vector4(0, 0, 0, -1500f),
- Shaded = false
- };
-
- return message;
- }
- }
-}
diff --git a/Content.Server/Projectiles/SharedProjectileSystem.cs b/Content.Server/Projectiles/SharedProjectileSystem.cs
index 22d205c064..3d9bd47922 100644
--- a/Content.Server/Projectiles/SharedProjectileSystem.cs
+++ b/Content.Server/Projectiles/SharedProjectileSystem.cs
@@ -14,6 +14,7 @@ using Robust.Shared.Audio;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
+using GunSystem = Content.Server.Weapon.Ranged.Systems.GunSystem;
namespace Content.Server.Projectiles
{
@@ -51,7 +52,7 @@ namespace Content.Server.Projectiles
$"Projectile {ToPrettyString(component.Owner):projectile} shot by {ToPrettyString(component.Shooter):user} hit {ToPrettyString(otherEntity):target} and dealt {modifiedDamage.Total:damage} damage");
}
- _guns.PlaySound(otherEntity, modifiedDamage, component.SoundHit, component.ForceSound);
+ _guns.PlayImpactSound(otherEntity, modifiedDamage, component.SoundHit, component.ForceSound);
// Damaging it can delete it
if (HasComp(otherEntity))
diff --git a/Content.Server/Tools/ToolSystem.MultipleTool.cs b/Content.Server/Tools/ToolSystem.MultipleTool.cs
index c469ca9e35..1ae571093c 100644
--- a/Content.Server/Tools/ToolSystem.MultipleTool.cs
+++ b/Content.Server/Tools/ToolSystem.MultipleTool.cs
@@ -64,7 +64,7 @@ namespace Content.Server.Tools
return;
// Sprite is optional.
- Resolve(uid, ref sprite);
+ Resolve(uid, ref sprite, false);
if (multiple.Entries.Length == 0)
{
diff --git a/Content.Server/Weapon/Ranged/Ammunition/Components/AmmoBoxComponent.cs b/Content.Server/Weapon/Ranged/Ammunition/Components/AmmoBoxComponent.cs
deleted file mode 100644
index b4cecd5f82..0000000000
--- a/Content.Server/Weapon/Ranged/Ammunition/Components/AmmoBoxComponent.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using Robust.Shared.Containers;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Weapon.Ranged.Ammunition.Components
-{
- ///
- /// Stores ammo and can quickly transfer ammo into a magazine.
- ///
- [RegisterComponent]
- [Friend(typeof(GunSystem))]
- public sealed class AmmoBoxComponent : Component
- {
- [DataField("caliber")]
- public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
-
- [DataField("capacity")]
- public int Capacity
- {
- get => _capacity;
- set
- {
- _capacity = value;
- SpawnedAmmo = new Stack(value);
- }
- }
-
- private int _capacity = 30;
-
- public int AmmoLeft => SpawnedAmmo.Count + UnspawnedCount;
- public Stack SpawnedAmmo = new();
-
- ///
- /// Container that holds any instantiated ammo.
- ///
- public Container AmmoContainer = default!;
-
- ///
- /// How many more deferred entities can be spawned. We defer these to avoid instantiating the entities until needed for performance reasons.
- ///
- public int UnspawnedCount;
-
- ///
- /// The prototype of the ammo to be retrieved when getting ammo.
- ///
- [DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))]
- public string? FillPrototype;
- }
-}
diff --git a/Content.Server/Weapon/Ranged/Ammunition/Components/AmmoComponent.cs b/Content.Server/Weapon/Ranged/Ammunition/Components/AmmoComponent.cs
deleted file mode 100644
index ebe0b62ce2..0000000000
--- a/Content.Server/Weapon/Ranged/Ammunition/Components/AmmoComponent.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-using Content.Shared.Sound;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Weapon.Ranged.Ammunition.Components
-{
- ///
- /// Allows this entity to be loaded into a ranged weapon (if the caliber matches)
- /// Generally used for bullets but can be used for other things like bananas
- ///
- [RegisterComponent]
- [Friend(typeof(GunSystem))]
- public sealed class AmmoComponent : Component, ISerializationHooks
- {
- [DataField("caliber")]
- public BallisticCaliber Caliber { get; } = BallisticCaliber.Unspecified;
-
- public bool Spent
- {
- get
- {
- if (AmmoIsProjectile)
- {
- return false;
- }
-
- return _spent;
- }
- set => _spent = value;
- }
-
- private bool _spent;
-
- // TODO: Make it so null projectile = dis
- ///
- /// Used for anything without a case that fires itself
- ///
- [DataField("isProjectile")] public bool AmmoIsProjectile;
-
- ///
- /// Used for something that is deleted when the projectile is retrieved
- ///
- [DataField("caseless")]
- public bool Caseless { get; }
-
- // Rather than managing bullet / case state seemed easier to just have 2 toggles
- // ammoIsProjectile being for a beanbag for example and caseless being for ClRifle rounds
-
- ///
- /// For shotguns where they might shoot multiple entities
- ///
- [DataField("projectilesFired")]
- public int ProjectilesFired { get; } = 1;
-
- [DataField("projectile", customTypeSerializer: typeof(PrototypeIdSerializer))]
- public string? ProjectileId;
-
- // How far apart each entity is if multiple are shot
- [DataField("ammoSpread")]
- public float EvenSpreadAngle { get; } = default;
-
- ///
- /// How fast the shot entities travel
- ///
- [DataField("ammoVelocity")]
- public float Velocity { get; } = 20f;
-
- [DataField("muzzleFlash")]
- public ResourcePath? MuzzleFlashSprite = new("Objects/Weapons/Guns/Projectiles/bullet_muzzle.png");
-
- [DataField("soundCollectionEject")]
- public SoundSpecifier SoundCollectionEject { get; } = new SoundCollectionSpecifier("CasingEject");
-
- void ISerializationHooks.AfterDeserialization()
- {
- // Being both caseless and shooting yourself doesn't make sense
- DebugTools.Assert(!(AmmoIsProjectile && Caseless));
-
- if (ProjectilesFired < 1)
- {
- Logger.Error("Ammo can't have less than 1 projectile");
- }
-
- if (EvenSpreadAngle > 0 && ProjectilesFired == 1)
- {
- Logger.Error("Can't have an even spread if only 1 projectile is fired");
- throw new InvalidOperationException();
- }
- }
- }
-
- public enum BallisticCaliber
- {
- Unspecified = 0,
- A357, // Placeholder?
- ClRifle,
- SRifle,
- Pistol,
- A35, // Placeholder?
- LRifle,
- HRifle,
- Magnum,
- AntiMaterial,
- Shotgun,
- Cap,
- Rocket,
- Dart, // Placeholder
- Grenade,
- Energy,
- }
-}
diff --git a/Content.Server/Weapon/Ranged/Ammunition/Components/AmmoComponentData.cs b/Content.Server/Weapon/Ranged/Ammunition/Components/AmmoComponentData.cs
deleted file mode 100644
index ebe5a62a59..0000000000
--- a/Content.Server/Weapon/Ranged/Ammunition/Components/AmmoComponentData.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using Robust.Shared.Serialization;
-
-namespace Content.Server.Weapon.Ranged.Ammunition.Components
-{
- public sealed partial class AmmoComponentData : ISerializationHooks
- {
- }
-}
diff --git a/Content.Server/Weapon/Ranged/Ammunition/Components/RangedMagazineComponent.cs b/Content.Server/Weapon/Ranged/Ammunition/Components/RangedMagazineComponent.cs
deleted file mode 100644
index a6f33617b1..0000000000
--- a/Content.Server/Weapon/Ranged/Ammunition/Components/RangedMagazineComponent.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using Content.Server.Weapon.Ranged.Barrels.Components;
-using Robust.Shared.Containers;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Weapon.Ranged.Ammunition.Components
-{
- [RegisterComponent]
- public sealed class RangedMagazineComponent : Component
- {
- public readonly Stack SpawnedAmmo = new();
- public Container AmmoContainer = default!;
-
- public int ShotsLeft => SpawnedAmmo.Count + UnspawnedCount;
- public int Capacity => _capacity;
- [DataField("capacity")]
- private int _capacity = 20;
-
- public MagazineType MagazineType => _magazineType;
- [DataField("magazineType")]
- private MagazineType _magazineType = MagazineType.Unspecified;
- public BallisticCaliber Caliber => _caliber;
- [DataField("caliber")]
- private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
-
- // If there's anything already in the magazine
- [DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))]
- public string? FillPrototype;
-
- // By default the magazine won't spawn the entity until needed so we need to keep track of how many left we can spawn
- // Generally you probablt don't want to use this
- public int UnspawnedCount;
- }
-}
diff --git a/Content.Server/Weapon/Ranged/Ammunition/Components/SpeedLoaderComponent.cs b/Content.Server/Weapon/Ranged/Ammunition/Components/SpeedLoaderComponent.cs
deleted file mode 100644
index 3d9222abe0..0000000000
--- a/Content.Server/Weapon/Ranged/Ammunition/Components/SpeedLoaderComponent.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using Robust.Shared.Containers;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Weapon.Ranged.Ammunition.Components
-{
- ///
- /// Used to load certain ranged weapons quickly
- ///
- [RegisterComponent]
- public sealed class SpeedLoaderComponent : Component
- {
- [DataField("caliber")] public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
- public int Capacity => _capacity;
- [DataField("capacity")]
- private int _capacity = 6;
-
- public Container AmmoContainer = default!;
- public Stack SpawnedAmmo = new();
- public int UnspawnedCount;
-
- public int AmmoLeft => SpawnedAmmo.Count + UnspawnedCount;
-
- [DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))]
- public string? FillPrototype;
- }
-}
diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/BatteryBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/BatteryBarrelComponent.cs
deleted file mode 100644
index ad36a606fd..0000000000
--- a/Content.Server/Weapon/Ranged/Barrels/Components/BatteryBarrelComponent.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using Content.Server.PowerCell;
-using Robust.Shared.Containers;
-using Robust.Shared.GameStates;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Weapon.Ranged.Barrels.Components
-{
- [RegisterComponent, NetworkedComponent, ComponentReference(typeof(ServerRangedBarrelComponent))]
- public sealed class BatteryBarrelComponent : ServerRangedBarrelComponent
- {
- // The minimum change we need before we can fire
- [DataField("lowerChargeLimit")]
- [ViewVariables]
- public float LowerChargeLimit = 10;
-
- [DataField("fireCost")]
- [ViewVariables]
- public int BaseFireCost = 300;
-
- // What gets fired
- [DataField("ammoPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))]
- [ViewVariables]
- public string? AmmoPrototype;
-
- public ContainerSlot AmmoContainer = default!;
-
- public override int ShotsLeft
- {
- get
- {
-
- if (!EntitySystem.Get().TryGetBatteryFromSlot(Owner, out var battery))
- {
- return 0;
- }
-
- return (int) Math.Ceiling(battery.CurrentCharge / BaseFireCost);
- }
- }
-
- public override int Capacity
- {
- get
- {
- if (!EntitySystem.Get().TryGetBatteryFromSlot(Owner, out var battery))
- {
- return 0;
- }
-
- return (int) Math.Ceiling(battery.MaxCharge / BaseFireCost);
- }
- }
- }
-}
diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/BoltActionBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/BoltActionBarrelComponent.cs
deleted file mode 100644
index 5a2f231af3..0000000000
--- a/Content.Server/Weapon/Ranged/Barrels/Components/BoltActionBarrelComponent.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-using Content.Server.Weapon.Ranged.Ammunition.Components;
-using Content.Shared.Sound;
-using Robust.Shared.Audio;
-using Robust.Shared.Containers;
-using Robust.Shared.GameStates;
-using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Weapon.Ranged.Barrels.Components
-{
- ///
- /// Shotguns mostly
- ///
- [RegisterComponent, NetworkedComponent, ComponentReference(typeof(ServerRangedBarrelComponent))]
- public sealed class BoltActionBarrelComponent : ServerRangedBarrelComponent
- {
- // Originally I had this logic shared with PumpBarrel and used a couple of variables to control things
- // but it felt a lot messier to play around with, especially when adding verbs
-
- public override int ShotsLeft
- {
- get
- {
- var chamberCount = ChamberContainer.ContainedEntity != null ? 1 : 0;
- return chamberCount + SpawnedAmmo.Count + UnspawnedCount;
- }
- }
- public override int Capacity => _capacity;
-
- [DataField("capacity")]
- internal int _capacity = 6;
-
- public ContainerSlot ChamberContainer = default!;
- public Stack SpawnedAmmo = default!;
- public Container AmmoContainer = default!;
-
- [ViewVariables]
- [DataField("caliber")]
- public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
-
- [ViewVariables]
- [DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))]
- public string? FillPrototype;
-
- [ViewVariables]
- public int UnspawnedCount;
-
- public bool BoltOpen
- {
- get => _boltOpen;
- set
- {
- if (_boltOpen == value)
- {
- return;
- }
-
- var gunSystem = EntitySystem.Get();
-
- if (value)
- {
- gunSystem.TryEjectChamber(this);
- SoundSystem.Play(Filter.Pvs(Owner), _soundBoltOpen.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
- }
- else
- {
- gunSystem.TryFeedChamber(this);
- SoundSystem.Play(Filter.Pvs(Owner), _soundBoltClosed.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
- }
-
- _boltOpen = value;
- gunSystem.UpdateBoltAppearance(this);
- Dirty();
- }
- }
- private bool _boltOpen;
-
- [DataField("autoCycle")] public bool AutoCycle;
-
- // Sounds
- [DataField("soundCycle")] public SoundSpecifier SoundCycle = new SoundPathSpecifier("/Audio/Weapons/Guns/Cock/sf_rifle_cock.ogg");
- [DataField("soundBoltOpen")]
- private SoundSpecifier _soundBoltOpen = new SoundPathSpecifier("/Audio/Weapons/Guns/Bolt/rifle_bolt_open.ogg");
- [DataField("soundBoltClosed")]
- private SoundSpecifier _soundBoltClosed = new SoundPathSpecifier("/Audio/Weapons/Guns/Bolt/rifle_bolt_closed.ogg");
- [DataField("soundInsert")] public SoundSpecifier SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/bullet_insert.ogg");
- }
-}
diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/MagazineBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/MagazineBarrelComponent.cs
deleted file mode 100644
index 0b2e7b4fe5..0000000000
--- a/Content.Server/Weapon/Ranged/Barrels/Components/MagazineBarrelComponent.cs
+++ /dev/null
@@ -1,131 +0,0 @@
-using Content.Server.Weapon.Ranged.Ammunition.Components;
-using Content.Shared.Sound;
-using Robust.Shared.Audio;
-using Robust.Shared.Containers;
-using Robust.Shared.GameStates;
-using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Weapon.Ranged.Barrels.Components
-{
- [RegisterComponent, NetworkedComponent, ComponentReference(typeof(ServerRangedBarrelComponent))]
- public sealed class MagazineBarrelComponent : ServerRangedBarrelComponent
- {
- [Dependency] private readonly IEntityManager _entities = default!;
-
- [ViewVariables] public ContainerSlot ChamberContainer = default!;
- [ViewVariables] public bool HasMagazine => MagazineContainer.ContainedEntity != null;
- public ContainerSlot MagazineContainer = default!;
-
- [ViewVariables] public MagazineType MagazineTypes => _magazineTypes;
- [DataField("magazineTypes")]
- private MagazineType _magazineTypes = default;
- [ViewVariables] public BallisticCaliber Caliber => _caliber;
- [DataField("caliber")]
- private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
-
- public override int ShotsLeft
- {
- get
- {
- var count = 0;
- if (ChamberContainer.ContainedEntity != null)
- {
- count++;
- }
-
- if (MagazineContainer.ContainedEntity is {Valid: true} magazine)
- {
- count += _entities.GetComponent(magazine).ShotsLeft;
- }
-
- return count;
- }
- }
-
- public override int Capacity
- {
- get
- {
- // Chamber
- var count = 1;
- if (MagazineContainer.ContainedEntity is {Valid: true} magazine)
- {
- count += _entities.GetComponent(magazine).Capacity;
- }
-
- return count;
- }
- }
-
- [DataField("magFillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))]
- public string? MagFillPrototype;
-
- public bool BoltOpen
- {
- get => _boltOpen;
- set
- {
- if (_boltOpen == value)
- {
- return;
- }
-
- var gunSystem = EntitySystem.Get();
-
- if (value)
- {
- gunSystem.TryEjectChamber(this);
- SoundSystem.Play(Filter.Pvs(Owner), SoundBoltOpen.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
- }
- else
- {
- gunSystem.TryFeedChamber(this);
- SoundSystem.Play(Filter.Pvs(Owner), SoundBoltClosed.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
- }
-
- _boltOpen = value;
- gunSystem.UpdateMagazineAppearance(this);
- Dirty(_entities);
- }
- }
- private bool _boltOpen = true;
-
- [DataField("autoEjectMag")] public bool AutoEjectMag;
- // If the bolt needs to be open before we can insert / remove the mag (i.e. for LMGs)
- public bool MagNeedsOpenBolt => _magNeedsOpenBolt;
- [DataField("magNeedsOpenBolt")]
- private bool _magNeedsOpenBolt = default;
-
- // Sounds
- [DataField("soundBoltOpen", required: true)]
- public SoundSpecifier SoundBoltOpen = default!;
- [DataField("soundBoltClosed", required: true)]
- public SoundSpecifier SoundBoltClosed = default!;
- [DataField("soundRack", required: true)]
- public SoundSpecifier SoundRack = default!;
- [DataField("soundMagInsert", required: true)]
- public SoundSpecifier SoundMagInsert = default!;
- [DataField("soundMagEject", required: true)]
- public SoundSpecifier SoundMagEject = default!;
- [DataField("soundAutoEject")] public SoundSpecifier SoundAutoEject = new SoundPathSpecifier("/Audio/Weapons/Guns/EmptyAlarm/smg_empty_alarm.ogg");
- }
-
- [Flags]
- public enum MagazineType
- {
- Unspecified = 0,
- LPistol = 1 << 0, // Placeholder?
- Pistol = 1 << 1,
- HCPistol = 1 << 2,
- Smg = 1 << 3,
- SmgTopMounted = 1 << 4,
- Rifle = 1 << 5,
- IH = 1 << 6, // Placeholder?
- Box = 1 << 7,
- Pan = 1 << 8,
- Dart = 1 << 9, // Placeholder
- CalicoTopMounted = 1 << 10,
- }
-}
diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/PumpBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/PumpBarrelComponent.cs
deleted file mode 100644
index c721828257..0000000000
--- a/Content.Server/Weapon/Ranged/Barrels/Components/PumpBarrelComponent.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-using Content.Server.Weapon.Ranged.Ammunition.Components;
-using Content.Shared.Sound;
-using Robust.Shared.Containers;
-using Robust.Shared.GameStates;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Weapon.Ranged.Barrels.Components
-{
- ///
- /// Bolt-action rifles
- ///
- [RegisterComponent, NetworkedComponent, ComponentReference(typeof(ServerRangedBarrelComponent))]
- public sealed class PumpBarrelComponent : ServerRangedBarrelComponent, ISerializationHooks
- {
- public override int ShotsLeft
- {
- get
- {
- var chamberCount = ChamberContainer.ContainedEntity != null ? 1 : 0;
- return chamberCount + SpawnedAmmo.Count + UnspawnedCount;
- }
- }
-
- private const int DefaultCapacity = 6;
- [DataField("capacity")]
- public override int Capacity { get; } = DefaultCapacity;
-
- // Even a point having a chamber? I guess it makes some of the below code cleaner
- public ContainerSlot ChamberContainer = default!;
- public Stack SpawnedAmmo = new(DefaultCapacity - 1);
- public Container AmmoContainer = default!;
-
- [ViewVariables]
- [DataField("caliber")]
- public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
-
- [ViewVariables]
- [DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))]
- public string? FillPrototype;
-
- [ViewVariables] public int UnspawnedCount;
-
- [DataField("manualCycle")] public bool ManualCycle = true;
-
- // Sounds
- [DataField("soundCycle")] public SoundSpecifier SoundCycle = new SoundPathSpecifier("/Audio/Weapons/Guns/Cock/sf_rifle_cock.ogg");
-
- [DataField("soundInsert")] public SoundSpecifier SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/bullet_insert.ogg");
-
- void ISerializationHooks.AfterDeserialization()
- {
- SpawnedAmmo = new Stack(Capacity - 1);
- }
- }
-}
diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/RevolverBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/RevolverBarrelComponent.cs
deleted file mode 100644
index 4d21fb1211..0000000000
--- a/Content.Server/Weapon/Ranged/Barrels/Components/RevolverBarrelComponent.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using Content.Server.Weapon.Ranged.Ammunition.Components;
-using Content.Shared.Sound;
-using Robust.Shared.Containers;
-using Robust.Shared.GameStates;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Weapon.Ranged.Barrels.Components
-{
- [RegisterComponent, NetworkedComponent, ComponentReference(typeof(ServerRangedBarrelComponent))]
- public sealed class RevolverBarrelComponent : ServerRangedBarrelComponent, ISerializationHooks
- {
- [ViewVariables]
- [DataField("caliber")]
- public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
-
- public Container AmmoContainer = default!;
-
- [ViewVariables]
- public int CurrentSlot;
-
- public override int Capacity => AmmoSlots.Length;
-
- [DataField("capacity")]
- private int _serializedCapacity = 6;
-
- [DataField("ammoSlots", readOnly: true)]
- public EntityUid?[] AmmoSlots = Array.Empty();
-
- public override int ShotsLeft => AmmoContainer.ContainedEntities.Count;
-
- [ViewVariables]
- [DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))]
- public string? FillPrototype;
-
- [ViewVariables]
- public int UnspawnedCount;
-
- // Sounds
- [DataField("soundEject")]
- public SoundSpecifier SoundEject = new SoundPathSpecifier("/Audio/Weapons/Guns/MagOut/revolver_magout.ogg");
-
- [DataField("soundInsert")]
- public SoundSpecifier SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/revolver_magin.ogg");
-
- [DataField("soundSpin")]
- public SoundSpecifier SoundSpin = new SoundPathSpecifier("/Audio/Weapons/Guns/Misc/revolver_spin.ogg");
-
- void ISerializationHooks.BeforeSerialization()
- {
- _serializedCapacity = AmmoSlots.Length;
- }
-
- void ISerializationHooks.AfterDeserialization()
- {
- AmmoSlots = new EntityUid?[_serializedCapacity];
- }
- }
-}
diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/ServerRangedBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/ServerRangedBarrelComponent.cs
deleted file mode 100644
index 85819f8db1..0000000000
--- a/Content.Server/Weapon/Ranged/Barrels/Components/ServerRangedBarrelComponent.cs
+++ /dev/null
@@ -1,136 +0,0 @@
-using Content.Shared.Sound;
-using Content.Shared.Weapons.Ranged.Components;
-using Robust.Shared.Serialization;
-
-namespace Content.Server.Weapon.Ranged.Barrels.Components
-{
- ///
- /// All of the ranged weapon components inherit from this to share mechanics like shooting etc.
- /// Only difference between them is how they retrieve a projectile to shoot (battery, magazine, etc.)
- ///
- [Friend(typeof(GunSystem))]
- public abstract class ServerRangedBarrelComponent : SharedRangedBarrelComponent, ISerializationHooks
- {
- public override FireRateSelector FireRateSelector => _fireRateSelector;
-
- [DataField("currentSelector")]
- private FireRateSelector _fireRateSelector = FireRateSelector.Safety;
-
- public override FireRateSelector AllRateSelectors => _fireRateSelector;
-
- [DataField("fireRate")]
- public override float FireRate { get; } = 2f;
-
- // _lastFire is when we actually fired (so if we hold the button then recoil doesn't build up if we're not firing)
- public TimeSpan LastFire;
-
- // Recoil / spray control
- [DataField("minAngle")]
- private float _minAngleDegrees;
-
- public Angle MinAngle { get; private set; }
-
- [DataField("maxAngle")]
- private float _maxAngleDegrees = 45;
-
- public Angle MaxAngle { get; private set; }
-
- public Angle CurrentAngle = Angle.Zero;
-
- [DataField("angleDecay")]
- private float _angleDecayDegrees = 20;
-
- ///
- /// How slowly the angle's theta decays per second in radians
- ///
- public float AngleDecay { get; private set; }
-
- [DataField("angleIncrease")]
- private float? _angleIncreaseDegrees;
-
- ///
- /// How quickly the angle's theta builds for every shot fired in radians
- ///
- public float AngleIncrease { get; private set; }
-
- // Multiplies the ammo spread to get the final spread of each pellet
- [DataField("ammoSpreadRatio")]
- public float SpreadRatio { get; private set; }
-
- [DataField("canMuzzleFlash")]
- public bool CanMuzzleFlash { get; } = true;
-
- // Sounds
- [DataField("soundGunshot", required: true)]
- public SoundSpecifier SoundGunshot { get; set; } = default!;
-
- [DataField("soundEmpty")]
- public SoundSpecifier SoundEmpty { get; } = new SoundPathSpecifier("/Audio/Weapons/Guns/Empty/empty.ogg");
-
- void ISerializationHooks.BeforeSerialization()
- {
- _minAngleDegrees = (float) (MinAngle.Degrees * 2);
- _maxAngleDegrees = (float) (MaxAngle.Degrees * 2);
- _angleIncreaseDegrees = MathF.Round(AngleIncrease / ((float) Math.PI / 180f), 2);
- AngleDecay = MathF.Round(AngleDecay / ((float) Math.PI / 180f), 2);
- }
-
- void ISerializationHooks.AfterDeserialization()
- {
- // This hard-to-read area's dealing with recoil
- // Use degrees in yaml as it's easier to read compared to "0.0125f"
- MinAngle = Angle.FromDegrees(_minAngleDegrees / 2f);
-
- // Random doubles it as it's +/- so uhh we'll just half it here for readability
- MaxAngle = Angle.FromDegrees(_maxAngleDegrees / 2f);
-
- _angleIncreaseDegrees ??= 40 / FireRate;
- AngleIncrease = _angleIncreaseDegrees.Value * (float) Math.PI / 180f;
-
- AngleDecay = _angleDecayDegrees * (float) Math.PI / 180f;
-
- // For simplicity we'll enforce it this way; ammo determines max spread
- if (SpreadRatio > 1.0f)
- {
- Logger.Error("SpreadRatio must be <= 1.0f for guns");
- throw new InvalidOperationException();
- }
- }
- }
-
- ///
- /// Raised on a gun when it fires projectiles.
- ///
- public sealed class GunShotEvent : EntityEventArgs
- {
- ///
- /// Uid of the entity that shot.
- ///
- public EntityUid Uid;
-
- public readonly EntityUid[] FiredProjectiles;
-
- public GunShotEvent(EntityUid[] firedProjectiles)
- {
- FiredProjectiles = firedProjectiles;
- }
- }
-
- ///
- /// Raised on ammo when it is fired.
- ///
- public sealed class AmmoShotEvent : EntityEventArgs
- {
- ///
- /// Uid of the entity that shot.
- ///
- public EntityUid Uid;
-
- public readonly EntityUid[] FiredProjectiles;
-
- public AmmoShotEvent(EntityUid[] firedProjectiles)
- {
- FiredProjectiles = firedProjectiles;
- }
- }
-}
diff --git a/Content.Server/Weapon/Ranged/Components/AmmoCounterComponent.cs b/Content.Server/Weapon/Ranged/Components/AmmoCounterComponent.cs
new file mode 100644
index 0000000000..8d38896abc
--- /dev/null
+++ b/Content.Server/Weapon/Ranged/Components/AmmoCounterComponent.cs
@@ -0,0 +1,6 @@
+using Content.Shared.Weapons.Ranged.Components;
+
+namespace Content.Server.Weapon.Ranged.Components;
+
+[RegisterComponent]
+public sealed class AmmoCounterComponent : SharedAmmoCounterComponent {}
diff --git a/Content.Server/Weapon/Ranged/Ammunition/Components/ChemicalAmmoComponent.cs b/Content.Server/Weapon/Ranged/Components/ChemicalAmmoComponent.cs
similarity index 81%
rename from Content.Server/Weapon/Ranged/Ammunition/Components/ChemicalAmmoComponent.cs
rename to Content.Server/Weapon/Ranged/Components/ChemicalAmmoComponent.cs
index 11d5c7540a..ce81de0a43 100644
--- a/Content.Server/Weapon/Ranged/Ammunition/Components/ChemicalAmmoComponent.cs
+++ b/Content.Server/Weapon/Ranged/Components/ChemicalAmmoComponent.cs
@@ -1,4 +1,4 @@
-namespace Content.Server.Weapon.Ranged.Ammunition.Components
+namespace Content.Server.Weapon.Ranged.Components
{
[RegisterComponent]
public sealed class ChemicalAmmoComponent : Component
diff --git a/Content.Server/Weapon/Ranged/RangedDamageSoundComponent.cs b/Content.Server/Weapon/Ranged/Components/RangedDamageSoundComponent.cs
similarity index 95%
rename from Content.Server/Weapon/Ranged/RangedDamageSoundComponent.cs
rename to Content.Server/Weapon/Ranged/Components/RangedDamageSoundComponent.cs
index 39303a5210..bb5fc27fbf 100644
--- a/Content.Server/Weapon/Ranged/RangedDamageSoundComponent.cs
+++ b/Content.Server/Weapon/Ranged/Components/RangedDamageSoundComponent.cs
@@ -2,7 +2,7 @@ using Content.Shared.Damage.Prototypes;
using Content.Shared.Sound;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
-namespace Content.Server.Weapon.Ranged;
+namespace Content.Server.Weapon.Ranged.Components;
///
/// Plays the specified sound upon receiving damage of that type.
diff --git a/Content.Server/Weapon/Ranged/FlyBySoundSystem.cs b/Content.Server/Weapon/Ranged/FlyBySoundSystem.cs
deleted file mode 100644
index 79e425e2e4..0000000000
--- a/Content.Server/Weapon/Ranged/FlyBySoundSystem.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-using Content.Shared.Weapons.Ranged;
-
-namespace Content.Server.Weapon.Ranged;
-
-public sealed class FlyBySoundSystem : SharedFlyBySoundSystem {}
diff --git a/Content.Server/Weapon/Ranged/GunSystem.Ammo.cs b/Content.Server/Weapon/Ranged/GunSystem.Ammo.cs
deleted file mode 100644
index cd714b9de5..0000000000
--- a/Content.Server/Weapon/Ranged/GunSystem.Ammo.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-using Content.Server.Weapon.Ranged.Ammunition.Components;
-using Content.Shared.Examine;
-using Content.Shared.Weapons.Ranged.Barrels.Components;
-using Robust.Shared.Map;
-
-namespace Content.Server.Weapon.Ranged;
-
-public sealed partial class GunSystem
-{
- private void OnAmmoExamine(EntityUid uid, AmmoComponent component, ExaminedEvent args)
- {
- var text = Loc.GetString("ammo-component-on-examine",("caliber", component.Caliber));
- args.PushMarkup(text);
- }
-
- public EntityUid? TakeBullet(AmmoComponent component, EntityCoordinates spawnAt)
- {
- if (component.AmmoIsProjectile)
- {
- return component.Owner;
- }
-
- if (component.Spent)
- {
- return null;
- }
-
- component.Spent = true;
-
- if (TryComp(component.Owner, out AppearanceComponent? appearanceComponent))
- {
- appearanceComponent.SetData(AmmoVisuals.Spent, true);
- }
-
- var entity = EntityManager.SpawnEntity(component.ProjectileId, spawnAt);
-
- return entity;
- }
-
- public void MuzzleFlash(EntityUid entity, AmmoComponent component, Angle angle)
- {
- if (component.MuzzleFlashSprite == null)
- {
- return;
- }
-
- var time = _gameTiming.CurTime;
- var deathTime = time + TimeSpan.FromMilliseconds(200);
- // Offset the sprite so it actually looks like it's coming from the gun
- var offset = new Vector2(0.0f, -0.5f);
-
- var message = new EffectSystemMessage
- {
- EffectSprite = component.MuzzleFlashSprite.ToString(),
- Born = time,
- DeathTime = deathTime,
- AttachedEntityUid = entity,
- AttachedOffset = offset,
- //Rotated from east facing
- Rotation = -MathF.PI / 2f,
- Color = Vector4.Multiply(new Vector4(255, 255, 255, 255), 1.0f),
- ColorDelta = new Vector4(0, 0, 0, -1500f),
- Shaded = false
- };
-
- /* TODO: Fix rotation when shooting sideways. This was the closest I got but still had issues.
- * var time = _gameTiming.CurTime;
- var deathTime = time + TimeSpan.FromMilliseconds(200);
- var entityRotation = EntityManager.GetComponent(entity).WorldRotation;
- var localAngle = entityRotation - (angle + MathF.PI / 2f);
- // Offset the sprite so it actually looks like it's coming from the gun
- var offset = localAngle.RotateVec(new Vector2(0.0f, -0.5f));
-
- var message = new EffectSystemMessage
- {
- EffectSprite = component.MuzzleFlashSprite.ToString(),
- Born = time,
- DeathTime = deathTime,
- AttachedEntityUid = entity,
- AttachedOffset = offset,
- //Rotated from east facing
- Rotation = (float) (localAngle - MathF.PI / 2),
- Color = Vector4.Multiply(new Vector4(255, 255, 255, 255), 1.0f),
- ColorDelta = new Vector4(0, 0, 0, -1500f),
- Shaded = false
- };
- */
-
- _effects.CreateParticle(message);
- }
-}
diff --git a/Content.Server/Weapon/Ranged/GunSystem.AmmoBox.cs b/Content.Server/Weapon/Ranged/GunSystem.AmmoBox.cs
deleted file mode 100644
index 4e1125205a..0000000000
--- a/Content.Server/Weapon/Ranged/GunSystem.AmmoBox.cs
+++ /dev/null
@@ -1,206 +0,0 @@
-using Content.Server.Hands.Components;
-using Content.Server.Weapon.Ranged.Ammunition.Components;
-using Content.Shared.Examine;
-using Content.Shared.Interaction;
-using Content.Shared.Interaction.Events;
-using Content.Shared.Verbs;
-using Content.Shared.Weapons.Ranged.Barrels.Components;
-using Robust.Shared.Containers;
-using Robust.Shared.Player;
-
-namespace Content.Server.Weapon.Ranged;
-
-public sealed partial class GunSystem
-{
- // Probably needs combining with magazines in future given the common functionality.
-
- private void OnAmmoBoxAltVerbs(EntityUid uid, AmmoBoxComponent component, GetVerbsEvent args)
- {
- if (args.Hands == null || !args.CanAccess || !args.CanInteract)
- return;
-
- if (component.AmmoLeft == 0)
- return;
-
- AlternativeVerb verb = new()
- {
- Text = Loc.GetString("dump-vert-get-data-text"),
- IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png",
- Act = () => AmmoBoxEjectContents(component, 10)
- };
- args.Verbs.Add(verb);
- }
-
- private void OnAmmoBoxInteractHand(EntityUid uid, AmmoBoxComponent component, InteractHandEvent args)
- {
- if (args.Handled) return;
-
- TryUse(args.User, component);
- }
-
- private void OnAmmoBoxUse(EntityUid uid, AmmoBoxComponent component, UseInHandEvent args)
- {
- if (args.Handled) return;
-
- TryUse(args.User, component);
- }
-
- private void OnAmmoBoxInteractUsing(EntityUid uid, AmmoBoxComponent component, InteractUsingEvent args)
- {
- if (args.Handled) return;
-
- if (TryComp(args.Used, out AmmoComponent? ammoComponent))
- {
- if (TryInsertAmmo(args.User, args.Used, component, ammoComponent))
- {
- args.Handled = true;
- }
-
- return;
- }
-
- if (!TryComp(args.Used, out RangedMagazineComponent? rangedMagazine)) return;
-
- for (var i = 0; i < Math.Max(10, rangedMagazine.ShotsLeft); i++)
- {
- if (TakeAmmo(rangedMagazine) is not {Valid: true} ammo)
- {
- continue;
- }
-
- if (!TryInsertAmmo(args.User, ammo, component))
- {
- TryInsertAmmo(args.User, ammo, rangedMagazine);
- args.Handled = true;
- return;
- }
- }
-
- args.Handled = true;
- }
-
- private void OnAmmoBoxInit(EntityUid uid, AmmoBoxComponent component, ComponentInit args)
- {
- component.AmmoContainer = uid.EnsureContainer($"{component.Name}-container", out var existing);
-
- if (existing)
- {
- foreach (var entity in component.AmmoContainer.ContainedEntities)
- {
- component.UnspawnedCount--;
- component.SpawnedAmmo.Push(entity);
- component.AmmoContainer.Insert(entity);
- }
- }
- }
-
- private void OnAmmoBoxExamine(EntityUid uid, AmmoBoxComponent component, ExaminedEvent args)
- {
- args.PushMarkup(Loc.GetString("ammo-box-component-on-examine-caliber-description", ("caliber", component.Caliber)));
- args.PushMarkup(Loc.GetString("ammo-box-component-on-examine-remaining-ammo-description", ("ammoLeft", component.AmmoLeft),("capacity", component.Capacity)));
- }
-
- private void OnAmmoBoxMapInit(EntityUid uid, AmmoBoxComponent component, MapInitEvent args)
- {
- component.UnspawnedCount += component.Capacity;
- UpdateAmmoBoxAppearance(uid, component);
- }
-
- private void UpdateAmmoBoxAppearance(EntityUid uid, AmmoBoxComponent ammoBox, AppearanceComponent? appearanceComponent = null)
- {
- if (!Resolve(uid, ref appearanceComponent, false)) return;
-
- appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
- appearanceComponent.SetData(AmmoVisuals.AmmoCount, ammoBox.AmmoLeft);
- appearanceComponent.SetData(AmmoVisuals.AmmoMax, ammoBox.Capacity);
- }
-
- private void AmmoBoxEjectContents(AmmoBoxComponent ammoBox, int count)
- {
- var ejectCount = Math.Min(count, ammoBox.Capacity);
- var ejectAmmo = new List(ejectCount);
-
- for (var i = 0; i < Math.Min(count, ammoBox.Capacity); i++)
- {
- if (TakeAmmo(ammoBox) is not { } ammo)
- {
- break;
- }
-
- ejectAmmo.Add(ammo);
- }
-
- EjectCasings(ejectAmmo);
- UpdateAmmoBoxAppearance(ammoBox.Owner, ammoBox);
- }
-
- private bool TryUse(EntityUid user, AmmoBoxComponent ammoBox)
- {
- if (!TryComp(user, out HandsComponent? handsComponent))
- {
- return false;
- }
-
- if (TakeAmmo(ammoBox) is not { } ammo)
- {
- return false;
- }
-
- if (!_handsSystem.TryPickup(user, ammo, handsComp: handsComponent))
- {
- TryInsertAmmo(user, ammo, ammoBox);
- return false;
- }
-
- UpdateAmmoBoxAppearance(ammoBox.Owner, ammoBox);
- return true;
- }
-
- public bool TryInsertAmmo(EntityUid user, EntityUid ammo, AmmoBoxComponent ammoBox, AmmoComponent? ammoComponent = null)
- {
- if (!Resolve(ammo, ref ammoComponent, false))
- {
- return false;
- }
-
- if (ammoComponent.Caliber != ammoBox.Caliber)
- {
- _popup.PopupEntity(Loc.GetString("ammo-box-component-try-insert-ammo-wrong-caliber"), ammo, Filter.Entities(user));
- return false;
- }
-
- if (ammoBox.AmmoLeft >= ammoBox.Capacity)
- {
- _popup.PopupEntity(Loc.GetString("ammo-box-component-try-insert-ammo-no-room"), ammo, Filter.Entities(user));
- return false;
- }
-
- ammoBox.SpawnedAmmo.Push(ammo);
- ammoBox.AmmoContainer.Insert(ammo);
- UpdateAmmoBoxAppearance(ammoBox.Owner, ammoBox);
- return true;
- }
-
- public EntityUid? TakeAmmo(AmmoBoxComponent ammoBox, TransformComponent? xform = null)
- {
- if (!Resolve(ammoBox.Owner, ref xform)) return null;
-
- if (ammoBox.SpawnedAmmo.TryPop(out var ammo))
- {
- ammoBox.AmmoContainer.Remove(ammo);
- return ammo;
- }
-
- if (ammoBox.UnspawnedCount > 0)
- {
- ammo = EntityManager.SpawnEntity(ammoBox.FillPrototype, xform.Coordinates);
-
- // when dumping from held ammo box, this detaches the spawned ammo from the player.
- EntityManager.GetComponent(ammo).AttachParentToContainerOrGrid();
-
- ammoBox.UnspawnedCount--;
- }
-
- return ammo;
- }
-}
diff --git a/Content.Server/Weapon/Ranged/GunSystem.Battery.cs b/Content.Server/Weapon/Ranged/GunSystem.Battery.cs
deleted file mode 100644
index 2604d8bec3..0000000000
--- a/Content.Server/Weapon/Ranged/GunSystem.Battery.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-using Content.Server.Projectiles.Components;
-using Content.Server.Weapon.Ranged.Barrels.Components;
-using Content.Shared.PowerCell.Components;
-using Content.Shared.Weapons.Ranged.Barrels.Components;
-using Robust.Shared.Containers;
-using Robust.Shared.Map;
-
-namespace Content.Server.Weapon.Ranged;
-
-public sealed partial class GunSystem
-{
- private void OnBatteryInit(EntityUid uid, BatteryBarrelComponent component, ComponentInit args)
- {
- if (component.AmmoPrototype != null)
- {
- component.AmmoContainer = uid.EnsureContainer($"{component.GetType()}-ammo-container");
- }
-
- component.Dirty(EntityManager);
- }
-
- private void OnBatteryMapInit(EntityUid uid, BatteryBarrelComponent component, MapInitEvent args)
- {
- UpdateBatteryAppearance(component);
- }
-
- private void OnCellSlotUpdated(EntityUid uid, BatteryBarrelComponent component, PowerCellChangedEvent args)
- {
- UpdateBatteryAppearance(component);
- }
-
- public void UpdateBatteryAppearance(BatteryBarrelComponent component)
- {
- if (!EntityManager.TryGetComponent(component.Owner, out AppearanceComponent? appearanceComponent)) return;
-
- appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, _cell.TryGetBatteryFromSlot(component.Owner, out _));
- appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
- appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
- }
-
- public EntityUid? PeekAmmo(BatteryBarrelComponent component)
- {
- // Spawn a dummy entity because it's easier to work with I guess
- // This will get re-used for the projectile
- var ammo = component.AmmoContainer.ContainedEntity;
- if (ammo == null)
- {
- ammo = EntityManager.SpawnEntity(component.AmmoPrototype, Transform(component.Owner).Coordinates);
- component.AmmoContainer.Insert(ammo.Value);
- }
-
- return ammo.Value;
- }
-
- public EntityUid? TakeProjectile(BatteryBarrelComponent component, EntityCoordinates spawnAt)
- {
- if (!_cell.TryGetBatteryFromSlot(component.Owner, out var capacitor))
- return null;
-
- if (capacitor.CurrentCharge < component.LowerChargeLimit)
- return null;
-
- // Can fire confirmed
- // Multiply the entity's damage / whatever by the percentage of charge the shot has.
- EntityUid? entity;
- var chargeChange = Math.Min(capacitor.CurrentCharge, component.BaseFireCost);
- if (capacitor.UseCharge(chargeChange) < component.LowerChargeLimit)
- {
- // Handling of funny exploding cells.
- return null;
- }
- var energyRatio = chargeChange / component.BaseFireCost;
-
- if (component.AmmoContainer.ContainedEntity != null)
- {
- entity = component.AmmoContainer.ContainedEntity;
- component.AmmoContainer.Remove(entity.Value);
- Transform(entity.Value).Coordinates = spawnAt;
- }
- else
- {
- entity = EntityManager.SpawnEntity(component.AmmoPrototype, spawnAt);
- }
-
- if (TryComp(entity.Value, out ProjectileComponent? projectileComponent))
- {
- if (energyRatio < 1.0)
- {
- projectileComponent.Damage *= energyRatio;
- }
- }
- else if (TryComp(entity.Value, out HitscanComponent? hitscanComponent))
- {
- hitscanComponent.Damage *= energyRatio;
- hitscanComponent.ColorModifier = energyRatio;
- }
- else
- {
- throw new InvalidOperationException("Ammo doesn't have hitscan or projectile?");
- }
-
- // capacitor.UseCharge() triggers a PowerCellChangedEvent which will cause appearance to be updated.
- // So let's not double-call UpdateAppearance() here.
- return entity.Value;
- }
-}
diff --git a/Content.Server/Weapon/Ranged/GunSystem.Bolt.cs b/Content.Server/Weapon/Ranged/GunSystem.Bolt.cs
deleted file mode 100644
index 4ec199d790..0000000000
--- a/Content.Server/Weapon/Ranged/GunSystem.Bolt.cs
+++ /dev/null
@@ -1,266 +0,0 @@
-using Content.Server.Weapon.Ranged.Ammunition.Components;
-using Content.Server.Weapon.Ranged.Barrels.Components;
-using Content.Shared.Examine;
-using Content.Shared.Interaction;
-using Content.Shared.Interaction.Events;
-using Content.Shared.Verbs;
-using Content.Shared.Weapons.Ranged.Barrels.Components;
-using Robust.Shared.Audio;
-using Robust.Shared.Containers;
-using Robust.Shared.GameStates;
-using Robust.Shared.Map;
-using Robust.Shared.Player;
-
-namespace Content.Server.Weapon.Ranged;
-
-public sealed partial class GunSystem
-{
- private void AddToggleBoltVerb(EntityUid uid, BoltActionBarrelComponent component, GetVerbsEvent args)
- {
- if (args.Hands == null ||
- !args.CanAccess ||
- !args.CanInteract)
- return;
-
- InteractionVerb verb = new()
- {
- Text = component.BoltOpen
- ? Loc.GetString("close-bolt-verb-get-data-text")
- : Loc.GetString("open-bolt-verb-get-data-text"),
- Act = () => component.BoltOpen = !component.BoltOpen
- };
- args.Verbs.Add(verb);
- }
-
- private void OnBoltExamine(EntityUid uid, BoltActionBarrelComponent component, ExaminedEvent args)
- {
- args.PushMarkup(Loc.GetString("bolt-action-barrel-component-on-examine", ("caliber", component.Caliber)));
- }
-
- private void OnBoltFireAttempt(EntityUid uid, BoltActionBarrelComponent component, GunFireAttemptEvent args)
- {
- if (args.Cancelled) return;
-
- if (component.BoltOpen || component.ChamberContainer.ContainedEntity == null)
- args.Cancel();
- }
-
- private void OnBoltMapInit(EntityUid uid, BoltActionBarrelComponent component, MapInitEvent args)
- {
- if (component.FillPrototype != null)
- {
- component.UnspawnedCount += component.Capacity;
- if (component.UnspawnedCount > 0)
- {
- component.UnspawnedCount--;
- var chamberEntity = EntityManager.SpawnEntity(component.FillPrototype, EntityManager.GetComponent(uid).Coordinates);
- component.ChamberContainer.Insert(chamberEntity);
- }
- }
-
- UpdateBoltAppearance(component);
- }
-
- public void UpdateBoltAppearance(BoltActionBarrelComponent component)
- {
- if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
-
- appearanceComponent.SetData(BarrelBoltVisuals.BoltOpen, component.BoltOpen);
- appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
- appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
- }
-
- private void OnBoltInit(EntityUid uid, BoltActionBarrelComponent component, ComponentInit args)
- {
- component.SpawnedAmmo = new Stack(component.Capacity - 1);
- component.AmmoContainer = uid.EnsureContainer($"{component.GetType()}-ammo-container", out var existing);
-
- if (existing)
- {
- foreach (var entity in component.AmmoContainer.ContainedEntities)
- {
- component.SpawnedAmmo.Push(entity);
- component.UnspawnedCount--;
- }
- }
-
- component.ChamberContainer = uid.EnsureContainer($"{component.GetType()}-chamber-container");
-
- if (TryComp(uid, out AppearanceComponent? appearanceComponent))
- {
- appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
- }
-
- component.Dirty(EntityManager);
- UpdateBoltAppearance(component);
- }
-
- private void OnBoltUse(EntityUid uid, BoltActionBarrelComponent component, UseInHandEvent args)
- {
- if (args.Handled) return;
-
- args.Handled = true;
-
- if (component.BoltOpen)
- {
- component.BoltOpen = false;
- _popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-bolt-closed"), uid, Filter.Entities(args.User));
- return;
- }
-
- CycleBolt(component, true);
- }
-
- private void CycleBolt(BoltActionBarrelComponent component, bool manual = false)
- {
- TryEjectChamber(component);
- TryFeedChamber(component);
-
- if (component.ChamberContainer.ContainedEntity == null && manual)
- {
- component.BoltOpen = true;
-
- if (_container.TryGetContainingContainer(component.Owner, out var container))
- {
- _popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-bolt-opened"), container.Owner, Filter.Entities(container.Owner));
- }
- return;
- }
- else
- {
- SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundCycle.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
- }
-
- component.Dirty(EntityManager);
- UpdateBoltAppearance(component);
- }
-
- public bool TryEjectChamber(BoltActionBarrelComponent component)
- {
- if (component.ChamberContainer.ContainedEntity is {Valid: true} chambered)
- {
- if (!component.ChamberContainer.Remove(chambered))
- return false;
-
- if (TryComp(chambered, out AmmoComponent? ammoComponent) && !ammoComponent.Caseless)
- EjectCasing(chambered);
-
- return true;
- }
-
- return false;
- }
-
- public bool TryFeedChamber(BoltActionBarrelComponent component)
- {
- if (component.ChamberContainer.ContainedEntity != null)
- {
- return false;
- }
- if (component.SpawnedAmmo.TryPop(out var next))
- {
- component.AmmoContainer.Remove(next);
- component.ChamberContainer.Insert(next);
- return true;
- }
- else if (component.UnspawnedCount > 0)
- {
- component.UnspawnedCount--;
- var ammoEntity = EntityManager.SpawnEntity(component.FillPrototype, EntityManager.GetComponent(component.Owner).Coordinates);
- component.ChamberContainer.Insert(ammoEntity);
- return true;
- }
- return false;
- }
-
- private void OnBoltInteractUsing(EntityUid uid, BoltActionBarrelComponent component, InteractUsingEvent args)
- {
- if (args.Handled) return;
-
- if (TryInsertBullet(args.User, args.Used, component))
- args.Handled = true;
- }
-
- public bool TryInsertBullet(EntityUid user, EntityUid ammo, BoltActionBarrelComponent component)
- {
- if (!TryComp(ammo, out AmmoComponent? ammoComponent))
- return false;
-
- if (ammoComponent.Caliber != component.Caliber)
- {
- _popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-try-insert-bullet-wrong-caliber"), component.Owner, Filter.Entities(user));
- return false;
- }
-
- if (!component.BoltOpen)
- {
- _popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-try-insert-bullet-bolt-closed"), component.Owner, Filter.Entities(user));
- return false;
- }
-
- if (component.ChamberContainer.ContainedEntity == null)
- {
- component.ChamberContainer.Insert(ammo);
- SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
- component.Dirty(EntityManager);
- UpdateBoltAppearance(component);
- return true;
- }
-
- if (component.AmmoContainer.ContainedEntities.Count < component.Capacity - 1)
- {
- component.AmmoContainer.Insert(ammo);
- component.SpawnedAmmo.Push(ammo);
- SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
- component.Dirty(EntityManager);
- UpdateBoltAppearance(component);
- return true;
- }
-
- _popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-try-insert-bullet-no-room"), component.Owner, Filter.Entities(user));
-
- return false;
- }
-
- private void OnBoltGetState(EntityUid uid, BoltActionBarrelComponent component, ref ComponentGetState args)
- {
- (int, int)? count = (component.ShotsLeft, component.Capacity);
- var chamberedExists = component.ChamberContainer.ContainedEntity != null;
- // (Is one chambered?, is the bullet spend)
- var chamber = (chamberedExists, false);
-
- if (chamberedExists && TryComp(component.ChamberContainer.ContainedEntity!.Value, out var ammo))
- {
- chamber.Item2 = ammo.Spent;
- }
-
- args.State = new BoltActionBarrelComponentState(
- chamber,
- component.FireRateSelector,
- count,
- component.SoundGunshot.GetSound());
- }
-
- public EntityUid? PeekAmmo(BoltActionBarrelComponent component)
- {
- return component.ChamberContainer.ContainedEntity;
- }
-
- public EntityUid? TakeProjectile(BoltActionBarrelComponent component, EntityCoordinates spawnAt)
- {
- if (component.AutoCycle)
- {
- CycleBolt(component);
- }
- else
- {
- component.Dirty(EntityManager);
- }
-
- if (component.ChamberContainer.ContainedEntity is not {Valid: true} chamberEntity) return null;
-
- var ammoComponent = EntityManager.GetComponentOrNull(chamberEntity);
-
- return ammoComponent == null ? null : TakeBullet(ammoComponent, spawnAt);
- }
-}
diff --git a/Content.Server/Weapon/Ranged/GunSystem.Guns.cs b/Content.Server/Weapon/Ranged/GunSystem.Guns.cs
deleted file mode 100644
index cf171540d7..0000000000
--- a/Content.Server/Weapon/Ranged/GunSystem.Guns.cs
+++ /dev/null
@@ -1,302 +0,0 @@
-using System.Linq;
-using Content.Server.CombatMode;
-using Content.Server.Hands.Components;
-using Content.Server.Interaction.Components;
-using Content.Server.Projectiles.Components;
-using Content.Server.Weapon.Melee;
-using Content.Server.Weapon.Ranged.Ammunition.Components;
-using Content.Server.Weapon.Ranged.Barrels.Components;
-using Content.Shared.Audio;
-using Content.Shared.Camera;
-using Content.Shared.Damage;
-using Content.Shared.Database;
-using Content.Shared.Interaction.Events;
-using Content.Shared.Popups;
-using Content.Shared.Sound;
-using Robust.Shared.Audio;
-using Robust.Shared.Map;
-using Robust.Shared.Physics;
-using Robust.Shared.Player;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Weapon.Ranged;
-
-public sealed partial class GunSystem
-{
- private void OnMeleeAttempt(EntityUid uid, ServerRangedWeaponComponent component, ref MeleeAttackAttemptEvent args)
- {
- args.Cancelled = true;
- }
-
- ///
- /// Tries to fire a round of ammo out of the weapon.
- ///
- private void TryFire(EntityUid user, EntityCoordinates targetCoords, ServerRangedWeaponComponent gun)
- {
- if (!TryComp(gun.Owner, out ServerRangedBarrelComponent? barrel)) return;
-
- if (!TryComp(user, out HandsComponent? hands) || hands.ActiveHand?.HeldEntity != gun.Owner) return;
-
- if (!TryComp(user, out CombatModeComponent? combat) ||
- !combat.IsInCombatMode ||
- !_blocker.CanInteract(user, gun.Owner)) return;
-
- var fireAttempt = new GunFireAttemptEvent(user, gun);
- EntityManager.EventBus.RaiseLocalEvent(gun.Owner, fireAttempt);
-
- if (fireAttempt.Cancelled) return;
-
- var curTime = _gameTiming.CurTime;
- var span = curTime - gun.LastFireTime;
- if (span.TotalSeconds < 1 / barrel.FireRate) return;
-
- // TODO: Clumsy should be eventbus I think?
-
- gun.LastFireTime = curTime;
- var coordinates = Transform(gun.Owner).Coordinates;
-
- if (gun.ClumsyCheck && EntityManager.TryGetComponent(user, out var clumsyComponent) && ClumsyComponent.TryRollClumsy(user, gun.ClumsyExplodeChance))
- {
- //Wound them
- _damageable.TryChangeDamage(user, clumsyComponent.ClumsyDamage);
- _stun.TryParalyze(user, TimeSpan.FromSeconds(3f), true);
-
- // Apply salt to the wound ("Honk!")
- SoundSystem.Play(
- Filter.Pvs(gun.Owner), gun.ClumsyWeaponHandlingSound.GetSound(),
- coordinates, AudioParams.Default.WithMaxDistance(5));
-
- SoundSystem.Play(
- Filter.Pvs(gun.Owner), gun.ClumsyWeaponShotSound.GetSound(),
- coordinates, AudioParams.Default.WithMaxDistance(5));
-
- user.PopupMessage(Loc.GetString("server-ranged-weapon-component-try-fire-clumsy"));
-
- EntityManager.DeleteEntity(gun.Owner);
- return;
- }
-
- // Firing confirmed
-
- if (gun.CanHotspot)
- _atmos.HotspotExpose(coordinates, 700, 50);
-
- EntityManager.EventBus.RaiseLocalEvent(gun.Owner, new GunShotEvent());
- Fire(user, barrel, targetCoords);
- }
-
- ///
- /// Fires a round of ammo out of the weapon.
- ///
- private void Fire(EntityUid shooter, ServerRangedBarrelComponent component, EntityCoordinates coordinates)
- {
- if (component.ShotsLeft == 0)
- {
- SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundEmpty.GetSound(), component.Owner);
- return;
- }
-
- var ammo = PeekAtAmmo(component);
- if (TakeOutProjectile(component, Transform(shooter).Coordinates) is not {Valid: true} projectile)
- {
- SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundEmpty.GetSound(), component.Owner);
- return;
- }
-
- var targetPos = coordinates.ToMapPos(EntityManager);
-
- // At this point firing is confirmed
- var direction = (targetPos - Transform(shooter).WorldPosition).ToAngle();
- var angle = GetRecoilAngle(component, direction);
- // This should really be client-side but for now we'll just leave it here
- if (HasComp(shooter))
- {
- var kick = -angle.ToVec() * 0.15f;
- _recoil.KickCamera(shooter, kick);
- }
-
- // This section probably needs tweaking so there can be caseless hitscan etc.
- if (TryComp(projectile, out HitscanComponent? hitscan))
- {
- FireHitscan(shooter, hitscan, component, angle);
- }
- else if (HasComp(projectile) &&
- TryComp(ammo, out AmmoComponent? ammoComponent))
- {
- FireProjectiles(shooter, projectile, component, ammoComponent.ProjectilesFired, ammoComponent.EvenSpreadAngle, angle, ammoComponent.Velocity, ammo!.Value);
-
- if (component.CanMuzzleFlash)
- {
- MuzzleFlash(component.Owner, ammoComponent, angle);
- }
-
- if (ammoComponent.Caseless)
- {
- EntityManager.DeleteEntity(ammo.Value);
- }
- }
- else
- {
- // Invalid types
- throw new InvalidOperationException();
- }
-
- SoundSystem.Play(Filter.Broadcast(), component.SoundGunshot.GetSound(), component.Owner);
-
- component.Dirty(EntityManager);
- component.LastFire = _gameTiming.CurTime;
- }
-
- #region Firing
-
- ///
- /// Handles firing one or many projectiles
- ///
- private void FireProjectiles(EntityUid shooter, EntityUid baseProjectile, ServerRangedBarrelComponent component, int count, float evenSpreadAngle, Angle angle, float velocity, EntityUid ammo)
- {
- List? sprayAngleChange = null;
- if (count > 1)
- {
- evenSpreadAngle *= component.SpreadRatio;
- sprayAngleChange = Linspace(-evenSpreadAngle / 2, evenSpreadAngle / 2, count);
- }
-
- var firedProjectiles = new EntityUid[count];
- for (var i = 0; i < count; i++)
- {
- EntityUid projectile;
-
- if (i == 0)
- {
- projectile = baseProjectile;
- }
- else
- {
- // TODO: Cursed as bruh
- projectile = EntityManager.SpawnEntity(
- MetaData(baseProjectile).EntityPrototype?.ID,
- Transform(baseProjectile).Coordinates);
- }
-
- firedProjectiles[i] = projectile;
-
- Angle projectileAngle;
-
- if (sprayAngleChange != null)
- {
- projectileAngle = angle + sprayAngleChange[i];
- }
- else
- {
- projectileAngle = angle;
- }
-
- var physics = EntityManager.GetComponent(projectile);
- physics.BodyStatus = BodyStatus.InAir;
-
- var projectileComponent = EntityManager.GetComponent(projectile);
- projectileComponent.IgnoreEntity(shooter);
-
- // FIXME: Work around issue where inserting and removing an entity from a container,
- // then setting its linear velocity in the same tick resets velocity back to zero.
- // See SharedBroadphaseSystem.HandleContainerInsert()... It sets Awake to false, which causes this.
- projectile.SpawnTimer(TimeSpan.FromMilliseconds(25), () =>
- {
- EntityManager.GetComponent(projectile)
- .LinearVelocity = projectileAngle.ToVec() * velocity;
- });
-
-
- Transform(projectile).WorldRotation = projectileAngle + MathHelper.PiOver2;
- }
-
- EntityManager.EventBus.RaiseLocalEvent(component.Owner, new Barrels.Components.GunShotEvent(firedProjectiles));
- EntityManager.EventBus.RaiseLocalEvent(ammo, new AmmoShotEvent(firedProjectiles));
- }
-
- ///
- /// Returns a list of numbers that form a set of equal intervals between the start and end value. Used to calculate shotgun spread angles.
- ///
- private List Linspace(double start, double end, int intervals)
- {
- DebugTools.Assert(intervals > 1);
-
- var linspace = new List(intervals);
-
- for (var i = 0; i <= intervals - 1; i++)
- {
- linspace.Add(Angle.FromDegrees(start + (end - start) * i / (intervals - 1)));
- }
- return linspace;
- }
-
- ///
- /// Fires hitscan entities and then displays their effects
- ///
- private void FireHitscan(EntityUid shooter, HitscanComponent hitscan, ServerRangedBarrelComponent component, Angle angle)
- {
- var ray = new CollisionRay(Transform(component.Owner).WorldPosition, angle.ToVec(), (int) hitscan.CollisionMask);
- var rayCastResults = _physics.IntersectRay(Transform(component.Owner).MapID, ray, hitscan.MaxLength, shooter, false).ToList();
-
- if (rayCastResults.Count >= 1)
- {
- var result = rayCastResults[0];
- var distance = result.Distance;
- hitscan.FireEffects(shooter, distance, angle, result.HitEntity);
- var modifiedDamage = _damageable.TryChangeDamage(result.HitEntity, hitscan.Damage);
- if (modifiedDamage != null)
- _adminLogger.Add(LogType.HitScanHit,
- $"{EntityManager.ToPrettyString(shooter):user} hit {EntityManager.ToPrettyString(result.HitEntity):target} using {EntityManager.ToPrettyString(hitscan.Owner):used} and dealt {modifiedDamage.Total:damage} damage");
-
- PlaySound(rayCastResults[0].HitEntity, modifiedDamage, hitscan.SoundHit, hitscan.ForceSound);
- }
- else
- {
- hitscan.FireEffects(shooter, hitscan.MaxLength, angle);
- }
- }
-
- #endregion
-
- #region Impact sounds
-
- public void PlaySound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound)
- {
- // Like projectiles and melee,
- // 1. Entity specific sound
- // 2. Ammo's sound
- // 3. Nothing
- var playedSound = false;
-
- if (!forceWeaponSound && modifiedDamage != null && modifiedDamage.Total > 0 && TryComp(otherEntity, out var rangedSound))
- {
- var type = MeleeWeaponSystem.GetHighestDamageSound(modifiedDamage, _protoManager);
-
- if (type != null && rangedSound.SoundTypes?.TryGetValue(type, out var damageSoundType) == true)
- {
- SoundSystem.Play(
- Filter.Pvs(otherEntity, entityManager: EntityManager),
- damageSoundType!.GetSound(),
- otherEntity,
- AudioHelpers.WithVariation(DamagePitchVariation));
-
- playedSound = true;
- }
- else if (type != null && rangedSound.SoundGroups?.TryGetValue(type, out var damageSoundGroup) == true)
- {
- SoundSystem.Play(
- Filter.Pvs(otherEntity, entityManager: EntityManager),
- damageSoundGroup!.GetSound(),
- otherEntity,
- AudioHelpers.WithVariation(DamagePitchVariation));
-
- playedSound = true;
- }
- }
-
- if (!playedSound && weaponSound != null)
- SoundSystem.Play(Filter.Pvs(otherEntity, entityManager: EntityManager), weaponSound.GetSound(), otherEntity);
- }
-
- #endregion
-}
diff --git a/Content.Server/Weapon/Ranged/GunSystem.Magazine.cs b/Content.Server/Weapon/Ranged/GunSystem.Magazine.cs
deleted file mode 100644
index c3606d3880..0000000000
--- a/Content.Server/Weapon/Ranged/GunSystem.Magazine.cs
+++ /dev/null
@@ -1,375 +0,0 @@
-using Content.Server.Weapon.Ranged.Ammunition.Components;
-using Content.Server.Weapon.Ranged.Barrels.Components;
-using Content.Shared.Examine;
-using Content.Shared.Interaction;
-using Content.Shared.Interaction.Events;
-using Content.Shared.Verbs;
-using Content.Shared.Weapons.Ranged;
-using Content.Shared.Weapons.Ranged.Barrels.Components;
-using Robust.Shared.Audio;
-using Robust.Shared.Containers;
-using Robust.Shared.GameStates;
-using Robust.Shared.Map;
-using Robust.Shared.Player;
-
-namespace Content.Server.Weapon.Ranged;
-
-public sealed partial class GunSystem
-{
- private void AddEjectMagazineVerb(EntityUid uid, MagazineBarrelComponent component, GetVerbsEvent args)
- {
- if (args.Hands == null ||
- !args.CanAccess ||
- !args.CanInteract ||
- component.MagazineContainer.ContainedEntity is not EntityUid mag ||
- !_blocker.CanPickup(args.User, mag))
- return;
-
- if (component.MagNeedsOpenBolt && !component.BoltOpen)
- return;
-
- AlternativeVerb verb = new()
- {
- Text = MetaData(mag).EntityName,
- Category = VerbCategory.Eject,
- Act = () => RemoveMagazine(args.User, component)
- };
- args.Verbs.Add(verb);
- }
-
- private void AddMagazineInteractionVerbs(EntityUid uid, MagazineBarrelComponent component, GetVerbsEvent args)
- {
- if (args.Hands == null ||
- !args.CanAccess ||
- !args.CanInteract)
- return;
-
- // Toggle bolt verb
- InteractionVerb toggleBolt = new()
- {
- Text = component.BoltOpen
- ? Loc.GetString("close-bolt-verb-get-data-text")
- : Loc.GetString("open-bolt-verb-get-data-text"),
- Act = () => component.BoltOpen = !component.BoltOpen
- };
- args.Verbs.Add(toggleBolt);
-
- // Are we holding a mag that we can insert?
- if (args.Using is not {Valid: true} @using ||
- !CanInsertMagazine(args.User, @using, component) ||
- !_blocker.CanDrop(args.User))
- return;
-
- // Insert mag verb
- InteractionVerb insert = new()
- {
- Text = MetaData(@using).EntityName,
- Category = VerbCategory.Insert,
- Act = () => InsertMagazine(args.User, @using, component)
- };
- args.Verbs.Add(insert);
- }
-
- private void OnMagazineExamine(EntityUid uid, MagazineBarrelComponent component, ExaminedEvent args)
- {
- args.PushMarkup(Loc.GetString("server-magazine-barrel-component-on-examine", ("caliber", component.Caliber)));
-
- foreach (var magazineType in GetMagazineTypes(component))
- {
- args.PushMarkup(Loc.GetString("server-magazine-barrel-component-on-examine-magazine-type", ("magazineType", magazineType)));
- }
- }
-
- private void OnMagazineUse(EntityUid uid, MagazineBarrelComponent component, UseInHandEvent args)
- {
- if (args.Handled) return;
-
- // Behavior:
- // If bolt open just close it
- // If bolt closed then cycle
- // If we cycle then get next round
- // If no more round then open bolt
-
- args.Handled = true;
-
- if (component.BoltOpen)
- {
- SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundBoltClosed.GetSound(), component.Owner, AudioParams.Default.WithVolume(-5));
- _popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-use-entity-bolt-closed"), component.Owner, Filter.Entities(args.User));
- component.BoltOpen = false;
- return;
- }
-
- // Could play a rack-slide specific sound here if you're so inclined (if the chamber is empty but rounds are available)
-
- CycleMagazine(component, true);
- return;
- }
-
- public void UpdateMagazineAppearance(MagazineBarrelComponent component)
- {
- if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
-
- appearanceComponent.SetData(BarrelBoltVisuals.BoltOpen, component.BoltOpen);
- appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, component.MagazineContainer.ContainedEntity != null);
- appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
- appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
- }
-
- private void OnMagazineGetState(EntityUid uid, MagazineBarrelComponent component, ref ComponentGetState args)
- {
- (int, int)? count = null;
- if (component.MagazineContainer.ContainedEntity is {Valid: true} magazine &&
- TryComp(magazine, out RangedMagazineComponent? rangedMagazineComponent))
- {
- count = (rangedMagazineComponent.ShotsLeft, rangedMagazineComponent.Capacity);
- }
-
- args.State = new MagazineBarrelComponentState(
- component.ChamberContainer.ContainedEntity != null,
- component.FireRateSelector,
- count,
- component.SoundGunshot.GetSound());
- }
-
- private void OnMagazineInteractUsing(EntityUid uid, MagazineBarrelComponent component, InteractUsingEvent args)
- {
- if (args.Handled) return;
-
- if (CanInsertMagazine(args.User, args.Used, component, false))
- {
- InsertMagazine(args.User, args.Used, component);
- args.Handled = true;
- return;
- }
-
- // Insert 1 ammo
- if (TryComp(args.Used, out AmmoComponent? ammoComponent))
- {
- if (!component.BoltOpen)
- {
- _popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-ammo-bolt-closed"), component.Owner, Filter.Entities(args.User));
- return;
- }
-
- if (ammoComponent.Caliber != component.Caliber)
- {
- _popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-wrong-caliber"), component.Owner, Filter.Entities(args.User));
- return;
- }
-
- if (component.ChamberContainer.ContainedEntity == null)
- {
- _popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-ammo-success"), component.Owner, Filter.Entities(args.User));
- component.ChamberContainer.Insert(args.Used);
- component.Dirty(EntityManager);
- UpdateMagazineAppearance(component);
- args.Handled = true;
- return;
- }
-
- _popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-ammo-full"), component.Owner, Filter.Entities(args.User));
- }
- }
-
- private void OnMagazineInit(EntityUid uid, MagazineBarrelComponent component, ComponentInit args)
- {
- component.ChamberContainer = uid.EnsureContainer($"{component.GetType()}-chamber");
- component.MagazineContainer = uid.EnsureContainer($"{component.GetType()}-magazine", out var existing);
-
- if (!existing && component.MagFillPrototype != null)
- {
- var magEntity = EntityManager.SpawnEntity(component.MagFillPrototype, Transform(uid).Coordinates);
- component.MagazineContainer.Insert(magEntity);
- }
-
- // Temporary coz client doesn't know about magfill.
- component.Dirty(EntityManager);
- }
-
- private void OnMagazineMapInit(EntityUid uid, MagazineBarrelComponent component, MapInitEvent args)
- {
- UpdateMagazineAppearance(component);
- }
-
- public bool TryEjectChamber(MagazineBarrelComponent component)
- {
- if (component.ChamberContainer.ContainedEntity is {Valid: true} chamberEntity)
- {
- if (!component.ChamberContainer.Remove(chamberEntity))
- {
- return false;
- }
- var ammoComponent = EntityManager.GetComponent(chamberEntity);
- if (!ammoComponent.Caseless)
- {
- EjectCasing(chamberEntity);
- }
- return true;
- }
- return false;
- }
-
- public bool TryFeedChamber(MagazineBarrelComponent component)
- {
- if (component.ChamberContainer.ContainedEntity != null)
- {
- return false;
- }
-
- // Try and pull a round from the magazine to replace the chamber if possible
- var magazine = component.MagazineContainer.ContainedEntity;
- var magComp = EntityManager.GetComponentOrNull(magazine);
-
- if (magComp == null || TakeAmmo(magComp) is not {Valid: true} nextRound)
- {
- return false;
- }
-
- component.ChamberContainer.Insert(nextRound);
-
- if (component.AutoEjectMag && magazine != null && EntityManager.GetComponent(magazine.Value).ShotsLeft == 0)
- {
- SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundAutoEject.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
-
- component.MagazineContainer.Remove(magazine.Value);
- // TODO: Should be a state or something, waste of bandwidth
- RaiseNetworkEvent(new MagazineAutoEjectEvent {Uid = component.Owner});
- }
- return true;
- }
-
- private void CycleMagazine(MagazineBarrelComponent component, bool manual = false)
- {
- if (component.BoltOpen)
- return;
-
- TryEjectChamber(component);
-
- TryFeedChamber(component);
-
- if (component.ChamberContainer.ContainedEntity == null && !component.BoltOpen)
- {
- SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundBoltOpen.GetSound(), component.Owner, AudioParams.Default.WithVolume(-5));
-
- if (_container.TryGetContainingContainer(component.Owner, out var container))
- {
- _popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-cycle-bolt-open"), component.Owner, Filter.Entities(container.Owner));
- }
-
- component.BoltOpen = true;
- return;
- }
-
- if (manual)
- {
- SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundRack.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
- }
-
- component.Dirty(EntityManager);
- UpdateMagazineAppearance(component);
- }
-
- public EntityUid? PeekAmmo(MagazineBarrelComponent component)
- {
- return component.BoltOpen ? null : component.ChamberContainer.ContainedEntity;
- }
-
- public EntityUid? TakeProjectile(MagazineBarrelComponent component, EntityCoordinates spawnAt)
- {
- if (component.BoltOpen)
- return null;
-
- var entity = component.ChamberContainer.ContainedEntity;
-
- CycleMagazine(component);
-
- return entity != null ? TakeBullet(EntityManager.GetComponent(entity.Value), spawnAt) : null;
- }
-
- public List GetMagazineTypes(MagazineBarrelComponent component)
- {
- var types = new List();
-
- foreach (MagazineType mag in Enum.GetValues(typeof(MagazineType)))
- {
- if ((component.MagazineTypes & mag) != 0)
- {
- types.Add(mag);
- }
- }
-
- return types;
- }
-
- public void RemoveMagazine(EntityUid user, MagazineBarrelComponent component)
- {
- var mag = component.MagazineContainer.ContainedEntity;
-
- if (mag == null)
- return;
-
- if (component.MagNeedsOpenBolt && !component.BoltOpen)
- {
- _popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-remove-magazine-bolt-closed"), component.Owner, Filter.Entities(user));
- return;
- }
-
- component.MagazineContainer.Remove(mag.Value);
- SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundMagEject.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
-
- _handsSystem.PickupOrDrop(user, mag.Value);
-
- component.Dirty(EntityManager);
- UpdateMagazineAppearance(component);
- }
-
- public bool CanInsertMagazine(EntityUid user, EntityUid magazine, MagazineBarrelComponent component, bool quiet = true)
- {
- if (!TryComp(magazine, out RangedMagazineComponent? magazineComponent))
- {
- return false;
- }
-
- if ((component.MagazineTypes & magazineComponent.MagazineType) == 0)
- {
- if (!quiet)
- _popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-wrong-magazine-type"), component.Owner, Filter.Entities(user));
-
- return false;
- }
-
- if (magazineComponent.Caliber != component.Caliber)
- {
- if (!quiet)
- _popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-wrong-caliber"), component.Owner, Filter.Entities(user));
-
- return false;
- }
-
- if (component.MagNeedsOpenBolt && !component.BoltOpen)
- {
- if (!quiet)
- _popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-bolt-closed"), component.Owner, Filter.Entities(user));
-
- return false;
- }
-
- if (component.MagazineContainer.ContainedEntity == null)
- return true;
-
- if (!quiet)
- _popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-already-holding-magazine"), component.Owner, Filter.Entities(user));
-
- return false;
- }
-
- public void InsertMagazine(EntityUid user, EntityUid magazine, MagazineBarrelComponent component)
- {
- SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundMagInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
- _popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-success"), component.Owner, Filter.Entities(user));
- component.MagazineContainer.Insert(magazine);
- component.Dirty(EntityManager);
- UpdateMagazineAppearance(component);
- }
-}
diff --git a/Content.Server/Weapon/Ranged/GunSystem.Pump.cs b/Content.Server/Weapon/Ranged/GunSystem.Pump.cs
deleted file mode 100644
index 4e81eebd0c..0000000000
--- a/Content.Server/Weapon/Ranged/GunSystem.Pump.cs
+++ /dev/null
@@ -1,190 +0,0 @@
-using Content.Server.Weapon.Ranged.Ammunition.Components;
-using Content.Server.Weapon.Ranged.Barrels.Components;
-using Content.Shared.Examine;
-using Content.Shared.Interaction;
-using Content.Shared.Interaction.Events;
-using Content.Shared.Weapons.Ranged.Barrels.Components;
-using Robust.Shared.Audio;
-using Robust.Shared.Containers;
-using Robust.Shared.GameStates;
-using Robust.Shared.Map;
-using Robust.Shared.Player;
-
-namespace Content.Server.Weapon.Ranged;
-
-public sealed partial class GunSystem
-{
- private void OnPumpExamine(EntityUid uid, PumpBarrelComponent component, ExaminedEvent args)
- {
- args.PushMarkup(Loc.GetString("pump-barrel-component-on-examine", ("caliber", component.Caliber)));
- }
-
- private void OnPumpGetState(EntityUid uid, PumpBarrelComponent component, ref ComponentGetState args)
- {
- (int, int)? count = (component.ShotsLeft, component.Capacity);
- var chamberedExists = component.ChamberContainer.ContainedEntity != null;
- // (Is one chambered?, is the bullet spend)
- var chamber = (chamberedExists, false);
-
- if (chamberedExists && TryComp(component.ChamberContainer.ContainedEntity!.Value, out var ammo))
- {
- chamber.Item2 = ammo.Spent;
- }
-
- args.State = new PumpBarrelComponentState(
- chamber,
- component.FireRateSelector,
- count,
- component.SoundGunshot.GetSound());
- }
-
- private void OnPumpMapInit(EntityUid uid, PumpBarrelComponent component, MapInitEvent args)
- {
- if (component.FillPrototype != null)
- {
- component.UnspawnedCount += component.Capacity - 1;
- }
-
- UpdatePumpAppearance(component);
- }
-
- private void UpdatePumpAppearance(PumpBarrelComponent component)
- {
- if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
-
- appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
- appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
- }
-
- private void OnPumpInit(EntityUid uid, PumpBarrelComponent component, ComponentInit args)
- {
- component.AmmoContainer =
- uid.EnsureContainer($"{component.GetType()}-ammo-container", out var existing);
-
- if (existing)
- {
- foreach (var entity in component.AmmoContainer.ContainedEntities)
- {
- component.SpawnedAmmo.Push(entity);
- component.UnspawnedCount--;
- }
- }
-
- component.ChamberContainer =
- uid.EnsureContainer($"{component.GetType()}-chamber-container", out existing);
-
- if (existing)
- {
- component.UnspawnedCount--;
- }
-
- if (TryComp(uid, out AppearanceComponent? appearanceComponent))
- {
- appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
- }
-
- component.Dirty(EntityManager);
- UpdatePumpAppearance(component);
- }
-
- private void OnPumpUse(EntityUid uid, PumpBarrelComponent component, UseInHandEvent args)
- {
- if (args.Handled) return;
-
- args.Handled = true;
- CyclePump(component, true);
- }
-
- private void OnPumpInteractUsing(EntityUid uid, PumpBarrelComponent component, InteractUsingEvent args)
- {
- if (args.Handled) return;
-
- if (TryInsertBullet(component, args))
- args.Handled = true;
- }
-
- public bool TryInsertBullet(PumpBarrelComponent component, InteractUsingEvent args)
- {
- if (!TryComp(args.Used, out AmmoComponent? ammoComponent))
- {
- return false;
- }
-
- if (ammoComponent.Caliber != component.Caliber)
- {
- _popup.PopupEntity(Loc.GetString("pump-barrel-component-try-insert-bullet-wrong-caliber"), component.Owner, Filter.Entities(args.User));
- return false;
- }
-
- if (component.AmmoContainer.ContainedEntities.Count < component.Capacity - 1)
- {
- component.AmmoContainer.Insert(args.Used);
- component.SpawnedAmmo.Push(args.Used);
- component.Dirty(EntityManager);
- UpdatePumpAppearance(component);
- SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
- return true;
- }
-
- _popup.PopupEntity(Loc.GetString("pump-barrel-component-try-insert-bullet-no-room"), component.Owner, Filter.Entities(args.User));
-
- return false;
- }
-
- private void CyclePump(PumpBarrelComponent component, bool manual = false)
- {
- if (component.ChamberContainer.ContainedEntity is {Valid: true} chamberedEntity)
- {
- component.ChamberContainer.Remove(chamberedEntity);
- var ammoComponent = EntityManager.GetComponent(chamberedEntity);
- if (!ammoComponent.Caseless)
- {
- EjectCasing(chamberedEntity);
- }
- }
-
- if (component.SpawnedAmmo.TryPop(out var next))
- {
- component.AmmoContainer.Remove(next);
- component.ChamberContainer.Insert(next);
- }
-
- if (component.UnspawnedCount > 0)
- {
- component.UnspawnedCount--;
- var ammoEntity = EntityManager.SpawnEntity(component.FillPrototype, Transform(component.Owner).Coordinates);
- component.ChamberContainer.Insert(ammoEntity);
- }
-
- if (manual)
- {
- SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundCycle.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
- }
-
- component.Dirty(EntityManager);
- UpdatePumpAppearance(component);
- }
-
- public EntityUid? PeekAmmo(PumpBarrelComponent component)
- {
- return component.ChamberContainer.ContainedEntity;
- }
-
- public EntityUid? TakeProjectile(PumpBarrelComponent component, EntityCoordinates spawnAt)
- {
- if (!component.ManualCycle)
- {
- CyclePump(component);
- }
- else
- {
- component.Dirty(EntityManager);
- }
-
- if (component.ChamberContainer.ContainedEntity is not {Valid: true} chamberEntity) return null;
-
- var ammoComponent = EntityManager.GetComponentOrNull(chamberEntity);
-
- return ammoComponent == null ? null : TakeBullet(ammoComponent, spawnAt);
- }
-}
diff --git a/Content.Server/Weapon/Ranged/GunSystem.RangedMagazine.cs b/Content.Server/Weapon/Ranged/GunSystem.RangedMagazine.cs
deleted file mode 100644
index ec857a449a..0000000000
--- a/Content.Server/Weapon/Ranged/GunSystem.RangedMagazine.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-using Content.Server.Hands.Components;
-using Content.Server.Weapon.Ranged.Ammunition.Components;
-using Content.Shared.Examine;
-using Content.Shared.Interaction;
-using Content.Shared.Interaction.Events;
-using Content.Shared.Weapons.Ranged.Barrels.Components;
-using Robust.Shared.Containers;
-using Robust.Shared.Player;
-
-namespace Content.Server.Weapon.Ranged;
-
-public sealed partial class GunSystem
-{
- private void OnRangedMagMapInit(EntityUid uid, RangedMagazineComponent component, MapInitEvent args)
- {
- if (component.FillPrototype != null)
- {
- component.UnspawnedCount += component.Capacity;
- }
-
- UpdateRangedMagAppearance(component);
- }
-
- private void OnRangedMagInit(EntityUid uid, RangedMagazineComponent component, ComponentInit args)
- {
- component.AmmoContainer = uid.EnsureContainer($"{component.GetType()}-magazine", out var existing);
-
- if (existing)
- {
- if (component.AmmoContainer.ContainedEntities.Count > component.Capacity)
- {
- throw new InvalidOperationException("Initialized capacity of magazine higher than its actual capacity");
- }
-
- foreach (var entity in component.AmmoContainer.ContainedEntities)
- {
- component.SpawnedAmmo.Push(entity);
- component.UnspawnedCount--;
- }
- }
-
- if (TryComp(component.Owner, out AppearanceComponent? appearanceComponent))
- {
- appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
- }
- }
-
- private void UpdateRangedMagAppearance(RangedMagazineComponent component)
- {
- if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
-
- appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
- appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
- }
-
- private void OnRangedMagUse(EntityUid uid, RangedMagazineComponent component, UseInHandEvent args)
- {
- if (args.Handled) return;
-
- if (!TryComp(args.User, out HandsComponent? handsComponent))
- {
- return;
- }
-
- if (TakeAmmo(component) is not {Valid: true} ammo)
- return;
-
- _handsSystem.PickupOrDrop(args.User, ammo, handsComp: handsComponent);
- EjectCasing(ammo);
-
- args.Handled = true;
- }
-
- private void OnRangedMagExamine(EntityUid uid, RangedMagazineComponent component, ExaminedEvent args)
- {
- args.PushMarkup(Loc.GetString("ranged-magazine-component-on-examine", ("magazineType", component.MagazineType),("caliber", component.Caliber)));
- }
-
- private void OnRangedMagInteractUsing(EntityUid uid, RangedMagazineComponent component, InteractUsingEvent args)
- {
- if (args.Handled) return;
-
- if (TryInsertAmmo(args.User, args.Used, component))
- args.Handled = true;
- }
-
- public bool TryInsertAmmo(EntityUid user, EntityUid ammo, RangedMagazineComponent component)
- {
- if (!TryComp(ammo, out AmmoComponent? ammoComponent))
- {
- return false;
- }
-
- if (ammoComponent.Caliber != component.Caliber)
- {
- _popup.PopupEntity(Loc.GetString("ranged-magazine-component-try-insert-ammo-wrong-caliber"), component.Owner, Filter.Entities(user));
- return false;
- }
-
- if (component.ShotsLeft >= component.Capacity)
- {
- _popup.PopupEntity(Loc.GetString("ranged-magazine-component-try-insert-ammo-is-full "), component.Owner, Filter.Entities(user));
- return false;
- }
-
- component.AmmoContainer.Insert(ammo);
- component.SpawnedAmmo.Push(ammo);
- UpdateRangedMagAppearance(component);
- return true;
- }
-
- public EntityUid? TakeAmmo(RangedMagazineComponent component)
- {
- EntityUid? ammo = null;
- // If anything's spawned use that first, otherwise use the fill prototype as a fallback (if we have spawn count left)
- if (component.SpawnedAmmo.TryPop(out var entity))
- {
- ammo = entity;
- component.AmmoContainer.Remove(entity);
- }
- else if (component.UnspawnedCount > 0)
- {
- component.UnspawnedCount--;
- ammo = EntityManager.SpawnEntity(component.FillPrototype, Transform(component.Owner).Coordinates);
- }
-
- UpdateRangedMagAppearance(component);
- return ammo;
- }
-}
diff --git a/Content.Server/Weapon/Ranged/GunSystem.Revolvers.cs b/Content.Server/Weapon/Ranged/GunSystem.Revolvers.cs
deleted file mode 100644
index bd2ed48d8c..0000000000
--- a/Content.Server/Weapon/Ranged/GunSystem.Revolvers.cs
+++ /dev/null
@@ -1,226 +0,0 @@
-using Content.Server.Weapon.Ranged.Ammunition.Components;
-using Content.Server.Weapon.Ranged.Barrels.Components;
-using Content.Shared.Interaction;
-using Content.Shared.Interaction.Events;
-using Content.Shared.Popups;
-using Content.Shared.Verbs;
-using Content.Shared.Weapons.Ranged.Barrels.Components;
-using Robust.Shared.Audio;
-using Robust.Shared.Containers;
-using Robust.Shared.GameStates;
-using Robust.Shared.Map;
-using Robust.Shared.Player;
-
-namespace Content.Server.Weapon.Ranged;
-
-public sealed partial class GunSystem
-{
- private void OnRevolverUse(EntityUid uid, RevolverBarrelComponent component, UseInHandEvent args)
- {
- if (args.Handled) return;
-
- EjectAllSlots(component);
- component.Dirty(EntityManager);
- UpdateRevolverAppearance(component);
- args.Handled = true;
- }
-
- private void OnRevolverInteractUsing(EntityUid uid, RevolverBarrelComponent component, InteractUsingEvent args)
- {
- if (args.Handled) return;
-
- if (TryInsertBullet(args.User, args.Used, component))
- args.Handled = true;
- }
-
- public bool TryInsertBullet(EntityUid user, EntityUid entity, RevolverBarrelComponent component)
- {
- if (!TryComp(entity, out AmmoComponent? ammoComponent))
- {
- return false;
- }
-
- if (ammoComponent.Caliber != component.Caliber)
- {
- _popup.PopupEntity(Loc.GetString("revolver-barrel-component-try-insert-bullet-wrong-caliber"), component.Owner, Filter.Entities(user));
- return false;
- }
-
- // Functions like a stack
- // These are inserted in reverse order but then when fired Cycle will go through in order
- // The reason we don't just use an actual stack is because spin can select a random slot to point at
- for (var i = component.AmmoSlots.Length - 1; i >= 0; i--)
- {
- var slot = component.AmmoSlots[i];
- if (slot == default)
- {
- component.CurrentSlot = i;
- component.AmmoSlots[i] = entity;
- component.AmmoContainer.Insert(entity);
- SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
-
- component.Dirty(EntityManager);
- UpdateRevolverAppearance(component);
- return true;
- }
- }
-
- _popup.PopupEntity(Loc.GetString("revolver-barrel-component-try-insert-bullet-ammo-full"), ammoComponent.Owner, Filter.Entities(user));
- return false;
- }
-
- ///
- /// Russian Roulette
- ///
- public void SpinRevolver(RevolverBarrelComponent component)
- {
- var random = _random.Next(component.AmmoSlots.Length - 1);
- component.CurrentSlot = random;
- SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundSpin.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
- component.Dirty(EntityManager);
- }
-
- public void CycleRevolver(RevolverBarrelComponent component)
- {
- // Move up a slot
- component.CurrentSlot = (component.CurrentSlot + 1) % component.AmmoSlots.Length;
- component.Dirty(EntityManager);
- UpdateRevolverAppearance(component);
- }
-
- private void EjectAllSlots(RevolverBarrelComponent component)
- {
- for (var i = 0; i < component.AmmoSlots.Length; i++)
- {
- var entity = component.AmmoSlots[i];
- if (entity == null) continue;
-
- component.AmmoContainer.Remove(entity.Value);
- EjectCasing(entity.Value);
- component.AmmoSlots[i] = null;
- }
-
- if (component.AmmoContainer.ContainedEntities.Count > 0)
- {
- SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundEject.GetSound(), component.Owner, AudioParams.Default.WithVolume(-1));
- }
-
- // May as well point back at the end?
- component.CurrentSlot = component.AmmoSlots.Length - 1;
- }
-
- private void OnRevolverGetState(EntityUid uid, RevolverBarrelComponent component, ref ComponentGetState args)
- {
- var slotsSpent = new bool?[component.Capacity];
- for (var i = 0; i < component.Capacity; i++)
- {
- slotsSpent[i] = null;
- var ammoEntity = component.AmmoSlots[i];
- if (ammoEntity != default && TryComp(ammoEntity, out AmmoComponent? ammo))
- {
- slotsSpent[i] = ammo.Spent;
- }
- }
-
- //TODO: make yaml var to not sent currentSlot/UI? (for russian roulette)
- args.State = new RevolverBarrelComponentState(
- component.CurrentSlot,
- component.FireRateSelector,
- slotsSpent,
- component.SoundGunshot.GetSound());
- }
-
- private void OnRevolverMapInit(EntityUid uid, RevolverBarrelComponent component, MapInitEvent args)
- {
- component.UnspawnedCount = component.Capacity;
- var idx = 0;
- component.AmmoContainer = component.Owner.EnsureContainer($"{component.GetType()}-ammoContainer", out var existing);
- if (existing)
- {
- foreach (var entity in component.AmmoContainer.ContainedEntities)
- {
- component.UnspawnedCount--;
- component.AmmoSlots[idx] = entity;
- idx++;
- }
- }
-
- // TODO: Revolvers should also defer spawning T B H
- var xform = EntityManager.GetComponent(uid);
-
- for (var i = 0; i < component.UnspawnedCount; i++)
- {
- var entity = EntityManager.SpawnEntity(component.FillPrototype, xform.Coordinates);
- component.AmmoSlots[idx] = entity;
- component.AmmoContainer.Insert(entity);
- idx++;
- }
-
- UpdateRevolverAppearance(component);
- component.Dirty(EntityManager);
- }
-
- private void UpdateRevolverAppearance(RevolverBarrelComponent component)
- {
- if (!TryComp(component.Owner, out AppearanceComponent? appearance))
- {
- return;
- }
-
- // Placeholder, at this stage it's just here for the RPG
- appearance.SetData(MagazineBarrelVisuals.MagLoaded, component.ShotsLeft > 0);
- appearance.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
- appearance.SetData(AmmoVisuals.AmmoMax, component.Capacity);
- }
-
- private void AddSpinVerb(EntityUid uid, RevolverBarrelComponent component, GetVerbsEvent args)
- {
- if (args.Hands == null || !args.CanAccess || !args.CanInteract)
- return;
-
- if (component.Capacity <= 1 || component.ShotsLeft == 0)
- return;
-
- AlternativeVerb verb = new()
- {
- Text = Loc.GetString("spin-revolver-verb-get-data-text"),
- IconTexture = "/Textures/Interface/VerbIcons/refresh.svg.192dpi.png",
- Act = () =>
- {
- SpinRevolver(component);
- component.Owner.PopupMessage(args.User, Loc.GetString("spin-revolver-verb-on-activate"));
- }
- };
- args.Verbs.Add(verb);
- }
-
- public EntityUid? PeekAmmo(RevolverBarrelComponent component)
- {
- return component.AmmoSlots[component.CurrentSlot];
- }
-
- ///
- /// Takes a projectile out if possible
- /// IEnumerable just to make supporting shotguns saner
- ///
- ///
- ///
- public EntityUid? TakeProjectile(RevolverBarrelComponent component, EntityCoordinates spawnAt)
- {
- var ammo = component.AmmoSlots[component.CurrentSlot];
- EntityUid? bullet = null;
- if (ammo != null)
- {
- var ammoComponent = EntityManager.GetComponent(ammo.Value);
- bullet = TakeBullet(ammoComponent, spawnAt);
- if (ammoComponent.Caseless)
- {
- component.AmmoSlots[component.CurrentSlot] = null;
- component.AmmoContainer.Remove(ammo.Value);
- }
- }
- CycleRevolver(component);
- UpdateRevolverAppearance(component);
- return bullet;
- }
-}
diff --git a/Content.Server/Weapon/Ranged/GunSystem.SpeedLoader.cs b/Content.Server/Weapon/Ranged/GunSystem.SpeedLoader.cs
deleted file mode 100644
index ff78ca0eb6..0000000000
--- a/Content.Server/Weapon/Ranged/GunSystem.SpeedLoader.cs
+++ /dev/null
@@ -1,181 +0,0 @@
-using Content.Server.Hands.Components;
-using Content.Server.Weapon.Ranged.Ammunition.Components;
-using Content.Server.Weapon.Ranged.Barrels.Components;
-using Content.Shared.Interaction;
-using Content.Shared.Interaction.Events;
-using Content.Shared.Weapons.Ranged.Barrels.Components;
-using Robust.Shared.Containers;
-using Robust.Shared.Player;
-
-namespace Content.Server.Weapon.Ranged;
-
-public sealed partial class GunSystem
-{
- private void OnSpeedLoaderInit(EntityUid uid, SpeedLoaderComponent component, ComponentInit args)
- {
- component.AmmoContainer = uid.EnsureContainer($"{component.GetType()}-container", out var existing);
-
- if (existing)
- {
- foreach (var ammo in component.AmmoContainer.ContainedEntities)
- {
- component.UnspawnedCount--;
- component.SpawnedAmmo.Push(ammo);
- }
- }
- }
-
- private void OnSpeedLoaderMapInit(EntityUid uid, SpeedLoaderComponent component, MapInitEvent args)
- {
- component.UnspawnedCount += component.Capacity;
- UpdateSpeedLoaderAppearance(component);
- }
-
- private void UpdateSpeedLoaderAppearance(SpeedLoaderComponent component)
- {
- if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
-
- appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
- appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.AmmoLeft);
- appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
- }
-
- private EntityUid? TakeAmmo(SpeedLoaderComponent component)
- {
- if (component.SpawnedAmmo.TryPop(out var entity))
- {
- component.AmmoContainer.Remove(entity);
- return entity;
- }
-
- if (component.UnspawnedCount > 0)
- {
- component.UnspawnedCount--;
- return EntityManager.SpawnEntity(component.FillPrototype, Transform(component.Owner).Coordinates);
- }
-
- return null;
- }
-
- private void OnSpeedLoaderUse(EntityUid uid, SpeedLoaderComponent component, UseInHandEvent args)
- {
- if (args.Handled) return;
-
- if (!TryComp(args.User, out HandsComponent? handsComponent))
- {
- return;
- }
-
- var ammo = TakeAmmo(component);
- if (ammo == null)
- {
- return;
- }
-
- if (!_handsSystem.TryPickup(args.User, ammo.Value, handsComp: handsComponent))
- {
- EjectCasing(ammo.Value);
- }
-
- UpdateSpeedLoaderAppearance(component);
- args.Handled = true;
- }
-
- private void OnSpeedLoaderAfterInteract(EntityUid uid, SpeedLoaderComponent component, AfterInteractEvent args)
- {
- if (args.Handled || !args.CanReach) return;
-
- if (args.Target == null)
- {
- return;
- }
-
- // This area is dirty but not sure of an easier way to do it besides add an interface or somethin
- var changed = false;
-
- if (TryComp(args.Target.Value, out RevolverBarrelComponent? revolverBarrel))
- {
- for (var i = 0; i < component.Capacity; i++)
- {
- var ammo = TakeAmmo(component);
- if (ammo == null)
- {
- break;
- }
-
- if (TryInsertBullet(args.User, ammo.Value, revolverBarrel))
- {
- changed = true;
- continue;
- }
-
- // Take the ammo back
- TryInsertAmmo(args.User, ammo.Value, component);
- break;
- }
- }
- else if (TryComp(args.Target.Value, out BoltActionBarrelComponent? boltActionBarrel))
- {
- for (var i = 0; i < component.Capacity; i++)
- {
- var ammo = TakeAmmo(component);
- if (ammo == null)
- {
- break;
- }
-
- if (TryInsertBullet(args.User, ammo.Value, boltActionBarrel))
- {
- changed = true;
- continue;
- }
-
- // Take the ammo back
- TryInsertAmmo(args.User, ammo.Value, component);
- break;
- }
-
- }
-
- if (changed)
- {
- UpdateSpeedLoaderAppearance(component);
- }
-
- args.Handled = true;
- }
-
- public bool TryInsertAmmo(EntityUid user, EntityUid entity, SpeedLoaderComponent component)
- {
- if (!TryComp(entity, out AmmoComponent? ammoComponent))
- {
- return false;
- }
-
- if (ammoComponent.Caliber != component.Caliber)
- {
- _popup.PopupEntity(Loc.GetString("speed-loader-component-try-insert-ammo-wrong-caliber"), component.Owner, Filter.Entities(user));
- return false;
- }
-
- if (component.AmmoLeft >= component.Capacity)
- {
- _popup.PopupEntity(Loc.GetString("speed-loader-component-try-insert-ammo-no-room"), component.Owner, Filter.Entities(user));
- return false;
- }
-
- component.SpawnedAmmo.Push(entity);
- component.AmmoContainer.Insert(entity);
- UpdateSpeedLoaderAppearance(component);
- return true;
-
- }
-
- private void OnSpeedLoaderInteractUsing(EntityUid uid, SpeedLoaderComponent component, InteractUsingEvent args)
- {
- if (args.Handled) return;
-
- if (TryInsertAmmo(args.User, args.Used, component))
- args.Handled = true;
- }
-}
diff --git a/Content.Server/Weapon/Ranged/GunSystem.cs b/Content.Server/Weapon/Ranged/GunSystem.cs
deleted file mode 100644
index 9ac7dc76c4..0000000000
--- a/Content.Server/Weapon/Ranged/GunSystem.cs
+++ /dev/null
@@ -1,258 +0,0 @@
-using Content.Server.Administration.Logs;
-using Content.Server.Atmos.EntitySystems;
-using Content.Server.Hands.Components;
-using Content.Server.PowerCell;
-using Content.Server.Stunnable;
-using Content.Server.Weapon.Melee;
-using Content.Server.Weapon.Ranged.Ammunition.Components;
-using Content.Server.Weapon.Ranged.Barrels.Components;
-using Content.Shared.ActionBlocker;
-using Content.Shared.Camera;
-using Content.Shared.Damage;
-using Content.Shared.Examine;
-using Content.Shared.Hands.EntitySystems;
-using Content.Shared.Interaction;
-using Content.Shared.Interaction.Events;
-using Content.Shared.Popups;
-using Content.Shared.PowerCell.Components;
-using Content.Shared.Verbs;
-using Content.Shared.Weapons.Ranged.Components;
-using Robust.Server.GameObjects;
-using Robust.Shared.Audio;
-using Robust.Shared.Containers;
-using Robust.Shared.GameStates;
-using Robust.Shared.Map;
-using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
-using Robust.Shared.Timing;
-
-namespace Content.Server.Weapon.Ranged;
-
-public sealed partial class GunSystem : EntitySystem
-{
- [Dependency] private readonly IGameTiming _gameTiming = default!;
- [Dependency] private readonly IPrototypeManager _protoManager = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly ActionBlockerSystem _blocker = default!;
- [Dependency] private readonly IAdminLogManager _adminLogger = default!;
- [Dependency] private readonly AtmosphereSystem _atmos = default!;
- [Dependency] private readonly CameraRecoilSystem _recoil = default!;
- [Dependency] private readonly DamageableSystem _damageable = default!;
- [Dependency] private readonly EffectSystem _effects = default!;
- [Dependency] private readonly PowerCellSystem _cell = default!;
- [Dependency] private readonly SharedContainerSystem _container = default!;
- [Dependency] private readonly SharedPhysicsSystem _physics = default!;
- [Dependency] private readonly SharedPopupSystem _popup = default!;
- [Dependency] private readonly StunSystem _stun = default!;
- [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
-
- public const float DamagePitchVariation = MeleeWeaponSystem.DamagePitchVariation;
-
- ///
- /// How many sounds are allowed to be played on ejecting multiple casings.
- ///
- private const int EjectionSoundMax = 3;
-
- public override void Initialize()
- {
- base.Initialize();
-
- // TODO: So at the time I thought there might've been a need to keep magazines
- // and ammo boxes separate.
- // There isn't.
- // They should be combined.
-
- SubscribeLocalEvent(OnAmmoExamine);
-
- SubscribeLocalEvent(OnAmmoBoxInit);
- SubscribeLocalEvent(OnAmmoBoxMapInit);
- SubscribeLocalEvent(OnAmmoBoxExamine);
- SubscribeLocalEvent(OnAmmoBoxInteractUsing);
- SubscribeLocalEvent(OnAmmoBoxUse);
- SubscribeLocalEvent(OnAmmoBoxInteractHand);
- SubscribeLocalEvent>(OnAmmoBoxAltVerbs);
-
- SubscribeLocalEvent(OnRangedMagInit);
- SubscribeLocalEvent(OnRangedMagMapInit);
- SubscribeLocalEvent(OnRangedMagUse);
- SubscribeLocalEvent(OnRangedMagExamine);
- SubscribeLocalEvent(OnRangedMagInteractUsing);
-
- // Whenever I get around to refactoring guns this is all going to change.
- // Essentially the idea is
- // You have GunComponent and ChamberedGunComponent (which is just guncomp + containerslot for chamber)
- // GunComponent has a component for an ammo provider on it (e.g. battery) and asks it for ammo to shoot
- // ALTERNATIVELY, it has a "MagazineAmmoProvider" that has its own containerslot that it can ask
- // (All of these would be comp references so max you only ever have 2 components on the gun).
- SubscribeLocalEvent(OnBatteryInit);
- SubscribeLocalEvent(OnBatteryMapInit);
- SubscribeLocalEvent(OnCellSlotUpdated);
-
- SubscribeLocalEvent(OnBoltInit);
- SubscribeLocalEvent(OnBoltMapInit);
- SubscribeLocalEvent(OnBoltFireAttempt);
- SubscribeLocalEvent(OnBoltUse);
- SubscribeLocalEvent(OnBoltInteractUsing);
- SubscribeLocalEvent(OnBoltGetState);
- SubscribeLocalEvent(OnBoltExamine);
- SubscribeLocalEvent>(AddToggleBoltVerb);
-
- SubscribeLocalEvent(OnMagazineInit);
- SubscribeLocalEvent(OnMagazineMapInit);
- SubscribeLocalEvent(OnMagazineExamine);
- SubscribeLocalEvent(OnMagazineUse);
- SubscribeLocalEvent(OnMagazineInteractUsing);
- SubscribeLocalEvent(OnMagazineGetState);
- SubscribeLocalEvent>(AddMagazineInteractionVerbs);
- SubscribeLocalEvent>(AddEjectMagazineVerb);
-
- SubscribeLocalEvent(OnPumpGetState);
- SubscribeLocalEvent(OnPumpInit);
- SubscribeLocalEvent(OnPumpMapInit);
- SubscribeLocalEvent(OnPumpExamine);
- SubscribeLocalEvent(OnPumpUse);
- SubscribeLocalEvent(OnPumpInteractUsing);
-
- SubscribeLocalEvent(OnRevolverMapInit);
- SubscribeLocalEvent(OnRevolverUse);
- SubscribeLocalEvent(OnRevolverInteractUsing);
- SubscribeLocalEvent(OnRevolverGetState);
- SubscribeLocalEvent>(AddSpinVerb);
-
- SubscribeLocalEvent(OnSpeedLoaderInit);
- SubscribeLocalEvent(OnSpeedLoaderMapInit);
- SubscribeLocalEvent(OnSpeedLoaderUse);
- SubscribeLocalEvent(OnSpeedLoaderAfterInteract);
- SubscribeLocalEvent(OnSpeedLoaderInteractUsing);
-
- // SubscribeLocalEvent(OnGunExamine);
- SubscribeNetworkEvent(OnFirePos);
- SubscribeLocalEvent(OnMeleeAttempt);
- }
-
- private void OnFirePos(FirePosEvent msg, EntitySessionEventArgs args)
- {
- if (args.SenderSession.AttachedEntity is not {Valid: true} user)
- return;
-
- if (!msg.Coordinates.IsValid(EntityManager))
- return;
-
- if (!TryComp(user, out HandsComponent? handsComponent))
- return;
-
- // TODO: Not exactly robust
- var gun = handsComponent.ActiveHand?.HeldEntity;
-
- if (gun == null || !TryComp(gun, out ServerRangedWeaponComponent? weapon))
- return;
-
- // map pos
- TryFire(user, msg.Coordinates, weapon);
- }
-
- public EntityUid? PeekAtAmmo(ServerRangedBarrelComponent component)
- {
- return component switch
- {
- BatteryBarrelComponent battery => PeekAmmo(battery),
- BoltActionBarrelComponent bolt => PeekAmmo(bolt),
- MagazineBarrelComponent mag => PeekAmmo(mag),
- PumpBarrelComponent pump => PeekAmmo(pump),
- RevolverBarrelComponent revolver => PeekAmmo(revolver),
- _ => throw new NotImplementedException()
- };
- }
-
- public EntityUid? TakeOutProjectile(ServerRangedBarrelComponent component, EntityCoordinates spawnAt)
- {
- return component switch
- {
- BatteryBarrelComponent battery => TakeProjectile(battery, spawnAt),
- BoltActionBarrelComponent bolt => TakeProjectile(bolt, spawnAt),
- MagazineBarrelComponent mag => TakeProjectile(mag, spawnAt),
- PumpBarrelComponent pump => TakeProjectile(pump, spawnAt),
- RevolverBarrelComponent revolver => TakeProjectile(revolver, spawnAt),
- _ => throw new NotImplementedException()
- };
- }
-
- ///
- /// Drops multiple cartridges / shells on the floor
- /// Wraps EjectCasing to make it less toxic for bulk ejections
- ///
- public void EjectCasings(IEnumerable entities)
- {
- var soundPlayCount = 0;
- var playSound = true;
-
- foreach (var entity in entities)
- {
- EjectCasing(entity, playSound);
- soundPlayCount++;
- if (soundPlayCount > EjectionSoundMax)
- {
- playSound = false;
- }
- }
- }
-
- ///
- /// Drops a single cartridge / shell
- ///
- public void EjectCasing(
- EntityUid entity,
- bool playSound = true,
- AmmoComponent? ammoComponent = null)
- {
- const float ejectOffset = 0.4f;
-
- if (!Resolve(entity, ref ammoComponent)) return;
-
- var offsetPos = (_random.NextFloat(-ejectOffset, ejectOffset), _random.NextFloat(-ejectOffset, ejectOffset));
-
- var xform = Transform(entity);
-
- var coordinates = xform.Coordinates;
- coordinates = coordinates.Offset(offsetPos);
-
- xform.LocalRotation = _random.NextFloat(MathF.Tau);
- xform.Coordinates = coordinates;
-
- if (playSound)
- SoundSystem.Play(Filter.Pvs(entity), ammoComponent.SoundCollectionEject.GetSound(), coordinates, AudioParams.Default.WithVolume(-1));
- }
-
- private Angle GetRecoilAngle(ServerRangedBarrelComponent component, Angle direction)
- {
- var currentTime = _gameTiming.CurTime;
- var timeSinceLastFire = (currentTime - component.LastFire).TotalSeconds;
- var newTheta = MathHelper.Clamp(component.CurrentAngle.Theta + component.AngleIncrease - component.AngleDecay * timeSinceLastFire, component.MinAngle.Theta, component.MaxAngle.Theta);
- component.CurrentAngle = new Angle(newTheta);
-
- var random = (_random.NextDouble(-1, 1));
- var angle = Angle.FromDegrees(direction.Degrees + component.CurrentAngle.Degrees * random);
- return angle;
- }
-
- ///
- /// Raised on a gun when it fires.
- ///
- public sealed class GunShotEvent : EntityEventArgs
- {
-
- }
-
- public sealed class GunFireAttemptEvent : CancellableEntityEventArgs
- {
- public EntityUid? User = null;
- public ServerRangedWeaponComponent Weapon;
-
- public GunFireAttemptEvent(EntityUid? user, ServerRangedWeaponComponent weapon)
- {
- User = user;
- Weapon = weapon;
- }
- }
-}
diff --git a/Content.Server/Weapon/Ranged/RangedWeaponSystem.cs b/Content.Server/Weapon/Ranged/RangedWeaponSystem.cs
deleted file mode 100644
index e6a2e8a5af..0000000000
--- a/Content.Server/Weapon/Ranged/RangedWeaponSystem.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using Content.Shared.Hands;
-
-namespace Content.Server.Weapon.Ranged
-{
- public sealed class RangedWeaponSysten : EntitySystem
- {
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnHandSelected);
- }
-
- private void OnHandSelected(EntityUid uid, ServerRangedWeaponComponent component, HandSelectedEvent args)
- {
- // Instead of dirtying on hand-select this component should probably by dirtied whenever it needs to be.
- // I take no responsibility for this code. It was like this when I got here.
-
- Dirty(component);
- }
- }
-}
diff --git a/Content.Server/Weapon/Ranged/ServerRangedWeaponComponent.cs b/Content.Server/Weapon/Ranged/ServerRangedWeaponComponent.cs
deleted file mode 100644
index 1719183f61..0000000000
--- a/Content.Server/Weapon/Ranged/ServerRangedWeaponComponent.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using Content.Shared.Sound;
-using Content.Shared.Weapons.Ranged.Components;
-
-namespace Content.Server.Weapon.Ranged
-{
- [RegisterComponent]
- public sealed class ServerRangedWeaponComponent : SharedRangedWeaponComponent
- {
- public TimeSpan LastFireTime;
-
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("clumsyCheck")]
- public bool ClumsyCheck { get; set; } = true;
-
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("clumsyExplodeChance")]
- public float ClumsyExplodeChance { get; set; } = 0.5f;
-
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("canHotspot")]
- public bool CanHotspot = true;
-
- [DataField("clumsyWeaponHandlingSound")]
- public SoundSpecifier ClumsyWeaponHandlingSound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg");
-
- [DataField("clumsyWeaponShotSound")]
- public SoundSpecifier ClumsyWeaponShotSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/bang.ogg");
-
- }
-}
diff --git a/Content.Server/Weapon/Ranged/ChemicalAmmoSystem.cs b/Content.Server/Weapon/Ranged/Systems/ChemicalAmmoSystem.cs
similarity index 87%
rename from Content.Server/Weapon/Ranged/ChemicalAmmoSystem.cs
rename to Content.Server/Weapon/Ranged/Systems/ChemicalAmmoSystem.cs
index 777e629ee3..49d3dbebc7 100644
--- a/Content.Server/Weapon/Ranged/ChemicalAmmoSystem.cs
+++ b/Content.Server/Weapon/Ranged/Systems/ChemicalAmmoSystem.cs
@@ -1,14 +1,14 @@
using System.Linq;
using Content.Server.Chemistry.EntitySystems;
-using Content.Server.Weapon.Ranged.Ammunition.Components;
-using Content.Server.Weapon.Ranged.Barrels.Components;
+using Content.Server.Weapon.Ranged.Components;
using Content.Shared.Chemistry.Components;
+using Content.Shared.Weapons.Ranged.Events;
-namespace Content.Server.Weapon.Ranged
+namespace Content.Server.Weapon.Ranged.Systems
{
public sealed class ChemicalAmmoSystem : EntitySystem
{
- [Dependency] private SolutionContainerSystem _solutionSystem = default!;
+ [Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
public override void Initialize()
{
diff --git a/Content.Server/Weapon/Ranged/Systems/FlyBySoundSystem.cs b/Content.Server/Weapon/Ranged/Systems/FlyBySoundSystem.cs
new file mode 100644
index 0000000000..993322a52d
--- /dev/null
+++ b/Content.Server/Weapon/Ranged/Systems/FlyBySoundSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Weapons.Ranged.Systems;
+
+namespace Content.Server.Weapon.Ranged.Systems;
+
+public sealed class FlyBySoundSystem : SharedFlyBySoundSystem {}
diff --git a/Content.Server/Weapon/Ranged/Systems/GunSystem.Ballistic.cs b/Content.Server/Weapon/Ranged/Systems/GunSystem.Ballistic.cs
new file mode 100644
index 0000000000..cbfe3c22c7
--- /dev/null
+++ b/Content.Server/Weapon/Ranged/Systems/GunSystem.Ballistic.cs
@@ -0,0 +1,31 @@
+using Content.Shared.Weapons.Ranged.Components;
+using Robust.Shared.Map;
+
+namespace Content.Server.Weapon.Ranged.Systems;
+
+public sealed partial class GunSystem
+{
+ protected override void Cycle(BallisticAmmoProviderComponent component, MapCoordinates coordinates)
+ {
+ EntityUid? ent = null;
+
+ // TODO: Combine with TakeAmmo
+ if (component.Entities.Count > 0)
+ {
+ var existing = component.Entities[^1];
+ component.Entities.RemoveAt(component.Entities.Count - 1);
+
+ component.Container.Remove(existing);
+ EnsureComp(existing);
+ }
+ else if (component.UnspawnedCount > 0)
+ {
+ component.UnspawnedCount--;
+ ent = Spawn(component.FillProto, coordinates);
+ EnsureComp(ent.Value);
+ }
+
+ if (ent != null)
+ EjectCartridge(ent.Value);
+ }
+}
diff --git a/Content.Server/Weapon/Ranged/Systems/GunSystem.Battery.cs b/Content.Server/Weapon/Ranged/Systems/GunSystem.Battery.cs
new file mode 100644
index 0000000000..34d089e62a
--- /dev/null
+++ b/Content.Server/Weapon/Ranged/Systems/GunSystem.Battery.cs
@@ -0,0 +1,59 @@
+using Content.Server.Power.Components;
+using Content.Shared.Weapons.Ranged.Components;
+
+namespace Content.Server.Weapon.Ranged.Systems;
+
+public sealed partial class GunSystem
+{
+ protected override void InitializeBattery()
+ {
+ base.InitializeBattery();
+
+ // Hitscan
+ SubscribeLocalEvent(OnBatteryStartup);
+ SubscribeLocalEvent(OnBatteryChargeChange);
+
+ // Projectile
+ SubscribeLocalEvent(OnBatteryStartup);
+ SubscribeLocalEvent(OnBatteryChargeChange);
+ }
+
+ private void OnBatteryStartup(EntityUid uid, BatteryAmmoProviderComponent component, ComponentStartup args)
+ {
+ UpdateShots(uid, component);
+ }
+
+ private void OnBatteryChargeChange(EntityUid uid, BatteryAmmoProviderComponent component, ChargeChangedEvent args)
+ {
+ UpdateShots(uid, component);
+ }
+
+ private void UpdateShots(EntityUid uid, BatteryAmmoProviderComponent component)
+ {
+ if (!TryComp(uid, out var battery)) return;
+ UpdateShots(component, battery);
+ }
+
+ private void UpdateShots(BatteryAmmoProviderComponent component, BatteryComponent battery)
+ {
+ var shots = (int) (battery.CurrentCharge / component.FireCost);
+ var maxShots = (int) (battery.MaxCharge / component.FireCost);
+
+ if (component.Shots != shots || component.Capacity != maxShots)
+ {
+ Dirty(component);
+ }
+
+ component.Shots = shots;
+ component.Capacity = maxShots;
+ UpdateBatteryAppearance(component.Owner, component);
+ }
+
+ protected override void TakeCharge(EntityUid uid, BatteryAmmoProviderComponent component)
+ {
+ if (!TryComp(uid, out var battery)) return;
+
+ battery.CurrentCharge -= component.FireCost;
+ UpdateShots(component, battery);
+ }
+}
diff --git a/Content.Server/Weapon/Ranged/Systems/GunSystem.Revolver.cs b/Content.Server/Weapon/Ranged/Systems/GunSystem.Revolver.cs
new file mode 100644
index 0000000000..7f68d20531
--- /dev/null
+++ b/Content.Server/Weapon/Ranged/Systems/GunSystem.Revolver.cs
@@ -0,0 +1,17 @@
+using Content.Shared.Weapons.Ranged.Components;
+
+namespace Content.Server.Weapon.Ranged.Systems;
+
+public sealed partial class GunSystem
+{
+ protected override void SpinRevolver(RevolverAmmoProviderComponent component, EntityUid? user = null)
+ {
+ base.SpinRevolver(component, user);
+ var index = Random.Next(component.Capacity);
+
+ if (component.CurrentIndex == index) return;
+
+ component.CurrentIndex = index;
+ Dirty(component);
+ }
+}
diff --git a/Content.Server/Weapon/Ranged/Systems/GunSystem.cs b/Content.Server/Weapon/Ranged/Systems/GunSystem.cs
new file mode 100644
index 0000000000..537dd5bc58
--- /dev/null
+++ b/Content.Server/Weapon/Ranged/Systems/GunSystem.cs
@@ -0,0 +1,298 @@
+using System.Linq;
+using Content.Server.Projectiles.Components;
+using Content.Server.Weapon.Melee;
+using Content.Server.Weapon.Ranged.Components;
+using Content.Shared.Audio;
+using Content.Shared.Damage;
+using Content.Shared.Database;
+using Content.Shared.Sound;
+using Content.Shared.Throwing;
+using Content.Shared.Weapons.Ranged;
+using Content.Shared.Weapons.Ranged.Components;
+using Content.Shared.Weapons.Ranged.Events;
+using Robust.Server.GameObjects;
+using Robust.Shared.Audio;
+using Robust.Shared.Map;
+using Robust.Shared.Physics;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+using Robust.Shared.Utility;
+using SharedGunSystem = Content.Shared.Weapons.Ranged.Systems.SharedGunSystem;
+
+namespace Content.Server.Weapon.Ranged.Systems;
+
+public sealed partial class GunSystem : SharedGunSystem
+{
+ [Dependency] private readonly EffectSystem _effects = default!;
+
+ public const float DamagePitchVariation = MeleeWeaponSystem.DamagePitchVariation;
+
+ public override void Shoot(GunComponent gun, List ammo, EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, EntityUid? user = null)
+ {
+ var fromMap = fromCoordinates.ToMap(EntityManager);
+ var toMap = toCoordinates.ToMapPos(EntityManager);
+ var mapDirection = toMap - fromMap.Position;
+ var mapAngle = mapDirection.ToAngle();
+ var angle = GetRecoilAngle(Timing.CurTime, gun, mapDirection.ToAngle());
+
+ // Update shot based on the recoil
+ toMap = fromMap.Position + angle.ToVec() * mapDirection.Length;
+ mapDirection = toMap - fromMap.Position;
+ var entityDirection = Transform(fromCoordinates.EntityId).InvWorldMatrix.Transform(toMap) - fromCoordinates.Position;
+
+ // I must be high because this was getting tripped even when true.
+ // DebugTools.Assert(direction != Vector2.Zero);
+ var shotProjectiles = new List(ammo.Count);
+
+ foreach (var shootable in ammo)
+ {
+ switch (shootable)
+ {
+ // Cartridge shoots something else
+ case CartridgeAmmoComponent cartridge:
+ if (!cartridge.Spent)
+ {
+ if (cartridge.Count > 1)
+ {
+ var angles = LinearSpread(mapAngle - Angle.FromDegrees(cartridge.Spread / 2f),
+ mapAngle + Angle.FromDegrees(cartridge.Spread / 2f), cartridge.Count);
+
+ for (var i = 0; i < cartridge.Count; i++)
+ {
+ var uid = Spawn(cartridge.Prototype, fromCoordinates);
+ ShootProjectile(uid, angles[i].ToVec(), user);
+ shotProjectiles.Add(uid);
+ }
+ }
+ else
+ {
+ var uid = Spawn(cartridge.Prototype, fromCoordinates);
+ ShootProjectile(uid, mapDirection, user);
+ shotProjectiles.Add(uid);
+ }
+
+ SetCartridgeSpent(cartridge, true);
+ MuzzleFlash(gun.Owner, cartridge, user);
+
+ if (cartridge.DeleteOnSpawn)
+ Del(cartridge.Owner);
+ }
+ else
+ {
+ PlaySound(gun.Owner, gun.SoundEmpty?.GetSound(Random, ProtoManager), user);
+ }
+
+ // Something like ballistic might want to leave it in the container still
+ if (!cartridge.DeleteOnSpawn && !Containers.IsEntityInContainer(cartridge.Owner))
+ EjectCartridge(cartridge.Owner);
+
+ Dirty(cartridge);
+ break;
+ // Ammo shoots itself
+ case AmmoComponent newAmmo:
+ shotProjectiles.Add(newAmmo.Owner);
+ MuzzleFlash(gun.Owner, newAmmo, user);
+
+ // Do a throw
+ if (!HasComp(newAmmo.Owner))
+ {
+ RemComp(newAmmo.Owner);
+ // TODO: Someone can probably yeet this a billion miles so need to pre-validate input somewhere up the call stack.
+ ThrowingSystem.TryThrow(newAmmo.Owner, mapDirection, 20f, user);
+ break;
+ }
+
+ ShootProjectile(newAmmo.Owner, mapDirection, user);
+ break;
+ case HitscanPrototype hitscan:
+ var ray = new CollisionRay(fromMap.Position, mapDirection.Normalized, hitscan.CollisionMask);
+ var rayCastResults = Physics.IntersectRay(fromMap.MapId, ray, hitscan.MaxLength, user, false).ToList();
+
+ if (rayCastResults.Count >= 1)
+ {
+ var result = rayCastResults[0];
+ var distance = result.Distance;
+ FireEffects(fromCoordinates, distance, entityDirection.ToAngle(), hitscan, result.HitEntity);
+
+ var dmg = hitscan.Damage;
+
+ if (dmg != null)
+ dmg = Damageable.TryChangeDamage(result.HitEntity, dmg);
+
+ if (dmg != null)
+ {
+ if (user != null)
+ {
+ Logs.Add(LogType.HitScanHit,
+ $"{ToPrettyString(user.Value):user} hit {ToPrettyString(result.HitEntity):target} using hitscan and dealt {dmg.Total:damage} damage");
+ }
+ else
+ {
+ Logs.Add(LogType.HitScanHit,
+ $"Hit {ToPrettyString(result.HitEntity):target} using hitscan and dealt {dmg.Total:damage} damage");
+ }
+ }
+ }
+ else
+ {
+ FireEffects(fromCoordinates, hitscan.MaxLength, entityDirection.ToAngle(), hitscan);
+ }
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ RaiseLocalEvent(gun.Owner, new AmmoShotEvent()
+ {
+ FiredProjectiles = shotProjectiles,
+ }, false);
+ }
+
+ private void ShootProjectile(EntityUid uid, Vector2 direction, EntityUid? user = null)
+ {
+ var physics = EnsureComp(uid);
+ physics.BodyStatus = BodyStatus.InAir;
+ physics.LinearVelocity = direction.Normalized * 20f;
+
+ if (user != null)
+ {
+ var projectile = EnsureComp(uid);
+ projectile.IgnoreEntity(user.Value);
+ }
+
+ Transform(uid).WorldRotation = direction.ToWorldAngle();
+ }
+
+ ///
+ /// Gets a linear spread of angles between start and end.
+ ///
+ /// Start angle in degrees
+ /// End angle in degrees
+ /// How many shots there are
+ private Angle[] LinearSpread(Angle start, Angle end, int intervals)
+ {
+ var angles = new Angle[intervals];
+ DebugTools.Assert(intervals > 1);
+
+ for (var i = 0; i <= intervals - 1; i++)
+ {
+ angles[i] = new Angle(start + (end - start) * i / (intervals - 1));
+ }
+
+ return angles;
+ }
+
+ 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);
+ component.CurrentAngle = new Angle(newTheta);
+ component.LastFire = component.NextFire;
+
+ // Convert it so angle can go either side.
+ var random = Random.NextGaussian(0, 0.5);
+ var angle = new Angle(direction.Theta + component.CurrentAngle.Theta * random);
+ return angle;
+ }
+
+ protected override void PlaySound(EntityUid gun, string? sound, EntityUid? user = null)
+ {
+ if (sound == null) return;
+
+ SoundSystem.Play(Filter.Pvs(gun, entityManager: EntityManager).RemoveWhereAttachedEntity(e => e == user), sound, gun);
+ }
+
+ protected override void Popup(string message, EntityUid? uid, EntityUid? user) {}
+
+ protected override void CreateEffect(EffectSystemMessage message, EntityUid? user = null)
+ {
+ // TODO: Fucking bad
+ if (TryComp(user, out var actor))
+ {
+ _effects.CreateParticle(message, actor.PlayerSession);
+ }
+ else
+ {
+ _effects.CreateParticle(message);
+ }
+ }
+
+ public void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound)
+ {
+ // Like projectiles and melee,
+ // 1. Entity specific sound
+ // 2. Ammo's sound
+ // 3. Nothing
+ var playedSound = false;
+
+ if (!forceWeaponSound && modifiedDamage != null && modifiedDamage.Total > 0 && TryComp(otherEntity, out var rangedSound))
+ {
+ var type = MeleeWeaponSystem.GetHighestDamageSound(modifiedDamage, ProtoManager);
+
+ if (type != null && rangedSound.SoundTypes?.TryGetValue(type, out var damageSoundType) == true)
+ {
+ SoundSystem.Play(
+ Filter.Pvs(otherEntity, entityManager: EntityManager),
+ damageSoundType!.GetSound(),
+ otherEntity,
+ AudioHelpers.WithVariation(DamagePitchVariation));
+
+ playedSound = true;
+ }
+ else if (type != null && rangedSound.SoundGroups?.TryGetValue(type, out var damageSoundGroup) == true)
+ {
+ SoundSystem.Play(
+ Filter.Pvs(otherEntity, entityManager: EntityManager),
+ damageSoundGroup!.GetSound(),
+ otherEntity,
+ AudioHelpers.WithVariation(DamagePitchVariation));
+
+ playedSound = true;
+ }
+ }
+
+ if (!playedSound && weaponSound != null)
+ SoundSystem.Play(Filter.Pvs(otherEntity, entityManager: EntityManager), weaponSound.GetSound(), otherEntity);
+ }
+
+ // TODO: Pseudo RNG so the client can predict these.
+ #region Hitscan effects
+
+ private void FireEffects(EntityCoordinates fromCoordinates, float distance, Angle angle, HitscanPrototype hitscan, EntityUid? hitEntity = null)
+ {
+ // Lord
+ // Forgive me for the shitcode I am about to do
+ // Effects tempt me not
+ var sprites = new List<(EntityCoordinates coordinates, Angle angle, SpriteSpecifier sprite, float scale)>();
+
+ // We'll get the effects relative to the grid / map of the firer
+ if (distance >= 1f)
+ {
+ if (hitscan.MuzzleFlash != null)
+ {
+ sprites.Add((fromCoordinates.Offset(angle.ToVec().Normalized / 2), angle, hitscan.MuzzleFlash, 1f));
+ }
+
+ if (hitscan.TravelFlash != null)
+ {
+ sprites.Add((fromCoordinates.Offset(angle.ToVec() * (distance + 0.5f) / 2), angle, hitscan.TravelFlash, distance - 1.5f));
+ }
+ }
+
+ if (hitscan.ImpactFlash != null)
+ {
+ sprites.Add((fromCoordinates.Offset(angle.ToVec() * distance), angle.FlipPositive(), hitscan.ImpactFlash, 1f));
+ }
+
+ if (sprites.Count > 0)
+ {
+ RaiseNetworkEvent(new HitscanEvent()
+ {
+ Sprites = sprites,
+ }, Filter.Pvs(fromCoordinates, entityMan: EntityManager));
+ }
+ }
+
+ #endregion
+}
diff --git a/Content.Server/Weapon/Ranged/TetherGunSystem.cs b/Content.Server/Weapon/Ranged/Systems/TetherGunSystem.cs
similarity index 98%
rename from Content.Server/Weapon/Ranged/TetherGunSystem.cs
rename to Content.Server/Weapon/Ranged/Systems/TetherGunSystem.cs
index 7dbb7526c8..e1fc7edda6 100644
--- a/Content.Server/Weapon/Ranged/TetherGunSystem.cs
+++ b/Content.Server/Weapon/Ranged/Systems/TetherGunSystem.cs
@@ -1,6 +1,6 @@
using Content.Server.Ghost.Components;
using Content.Shared.Administration;
-using Content.Shared.Weapons.Ranged;
+using Content.Shared.Weapons.Ranged.Systems;
using Robust.Server.Console;
using Robust.Server.Player;
using Robust.Shared.Containers;
@@ -11,7 +11,7 @@ using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
-namespace Content.Server.Weapon.Ranged;
+namespace Content.Server.Weapon.Ranged.Systems;
public sealed class TetherGunSystem : SharedTetherGunSystem
{
diff --git a/Content.Shared/CombatMode/SharedCombatModeSystem.cs b/Content.Shared/CombatMode/SharedCombatModeSystem.cs
index 3495f807ca..d08544ad47 100644
--- a/Content.Shared/CombatMode/SharedCombatModeSystem.cs
+++ b/Content.Shared/CombatMode/SharedCombatModeSystem.cs
@@ -52,6 +52,11 @@ namespace Content.Shared.CombatMode
_actionsSystem.RemoveAction(uid, component.DisarmAction);
}
+ public bool IsInCombatMode(EntityUid entity)
+ {
+ return TryComp(entity, out var combatMode) && combatMode.IsInCombatMode;
+ }
+
private void OnActionPerform(EntityUid uid, SharedCombatModeComponent component, ToggleCombatActionEvent args)
{
if (args.Handled)
diff --git a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs
index b872af8b53..50d0c17344 100644
--- a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs
+++ b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs
@@ -203,6 +203,8 @@ namespace Content.Shared.Containers.ItemSlots
// ContainerSlot automatically raises a directed EntInsertedIntoContainerMessage
PlaySound(uid, slot.InsertSound, slot.SoundOptions, excludeUserAudio ? user : null);
+ var ev = new ItemSlotChangedEvent();
+ RaiseLocalEvent(uid, ref ev);
}
///
@@ -326,6 +328,8 @@ namespace Content.Shared.Containers.ItemSlots
// ContainerSlot automatically raises a directed EntRemovedFromContainerMessage
PlaySound(uid, slot.EjectSound, slot.SoundOptions, excludeUserAudio ? user : null);
+ var ev = new ItemSlotChangedEvent();
+ RaiseLocalEvent(uid, ref ev);
}
///
@@ -336,13 +340,13 @@ namespace Content.Shared.Containers.ItemSlots
{
item = null;
- /// This handles logic with the slot itself
+ // This handles logic with the slot itself
if (!CanEject(slot))
return false;
item = slot.Item;
- /// This handles user logic
+ // This handles user logic
if (user != null && item != null && !_actionBlockerSystem.CanPickup(user.Value, item.Value))
return false;
@@ -354,7 +358,7 @@ namespace Content.Shared.Containers.ItemSlots
/// Try to eject item from a slot.
///
/// False if the id is not valid, the item slot is locked, or it has no item inserted
- public bool TryEject(EntityUid uid, string id, EntityUid user,
+ public bool TryEject(EntityUid uid, string id, EntityUid? user,
[NotNullWhen(true)] out EntityUid? item, ItemSlotsComponent? itemSlots = null, bool excludeUserAudio = false)
{
item = null;
@@ -586,4 +590,10 @@ namespace Content.Shared.Containers.ItemSlots
args.State = new ItemSlotsComponentState(component.Slots);
}
}
+
+ ///
+ /// Raised directed on an entity when one of its item slots changes.
+ ///
+ [ByRefEvent]
+ public readonly struct ItemSlotChangedEvent {}
}
diff --git a/Content.Shared/Weapons/Ranged/Barrels/Components/SharedBoltActionBarrelComponent.cs b/Content.Shared/Weapons/Ranged/Barrels/Components/SharedBoltActionBarrelComponent.cs
deleted file mode 100644
index ff87754280..0000000000
--- a/Content.Shared/Weapons/Ranged/Barrels/Components/SharedBoltActionBarrelComponent.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using Content.Shared.Weapons.Ranged.Components;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Weapons.Ranged.Barrels.Components
-{
- [Serializable, NetSerializable]
- public sealed class BoltActionBarrelComponentState : ComponentState
- {
- public (bool chambered, bool spent) Chamber { get; }
- public FireRateSelector FireRateSelector { get; }
- public (int count, int max)? Magazine { get; }
- public string? SoundGunshot { get; }
-
- public BoltActionBarrelComponentState(
- (bool chambered, bool spent) chamber,
- FireRateSelector fireRateSelector,
- (int count, int max)? magazine,
- string? soundGunshot)
- {
- Chamber = chamber;
- FireRateSelector = fireRateSelector;
- Magazine = magazine;
- SoundGunshot = soundGunshot;
- }
- }
-}
diff --git a/Content.Shared/Weapons/Ranged/Barrels/Components/SharedMagazineBarrelComponent.cs b/Content.Shared/Weapons/Ranged/Barrels/Components/SharedMagazineBarrelComponent.cs
deleted file mode 100644
index 6d07fcb6d5..0000000000
--- a/Content.Shared/Weapons/Ranged/Barrels/Components/SharedMagazineBarrelComponent.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using Content.Shared.Weapons.Ranged.Components;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Weapons.Ranged.Barrels.Components
-{
- [Serializable, NetSerializable]
- public enum AmmoVisuals
- {
- AmmoCount,
- AmmoMax,
- Spent,
- }
-
- [Serializable, NetSerializable]
- public enum MagazineBarrelVisuals
- {
- MagLoaded
- }
-
- [Serializable, NetSerializable]
- public enum BarrelBoltVisuals
- {
- BoltOpen,
- }
-
- [Serializable, NetSerializable]
- public sealed class MagazineBarrelComponentState : ComponentState
- {
- public bool Chambered { get; }
- public FireRateSelector FireRateSelector { get; }
- public (int count, int max)? Magazine { get; }
- public string? SoundGunshot { get; }
-
- public MagazineBarrelComponentState(
- bool chambered,
- FireRateSelector fireRateSelector,
- (int count, int max)? magazine,
- string? soundGunshot)
- {
- Chambered = chambered;
- FireRateSelector = fireRateSelector;
- Magazine = magazine;
- SoundGunshot = soundGunshot;
- }
- }
-}
diff --git a/Content.Shared/Weapons/Ranged/Barrels/Components/SharedPumpBarrelComponent.cs b/Content.Shared/Weapons/Ranged/Barrels/Components/SharedPumpBarrelComponent.cs
deleted file mode 100644
index bb910abc2b..0000000000
--- a/Content.Shared/Weapons/Ranged/Barrels/Components/SharedPumpBarrelComponent.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using Content.Shared.Weapons.Ranged.Components;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Weapons.Ranged.Barrels.Components
-{
- [Serializable, NetSerializable]
- public sealed class PumpBarrelComponentState : ComponentState
- {
- public (bool chambered, bool spent) Chamber { get; }
- public FireRateSelector FireRateSelector { get; }
- public (int count, int max)? Magazine { get; }
- public string? SoundGunshot { get; }
-
- public PumpBarrelComponentState(
- (bool chambered, bool spent) chamber,
- FireRateSelector fireRateSelector,
- (int count, int max)? magazine,
- string? soundGunshot)
- {
- Chamber = chamber;
- FireRateSelector = fireRateSelector;
- Magazine = magazine;
- SoundGunshot = soundGunshot;
- }
- }
-}
diff --git a/Content.Shared/Weapons/Ranged/Barrels/Components/SharedRevolverBarrelComponent.cs b/Content.Shared/Weapons/Ranged/Barrels/Components/SharedRevolverBarrelComponent.cs
deleted file mode 100644
index 133ca84462..0000000000
--- a/Content.Shared/Weapons/Ranged/Barrels/Components/SharedRevolverBarrelComponent.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using Content.Shared.Weapons.Ranged.Components;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Weapons.Ranged.Barrels.Components
-{
- [Serializable, NetSerializable]
- public sealed class RevolverBarrelComponentState : ComponentState
- {
- public int CurrentSlot { get; }
- public FireRateSelector FireRateSelector { get; }
- public bool?[] Bullets { get; }
- public string? SoundGunshot { get; }
-
- public RevolverBarrelComponentState(
- int currentSlot,
- FireRateSelector fireRateSelector,
- bool?[] bullets,
- string? soundGunshot)
- {
- CurrentSlot = currentSlot;
- FireRateSelector = fireRateSelector;
- Bullets = bullets;
- SoundGunshot = soundGunshot;
- }
- }
-}
diff --git a/Content.Shared/Weapons/Ranged/Components/AmmoComponent.cs b/Content.Shared/Weapons/Ranged/Components/AmmoComponent.cs
new file mode 100644
index 0000000000..6bcbcef148
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Components/AmmoComponent.cs
@@ -0,0 +1,54 @@
+using Content.Shared.Sound;
+using Content.Shared.Weapons.Ranged.Systems;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Weapons.Ranged.Components;
+
+///
+/// Allows the entity to be fired from a gun.
+///
+[RegisterComponent, Virtual]
+public class AmmoComponent : Component, IShootable
+{
+ // Muzzle flash stored on ammo because if we swap a gun to whatever we may want to override it.
+
+ [ViewVariables, DataField("muzzleFlash")]
+ public ResourcePath? MuzzleFlash = new ResourcePath("Objects/Weapons/Guns/Projectiles/projectiles.rsi/muzzle_bullet.png");
+}
+
+///
+/// Spawns another prototype to be shot instead of itself.
+///
+[RegisterComponent, NetworkedComponent, ComponentReference(typeof(AmmoComponent))]
+public sealed class CartridgeAmmoComponent : AmmoComponent
+{
+ [ViewVariables(VVAccess.ReadWrite), DataField("proto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string Prototype = default!;
+
+ [ViewVariables(VVAccess.ReadWrite), DataField("spent")]
+ public bool Spent = false;
+
+ ///
+ /// How much the ammo spreads when shot, in degrees. Does nothing if count is 0.
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField("spread")]
+ public float Spread = 10f;
+
+ ///
+ /// How many prototypes are spawned when shot.
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField("count")]
+ public int Count = 1;
+
+ ///
+ /// Caseless ammunition.
+ ///
+ [ViewVariables, DataField("deleteOnSpawn")]
+ public bool DeleteOnSpawn;
+
+ [ViewVariables, DataField("soundEject")]
+ public SoundSpecifier? EjectSound = new SoundCollectionSpecifier("CasingEject");
+}
diff --git a/Content.Shared/Weapons/Ranged/Components/AmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/AmmoProviderComponent.cs
new file mode 100644
index 0000000000..c54d081c88
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Components/AmmoProviderComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Weapons.Ranged.Components;
+
+[NetworkedComponent]
+public abstract class AmmoProviderComponent : Component {}
diff --git a/Content.Shared/Weapons/Ranged/Components/BallisticAmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/BallisticAmmoProviderComponent.cs
new file mode 100644
index 0000000000..73a85c5641
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Components/BallisticAmmoProviderComponent.cs
@@ -0,0 +1,48 @@
+using Content.Shared.Sound;
+using Content.Shared.Whitelist;
+using Robust.Shared.Containers;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Weapons.Ranged.Components;
+
+[RegisterComponent, NetworkedComponent]
+public sealed class BallisticAmmoProviderComponent : Component
+{
+ [ViewVariables(VVAccess.ReadOnly), DataField("soundRack")]
+ public SoundSpecifier? SoundRack = new SoundPathSpecifier("/Audio/Weapons/Guns/Cock/smg_cock.ogg");
+
+ [ViewVariables(VVAccess.ReadOnly), DataField("soundInsert")]
+ public SoundSpecifier? SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/bullet_insert.ogg");
+
+ [ViewVariables, DataField("proto", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string? FillProto;
+
+ [ViewVariables(VVAccess.ReadWrite), DataField("capacity")]
+ public int Capacity = 30;
+
+ [ViewVariables, DataField("unspawnedCount")]
+ public int UnspawnedCount;
+
+ [ViewVariables(VVAccess.ReadWrite), DataField("whitelist")]
+ public EntityWhitelist? Whitelist;
+
+ public Container Container = default!;
+
+ // TODO: Make this use stacks when the typeserializer is done.
+ [ViewVariables, DataField("entities")]
+ public List Entities = new();
+
+ ///
+ /// Will the ammoprovider automatically cycle through rounds or does it need doing manually.
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField("autoCycle")]
+ public bool AutoCycle = true;
+
+ ///
+ /// Is the gun ready to shoot; if AutoCycle is true then this will always stay true and not need to be manually done.
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField("cycled")]
+ public bool Cycled = true;
+}
diff --git a/Content.Shared/Weapons/Ranged/Components/BatteryAmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/BatteryAmmoProviderComponent.cs
new file mode 100644
index 0000000000..438276e21f
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Components/BatteryAmmoProviderComponent.cs
@@ -0,0 +1,18 @@
+namespace Content.Shared.Weapons.Ranged.Components;
+
+public abstract class BatteryAmmoProviderComponent : AmmoProviderComponent
+{
+ ///
+ /// How much battery it costs to fire once.
+ ///
+ [ViewVariables, DataField("fireCost")]
+ public float FireCost = 100;
+
+ // Batteries aren't predicted which means we need to track the battery and manually count it ourselves woo!
+
+ [ViewVariables]
+ public int Shots;
+
+ [ViewVariables]
+ public int Capacity;
+}
diff --git a/Content.Shared/Weapons/Ranged/Components/ChamberMagazineAmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/ChamberMagazineAmmoProviderComponent.cs
new file mode 100644
index 0000000000..10fcb6ddd6
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Components/ChamberMagazineAmmoProviderComponent.cs
@@ -0,0 +1,7 @@
+namespace Content.Shared.Weapons.Ranged.Components;
+
+///
+/// Chamber + mags in one package. If you need just magazine then use
+///
+[RegisterComponent]
+public sealed class ChamberMagazineAmmoProviderComponent : MagazineAmmoProviderComponent {}
diff --git a/Content.Shared/Weapons/Ranged/FlyBySoundComponent.cs b/Content.Shared/Weapons/Ranged/Components/FlyBySoundComponent.cs
similarity index 93%
rename from Content.Shared/Weapons/Ranged/FlyBySoundComponent.cs
rename to Content.Shared/Weapons/Ranged/Components/FlyBySoundComponent.cs
index 4d10fbc817..43fd067e26 100644
--- a/Content.Shared/Weapons/Ranged/FlyBySoundComponent.cs
+++ b/Content.Shared/Weapons/Ranged/Components/FlyBySoundComponent.cs
@@ -2,7 +2,7 @@ using Content.Shared.Sound;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
-namespace Content.Shared.Weapons.Ranged;
+namespace Content.Shared.Weapons.Ranged.Components;
///
/// Plays a sound when its non-hard fixture collides with a player.
diff --git a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs
new file mode 100644
index 0000000000..f153794f09
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs
@@ -0,0 +1,119 @@
+using Content.Shared.Actions.ActionTypes;
+using Content.Shared.Sound;
+using Robust.Shared.GameStates;
+using Robust.Shared.Map;
+
+namespace Content.Shared.Weapons.Ranged.Components;
+
+[RegisterComponent, NetworkedComponent, Virtual]
+public class GunComponent : Component
+{
+ #region Sound
+
+ [ViewVariables(VVAccess.ReadWrite), DataField("soundGunshot")]
+ public SoundSpecifier? SoundGunshot = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/smg.ogg");
+
+ [ViewVariables(VVAccess.ReadWrite), DataField("soundEmpty")]
+ 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");
+
+ #endregion
+
+ #region Recoil
+
+ // These values are very small for now until we get a debug overlay and fine tune it
+
+ ///
+ /// Last time the gun fired.
+ /// Used for recoil purposes.
+ ///
+ [ViewVariables, DataField("lastFire")]
+ public TimeSpan LastFire = TimeSpan.Zero;
+
+ ///
+ /// What the current spread is for shooting. This gets changed every time the gun fires.
+ ///
+ [ViewVariables, DataField("currentAngle")]
+ public Angle CurrentAngle;
+
+ ///
+ /// How much the spread increases every time the gun fires.
+ ///
+ [ViewVariables, DataField("angleIncrease")]
+ public Angle AngleIncrease = Angle.FromDegrees(0.5);
+
+ ///
+ /// How much the decreases per second.
+ ///
+ [ViewVariables, DataField("angleDecay")]
+ public Angle AngleDecay = Angle.FromDegrees(4);
+
+ ///
+ /// The maximum angle allowed for
+ ///
+ [ViewVariables, DataField("maxAngle")]
+ public Angle MaxAngle = Angle.FromDegrees(2);
+
+ ///
+ /// The minimum angle allowed for
+ ///
+ [ViewVariables, DataField("minAngle")]
+ public Angle MinAngle = Angle.FromDegrees(1);
+
+ #endregion
+
+ ///
+ /// Where the gun is being requested to shoot.
+ ///
+ [ViewVariables]
+ public EntityCoordinates? ShootCoordinates = null;
+
+ ///
+ /// Used for tracking semi-auto / burst
+ ///
+ [ViewVariables]
+ public int ShotCounter = 0;
+
+ ///
+ /// How many times it shoots per second.
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField("fireRate")]
+ public float FireRate = 8f;
+
+ ///
+ /// 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.
+ ///
+ [ViewVariables, DataField("nextFire")]
+ public TimeSpan NextFire = TimeSpan.Zero;
+
+ ///
+ /// What firemodes can be selected.
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField("availableModes")]
+ public SelectiveFire AvailableModes = SelectiveFire.SemiAuto;
+
+ ///
+ /// What firemode is currently selected.
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField("selectedMode")]
+ public SelectiveFire SelectedMode = SelectiveFire.SemiAuto;
+
+ [DataField("selectModeAction")]
+ public InstantAction? SelectModeAction;
+}
+
+[Flags]
+public enum SelectiveFire : byte
+{
+ Invalid = 0,
+ // Combat mode already functions as the equivalent of Safety
+ SemiAuto = 1 << 0,
+ Burst = 1 << 1,
+ FullAuto = 1 << 2, // Not in the building!
+}
diff --git a/Content.Shared/Weapons/Ranged/Components/HitscanBatteryAmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/HitscanBatteryAmmoProviderComponent.cs
new file mode 100644
index 0000000000..57b9ac91fa
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Components/HitscanBatteryAmmoProviderComponent.cs
@@ -0,0 +1,11 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Weapons.Ranged.Components;
+
+[RegisterComponent, NetworkedComponent]
+public sealed class HitscanBatteryAmmoProviderComponent : BatteryAmmoProviderComponent
+{
+ [ViewVariables(VVAccess.ReadWrite), DataField("proto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string Prototype = default!;
+}
diff --git a/Content.Shared/Weapons/Ranged/Components/MagazineAmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/MagazineAmmoProviderComponent.cs
new file mode 100644
index 0000000000..18d5d04397
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Components/MagazineAmmoProviderComponent.cs
@@ -0,0 +1,20 @@
+using Content.Shared.Sound;
+using Content.Shared.Weapons.Ranged.Components;
+
+namespace Content.Shared.Weapons.Ranged;
+
+///
+/// Wrapper around a magazine (handled via ItemSlot). Passes all AmmoProvider logic onto it.
+///
+[RegisterComponent, Virtual]
+public class MagazineAmmoProviderComponent : AmmoProviderComponent
+{
+ [ViewVariables(VVAccess.ReadWrite), DataField("soundAutoEject")]
+ public SoundSpecifier? SoundAutoEject = new SoundPathSpecifier("/Audio/Weapons/Guns/EmptyAlarm/smg_empty_alarm.ogg");
+
+ ///
+ /// Should the magazine automatically eject when empty.
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField("autoEject")]
+ public bool AutoEject = false;
+}
diff --git a/Content.Shared/Weapons/Ranged/Components/ProjectileBatteryAmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/ProjectileBatteryAmmoProviderComponent.cs
new file mode 100644
index 0000000000..0e5e1bb238
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Components/ProjectileBatteryAmmoProviderComponent.cs
@@ -0,0 +1,12 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Weapons.Ranged.Components;
+
+[RegisterComponent, NetworkedComponent]
+public sealed class ProjectileBatteryAmmoProviderComponent : BatteryAmmoProviderComponent
+{
+ [ViewVariables(VVAccess.ReadWrite), DataField("proto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string Prototype = default!;
+}
diff --git a/Content.Shared/Weapons/Ranged/Components/RevolverAmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/RevolverAmmoProviderComponent.cs
new file mode 100644
index 0000000000..9dbba734b4
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Components/RevolverAmmoProviderComponent.cs
@@ -0,0 +1,50 @@
+using Content.Shared.Sound;
+using Content.Shared.Whitelist;
+using Robust.Shared.Containers;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Weapons.Ranged.Components;
+
+[RegisterComponent, NetworkedComponent]
+public sealed class RevolverAmmoProviderComponent : AmmoProviderComponent
+{
+ /*
+ * Revolver has an array of its slots of which we can fire from any index.
+ * We also keep a separate array of slots we haven't spawned entities for, Chambers. This means that rather than creating
+ * for example 7 entities when revolver spawns (1 for the revolver and 6 cylinders) we can instead defer it.
+ */
+
+ [ViewVariables, DataField("whitelist")]
+ public EntityWhitelist? Whitelist;
+
+ public Container AmmoContainer = default!;
+
+ [ViewVariables, DataField("currentSlot")]
+ public int CurrentIndex;
+
+ [ViewVariables, DataField("capacity")]
+ public int Capacity = 6;
+
+ // Like BallisticAmmoProvider we defer spawning until necessary
+ // AmmoSlots is the instantiated ammo and Chambers is the unspawned ammo (that may or may not have been shot).
+
+ [DataField("ammoSlots")]
+ public EntityUid?[] AmmoSlots = Array.Empty();
+
+ [DataField("chambers")]
+ public bool?[] Chambers = Array.Empty();
+
+ [DataField("proto", customTypeSerializer:typeof(PrototypeIdSerializer))]
+ public string? FillPrototype = "CartridgeMagnum";
+
+ [ViewVariables, DataField("soundEject")]
+ public SoundSpecifier? SoundEject = new SoundPathSpecifier("/Audio/Weapons/Guns/MagOut/revolver_magout.ogg");
+
+ [ViewVariables, DataField("soundInsert")]
+ public SoundSpecifier? SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/revolver_magin.ogg");
+
+ [ViewVariables, DataField("soundSpin")]
+ public SoundSpecifier? SoundSpin = new SoundPathSpecifier("/Audio/Weapons/Guns/Misc/revolver_spin.ogg");
+}
diff --git a/Content.Shared/Weapons/Ranged/Components/SharedAmmoCounterComponent.cs b/Content.Shared/Weapons/Ranged/Components/SharedAmmoCounterComponent.cs
new file mode 100644
index 0000000000..cf70ca516e
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Components/SharedAmmoCounterComponent.cs
@@ -0,0 +1,9 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Weapons.Ranged.Components;
+
+///
+/// Shows an ItemStatus with the ammo of the gun. Adjusts based on what the ammoprovider is.
+///
+[NetworkedComponent]
+public abstract class SharedAmmoCounterComponent : Component {}
diff --git a/Content.Shared/Weapons/Ranged/Components/SharedRangedBarrelComponent.cs b/Content.Shared/Weapons/Ranged/Components/SharedRangedBarrelComponent.cs
deleted file mode 100644
index 0e49acfe89..0000000000
--- a/Content.Shared/Weapons/Ranged/Components/SharedRangedBarrelComponent.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-namespace Content.Shared.Weapons.Ranged.Components
-{
- public abstract class SharedRangedBarrelComponent : Component
- {
- [ViewVariables]
- public abstract FireRateSelector FireRateSelector { get; }
- [ViewVariables]
- public abstract FireRateSelector AllRateSelectors { get; }
- [ViewVariables]
- public abstract float FireRate { get; }
- [ViewVariables]
- public abstract int ShotsLeft { get; }
- [ViewVariables]
- public abstract int Capacity { get; }
- }
-
- [Flags]
- public enum FireRateSelector
- {
- Safety = 0,
- Single = 1 << 0,
- Automatic = 1 << 1,
- }
-}
diff --git a/Content.Shared/Weapons/Ranged/Components/SharedRangedWeaponComponent.cs b/Content.Shared/Weapons/Ranged/Components/SharedRangedWeaponComponent.cs
deleted file mode 100644
index 0a9749752c..0000000000
--- a/Content.Shared/Weapons/Ranged/Components/SharedRangedWeaponComponent.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using Robust.Shared.GameStates;
-using Robust.Shared.Map;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Weapons.Ranged.Components
-{
- [NetworkedComponent()]
- public abstract class SharedRangedWeaponComponent : Component
- {
- // Each RangedWeapon should have a RangedWeapon component +
- // some kind of RangedBarrelComponent (this dictates what ammo is retrieved).
- }
-
- [Serializable, NetSerializable]
- public sealed class RangedWeaponComponentState : ComponentState
- {
- public FireRateSelector FireRateSelector { get; }
-
- public RangedWeaponComponentState(
- FireRateSelector fireRateSelector
- )
- {
- FireRateSelector = fireRateSelector;
- }
- }
-
- ///
- /// An event raised when the weapon is fired at a position on the map by a client.
- ///
- [Serializable, NetSerializable]
- public sealed class FirePosEvent : EntityEventArgs
- {
- public EntityCoordinates Coordinates;
-
- public FirePosEvent(EntityCoordinates coordinates)
- {
- Coordinates = coordinates;
- }
- }
-}
diff --git a/Content.Shared/Weapons/Ranged/Events/AmmoShotEvent.cs b/Content.Shared/Weapons/Ranged/Events/AmmoShotEvent.cs
new file mode 100644
index 0000000000..3b9ea0ebb0
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Events/AmmoShotEvent.cs
@@ -0,0 +1,9 @@
+namespace Content.Shared.Weapons.Ranged.Events;
+
+///
+/// Raised on a gun when projectiles have been fired from it.
+///
+public sealed class AmmoShotEvent : EntityEventArgs
+{
+ public List FiredProjectiles = default!;
+}
diff --git a/Content.Shared/Weapons/Ranged/Events/GetAmmoCountEvent.cs b/Content.Shared/Weapons/Ranged/Events/GetAmmoCountEvent.cs
new file mode 100644
index 0000000000..e080ff5848
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Events/GetAmmoCountEvent.cs
@@ -0,0 +1,11 @@
+namespace Content.Shared.Weapons.Ranged.Events;
+
+///
+/// Raised on an AmmoProvider to request deets.
+///
+[ByRefEvent]
+public struct GetAmmoCountEvent
+{
+ public int Count;
+ public int Capacity;
+}
\ No newline at end of file
diff --git a/Content.Shared/Weapons/Ranged/MagazineAutoEjectEvent.cs b/Content.Shared/Weapons/Ranged/Events/MagazineAutoEjectEvent.cs
similarity index 85%
rename from Content.Shared/Weapons/Ranged/MagazineAutoEjectEvent.cs
rename to Content.Shared/Weapons/Ranged/Events/MagazineAutoEjectEvent.cs
index d241cb9703..e20b36eea8 100644
--- a/Content.Shared/Weapons/Ranged/MagazineAutoEjectEvent.cs
+++ b/Content.Shared/Weapons/Ranged/Events/MagazineAutoEjectEvent.cs
@@ -1,6 +1,6 @@
using Robust.Shared.Serialization;
-namespace Content.Shared.Weapons.Ranged
+namespace Content.Shared.Weapons.Ranged.Events
{
///
/// This is sent if the MagazineBarrel AutoEjects the magazine
diff --git a/Content.Shared/Weapons/Ranged/Events/RequestShootEvent.cs b/Content.Shared/Weapons/Ranged/Events/RequestShootEvent.cs
new file mode 100644
index 0000000000..af352d8445
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Events/RequestShootEvent.cs
@@ -0,0 +1,14 @@
+using Robust.Shared.Map;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Weapons.Ranged.Events;
+
+///
+/// Raised on the client to indicate it'd like to shoot.
+///
+[Serializable, NetSerializable]
+public sealed class RequestShootEvent : EntityEventArgs
+{
+ public EntityUid Gun;
+ public EntityCoordinates Coordinates;
+}
\ No newline at end of file
diff --git a/Content.Shared/Weapons/Ranged/Events/RequestStopShootEvent.cs b/Content.Shared/Weapons/Ranged/Events/RequestStopShootEvent.cs
new file mode 100644
index 0000000000..5fc1f5dc4e
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Events/RequestStopShootEvent.cs
@@ -0,0 +1,12 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Weapons.Ranged.Events;
+
+///
+/// Raised on the client to request it would like to stop hooting.
+///
+[Serializable, NetSerializable]
+public sealed class RequestStopShootEvent : EntityEventArgs
+{
+ public EntityUid Gun;
+}
\ No newline at end of file
diff --git a/Content.Shared/Weapons/Ranged/Events/TakeAmmoEvent.cs b/Content.Shared/Weapons/Ranged/Events/TakeAmmoEvent.cs
new file mode 100644
index 0000000000..5ad53fd970
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Events/TakeAmmoEvent.cs
@@ -0,0 +1,26 @@
+using Robust.Shared.Map;
+
+namespace Content.Shared.Weapons.Ranged.Events;
+
+///
+/// Raised on a gun when it would like to take the specified amount of ammo.
+///
+public sealed class TakeAmmoEvent : EntityEventArgs
+{
+ public EntityUid? User;
+ public readonly int Shots;
+ public List Ammo;
+
+ ///
+ /// Coordinates to spawn the ammo at.
+ ///
+ public EntityCoordinates Coordinates;
+
+ public TakeAmmoEvent(int shots, List ammo, EntityCoordinates coordinates, EntityUid? user)
+ {
+ Shots = shots;
+ Ammo = ammo;
+ Coordinates = coordinates;
+ User = user;
+ }
+}
diff --git a/Content.Shared/Weapons/Ranged/HitscanPrototype.cs b/Content.Shared/Weapons/Ranged/HitscanPrototype.cs
new file mode 100644
index 0000000000..d7bab2668d
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/HitscanPrototype.cs
@@ -0,0 +1,39 @@
+using Content.Shared.Damage;
+using Content.Shared.Physics;
+using Content.Shared.Weapons.Ranged.Systems;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Weapons.Ranged;
+
+[Prototype("hitscan")]
+public sealed class HitscanPrototype : IPrototype, IShootable
+{
+ [ViewVariables]
+ [IdDataFieldAttribute]
+ public string ID { get; } = default!;
+
+ [ViewVariables(VVAccess.ReadWrite), DataField("damage")]
+ public DamageSpecifier? Damage;
+
+ [ViewVariables(VVAccess.ReadOnly), DataField("muzzleFlash")]
+ public SpriteSpecifier? MuzzleFlash;
+
+ [ViewVariables(VVAccess.ReadOnly), DataField("travelFlash")]
+ public SpriteSpecifier? TravelFlash;
+
+ [ViewVariables(VVAccess.ReadOnly), DataField("impactFlash")]
+ public SpriteSpecifier? ImpactFlash;
+
+ [ViewVariables, DataField("collisionMask")]
+ public int CollisionMask = (int) CollisionGroup.Opaque;
+
+ [ViewVariables(VVAccess.ReadWrite), DataField("color")]
+ public Color Color = Color.White;
+
+ ///
+ /// Try not to set this too high.
+ ///
+ [ViewVariables, DataField("maxLength")]
+ public float MaxLength = 20f;
+}
diff --git a/Content.Shared/Weapons/Ranged/IShootable.cs b/Content.Shared/Weapons/Ranged/IShootable.cs
new file mode 100644
index 0000000000..b136b6a122
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/IShootable.cs
@@ -0,0 +1,6 @@
+namespace Content.Shared.Weapons.Ranged;
+
+///
+/// Interface that says this can be shot from a gun. Exists to facilitate hitscan OR prototype shooting.
+///
+public interface IShootable {}
\ No newline at end of file
diff --git a/Content.Shared/Weapons/Ranged/SharedFlyBySoundSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedFlyBySoundSystem.cs
similarity index 95%
rename from Content.Shared/Weapons/Ranged/SharedFlyBySoundSystem.cs
rename to Content.Shared/Weapons/Ranged/Systems/SharedFlyBySoundSystem.cs
index eefc23f314..084c65ce24 100644
--- a/Content.Shared/Weapons/Ranged/SharedFlyBySoundSystem.cs
+++ b/Content.Shared/Weapons/Ranged/Systems/SharedFlyBySoundSystem.cs
@@ -1,12 +1,13 @@
using Content.Shared.Physics;
using Content.Shared.Sound;
+using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.GameStates;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Serialization;
-namespace Content.Shared.Weapons.Ranged;
+namespace Content.Shared.Weapons.Ranged.Systems;
public abstract class SharedFlyBySoundSystem : EntitySystem
{
diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs
new file mode 100644
index 0000000000..5295861577
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs
@@ -0,0 +1,215 @@
+using Content.Shared.Examine;
+using Content.Shared.Interaction;
+using Content.Shared.Verbs;
+using Content.Shared.Weapons.Ranged.Components;
+using Content.Shared.Weapons.Ranged.Events;
+using Robust.Shared.Containers;
+using Robust.Shared.GameStates;
+using Robust.Shared.Map;
+using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Weapons.Ranged.Systems;
+
+public abstract partial class SharedGunSystem
+{
+ protected virtual void InitializeBallistic()
+ {
+ SubscribeLocalEvent(OnBallisticInit);
+ SubscribeLocalEvent(OnBallisticTakeAmmo);
+ SubscribeLocalEvent(OnBallisticAmmoCount);
+ SubscribeLocalEvent(OnBallisticGetState);
+ SubscribeLocalEvent(OnBallisticHandleState);
+
+ SubscribeLocalEvent(OnBallisticExamine);
+ SubscribeLocalEvent>(OnBallisticVerb);
+ SubscribeLocalEvent(OnBallisticInteractUsing);
+ SubscribeLocalEvent(OnBallisticActivate);
+ }
+
+ private void OnBallisticActivate(EntityUid uid, BallisticAmmoProviderComponent component, ActivateInWorldEvent args)
+ {
+ ManualCycle(component, Transform(uid).MapPosition, args.User);
+ args.Handled = true;
+ }
+
+ private void OnBallisticInteractUsing(EntityUid uid, BallisticAmmoProviderComponent component, InteractUsingEvent args)
+ {
+ if (args.Handled || component.Whitelist?.IsValid(args.Used, EntityManager) != true) return;
+
+ if (GetBallisticShots(component) >= component.Capacity) return;
+
+ component.Entities.Add(args.Used);
+ component.Container.Insert(args.Used);
+ // Not predicted so
+ PlaySound(uid, component.SoundInsert?.GetSound(Random, ProtoManager), args.User);
+ args.Handled = true;
+ UpdateBallisticAppearance(component);
+ Dirty(component);
+ }
+
+ private void OnBallisticVerb(EntityUid uid, BallisticAmmoProviderComponent component, GetVerbsEvent args)
+ {
+ if (!args.CanAccess || !args.CanInteract) return;
+
+ args.Verbs.Add(new Verb()
+ {
+ Text = Loc.GetString("gun-ballistic-cycle"),
+ Disabled = GetBallisticShots(component) == 0,
+ Act = () => ManualCycle(component, Transform(uid).MapPosition, args.User),
+ });
+ }
+
+ private void OnBallisticExamine(EntityUid uid, BallisticAmmoProviderComponent component, ExaminedEvent args)
+ {
+ args.PushMarkup(Loc.GetString("gun-magazine-examine", ("color", AmmoExamineColor), ("count", GetBallisticShots(component))));
+ }
+
+ private void ManualCycle(BallisticAmmoProviderComponent component, MapCoordinates coordinates, EntityUid? user = null)
+ {
+ // Reset shotting for cycling
+ if (TryComp(component.Owner, out var gunComp) &&
+ gunComp is { FireRate: > 0f })
+ {
+ gunComp.NextFire = Timing.CurTime + TimeSpan.FromSeconds(1 / gunComp.FireRate);
+ }
+
+ Dirty(component);
+ var sound = component.SoundRack?.GetSound(Random, ProtoManager);
+
+ if (sound != null)
+ PlaySound(component.Owner, sound, user);
+
+ var shots = GetBallisticShots(component);
+ component.Cycled = true;
+
+ Cycle(component, coordinates);
+
+ var text = Loc.GetString(shots == 0 ? "gun-ballistic-cycled-empty" : "gun-ballistic-cycled");
+
+ Popup(text, component.Owner, user);
+ UpdateBallisticAppearance(component);
+ UpdateAmmoCount(component.Owner);
+ }
+
+ protected abstract void Cycle(BallisticAmmoProviderComponent component, MapCoordinates coordinates);
+
+ private void OnBallisticGetState(EntityUid uid, BallisticAmmoProviderComponent component, ref ComponentGetState args)
+ {
+ args.State = new BallisticAmmoProviderComponentState()
+ {
+ UnspawnedCount = component.UnspawnedCount,
+ Entities = component.Entities,
+ Cycled = component.Cycled,
+ };
+ }
+
+ private void OnBallisticHandleState(EntityUid uid, BallisticAmmoProviderComponent component, ref ComponentHandleState args)
+ {
+ if (args.Current is not BallisticAmmoProviderComponentState state) return;
+
+ component.Cycled = state.Cycled;
+ component.UnspawnedCount = state.UnspawnedCount;
+
+ component.Entities.Clear();
+
+ foreach (var ent in state.Entities)
+ {
+ component.Entities.Add(ent);
+ }
+ }
+
+ private void OnBallisticInit(EntityUid uid, BallisticAmmoProviderComponent component, ComponentInit args)
+ {
+ component.Container = Containers.EnsureContainer(uid, "ballistic-ammo");
+ component.UnspawnedCount = component.Capacity;
+
+ if (component.FillProto != null)
+ {
+ component.UnspawnedCount -= Math.Min(component.UnspawnedCount, component.Container.ContainedEntities.Count);
+ }
+ else
+ {
+ component.UnspawnedCount = 0;
+ }
+ }
+
+ protected int GetBallisticShots(BallisticAmmoProviderComponent component)
+ {
+ return component.Entities.Count + component.UnspawnedCount;
+ }
+
+ private void OnBallisticTakeAmmo(EntityUid uid, BallisticAmmoProviderComponent component, TakeAmmoEvent args)
+ {
+ for (var i = 0; i < args.Shots; i++)
+ {
+ if (!component.Cycled) break;
+
+ EntityUid entity;
+
+ if (component.Entities.Count > 0)
+ {
+ entity = component.Entities[^1];
+
+ // Leave the entity as is if it doesn't auto cycle
+ // TODO: Suss this out with NewAmmoComponent as I don't think it gets removed from container properly
+ if (HasComp(entity) && component.AutoCycle)
+ {
+ component.Entities.RemoveAt(component.Entities.Count - 1);
+ component.Container.Remove(entity);
+ }
+
+ args.Ammo.Add(EnsureComp(entity));
+ }
+ else if (component.UnspawnedCount > 0)
+ {
+ component.UnspawnedCount--;
+ entity = Spawn(component.FillProto, args.Coordinates);
+ args.Ammo.Add(EnsureComp(entity));
+
+ // Put it back in if it doesn't auto-cycle
+ if (HasComp(entity) && !component.AutoCycle)
+ {
+ if (!entity.IsClientSide())
+ {
+ component.Entities.Add(entity);
+ component.Container.Insert(entity);
+ }
+ else
+ {
+ component.UnspawnedCount++;
+ }
+ }
+ }
+
+ if (!component.AutoCycle)
+ {
+ component.Cycled = false;
+ }
+ }
+
+ UpdateBallisticAppearance(component);
+ Dirty(component);
+ }
+
+ private void OnBallisticAmmoCount(EntityUid uid, BallisticAmmoProviderComponent component, ref GetAmmoCountEvent args)
+ {
+ args.Count = GetBallisticShots(component);
+ args.Capacity = component.Capacity;
+ }
+
+ private void UpdateBallisticAppearance(BallisticAmmoProviderComponent component)
+ {
+ if (!Timing.IsFirstTimePredicted || !TryComp(component.Owner, out var appearance)) return;
+ appearance.SetData(AmmoVisuals.AmmoCount, GetBallisticShots(component));
+ appearance.SetData(AmmoVisuals.AmmoMax, component.Capacity);
+ }
+
+ [Serializable, NetSerializable]
+ private sealed class BallisticAmmoProviderComponentState : ComponentState
+ {
+ public int UnspawnedCount;
+ public List Entities = default!;
+ public bool Cycled;
+ }
+}
diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Battery.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Battery.cs
new file mode 100644
index 0000000000..10c23ebcb9
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Battery.cs
@@ -0,0 +1,111 @@
+using Content.Shared.Examine;
+using Content.Shared.Weapons.Ranged.Components;
+using Content.Shared.Weapons.Ranged.Events;
+using Robust.Shared.GameStates;
+using Robust.Shared.Map;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Weapons.Ranged.Systems;
+
+public abstract partial class SharedGunSystem
+{
+ protected virtual void InitializeBattery()
+ {
+ // Trying to dump comp references hence the below
+ // Hitscan
+ SubscribeLocalEvent(OnBatteryGetState);
+ SubscribeLocalEvent(OnBatteryHandleState);
+ SubscribeLocalEvent(OnBatteryTakeAmmo);
+ SubscribeLocalEvent(OnBatteryAmmoCount);
+ SubscribeLocalEvent(OnBatteryExamine);
+
+ // Projectile
+ SubscribeLocalEvent(OnBatteryGetState);
+ SubscribeLocalEvent(OnBatteryHandleState);
+ SubscribeLocalEvent(OnBatteryTakeAmmo);
+ SubscribeLocalEvent(OnBatteryAmmoCount);
+ SubscribeLocalEvent(OnBatteryExamine);
+ }
+
+ private void OnBatteryHandleState(EntityUid uid, BatteryAmmoProviderComponent component, ref ComponentHandleState args)
+ {
+ if (args.Current is not BatteryAmmoProviderComponentState state) return;
+
+ component.Shots = state.Shots;
+ component.Capacity = state.MaxShots;
+ component.FireCost = state.FireCost;
+ }
+
+ private void OnBatteryGetState(EntityUid uid, BatteryAmmoProviderComponent component, ref ComponentGetState args)
+ {
+ args.State = new BatteryAmmoProviderComponentState()
+ {
+ Shots = component.Shots,
+ MaxShots = component.Capacity,
+ FireCost = component.FireCost,
+ };
+ }
+
+ private void OnBatteryExamine(EntityUid uid, BatteryAmmoProviderComponent component, ExaminedEvent args)
+ {
+ args.PushMarkup(Loc.GetString("gun-battery-examine", ("color", AmmoExamineColor), ("count", component.Shots)));
+ }
+
+ private void OnBatteryTakeAmmo(EntityUid uid, BatteryAmmoProviderComponent component, TakeAmmoEvent args)
+ {
+ var shots = Math.Min(args.Shots, component.Shots);
+
+ // Don't dirty if it's an empty fire.
+ if (shots == 0) return;
+
+ for (var i = 0; i < shots; i++)
+ {
+ args.Ammo.Add(GetShootable(component, args.Coordinates));
+ component.Shots--;
+ }
+
+ TakeCharge(uid, component);
+ UpdateBatteryAppearance(uid, component);
+ Dirty(component);
+ }
+
+ private void OnBatteryAmmoCount(EntityUid uid, BatteryAmmoProviderComponent component, ref GetAmmoCountEvent args)
+ {
+ args.Count = component.Shots;
+ args.Capacity = component.Capacity;
+ }
+
+ ///
+ /// Update the battery (server-only) whenever fired.
+ ///
+ protected virtual void TakeCharge(EntityUid uid, BatteryAmmoProviderComponent component) {}
+
+ protected void UpdateBatteryAppearance(EntityUid uid, BatteryAmmoProviderComponent component)
+ {
+ if (!TryComp(uid, out var appearance)) return;
+ appearance.SetData(AmmoVisuals.AmmoCount, component.Shots);
+ appearance.SetData(AmmoVisuals.AmmoMax, component.Capacity);
+ }
+
+ private IShootable GetShootable(BatteryAmmoProviderComponent component, EntityCoordinates coordinates)
+ {
+ switch (component)
+ {
+ case ProjectileBatteryAmmoProviderComponent proj:
+ var ent = Spawn(proj.Prototype, coordinates);
+ return EnsureComp(ent);
+ case HitscanBatteryAmmoProviderComponent hitscan:
+ return ProtoManager.Index(hitscan.Prototype);
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ [Serializable, NetSerializable]
+ private sealed class BatteryAmmoProviderComponentState : ComponentState
+ {
+ public int Shots;
+ public int MaxShots;
+ public float FireCost;
+ }
+}
diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Cartridges.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Cartridges.cs
new file mode 100644
index 0000000000..e1f7c9da8f
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Cartridges.cs
@@ -0,0 +1,34 @@
+using Content.Shared.Weapons.Ranged.Components;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Weapons.Ranged.Systems;
+
+public abstract partial class SharedGunSystem
+{
+ private void InitializeCartridge()
+ {
+ SubscribeLocalEvent(OnCartridgeGetState);
+ SubscribeLocalEvent(OnCartridgeHandleState);
+ }
+
+ private void OnCartridgeHandleState(EntityUid uid, CartridgeAmmoComponent component, ref ComponentHandleState args)
+ {
+ if (args.Current is not CartridgeAmmoComponentState state) return;
+ component.Spent = state.Spent;
+ }
+
+ private void OnCartridgeGetState(EntityUid uid, CartridgeAmmoComponent component, ref ComponentGetState args)
+ {
+ args.State = new CartridgeAmmoComponentState()
+ {
+ Spent = component.Spent,
+ };
+ }
+
+ [Serializable, NetSerializable]
+ private sealed class CartridgeAmmoComponentState : ComponentState
+ {
+ public bool Spent;
+ }
+}
diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.ChamberMagazine.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.ChamberMagazine.cs
new file mode 100644
index 0000000000..41be6f1ba4
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.ChamberMagazine.cs
@@ -0,0 +1,121 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Examine;
+using Content.Shared.Interaction;
+using Content.Shared.Verbs;
+using Content.Shared.Weapons.Ranged.Components;
+using Content.Shared.Weapons.Ranged.Events;
+using Robust.Shared.Containers;
+
+namespace Content.Shared.Weapons.Ranged.Systems;
+
+public abstract partial class SharedGunSystem
+{
+ protected const string ChamberSlot = "gun-chamber";
+
+ protected virtual void InitializeChamberMagazine()
+ {
+ SubscribeLocalEvent(OnChamberMagazineTakeAmmo);
+ SubscribeLocalEvent>(OnMagazineVerb);
+ SubscribeLocalEvent(OnMagazineSlotChange);
+ SubscribeLocalEvent(OnMagazineActivate);
+ SubscribeLocalEvent(OnChamberMagazineExamine);
+ }
+
+ private void OnChamberMagazineExamine(EntityUid uid, ChamberMagazineAmmoProviderComponent component, ExaminedEvent args)
+ {
+ var (count, _) = GetChamberMagazineCountCapacity(component);
+ args.PushMarkup(Loc.GetString("gun-magazine-examine", ("color", AmmoExamineColor), ("count", count)));
+ }
+
+ private bool TryTakeChamberEntity(EntityUid uid, [NotNullWhen(true)] out EntityUid? entity)
+ {
+ if (!Containers.TryGetContainer(uid, ChamberSlot, out var container) ||
+ container is not ContainerSlot slot)
+ {
+ entity = null;
+ return false;
+ }
+
+ entity = slot.ContainedEntity;
+ if (entity == null) return false;
+ container.Remove(entity.Value);
+ return true;
+ }
+
+ protected EntityUid? GetChamberEntity(EntityUid uid)
+ {
+ if (!Containers.TryGetContainer(uid, ChamberSlot, out var container) ||
+ container is not ContainerSlot slot)
+ {
+ return null;
+ }
+
+ return slot.ContainedEntity;
+ }
+
+ protected (int, int) GetChamberMagazineCountCapacity(ChamberMagazineAmmoProviderComponent component)
+ {
+ var count = GetChamberEntity(component.Owner) != null ? 1 : 0;
+ var (magCount, magCapacity) = GetMagazineCountCapacity(component);
+ return (count + magCount, magCapacity);
+ }
+
+ private bool TryInsertChamber(EntityUid uid, EntityUid ammo)
+ {
+ return Containers.TryGetContainer(uid, ChamberSlot, out var container) &&
+ container is ContainerSlot slot &&
+ slot.Insert(ammo);
+ }
+
+ private void OnChamberMagazineTakeAmmo(EntityUid uid, ChamberMagazineAmmoProviderComponent component, TakeAmmoEvent args)
+ {
+ // So chamber logic is kinda sussier than the others
+ // Essentially we want to treat the chamber as a potentially free slot and then the mag as the remaining slots
+ // i.e. if we shoot 3 times, then we use the chamber once (regardless if it's empty or not) and 2 from the mag
+ // We move the n + 1 shot into the chamber as we essentially treat it like a stack.
+ TryComp(uid, out var appearance);
+
+ if (TryTakeChamberEntity(uid, out var chamberEnt))
+ {
+ args.Ammo.Add(EnsureComp(chamberEnt.Value));
+ }
+
+ var magEnt = GetMagazineEntity(uid);
+
+ // Pass an event to the magazine to get more (to refill chamber or for shooting).
+ if (magEnt != null)
+ {
+ // We pass in Shots not Shots - 1 as we'll take the last entity and move it into the chamber.
+ var relayedArgs = new TakeAmmoEvent(args.Shots, new List(), args.Coordinates, args.User);
+ RaiseLocalEvent(magEnt.Value, relayedArgs, false);
+
+ // Put in the nth slot back into the chamber
+ // Rest of the ammo gets shot
+ if (relayedArgs.Ammo.Count > 0)
+ {
+ var newChamberEnt = ((AmmoComponent) relayedArgs.Ammo[^1]).Owner;
+ TryInsertChamber(uid, newChamberEnt);
+ }
+
+ // Anything above the chamber-refill amount gets fired.
+ for (var i = 0; i < relayedArgs.Ammo.Count - 1; i++)
+ {
+ args.Ammo.Add(relayedArgs.Ammo[i]);
+ }
+ }
+ else
+ {
+ appearance?.SetData(AmmoVisuals.MagLoaded, false);
+ return;
+ }
+
+ var count = chamberEnt != null ? 1 : 0;
+ const int capacity = 1;
+
+ var ammoEv = new GetAmmoCountEvent();
+ RaiseLocalEvent(magEnt.Value, ref ammoEv, false);
+
+ FinaliseMagazineTakeAmmo(uid, component, args, count + ammoEv.Count, capacity + ammoEv.Capacity, appearance);
+ }
+}
diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Interactions.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Interactions.cs
new file mode 100644
index 0000000000..0f4ba1f2ee
--- /dev/null
+++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Interactions.cs
@@ -0,0 +1,101 @@
+using Content.Shared.Actions;
+using Content.Shared.Actions.ActionTypes;
+using Content.Shared.CombatMode;
+using Content.Shared.Examine;
+using Content.Shared.Verbs;
+using Content.Shared.Weapons.Ranged.Components;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Weapons.Ranged.Systems;
+
+public abstract partial class SharedGunSystem
+{
+ private void OnExamine(EntityUid uid, GunComponent component, ExaminedEvent args)
+ {
+ 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)));
+ }
+
+ private string GetLocSelector(SelectiveFire mode)
+ {
+ return Loc.GetString($"gun-{mode.ToString()}");
+ }
+
+ private void OnAltVerb(EntityUid uid, GunComponent component, GetVerbsEvent args)
+ {
+ if (!args.CanAccess || !args.CanInteract || component.SelectedMode == component.AvailableModes)
+ return;
+
+ var nextMode = GetNextMode(component);
+
+ AlternativeVerb verb = new()
+ {
+ Act = () => SelectFire(component, nextMode, args.User),
+ Text = Loc.GetString("gun-selector-verb", ("mode", GetLocSelector(nextMode))),
+ IconTexture = "/Textures/Interface/VerbIcons/fold.svg.192dpi.png",
+ };
+
+ args.Verbs.Add(verb);
+ }
+
+ private SelectiveFire GetNextMode(GunComponent component)
+ {
+ var modes = new List();
+
+ foreach (var mode in Enum.GetValues())
+ {
+ if ((mode & component.AvailableModes) == 0x0) continue;
+ modes.Add(mode);
+ }
+
+ var index = modes.IndexOf(component.SelectedMode);
+ return modes[(index + 1) % modes.Count];
+ }
+
+ private void SelectFire(GunComponent component, SelectiveFire fire, EntityUid? user = null)
+ {
+ if (component.SelectedMode == fire) return;
+
+ DebugTools.Assert((component.AvailableModes & fire) != 0x0);
+ component.SelectedMode = fire;
+ var curTime = Timing.CurTime;
+ var cooldown = TimeSpan.FromSeconds(InteractNextFire);
+
+ if (component.NextFire < curTime)
+ component.NextFire = curTime + cooldown;
+ else
+ component.NextFire += cooldown;
+
+ PlaySound(component.Owner, component.SoundModeToggle?.GetSound(Random, ProtoManager), user);
+ Popup(Loc.GetString("gun-selected-mode", ("mode", GetLocSelector(fire))), component.Owner, user);
+ Dirty(component);
+ }
+
+ ///