diff --git a/Content.Client/Content.Client.csproj b/Content.Client/Content.Client.csproj
index eb6b73f8fc..9091c2f9c1 100644
--- a/Content.Client/Content.Client.csproj
+++ b/Content.Client/Content.Client.csproj
@@ -77,6 +77,7 @@
+
@@ -84,7 +85,10 @@
+
+
+
diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs
index e293421bc4..6b9bcbdcd4 100644
--- a/Content.Client/EntryPoint.cs
+++ b/Content.Client/EntryPoint.cs
@@ -25,8 +25,10 @@ using SS14.Shared.Interfaces.GameObjects;
using SS14.Shared.IoC;
using SS14.Shared.Prototypes;
using System;
+using Content.Client.GameObjects.Components.Mobs;
using Content.Client.UserInterface;
using Content.Shared.GameObjects.Components.Markers;
+using Content.Shared.GameObjects.Components.Mobs;
using SS14.Client.Interfaces.UserInterface;
using SS14.Shared.Log;
@@ -100,6 +102,9 @@ namespace Content.Client
factory.Register();
+ factory.Register();
+ factory.RegisterReference();
+
IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
diff --git a/Content.Client/GameObjects/Components/Mobs/CameraRecoilComponent.cs b/Content.Client/GameObjects/Components/Mobs/CameraRecoilComponent.cs
new file mode 100644
index 0000000000..4c25b5b94a
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Mobs/CameraRecoilComponent.cs
@@ -0,0 +1,87 @@
+using System;
+using Content.Shared.GameObjects.Components.Mobs;
+using SS14.Client.GameObjects;
+using SS14.Shared.GameObjects;
+using SS14.Shared.Interfaces.GameObjects;
+using SS14.Shared.Interfaces.Network;
+using SS14.Shared.Maths;
+
+namespace Content.Client.GameObjects.Components.Mobs
+{
+ public sealed class CameraRecoilComponent : SharedCameraRecoilComponent
+ {
+ // Maximum rate of magnitude restore towards 0 kick.
+ private const float RestoreRateMax = 1.5f;
+
+ // Minimum rate of magnitude restore towards 0 kick.
+ private const float RestoreRateMin = 0.5f;
+
+ // Time in seconds since the last kick that lerps RestoreRateMin and RestoreRateMax
+ private const float RestoreRateRamp = 0.05f;
+
+ // The maximum magnitude of the kick applied to the camera at any point.
+ private const float KickMagnitudeMax = 0.25f;
+
+ private Vector2 _currentKick;
+ private float _lastKickTime;
+
+ private EyeComponent _eye;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _eye = Owner.GetComponent();
+ }
+
+ public override void Kick(Vector2 recoil)
+ {
+ // Use really bad math to "dampen" kicks when we're already kicked.
+ var existing = _currentKick.Length;
+ var dampen = existing/KickMagnitudeMax;
+ _currentKick += recoil * (1-dampen);
+ if (_currentKick.Length > KickMagnitudeMax)
+ {
+ _currentKick = _currentKick.Normalized * KickMagnitudeMax;
+ }
+
+ _lastKickTime = 0;
+ _updateEye();
+ }
+
+ public override void HandleMessage(ComponentMessage message, INetChannel netChannel = null, IComponent component = null)
+ {
+ base.HandleMessage(message, netChannel, component);
+
+ switch (message)
+ {
+ case RecoilKickMessage msg:
+ Kick(msg.Recoil);
+ break;
+ }
+ }
+
+ public void FrameUpdate(float frameTime)
+ {
+ var magnitude = _currentKick.Length;
+ if (magnitude <= 0.005f)
+ {
+ _currentKick = Vector2.Zero;
+ _updateEye();
+ return;
+ }
+
+ // Continually restore camera to 0.
+ var normalized = _currentKick.Normalized;
+ var restoreRate = FloatMath.Lerp(RestoreRateMin, RestoreRateMax, Math.Min(1, _lastKickTime/RestoreRateRamp));
+ var restore = normalized * restoreRate * frameTime;
+ _currentKick -= restore;
+ _updateEye();
+ }
+
+ private void _updateEye()
+ {
+ _eye.Offset = _currentKick;
+ }
+ }
+}
diff --git a/Content.Client/GameObjects/Components/Weapons/Ranged/BallisticMagazineVisualizer2D.cs b/Content.Client/GameObjects/Components/Weapons/Ranged/BallisticMagazineVisualizer2D.cs
new file mode 100644
index 0000000000..f4a4df10b7
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Weapons/Ranged/BallisticMagazineVisualizer2D.cs
@@ -0,0 +1,41 @@
+using Content.Shared.GameObjects.Components.Weapons.Ranged;
+using Content.Shared.Utility;
+using SS14.Client.GameObjects;
+using SS14.Client.Interfaces.GameObjects.Components;
+using SS14.Shared.Utility;
+using YamlDotNet.RepresentationModel;
+
+namespace Content.Client.GameObjects.Components.Weapons.Ranged
+{
+ public sealed class BallisticMagazineVisualizer2D : AppearanceVisualizer
+ {
+ private string _baseState;
+ private int _steps;
+
+ public override void LoadData(YamlMappingNode node)
+ {
+ base.LoadData(node);
+
+ _baseState = node.GetNode("base_state").AsString();
+ _steps = node.GetNode("steps").AsInt();
+ }
+
+ public override void OnChangeData(AppearanceComponent component)
+ {
+ var sprite = component.Owner.GetComponent();
+
+ if (!component.TryGetData(BallisticMagazineVisuals.AmmoCapacity, out int capacity))
+ {
+ return;
+ }
+ if (!component.TryGetData(BallisticMagazineVisuals.AmmoLeft, out int current))
+ {
+ return;
+ }
+
+ var step = ContentHelpers.RoundToLevels(current, capacity, _steps);
+
+ sprite.LayerSetState(0, $"{_baseState}-{step}");
+ }
+ }
+}
diff --git a/Content.Client/GameObjects/Components/Weapons/Ranged/BallisticMagazineWeaponVisualizer2D.cs b/Content.Client/GameObjects/Components/Weapons/Ranged/BallisticMagazineWeaponVisualizer2D.cs
new file mode 100644
index 0000000000..6f50eb0eea
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Weapons/Ranged/BallisticMagazineWeaponVisualizer2D.cs
@@ -0,0 +1,50 @@
+using Content.Shared.GameObjects.Components.Weapons.Ranged;
+using Content.Shared.Utility;
+using SS14.Client.GameObjects;
+using SS14.Client.Interfaces.GameObjects.Components;
+using SS14.Shared.Utility;
+using YamlDotNet.RepresentationModel;
+
+namespace Content.Client.GameObjects.Components.Weapons.Ranged
+{
+ public sealed class BallisticMagazineWeaponVisualizer2D : AppearanceVisualizer
+ {
+ private string _baseState;
+ private int _steps;
+
+ public override void LoadData(YamlMappingNode node)
+ {
+ base.LoadData(node);
+
+ _baseState = node.GetNode("base_state").AsString();
+ _steps = node.GetNode("steps").AsInt();
+ }
+
+ public override void OnChangeData(AppearanceComponent component)
+ {
+ var sprite = component.Owner.GetComponent();
+
+ component.TryGetData(BallisticMagazineWeaponVisuals.MagazineLoaded, out bool loaded);
+
+ if (loaded)
+ {
+ if (!component.TryGetData(BallisticMagazineWeaponVisuals.AmmoCapacity, out int capacity))
+ {
+ return;
+ }
+ if (!component.TryGetData(BallisticMagazineWeaponVisuals.AmmoLeft, out int current))
+ {
+ return;
+ }
+
+ var step = ContentHelpers.RoundToLevels(current, capacity, _steps);
+
+ sprite.LayerSetState(0, $"{_baseState}-{step}");
+ }
+ else
+ {
+ sprite.LayerSetState(0, _baseState);
+ }
+ }
+ }
+}
diff --git a/Content.Client/GameObjects/EntitySystems/CameraRecoilSystem.cs b/Content.Client/GameObjects/EntitySystems/CameraRecoilSystem.cs
new file mode 100644
index 0000000000..e1c7d9c8ee
--- /dev/null
+++ b/Content.Client/GameObjects/EntitySystems/CameraRecoilSystem.cs
@@ -0,0 +1,27 @@
+using Content.Client.GameObjects.Components.Mobs;
+using SS14.Shared.GameObjects;
+using SS14.Shared.GameObjects.Systems;
+
+namespace Content.Client.GameObjects.EntitySystems
+{
+ public sealed class CameraRecoilSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ EntityQuery = new TypeEntityQuery(typeof(CameraRecoilComponent));
+ }
+
+ public override void FrameUpdate(float frameTime)
+ {
+ base.FrameUpdate(frameTime);
+
+ foreach (var entity in RelevantEntities)
+ {
+ var recoil = entity.GetComponent();
+ recoil.FrameUpdate(frameTime);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj
index 79820211c8..2c678c7603 100644
--- a/Content.Server/Content.Server.csproj
+++ b/Content.Server/Content.Server.csproj
@@ -81,6 +81,7 @@
+
@@ -100,6 +101,10 @@
+
+
+
+
diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs
index 8016d35a2f..9524afa2ad 100644
--- a/Content.Server/EntryPoint.cs
+++ b/Content.Server/EntryPoint.cs
@@ -41,6 +41,7 @@ using Content.Server.Interfaces;
using Content.Server.Interfaces.GameTicking;
using Content.Shared.GameObjects.Components.Inventory;
using Content.Shared.GameObjects.Components.Markers;
+using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.Interfaces;
using SS14.Server.Interfaces.ServerStatus;
using SS14.Shared.Timing;
@@ -98,7 +99,7 @@ namespace Content.Server
factory.Register();
factory.Register();
- factory.Register();
+ factory.Register();
factory.Register();
factory.Register();
factory.Register();
@@ -127,6 +128,12 @@ namespace Content.Server
factory.Register();
factory.RegisterReference();
+ factory.Register();
+ factory.Register();
+
+ factory.Register();
+ factory.RegisterReference();
+
IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
diff --git a/Content.Server/GameObjects/Components/Mobs/CameraRecoilComponent.cs b/Content.Server/GameObjects/Components/Mobs/CameraRecoilComponent.cs
new file mode 100644
index 0000000000..356f71cee5
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Mobs/CameraRecoilComponent.cs
@@ -0,0 +1,14 @@
+using Content.Shared.GameObjects.Components.Mobs;
+using SS14.Shared.Maths;
+
+namespace Content.Server.GameObjects.Components.Mobs
+{
+ public sealed class CameraRecoilComponent : SharedCameraRecoilComponent
+ {
+ public override void Kick(Vector2 recoil)
+ {
+ var msg = new RecoilKickMessage(recoil);
+ SendNetworkMessage(msg);
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticBulletComponent.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticBulletComponent.cs
new file mode 100644
index 0000000000..fd686b84d0
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticBulletComponent.cs
@@ -0,0 +1,31 @@
+using SS14.Shared.GameObjects;
+using SS14.Shared.Serialization;
+
+namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
+{
+ public class BallisticBulletComponent : Component
+ {
+ public override string Name => "BallisticBullet";
+
+ private BallisticCaliber _caliber;
+ private string _projectileType;
+ private bool _spent;
+
+ public string ProjectileType => _projectileType;
+ public BallisticCaliber Caliber => _caliber;
+ public bool Spent
+ {
+ get => _spent;
+ set => _spent = value;
+ }
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+
+ serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
+ serializer.DataField(ref _projectileType, "projectile", null);
+ serializer.DataField(ref _spent, "spent", false);
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineComponent.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineComponent.cs
new file mode 100644
index 0000000000..c1c168e3d0
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineComponent.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Collections.Generic;
+using Content.Shared.GameObjects.Components.Weapons.Ranged;
+using SS14.Server.GameObjects;
+using SS14.Server.GameObjects.Components.Container;
+using SS14.Shared.GameObjects;
+using SS14.Shared.Interfaces.GameObjects;
+using SS14.Shared.Serialization;
+
+namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
+{
+ public class BallisticMagazineComponent : Component
+ {
+ public override string Name => "BallisticMagazine";
+
+ // Stack of loaded bullets.
+ private readonly Stack _loadedBullets = new Stack();
+ private string _fillType;
+
+ private Container _bulletContainer;
+ private AppearanceComponent _appearance;
+
+ private BallisticMagazineType _magazineType;
+ private BallisticCaliber _caliber;
+ private int _capacity;
+
+ public BallisticMagazineType MagazineType => _magazineType;
+ public BallisticCaliber Caliber => _caliber;
+ public int Capacity => _capacity;
+
+ public int CountLoaded => _loadedBullets.Count;
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+
+ serializer.DataField(ref _magazineType, "magazine", BallisticMagazineType.Unspecified);
+ serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
+ serializer.DataField(ref _fillType, "fill", null);
+ serializer.DataField(ref _capacity, "capacity", 20);
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _appearance = Owner.GetComponent();
+
+ _bulletContainer =
+ ContainerManagerComponent.Ensure("magazine_bullet_container", Owner, out var existed);
+
+ if (!existed && _fillType != null)
+ {
+ // Load up bullets from fill.
+ for (var i = 0; i < Capacity; i++)
+ {
+ var bullet = Owner.EntityManager.SpawnEntity(_fillType);
+ AddBullet(bullet);
+ }
+ }
+
+ _appearance.SetData(BallisticMagazineVisuals.AmmoCapacity, Capacity);
+ }
+
+ public void AddBullet(IEntity bullet)
+ {
+ if (!bullet.TryGetComponent(out BallisticBulletComponent component))
+ {
+ throw new ArgumentException("entity isn't a bullet.", nameof(bullet));
+ }
+
+ if (component.Caliber != Caliber)
+ {
+ throw new ArgumentException("entity is of the wrong caliber.", nameof(bullet));
+ }
+
+ _bulletContainer.Insert(bullet);
+ _loadedBullets.Push(bullet);
+ _updateAppearance();
+ }
+
+ public IEntity TakeBullet()
+ {
+ if (_loadedBullets.Count == 0)
+ {
+ return null;
+ }
+
+ var bullet = _loadedBullets.Pop();
+ _updateAppearance();
+ return bullet;
+ }
+
+ private void _updateAppearance()
+ {
+ _appearance.SetData(BallisticMagazineVisuals.AmmoLeft, CountLoaded);
+ }
+ }
+
+ public enum BallisticMagazineType
+ {
+ Unspecified = 0,
+ A12mm,
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineWeaponComponent.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineWeaponComponent.cs
new file mode 100644
index 0000000000..2914d2e65e
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineWeaponComponent.cs
@@ -0,0 +1,251 @@
+using System;
+using Content.Server.GameObjects.EntitySystems;
+using Content.Shared.GameObjects;
+using Content.Shared.GameObjects.Components.Weapons.Ranged;
+using Content.Shared.Interfaces;
+using SS14.Server.GameObjects;
+using SS14.Server.GameObjects.Components.Container;
+using SS14.Server.GameObjects.EntitySystems;
+using SS14.Shared.Audio;
+using SS14.Shared.Interfaces.GameObjects;
+using SS14.Shared.IoC;
+using SS14.Shared.Maths;
+using SS14.Shared.Serialization;
+using SS14.Shared.Utility;
+
+namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
+{
+ public class BallisticMagazineWeaponComponent : BallisticWeaponComponent, IUse, IAttackby
+ {
+ public override string Name => "BallisticMagazineWeapon";
+
+ private string _defaultMagazine;
+
+ private ContainerSlot _magazineSlot;
+ private BallisticMagazineType _magazineType;
+
+ public BallisticMagazineType MagazineType => _magazineType;
+ private IEntity Magazine => _magazineSlot.ContainedEntity;
+
+ private Random _bulletDropRandom;
+ private string _magInSound;
+ private string _magOutSound;
+ private string _autoEjectSound;
+ private bool _autoEjectMagazine;
+ private AppearanceComponent _appearance;
+
+ private static readonly Direction[] _randomBulletDirs = {
+ Direction.North,
+ Direction.East,
+ Direction.South,
+ Direction.West
+ };
+
+ protected override int ChamberCount => 1;
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+
+ serializer.DataField(ref _magazineType, "magazine", BallisticMagazineType.Unspecified);
+ serializer.DataField(ref _defaultMagazine, "default_magazine", null);
+ serializer.DataField(ref _autoEjectMagazine, "auto_eject_magazine", false);
+ serializer.DataField(ref _autoEjectSound, "sound_auto_eject", null);
+ serializer.DataField(ref _magInSound, "sound_magazine_in", null);
+ serializer.DataField(ref _magOutSound, "sound_magazine_out", null);
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _appearance = Owner.GetComponent();
+
+ _magazineSlot =
+ ContainerManagerComponent.Ensure("ballistic_gun_magazine", Owner,
+ out var alreadyExisted);
+
+ _bulletDropRandom = new Random(Owner.Uid.GetHashCode() ^ DateTime.Now.GetHashCode());
+
+ if (!alreadyExisted && _defaultMagazine != null)
+ {
+ var magazine = Owner.EntityManager.SpawnEntity(_defaultMagazine);
+ InsertMagazine(magazine, false);
+ }
+ _updateAppearance();
+ }
+
+ public bool InsertMagazine(IEntity magazine, bool playSound=true)
+ {
+ if (!magazine.TryGetComponent(out BallisticMagazineComponent component))
+ {
+ throw new ArgumentException("Not a magazine", nameof(magazine));
+ }
+
+ if (component.MagazineType != MagazineType)
+ {
+ throw new ArgumentException("Wrong magazine type", nameof(magazine));
+ }
+
+ if (component.Caliber != Caliber)
+ {
+ throw new ArgumentException("Wrong caliber", nameof(magazine));
+ }
+
+ if (!_magazineSlot.Insert(magazine))
+ {
+ return false;
+ }
+
+ if (_magInSound != null)
+ {
+ var audioSystem = IoCManager.Resolve().GetEntitySystem();
+ audioSystem.Play(_magInSound, Owner);
+ }
+
+ if (GetChambered(0) == null)
+ {
+ // No bullet in chamber, load one from magazine.
+ var bullet = component.TakeBullet();
+ if (bullet != null)
+ {
+ LoadIntoChamber(0, bullet);
+ }
+ }
+
+ _updateAppearance();
+ return true;
+ }
+
+ public bool EjectMagazine(bool playSound=true)
+ {
+ var entity = Magazine;
+ if (entity == null)
+ {
+ return false;
+ }
+
+ if (_magazineSlot.Remove(entity))
+ {
+ entity.Transform.GridPosition = Owner.Transform.GridPosition;
+ if (_magOutSound != null)
+ {
+ var audioSystem = IoCManager.Resolve().GetEntitySystem();
+ audioSystem.Play(_magOutSound, Owner);
+ }
+ _updateAppearance();
+ return true;
+ }
+
+ _updateAppearance();
+ return false;
+ }
+
+ protected override void CycleChamberedBullet(int chamber)
+ {
+ DebugTools.Assert(chamber == 0);
+
+ // Eject chambered bullet.
+ var entity = RemoveFromChamber(chamber);
+ entity.Transform.GridPosition = Owner.Transform.GridPosition;
+ entity.Transform.LocalRotation = _bulletDropRandom.Pick(_randomBulletDirs).ToAngle();
+ var audioSystem = IoCManager.Resolve().GetEntitySystem();
+ var effect = $"/Audio/items/weapons/casingfall{_bulletDropRandom.Next(1, 4)}.ogg";
+ audioSystem.Play(effect, entity, AudioParams.Default.WithVolume(-3));
+
+ if (Magazine != null)
+ {
+ var magComponent = Magazine.GetComponent();
+ var bullet = magComponent.TakeBullet();
+ if (bullet != null)
+ {
+ LoadIntoChamber(0, bullet);
+ }
+
+ if (magComponent.CountLoaded == 0 && _autoEjectMagazine)
+ {
+ EjectMagazine();
+ if (_autoEjectSound != null)
+ {
+ audioSystem.Play(_autoEjectSound, Owner, AudioParams.Default.WithVolume(-5));
+ }
+ }
+ }
+
+ _updateAppearance();
+ }
+
+ public bool UseEntity(IEntity user)
+ {
+ var ret = EjectMagazine();
+ if (ret)
+ {
+ Owner.PopupMessage(user, "Magazine ejected");
+ }
+ else
+ {
+ Owner.PopupMessage(user, "No magazine");
+ }
+
+ return true;
+ }
+
+ public bool Attackby(IEntity user, IEntity attackwith)
+ {
+ if (!attackwith.TryGetComponent(out BallisticMagazineComponent component))
+ {
+ return false;
+ }
+
+ if (Magazine != null)
+ {
+ Owner.PopupMessage(user, "Already got a magazine.");
+ return false;
+ }
+
+ if (component.MagazineType != MagazineType || component.Caliber != Caliber)
+ {
+ Owner.PopupMessage(user, "Magazine doesn't fit.");
+ return false;
+ }
+
+ return InsertMagazine(attackwith);
+ }
+
+ private void _updateAppearance()
+ {
+ if (Magazine != null)
+ {
+ var comp = Magazine.GetComponent();
+ _appearance.SetData(BallisticMagazineWeaponVisuals.AmmoLeft, comp.CountLoaded);
+ _appearance.SetData(BallisticMagazineWeaponVisuals.AmmoCapacity, comp.Capacity);
+ _appearance.SetData(BallisticMagazineWeaponVisuals.MagazineLoaded, true);
+ }
+ else
+ {
+ _appearance.SetData(BallisticMagazineWeaponVisuals.AmmoLeft, 0);
+ _appearance.SetData(BallisticMagazineWeaponVisuals.AmmoLeft, 0);
+ _appearance.SetData(BallisticMagazineWeaponVisuals.MagazineLoaded, false);
+ }
+ }
+
+ [Verb]
+ public sealed class EjectMagazineVerb : Verb
+ {
+ protected override string GetText(IEntity user, BallisticMagazineWeaponComponent component)
+ {
+ return component.Magazine == null ? "Eject magazine (magazine missing)" : "Eject magazine";
+ }
+
+ protected override bool IsDisabled(IEntity user, BallisticMagazineWeaponComponent component)
+ {
+ return component.Magazine == null;
+ }
+
+ protected override void Activate(IEntity user, BallisticMagazineWeaponComponent component)
+ {
+ component.EjectMagazine();
+ }
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticWeaponComponent.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticWeaponComponent.cs
new file mode 100644
index 0000000000..cb23dd4e0e
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticWeaponComponent.cs
@@ -0,0 +1,126 @@
+using System;
+using Content.Server.GameObjects.Components.Interactable;
+using Content.Shared.GameObjects;
+using SS14.Server.Chat;
+using SS14.Server.GameObjects.Components.Container;
+using SS14.Server.GameObjects.EntitySystems;
+using SS14.Shared.Interfaces.GameObjects;
+using SS14.Shared.IoC;
+using SS14.Shared.Serialization;
+
+namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
+{
+ public abstract class BallisticWeaponComponent : ProjectileWeaponComponent
+ {
+ private BallisticCaliber _caliber;
+ private Chamber[] _chambers;
+
+ public BallisticCaliber Caliber => _caliber;
+ protected abstract int ChamberCount { get; }
+
+ private string _soundGunEmpty;
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+
+ serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
+ serializer.DataField(ref _soundGunEmpty, "sound_empty", null);
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _chambers = new Chamber[ChamberCount];
+ for (var i = 0; i < _chambers.Length; i++)
+ {
+ var container = ContainerManagerComponent.Ensure($"ballistics_chamber_{i}", Owner);
+ _chambers[i] = new Chamber(container);
+ }
+ }
+
+ public IEntity GetChambered(int chamber) => _chambers[chamber].Slot.ContainedEntity;
+
+ public bool LoadIntoChamber(int chamber, IEntity bullet)
+ {
+ if (!bullet.TryGetComponent(out BallisticBulletComponent component))
+ {
+ throw new ArgumentException("entity isn't a bullet.", nameof(bullet));
+ }
+
+ if (component.Caliber != Caliber)
+ {
+ throw new ArgumentException("entity is of the wrong caliber.", nameof(bullet));
+ }
+
+ if (GetChambered(chamber) != null)
+ {
+ return false;
+ }
+
+ _chambers[chamber].Slot.Insert(bullet);
+ return true;
+ }
+
+ protected sealed override IEntity GetFiredProjectile()
+ {
+ void PlayEmpty()
+ {
+ if (_soundGunEmpty != null)
+ {
+ var audioSystem = IoCManager.Resolve().GetEntitySystem();
+ audioSystem.Play(_soundGunEmpty, Owner);
+ }
+ }
+ var chambered = GetChambered(0);
+ if (chambered != null)
+ {
+ var bullet = chambered.GetComponent();
+ if (bullet.Spent)
+ {
+ PlayEmpty();
+ return null;
+ }
+
+ var projectile = Owner.EntityManager.SpawnEntity(bullet.ProjectileType);
+ bullet.Spent = true;
+
+ CycleChamberedBullet(0);
+
+ // Load a new bullet into the chamber from magazine.
+ return projectile;
+ }
+
+ PlayEmpty();
+ return null;
+ }
+
+ protected virtual void CycleChamberedBullet(int chamber)
+ {
+
+ }
+
+ public IEntity RemoveFromChamber(int chamber)
+ {
+ var c = _chambers[chamber];
+ var loaded = c.Slot.ContainedEntity;
+ if (loaded != null)
+ {
+ c.Slot.Remove(loaded);
+ }
+ return loaded;
+ }
+
+ private sealed class Chamber
+ {
+ public Chamber(ContainerSlot slot)
+ {
+ Slot = slot;
+ }
+
+ public ContainerSlot Slot { get; }
+ }
+
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/ProjectileWeapon.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/ProjectileWeapon.cs
index b08b479da5..9fa5d127f4 100644
--- a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/ProjectileWeapon.cs
+++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/ProjectileWeapon.cs
@@ -1,4 +1,5 @@
using System;
+using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Projectiles;
using SS14.Server.GameObjects;
using SS14.Server.GameObjects.EntitySystems;
@@ -15,12 +16,8 @@ using SS14.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
{
- public class ProjectileWeaponComponent : Component
+ public abstract class ProjectileWeaponComponent : Component
{
- public override string Name => "ProjectileWeapon";
-
- private string _ProjectilePrototype = "ProjectileBullet";
-
private float _velocity = 20f;
private float _spreadStdDev = 3;
private bool _spread = true;
@@ -61,16 +58,27 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
private void Fire(IEntity user, GridCoordinates clickLocation)
{
+ var projectile = GetFiredProjectile();
+ if (projectile == null)
+ {
+ return;
+ }
+
var userPosition = user.Transform.GridPosition; //Remember world positions are ephemeral and can only be used instantaneously
var angle = new Angle(clickLocation.Position - userPosition.Position);
+ if (user.TryGetComponent(out CameraRecoilComponent recoil))
+ {
+ var recoilVec = angle.ToVec() * -0.15f;
+ recoil.Kick(recoilVec);
+ }
+
if (Spread)
{
angle += Angle.FromDegrees(_spreadRandom.NextGaussian(0, SpreadStdDev));
}
- //Spawn the projectilePrototype
- var projectile = IoCManager.Resolve().ForceSpawnEntityAt(_ProjectilePrototype, userPosition);
+ projectile.Transform.GridPosition = userPosition;
//Give it the velocity we fire from this weapon, and make sure it doesn't shoot our character
projectile.GetComponent().IgnoreEntity(user);
@@ -84,5 +92,16 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
// Sound!
IoCManager.Resolve().GetEntitySystem().Play("/Audio/gunshot_c20.ogg", user);
}
+
+ ///
+ /// Try to get a projectile for firing. If null, nothing will be fired.
+ ///
+ protected abstract IEntity GetFiredProjectile();
+ }
+
+ public enum BallisticCaliber
+ {
+ Unspecified = 0,
+ A12mm,
}
}
diff --git a/Content.Shared/Content.Shared.csproj b/Content.Shared/Content.Shared.csproj
index 2ef69f42e4..dc4fbedaf8 100644
--- a/Content.Shared/Content.Shared.csproj
+++ b/Content.Shared/Content.Shared.csproj
@@ -69,9 +69,12 @@
+
+
+
diff --git a/Content.Shared/GameObjects/Components/Mobs/SharedCameraRecoilComponent.cs b/Content.Shared/GameObjects/Components/Mobs/SharedCameraRecoilComponent.cs
new file mode 100644
index 0000000000..761b1ac692
--- /dev/null
+++ b/Content.Shared/GameObjects/Components/Mobs/SharedCameraRecoilComponent.cs
@@ -0,0 +1,28 @@
+using System;
+using SS14.Shared.GameObjects;
+using SS14.Shared.Maths;
+using SS14.Shared.Serialization;
+
+namespace Content.Shared.GameObjects.Components.Mobs
+{
+ public abstract class SharedCameraRecoilComponent : Component
+ {
+ public sealed override string Name => "CameraRecoil";
+
+ public override uint? NetID => ContentNetIDs.CAMERA_RECOIL;
+
+ public abstract void Kick(Vector2 recoil);
+
+ [Serializable, NetSerializable]
+ protected class RecoilKickMessage : ComponentMessage
+ {
+ public readonly Vector2 Recoil;
+
+ public RecoilKickMessage(Vector2 recoil)
+ {
+ Directed = true;
+ Recoil = recoil;
+ }
+ }
+ }
+}
diff --git a/Content.Shared/GameObjects/Components/Weapons/Ranged/SharedBallisticMagazineComponent.cs b/Content.Shared/GameObjects/Components/Weapons/Ranged/SharedBallisticMagazineComponent.cs
new file mode 100644
index 0000000000..b51136fcb1
--- /dev/null
+++ b/Content.Shared/GameObjects/Components/Weapons/Ranged/SharedBallisticMagazineComponent.cs
@@ -0,0 +1,12 @@
+using System;
+using SS14.Shared.Serialization;
+
+namespace Content.Shared.GameObjects.Components.Weapons.Ranged
+{
+ [Serializable, NetSerializable]
+ public enum BallisticMagazineVisuals
+ {
+ AmmoCapacity,
+ AmmoLeft,
+ }
+}
diff --git a/Content.Shared/GameObjects/Components/Weapons/Ranged/SharedBallisticMagazineWeaponComponent.cs b/Content.Shared/GameObjects/Components/Weapons/Ranged/SharedBallisticMagazineWeaponComponent.cs
new file mode 100644
index 0000000000..b4500a7f59
--- /dev/null
+++ b/Content.Shared/GameObjects/Components/Weapons/Ranged/SharedBallisticMagazineWeaponComponent.cs
@@ -0,0 +1,13 @@
+using System;
+using SS14.Shared.Serialization;
+
+namespace Content.Shared.GameObjects.Components.Weapons.Ranged
+{
+ [Serializable, NetSerializable]
+ public enum BallisticMagazineWeaponVisuals
+ {
+ MagazineLoaded,
+ AmmoCapacity,
+ AmmoLeft,
+ }
+}
diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs
index b84b23dbd4..14871ceb3f 100644
--- a/Content.Shared/GameObjects/ContentNetIDs.cs
+++ b/Content.Shared/GameObjects/ContentNetIDs.cs
@@ -11,7 +11,8 @@
public const uint INVENTORY = 1006;
public const uint POWER_DEBUG_TOOL = 1007;
public const uint CONSTRUCTOR = 1008;
- public const uint RANGED_WEAPON = 1010;
public const uint SPECIES = 1009;
+ public const uint RANGED_WEAPON = 1010;
+ public const uint CAMERA_RECOIL = 1011;
}
}
diff --git a/Resources/Audio/items/weapons/casingfall1.ogg b/Resources/Audio/items/weapons/casingfall1.ogg
new file mode 100644
index 0000000000..72a04c0fca
Binary files /dev/null and b/Resources/Audio/items/weapons/casingfall1.ogg differ
diff --git a/Resources/Audio/items/weapons/casingfall2.ogg b/Resources/Audio/items/weapons/casingfall2.ogg
new file mode 100644
index 0000000000..2317ff5d75
Binary files /dev/null and b/Resources/Audio/items/weapons/casingfall2.ogg differ
diff --git a/Resources/Audio/items/weapons/casingfall3.ogg b/Resources/Audio/items/weapons/casingfall3.ogg
new file mode 100644
index 0000000000..39639055c2
Binary files /dev/null and b/Resources/Audio/items/weapons/casingfall3.ogg differ
diff --git a/Resources/Audio/items/weapons/gun_empty.ogg b/Resources/Audio/items/weapons/gun_empty.ogg
new file mode 100644
index 0000000000..817d72e0ed
Binary files /dev/null and b/Resources/Audio/items/weapons/gun_empty.ogg differ
diff --git a/Resources/Audio/items/weapons/smg_empty_alarm.ogg b/Resources/Audio/items/weapons/smg_empty_alarm.ogg
new file mode 100644
index 0000000000..f031b4e1d8
Binary files /dev/null and b/Resources/Audio/items/weapons/smg_empty_alarm.ogg differ
diff --git a/Resources/Audio/items/weapons/smg_magin.ogg b/Resources/Audio/items/weapons/smg_magin.ogg
new file mode 100644
index 0000000000..2bdb3c99e7
Binary files /dev/null and b/Resources/Audio/items/weapons/smg_magin.ogg differ
diff --git a/Resources/Audio/items/weapons/smg_magout.ogg b/Resources/Audio/items/weapons/smg_magout.ogg
new file mode 100644
index 0000000000..d85ff5a40c
Binary files /dev/null and b/Resources/Audio/items/weapons/smg_magout.ogg differ
diff --git a/Resources/Audio/items/weapons/sources.txt b/Resources/Audio/items/weapons/sources.txt
new file mode 100644
index 0000000000..5e7f1a1173
--- /dev/null
+++ b/Resources/Audio/items/weapons/sources.txt
@@ -0,0 +1,7 @@
+gun_empty.ogg: https://github.com/discordia-space/CEV-Eris/blob/fbde37a8647a82587d363da999a94cf02c2e128c/sound/weapons/guns/misc/gun_empty.ogg
+smg_magin.ogg: https://github.com/discordia-space/CEV-Eris/blob/fbde37a8647a82587d363da999a94cf02c2e128c/sound/weapons/guns/interact/smg_magin.ogg
+smg_magout.ogg: https://github.com/discordia-space/CEV-Eris/blob/fbde37a8647a82587d363da999a94cf02c2e128c/sound/weapons/guns/interact/smg_magout.ogg
+smg_empty_alarm.ogg: https://github.com/discordia-space/CEV-Eris/blob/fbde37a8647a82587d363da999a94cf02c2e128c/sound/weapons/smg_empty_alarm.ogg
+casingfall1.ogg: https://github.com/discordia-space/CEV-Eris/blob/fbde37a8647a82587d363da999a94cf02c2e128c/sound/weapons/guns/misc/casingfall1.ogg
+casingfall2.ogg: https://github.com/discordia-space/CEV-Eris/blob/fbde37a8647a82587d363da999a94cf02c2e128c/sound/weapons/guns/misc/casingfall2.ogg
+casingfall3.ogg: https://github.com/discordia-space/CEV-Eris/blob/fbde37a8647a82587d363da999a94cf02c2e128c/sound/weapons/guns/misc/casingfall3.ogg
diff --git a/Resources/Maps/stationstation.yml b/Resources/Maps/stationstation.yml
index 6a3c92cb67..b0924ee8e8 100644
--- a/Resources/Maps/stationstation.yml
+++ b/Resources/Maps/stationstation.yml
@@ -286,13 +286,13 @@ entities:
pos: -2.015625,-3.859375
rot: -1.570796 rad
type: Transform
-- type: GUNITEM
+- type: smg_c20r
components:
- grid: 0
pos: -2.890625,-4.484375
rot: -1.570796 rad
type: Transform
-- type: GUNITEM
+- type: smg_c20r
components:
- grid: 0
pos: -1.984375,-4.484375
diff --git a/Resources/Prototypes/Entities/Mobs.yml b/Resources/Prototypes/Entities/Mobs.yml
index 76641d5435..060a008ba3 100644
--- a/Resources/Prototypes/Entities/Mobs.yml
+++ b/Resources/Prototypes/Entities/Mobs.yml
@@ -55,6 +55,8 @@
- type: Eye
zoom: 0.5, 0.5
+ - type: CameraRecoil
+
- type: entity
id: MobObserver
name: Observer
diff --git a/Resources/Prototypes/Entities/Projectiles.yml b/Resources/Prototypes/Entities/Projectiles.yml
index db49558a8c..e69de29bb2 100644
--- a/Resources/Prototypes/Entities/Projectiles.yml
+++ b/Resources/Prototypes/Entities/Projectiles.yml
@@ -1,19 +0,0 @@
-- type: entity
- id: ProjectileBullet
- name: ProjectileBullet
- description: If you can see this you're dead!
- components:
- - type: Sprite
- directional: false
- texture: Objects/projectilebullet.png
- #rotation: -180
-
- - type: Icon
- texture: Objects/projectilebullet.png
- - type: BoundingBox
- aabb: -0.2,-0.2,0.2,0.2
- - type: Physics
- edgeslide: false
- - type: Projectile
- - type: Collidable
- hard: false
diff --git a/Resources/Prototypes/Entities/Weapons.yml b/Resources/Prototypes/Entities/Weapons.yml
index 6af9612483..fe9adcf64d 100644
--- a/Resources/Prototypes/Entities/Weapons.yml
+++ b/Resources/Prototypes/Entities/Weapons.yml
@@ -19,28 +19,6 @@
sprite: Objects/laser_retro.rsi
prefix: 100
-
-- type: entity
- name: C-20r Sub Machine Gun
- parent: BaseItem
- id: GUNITEM
- description: A rooty tooty point and shooty
- components:
- - type: Sprite
- sprite: Objects/c20r.rsi
- state: c20r-20
- - type: Icon
- sprite: Objects/c20r.rsi
- state: c20r-20
- - type: RangedWeapon
- automatic: true
- firerate: 8
- - type: ProjectileWeapon
- - type: Item
- Size: 24
- sprite: Objects/c20r.rsi
-
-
- type: entity
name: Spear
parent: BaseItem
diff --git a/Resources/Prototypes/Entities/weapons/ammunition.yml b/Resources/Prototypes/Entities/weapons/ammunition.yml
new file mode 100644
index 0000000000..7e2a273fc2
--- /dev/null
+++ b/Resources/Prototypes/Entities/weapons/ammunition.yml
@@ -0,0 +1,50 @@
+- type: entity
+ id: magazine_12mm
+ name: "12mm magazine"
+ parent: BaseItem
+ components:
+ - type: BallisticMagazine
+ caliber: A12mm
+ magazine: A12mm
+ capacity: 20
+ - type: Icon
+ sprite: Objects/items/magazine_12mm.rsi
+ state: 12mm-0
+ - type: Sprite
+ netsync: false
+ sprite: Objects/items/magazine_12mm.rsi
+ state: 12mm-0
+ - type: Appearance
+ visuals:
+ - type: BallisticMagazineVisualizer2D
+ base_state: 12mm
+ steps: 11
+
+- type: entity
+ id: magazine_12mm_filled
+ name: "12mm magazine"
+ parent: magazine_12mm
+ components:
+ - type: BallisticMagazine
+ fill: ammo_casing_12mm
+ - type: Icon
+ state: 12mm-10
+ - type: Sprite
+ state: 12mm-10
+
+- type: entity
+ id: ammo_casing_12mm
+ name: "12mm bullet"
+ parent: BaseItem
+ components:
+ - type: BallisticBullet
+ caliber: A12mm
+ projectile: ProjectileBullet
+ - type: Sprite
+ sprite: Objects/items/ammo_casing.rsi
+ state: s-casing
+ drawdepth: FloorObjects
+ - type: Icon
+ sprite: Objects/items/ammo_casing.rsi
+ state: s-casing
+
diff --git a/Resources/Prototypes/Entities/weapons/guns.yml b/Resources/Prototypes/Entities/weapons/guns.yml
new file mode 100644
index 0000000000..fc9f855527
--- /dev/null
+++ b/Resources/Prototypes/Entities/weapons/guns.yml
@@ -0,0 +1,35 @@
+- type: entity
+ name: C-20r Sub Machine Gun
+ parent: BaseItem
+ id: smg_c20r
+ description: A rooty tooty point and shooty.
+ components:
+ - type: Sprite
+ netsync: false
+ sprite: Objects/c20r.rsi
+ state: c20r-5
+ - type: Icon
+ sprite: Objects/c20r.rsi
+ state: c20r-5
+ - type: RangedWeapon
+ automatic: true
+ firerate: 8
+ - type: BallisticMagazineWeapon
+ caliber: A12mm
+ magazine: A12mm
+ default_magazine: magazine_12mm_filled
+ auto_eject_magazine: true
+ sound_auto_eject: /Audio/items/weapons/smg_empty_alarm.ogg
+ sound_magazine_in: /Audio/items/weapons/smg_magin.ogg
+ sound_magazine_out: /Audio/items/weapons/smg_magout.ogg
+ sound_empty: /Audio/items/weapons/gun_empty.ogg
+
+ - type: Appearance
+ visuals:
+ - type: BallisticMagazineWeaponVisualizer2D
+ base_state: c20r
+ steps: 6
+
+ - type: Item
+ Size: 24
+ sprite: Objects/c20r.rsi
diff --git a/Resources/Prototypes/Entities/weapons/projectiles.yml b/Resources/Prototypes/Entities/weapons/projectiles.yml
new file mode 100644
index 0000000000..db49558a8c
--- /dev/null
+++ b/Resources/Prototypes/Entities/weapons/projectiles.yml
@@ -0,0 +1,19 @@
+- type: entity
+ id: ProjectileBullet
+ name: ProjectileBullet
+ description: If you can see this you're dead!
+ components:
+ - type: Sprite
+ directional: false
+ texture: Objects/projectilebullet.png
+ #rotation: -180
+
+ - type: Icon
+ texture: Objects/projectilebullet.png
+ - type: BoundingBox
+ aabb: -0.2,-0.2,0.2,0.2
+ - type: Physics
+ edgeslide: false
+ - type: Projectile
+ - type: Collidable
+ hard: false
diff --git a/Resources/Textures/Objects/c20r.rsi/c20r-1.png b/Resources/Textures/Objects/c20r.rsi/c20r-1.png
new file mode 100644
index 0000000000..29a2fde1f2
Binary files /dev/null and b/Resources/Textures/Objects/c20r.rsi/c20r-1.png differ
diff --git a/Resources/Textures/Objects/c20r.rsi/c20r-16.png b/Resources/Textures/Objects/c20r.rsi/c20r-16.png
deleted file mode 100644
index a3ecf32636..0000000000
Binary files a/Resources/Textures/Objects/c20r.rsi/c20r-16.png and /dev/null differ
diff --git a/Resources/Textures/Objects/c20r.rsi/c20r-8.png b/Resources/Textures/Objects/c20r.rsi/c20r-2.png
similarity index 100%
rename from Resources/Textures/Objects/c20r.rsi/c20r-8.png
rename to Resources/Textures/Objects/c20r.rsi/c20r-2.png
diff --git a/Resources/Textures/Objects/c20r.rsi/c20r-12.png b/Resources/Textures/Objects/c20r.rsi/c20r-3.png
similarity index 100%
rename from Resources/Textures/Objects/c20r.rsi/c20r-12.png
rename to Resources/Textures/Objects/c20r.rsi/c20r-3.png
diff --git a/Resources/Textures/Objects/c20r.rsi/c20r-4.png b/Resources/Textures/Objects/c20r.rsi/c20r-4.png
index 29a2fde1f2..a3ecf32636 100644
Binary files a/Resources/Textures/Objects/c20r.rsi/c20r-4.png and b/Resources/Textures/Objects/c20r.rsi/c20r-4.png differ
diff --git a/Resources/Textures/Objects/c20r.rsi/c20r-20.png b/Resources/Textures/Objects/c20r.rsi/c20r-5.png
similarity index 100%
rename from Resources/Textures/Objects/c20r.rsi/c20r-20.png
rename to Resources/Textures/Objects/c20r.rsi/c20r-5.png
diff --git a/Resources/Textures/Objects/c20r.rsi/meta.json b/Resources/Textures/Objects/c20r.rsi/meta.json
index 02ca7aea30..b243c2673d 100644
--- a/Resources/Textures/Objects/c20r.rsi/meta.json
+++ b/Resources/Textures/Objects/c20r.rsi/meta.json
@@ -1 +1 @@
-{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "Taken from https://github.com/vgstation-coders/vgstation13 at commit 125c975f1b3bf9826b37029e9ab5a5f89e975a7e", "states": [{"name": "c20r", "directions": 1, "delays": [[1.0]]}, {"name": "c20r-0", "directions": 1, "delays": [[1.0]]}, {"name": "c20r-12", "directions": 1, "delays": [[1.0]]}, {"name": "c20r-16", "directions": 1, "delays": [[1.0]]}, {"name": "c20r-20", "directions": 1, "delays": [[1.0]]}, {"name": "c20r-4", "directions": 1, "delays": [[1.0]]}, {"name": "c20r-8", "directions": 1, "delays": [[1.0]]}, {"name": "inhand-left", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "inhand-right", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}]}
\ No newline at end of file
+{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "Taken from https://github.com/vgstation-coders/vgstation13 at commit 125c975f1b3bf9826b37029e9ab5a5f89e975a7e", "states": [{"name": "c20r", "directions": 1, "delays": [[1.0]]}, {"name": "c20r-0", "directions": 1, "delays": [[1.0]]}, {"name": "c20r-3", "directions": 1, "delays": [[1.0]]}, {"name": "c20r-4", "directions": 1, "delays": [[1.0]]}, {"name": "c20r-5", "directions": 1, "delays": [[1.0]]}, {"name": "c20r-1", "directions": 1, "delays": [[1.0]]}, {"name": "c20r-2", "directions": 1, "delays": [[1.0]]}, {"name": "inhand-left", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "inhand-right", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}]}
diff --git a/Resources/Textures/Objects/items/ammo_casing.rsi/meta.json b/Resources/Textures/Objects/items/ammo_casing.rsi/meta.json
new file mode 100644
index 0000000000..1402537a73
--- /dev/null
+++ b/Resources/Textures/Objects/items/ammo_casing.rsi/meta.json
@@ -0,0 +1 @@
+{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/blob/0b3ab17dbad632ddf738b63900ef8df1926bba47/icons/obj/ammo.dmi", "states": [{"name": "s-casing", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}]}
\ No newline at end of file
diff --git a/Resources/Textures/Objects/items/ammo_casing.rsi/s-casing.png b/Resources/Textures/Objects/items/ammo_casing.rsi/s-casing.png
new file mode 100644
index 0000000000..1b01712316
Binary files /dev/null and b/Resources/Textures/Objects/items/ammo_casing.rsi/s-casing.png differ
diff --git a/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-0.png b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-0.png
new file mode 100644
index 0000000000..0f693f78ec
Binary files /dev/null and b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-0.png differ
diff --git a/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-1.png b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-1.png
new file mode 100644
index 0000000000..f3bfbadc0d
Binary files /dev/null and b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-1.png differ
diff --git a/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-10.png b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-10.png
new file mode 100644
index 0000000000..d73be4d480
Binary files /dev/null and b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-10.png differ
diff --git a/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-2.png b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-2.png
new file mode 100644
index 0000000000..41783f5bdd
Binary files /dev/null and b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-2.png differ
diff --git a/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-3.png b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-3.png
new file mode 100644
index 0000000000..25d4402931
Binary files /dev/null and b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-3.png differ
diff --git a/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-4.png b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-4.png
new file mode 100644
index 0000000000..bbc7568ced
Binary files /dev/null and b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-4.png differ
diff --git a/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-5.png b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-5.png
new file mode 100644
index 0000000000..c8ea365d96
Binary files /dev/null and b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-5.png differ
diff --git a/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-6.png b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-6.png
new file mode 100644
index 0000000000..20e563a505
Binary files /dev/null and b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-6.png differ
diff --git a/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-7.png b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-7.png
new file mode 100644
index 0000000000..3b737239d5
Binary files /dev/null and b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-7.png differ
diff --git a/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-8.png b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-8.png
new file mode 100644
index 0000000000..cae73c2b31
Binary files /dev/null and b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-8.png differ
diff --git a/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-9.png b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-9.png
new file mode 100644
index 0000000000..13e8fbd0d2
Binary files /dev/null and b/Resources/Textures/Objects/items/magazine_12mm.rsi/12mm-9.png differ
diff --git a/Resources/Textures/Objects/items/magazine_12mm.rsi/meta.json b/Resources/Textures/Objects/items/magazine_12mm.rsi/meta.json
new file mode 100644
index 0000000000..a0ba166ed1
--- /dev/null
+++ b/Resources/Textures/Objects/items/magazine_12mm.rsi/meta.json
@@ -0,0 +1 @@
+{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/blob/0b3ab17dbad632ddf738b63900ef8df1926bba47/icons/obj/ammo.dmi", "states": [{"name": "12mm-0", "directions": 1, "delays": [[1.0]]}, {"name": "12mm-1", "directions": 1, "delays": [[1.0]]}, {"name": "12mm-10", "directions": 1, "delays": [[1.0]]}, {"name": "12mm-2", "directions": 1, "delays": [[1.0]]}, {"name": "12mm-3", "directions": 1, "delays": [[1.0]]}, {"name": "12mm-4", "directions": 1, "delays": [[1.0]]}, {"name": "12mm-5", "directions": 1, "delays": [[1.0]]}, {"name": "12mm-6", "directions": 1, "delays": [[1.0]]}, {"name": "12mm-7", "directions": 1, "delays": [[1.0]]}, {"name": "12mm-8", "directions": 1, "delays": [[1.0]]}, {"name": "12mm-9", "directions": 1, "delays": [[1.0]]}]}
\ No newline at end of file
diff --git a/engine b/engine
index 9e0c33c9bf..4767b23fb8 160000
--- a/engine
+++ b/engine
@@ -1 +1 @@
-Subproject commit 9e0c33c9bf12104cd794708c23ebd872052b0557
+Subproject commit 4767b23fb8688b0aada5c41c7a473b546ce32931