From 196950fb7efdc4e1a9f32f9ad90c8a14081eb762 Mon Sep 17 00:00:00 2001
From: py01 <60152240+collinlunn@users.noreply.github.com>
Date: Sat, 29 Feb 2020 11:09:41 -0600
Subject: [PATCH] Gun Code Modularization + Magazine Fed Shotgun (#751)
* Adds shotgun YAML files
* Adds shotgu ammo and magazine enum value, modularizes some projectile weapon methods
* Fixes guns consuming two bullets on fire
* Finishes shotgun behavior in Projectile Weapon Components
* Gun and ammo modularization
* Updates BallisticMagazineWeapons to be compatible with new AmmoWeapon
* Fixes shotgun spread angles, Fixes shogun YAML
* Gun documentation and ViewVariable specifications
* Removes todo messsage in gun code
* Default gun evenspread fix
* AmmoComponent Name fix
* Fixes Component name
* Projectile refactor code review fixes
---
.../Projectile/BallisticBulletComponent.cs | 98 ++++++++++--
.../Projectile/BallisticMagazineComponent.cs | 11 +-
.../BallisticMagazineWeaponComponent.cs | 70 +++-----
.../Ranged/Projectile/BallisticWeapon.cs | 149 ++++++++++++++++++
.../Projectile/BallisticWeaponComponent.cs | 121 --------------
.../BaseProjectileWeaponComponent.cs | 95 +++++++++++
.../Ranged/Projectile/ProjectileWeapon.cs | 130 ---------------
.../Weapons/Ammunition/12g/ammunition.yml | 54 +++++++
.../Entities/Weapons/Ammunition/12g/boxes.yml | 34 ++++
.../Weapons/Ammunition/12g/projectiles.yml | 10 ++
.../Entities/Weapons/Shotguns/shotguns.yml | 47 ++++++
11 files changed, 506 insertions(+), 313 deletions(-)
create mode 100644 Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticWeapon.cs
delete mode 100644 Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticWeaponComponent.cs
create mode 100644 Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BaseProjectileWeaponComponent.cs
delete mode 100644 Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/ProjectileWeapon.cs
create mode 100644 Resources/Prototypes/Entities/Weapons/Ammunition/12g/ammunition.yml
create mode 100644 Resources/Prototypes/Entities/Weapons/Ammunition/12g/boxes.yml
create mode 100644 Resources/Prototypes/Entities/Weapons/Ammunition/12g/projectiles.yml
create mode 100644 Resources/Prototypes/Entities/Weapons/Shotguns/shotguns.yml
diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticBulletComponent.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticBulletComponent.cs
index 93176615a1..5060a28b9d 100644
--- a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticBulletComponent.cs
+++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticBulletComponent.cs
@@ -1,32 +1,106 @@
-using Robust.Shared.GameObjects;
+using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
+using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
{
+ ///
+ /// Passes information about the projectiles to be fired by AmmoWeapons
+ ///
[RegisterComponent]
public class BallisticBulletComponent : Component
{
public override string Name => "BallisticBullet";
private BallisticCaliber _caliber;
- private string _projectileType;
- private bool _spent;
+ ///
+ /// Cartridge calibre, restricts what AmmoWeapons this ammo can be fired from.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public BallisticCaliber Caliber { get => _caliber; set => _caliber = value; }
- public string ProjectileType => _projectileType;
- public BallisticCaliber Caliber => _caliber;
- public bool Spent
- {
- get => _spent;
- set => _spent = value;
- }
+ private string _projectileID;
+ ///
+ /// YAML ID of the projectiles to be created when firing this ammo.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public string ProjectileID { get => _projectileID; set => _projectileID = value; }
+
+ private int _projectilesFired;
+ ///
+ /// How many copies of the projectile are shot.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int ProjectilesFired { get => _projectilesFired; set => _projectilesFired = value; }
+
+ private float _spreadStdDev_Ammo;
+ ///
+ /// Weapons that fire projectiles from ammo types.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float SpreadStdDev_Ammo { get => _spreadStdDev_Ammo; set => _spreadStdDev_Ammo = value; }
+
+ private float _evenSpreadAngle_Ammo;
+ ///
+ /// Arc angle of shotgun pellet spreads, only used if multiple projectiles are being fired.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float EvenSpreadAngle_Ammo { get => _evenSpreadAngle_Ammo; set => _evenSpreadAngle_Ammo = value; }
+
+ private float _velocity_Ammo;
+ ///
+ /// Adds additional velocity to the projectile, on top of what it already has.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float Velocity_Ammo { get => _velocity_Ammo; set => _velocity_Ammo = value; }
+
+ private bool _spent;
+ ///
+ /// If the ammo cartridge has been shot already.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ 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 _projectileID, "projectile", null);
serializer.DataField(ref _spent, "spent", false);
+ serializer.DataField(ref _projectilesFired, "projectilesfired", 1);
+ serializer.DataField(ref _spreadStdDev_Ammo, "ammostddev", 0);
+ serializer.DataField(ref _evenSpreadAngle_Ammo, "ammospread", 0);
+ serializer.DataField(ref _velocity_Ammo, "ammovelocity", 0);
}
}
+ public enum BallisticCaliber
+ {
+ Unspecified = 0,
+ // .32
+ A32,
+ // .357
+ A357,
+ // .44
+ A44,
+ // .45mm
+ A45mm,
+ // .50 cal
+ A50,
+ // 5.56mm
+ A556mm,
+ // 6.5mm
+ A65mm,
+ // 7.62mm
+ A762mm,
+ // 9mm
+ A9mm,
+ // 10mm
+ A10mm,
+ // 20mm
+ A20mm,
+ // 24mm
+ A24mm,
+ // 12g
+ A12g,
+ }
}
diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineComponent.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineComponent.cs
index 280628b2bb..0fa594fa6b 100644
--- a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineComponent.cs
+++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineComponent.cs
@@ -74,7 +74,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
}
}
- _updateAppearance();
+ UpdateAppearance();
OnAmmoCountChanged?.Invoke();
_appearance.SetData(BallisticMagazineVisuals.AmmoCapacity, Capacity);
@@ -99,7 +99,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
_bulletContainer.Insert(bullet);
_loadedBullets.Push(bullet);
- _updateAppearance();
+ UpdateAppearance();
OnAmmoCountChanged?.Invoke();
}
@@ -122,7 +122,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
_bulletContainer.Remove(bullet);
}
- _updateAppearance();
+ UpdateAppearance();
OnAmmoCountChanged?.Invoke();
return bullet;
}
@@ -224,7 +224,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
return false;
}
- private void _updateAppearance()
+ private void UpdateAppearance()
{
_appearance.SetData(BallisticMagazineVisuals.AmmoLeft, CountLoaded);
}
@@ -278,5 +278,8 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
// 24mm
A24mm,
+
+ // 12g
+ A12g,
}
}
diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineWeaponComponent.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineWeaponComponent.cs
index d38bc355e6..ed93dfe541 100644
--- a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineWeaponComponent.cs
+++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineWeaponComponent.cs
@@ -21,6 +21,9 @@ using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
{
+ ///
+ /// Guns that have a magazine.
+ ///
[RegisterComponent]
public class BallisticMagazineWeaponComponent : BallisticWeaponComponent, IUse, IAttackBy, IMapInit
{
@@ -46,7 +49,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
[ViewVariables] private bool _autoEjectMagazine;
[ViewVariables] private AppearanceComponent _appearance;
- private static readonly Direction[] _randomBulletDirs =
+ private static readonly Direction[] RandomBulletDirs =
{
Direction.North,
Direction.East,
@@ -54,12 +57,9 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
Direction.West
};
- protected override int ChamberCount => 1;
-
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
-
serializer.DataField(ref _magazineTypes, "magazines",
new List {BallisticMagazineType.Unspecified});
serializer.DataField(ref _defaultMagazine, "default_magazine", null);
@@ -72,7 +72,6 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
public override void Initialize()
{
base.Initialize();
-
_appearance = Owner.GetComponent();
}
@@ -80,57 +79,44 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
protected override void Startup()
{
base.Startup();
-
_magazineSlot = ContainerManagerComponent.Ensure("ballistic_gun_magazine", Owner);
-
if (Magazine != null)
{
// Already got magazine from loading a container.
- Magazine.GetComponent().OnAmmoCountChanged += _magazineAmmoCountChanged;
+ Magazine.GetComponent().OnAmmoCountChanged += MagazineAmmoCountChanged;
}
-
- _updateAppearance();
+ UpdateAppearance();
}
public bool InsertMagazine(IEntity magazine, bool playSound = true)
{
- if (!magazine.TryGetComponent(out BallisticMagazineComponent component))
+ if (!magazine.TryGetComponent(out BallisticMagazineComponent magazinetype))
{
throw new ArgumentException("Not a magazine", nameof(magazine));
}
-
- if (!MagazineTypes.Contains(component.MagazineType))
+ if (!MagazineTypes.Contains(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)
{
Owner.GetComponent().Play(_magInSound);
}
-
- component.OnAmmoCountChanged += _magazineAmmoCountChanged;
+ magazinetype.OnAmmoCountChanged += MagazineAmmoCountChanged;
if (GetChambered(0) == null)
{
// No bullet in chamber, load one from magazine.
- var bullet = component.TakeBullet();
+ var bullet = magazinetype.TakeBullet();
if (bullet != null)
{
LoadIntoChamber(0, bullet);
}
}
-
- _updateAppearance();
+ UpdateAppearance();
Dirty();
return true;
}
@@ -142,7 +128,6 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
{
return false;
}
-
if (_magazineSlot.Remove(entity))
{
entity.Transform.GridPosition = Owner.Transform.GridPosition;
@@ -150,14 +135,12 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
{
Owner.GetComponent().Play(_magOutSound, AudioParams.Default.WithVolume(20));
}
-
- _updateAppearance();
+ UpdateAppearance();
Dirty();
- entity.GetComponent().OnAmmoCountChanged -= _magazineAmmoCountChanged;
+ entity.GetComponent().OnAmmoCountChanged -= MagazineAmmoCountChanged;
return true;
}
-
- _updateAppearance();
+ UpdateAppearance();
Dirty();
return false;
}
@@ -168,9 +151,13 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
// Eject chambered bullet.
var entity = RemoveFromChamber(chamber);
+ if (entity == null)
+ {
+ return;
+ }
var offsetPos = (CalcBulletOffset(), CalcBulletOffset());
entity.Transform.GridPosition = Owner.Transform.GridPosition.Offset(offsetPos);
- entity.Transform.LocalRotation = _bulletDropRandom.Pick(_randomBulletDirs).ToAngle();
+ entity.Transform.LocalRotation = _bulletDropRandom.Pick(RandomBulletDirs).ToAngle();
var effect = $"/Audio/Guns/Casings/casingfall{_bulletDropRandom.Next(1, 4)}.ogg";
Owner.GetComponent().Play(effect, AudioParams.Default.WithVolume(-3));
@@ -188,9 +175,8 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
DoAutoEject();
}
}
-
Dirty();
- _updateAppearance();
+ UpdateAppearance();
}
private float CalcBulletOffset()
@@ -206,7 +192,6 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
{
Owner.GetComponent().Play(_autoEjectSound, AudioParams.Default.WithVolume(-5));
}
-
Dirty();
}
@@ -221,7 +206,6 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
{
Owner.PopupMessage(eventArgs.User, "No magazine");
}
-
return true;
}
@@ -231,29 +215,26 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
{
return false;
}
-
if (Magazine != null)
{
Owner.PopupMessage(eventArgs.User, "Already got a magazine.");
return false;
}
-
- if (!MagazineTypes.Contains(component.MagazineType) || component.Caliber != Caliber)
+ if (!MagazineTypes.Contains(component.MagazineType))
{
Owner.PopupMessage(eventArgs.User, "Magazine doesn't fit.");
return false;
}
-
return InsertMagazine(eventArgs.AttackWith);
}
- private void _magazineAmmoCountChanged()
+ private void MagazineAmmoCountChanged()
{
Dirty();
- _updateAppearance();
+ UpdateAppearance();
}
- private void _updateAppearance()
+ private void UpdateAppearance()
{
if (Magazine != null)
{
@@ -273,15 +254,12 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
public override ComponentState GetComponentState()
{
var chambered = GetChambered(0) != null;
-
(int, int)? count = null;
-
if (Magazine != null)
{
var magComponent = Magazine.GetComponent();
count = (magComponent.CountLoaded, magComponent.Capacity);
}
-
return new BallisticMagazineWeaponComponentState(chambered, count);
}
diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticWeapon.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticWeapon.cs
new file mode 100644
index 0000000000..6332e89f20
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticWeapon.cs
@@ -0,0 +1,149 @@
+using Content.Server.GameObjects.Components.Sound;
+using Robust.Server.GameObjects.Components.Container;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Serialization;
+using Robust.Shared.ViewVariables;
+using System;
+
+namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
+{
+ ///
+ /// Handles firing projectiles from a contained .
+ ///
+ public abstract class BallisticWeaponComponent : BaseProjectileWeaponComponent
+ {
+ private Chamber[] _chambers;
+
+ ///
+ /// Number of chambers created during initialization.
+ ///
+ private int _chamberCount;
+
+ [ViewVariables]
+ private BallisticCaliber _caliber ;
+ ///
+ /// What type of ammo this gun can fire.
+ ///
+
+ private string _soundGunEmpty;
+ ///
+ /// Sound played when trying to shoot if there is no ammo available.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public string SoundGunEmpty { get => _soundGunEmpty; set => _soundGunEmpty = value; }
+
+ private float _spreadStdDevGun;
+ ///
+ /// Increases the standard deviation of the ammo being fired.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float SpreadStdDevGun { get => _spreadStdDevGun; set => _spreadStdDevGun = value; }
+
+ private float _evenSpreadAngleGun;
+ ///
+ /// Increases the evenspread of the ammo being fired.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float EvenSpreadAngleGun { get => _evenSpreadAngleGun; set => _evenSpreadAngleGun = value; }
+
+ private float _velocityGun;
+ ///
+ /// Increases the velocity of the ammo being fired.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float VelocityGun { get => _velocityGun; set => _velocityGun = value; }
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+ serializer.DataField(ref _soundGunEmpty, "sound_empty", "/Audio/Guns/Empty/empty.ogg");
+ serializer.DataField(ref _spreadStdDevGun, "spreadstddev", 0);
+ serializer.DataField(ref _evenSpreadAngleGun, "evenspread", 0);
+ serializer.DataField(ref _velocityGun, "gunvelocity", 0);
+ serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
+ serializer.DataField(ref _chamberCount, "chambers", 1);
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ Owner.GetComponent().FireHandler = TryShoot;
+ _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);
+ }
+ }
+
+ ///
+ /// Fires projectiles based on loaded ammo from entity to a coordinate.
+ ///
+ protected void TryShoot(IEntity user, GridCoordinates clickLocation)
+ {
+ var ammo = GetChambered(FirstChamber)?.GetComponent();
+ CycleChamberedBullet(FirstChamber);
+ if (ammo == null || ammo?.Spent == true || ammo?.Caliber != _caliber)
+ {
+ PlayEmptySound();
+ return;
+ }
+ ammo.Spent = true;
+ var total_stdev = _spreadStdDevGun + ammo.SpreadStdDev_Ammo;
+ var final_evenspread = _evenSpreadAngleGun + ammo.EvenSpreadAngle_Ammo;
+ var final_velocity = _velocityGun + ammo.Velocity_Ammo;
+ FireAtCoord(user, clickLocation, ammo.ProjectileID, total_stdev, ammo.ProjectilesFired, final_evenspread, final_velocity);
+ }
+
+ protected IEntity GetChambered(int chamber) => _chambers[chamber].Slot.ContainedEntity;
+
+ ///
+ /// Loads the next ammo casing into the chamber.
+ ///
+ 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;
+ }
+
+ protected 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;
+ }
+
+ private void PlayEmptySound() => Owner.GetComponent().Play(_soundGunEmpty);
+
+ protected sealed class Chamber
+ {
+ public Chamber(ContainerSlot slot)
+ {
+ Slot = slot;
+ }
+
+ public ContainerSlot Slot { get; }
+ }
+
+ private const int FirstChamber = 0;
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticWeaponComponent.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticWeaponComponent.cs
deleted file mode 100644
index 50bf07b847..0000000000
--- a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticWeaponComponent.cs
+++ /dev/null
@@ -1,121 +0,0 @@
-using System;
-using Content.Server.GameObjects.Components.Sound;
-using Robust.Server.GameObjects.Components.Container;
-using Robust.Shared.Interfaces.GameObjects;
-using Robust.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)
- {
- Owner.GetComponent().Play(_soundGunEmpty);
- }
- }
- var chambered = GetChambered(0);
- if (chambered != null)
- {
- var bullet = chambered.GetComponent();
- if (bullet.Spent)
- {
- PlayEmpty();
- return null;
- }
-
- var projectile = Owner.EntityManager.SpawnEntity(bullet.ProjectileType, Owner.Transform.GridPosition);
- 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/BaseProjectileWeaponComponent.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BaseProjectileWeaponComponent.cs
new file mode 100644
index 0000000000..3090ebdcfc
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BaseProjectileWeaponComponent.cs
@@ -0,0 +1,95 @@
+using Content.Server.GameObjects.Components.Mobs;
+using Content.Server.GameObjects.Components.Projectiles;
+using Content.Server.GameObjects.Components.Sound;
+using Robust.Server.GameObjects;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Interfaces.Random;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.Random;
+using Robust.Shared.Serialization;
+using Robust.Shared.ViewVariables;
+using System.Collections.Generic;
+
+namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
+{
+ ///
+ /// Methods to shoot projectiles.
+ ///
+ public abstract class BaseProjectileWeaponComponent : Component
+ {
+ private string _soundGunshot;
+ [ViewVariables(VVAccess.ReadWrite)]
+ public string SoundGunshot
+ { get => _soundGunshot; set => _soundGunshot = value; }
+
+#pragma warning disable 649
+ [Dependency] private IRobustRandom _spreadRandom;
+#pragma warning restore 649
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+ serializer.DataField(ref _soundGunshot, "sound_gunshot", "/Audio/Guns/Gunshots/smg.ogg");
+ }
+
+ ///
+ /// Fires projectile from an entity at a coordinate.
+ ///
+ protected void FireAtCoord(IEntity source, GridCoordinates coord, string projectileType, double spreadStdDev, int projectilesFired = 1, double evenSpreadAngle = 0, float velocity = 0)
+ {
+ var angle = GetAngleFromClickLocation(source, coord);
+ FireAtAngle(source, angle, projectileType, spreadStdDev, projectilesFired, evenSpreadAngle, velocity);
+ }
+
+ ///
+ /// Fires projectile in the direction of an angle.
+ ///
+ protected void FireAtAngle(IEntity source, Angle angle, string projectileType = null, double spreadStdDev = 0, int projectilesFired = 1, double evenSpreadAngle = 0, float velocity = 0)
+ {
+ List sprayanglechange = null;
+ if (evenSpreadAngle != 0 & projectilesFired > 1)
+ {
+ sprayanglechange = Linspace(-evenSpreadAngle/2, evenSpreadAngle/2, projectilesFired);
+ }
+ for (var i = 1; i <= projectilesFired; i++)
+ {
+ Angle finalangle = angle + Angle.FromDegrees(_spreadRandom.NextGaussian(0, spreadStdDev)) + (sprayanglechange != null ? sprayanglechange[i - 1] : 0);
+ var projectile = Owner.EntityManager.SpawnEntity(projectileType, Owner.Transform.GridPosition);
+ projectile.Transform.GridPosition = source.Transform.GridPosition; //move projectile to entity it is being fired from
+ projectile.GetComponent().IgnoreEntity(source);//make sure it doesn't hit the source entity
+ var finalvelocity = projectile.GetComponent().Velocity + velocity;//add velocity
+ projectile.GetComponent().LinearVelocity = finalangle.ToVec() * finalvelocity;//Rotate the bullets sprite to the correct direction
+ projectile.Transform.LocalRotation = finalangle.Theta;
+ }
+ PlayFireSound();
+ if (source.TryGetComponent(out CameraRecoilComponent recoil))
+ {
+ var recoilVec = angle.ToVec() * -0.15f;
+ recoil.Kick(recoilVec);
+ }
+ }
+
+ private void PlayFireSound() => Owner.GetComponent().Play(_soundGunshot);
+
+ ///
+ /// Gets the angle from an entity to a coordinate.
+ ///
+ protected Angle GetAngleFromClickLocation(IEntity source, GridCoordinates clickLocation) => new Angle(clickLocation.Position - source.Transform.GridPosition.Position);
+
+ ///
+ /// Returns a list of numbers that form a set of equal intervals between the start and end value. Used to calculate shotgun spread angles.
+ ///
+ protected List Linspace(double start, double end, int intervals)
+ {
+ var linspace = new List { };
+ for (var i = 0; i <= intervals - 1; i++)
+ {
+ linspace.Add(Angle.FromDegrees(start + (end - start) * i / (intervals - 1)));
+ }
+ return linspace;
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/ProjectileWeapon.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/ProjectileWeapon.cs
deleted file mode 100644
index e79b680082..0000000000
--- a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/ProjectileWeapon.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-using Content.Server.GameObjects.Components.Mobs;
-using Content.Server.GameObjects.Components.Projectiles;
-using Content.Server.GameObjects.Components.Sound;
-using Robust.Server.GameObjects;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Interfaces.GameObjects;
-using Robust.Shared.Interfaces.Random;
-using Robust.Shared.IoC;
-using Robust.Shared.Map;
-using Robust.Shared.Maths;
-using Robust.Shared.Random;
-using Robust.Shared.Serialization;
-using Robust.Shared.ViewVariables;
-
-namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
-{
- public abstract class ProjectileWeaponComponent : Component
- {
- private float _spreadStdDev = 3;
- private bool _spread = true;
- private string _soundGunshot;
-
-#pragma warning disable 649
- [Dependency] private IRobustRandom _spreadRandom;
-#pragma warning restore 649
-
- [ViewVariables(VVAccess.ReadWrite)]
- public bool Spread
- {
- get => _spread;
- set => _spread = value;
- }
-
- [ViewVariables(VVAccess.ReadWrite)]
- public float SpreadStdDev
- {
- get => _spreadStdDev;
- set => _spreadStdDev = value;
- }
-
- public override void Initialize()
- {
- base.Initialize();
-
- var rangedWeapon = Owner.GetComponent();
- rangedWeapon.FireHandler = Fire;
- }
-
- public override void ExposeData(ObjectSerializer serializer)
- {
- base.ExposeData(serializer);
-
- serializer.DataField(ref _spread, "spread", true);
- serializer.DataField(ref _spreadStdDev, "spreadstddev", 3);
- serializer.DataField(ref _soundGunshot, "sound_gunshot", "/Audio/Guns/Gunshots/smg.ogg");
- }
-
- 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));
- }
-
- 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);
- var velocity = projectile.GetComponent().Velocity;
-
- //Give it the velocity this weapon gives to things it fires from itself
- projectile.GetComponent().LinearVelocity = angle.ToVec() * velocity;
-
- //Rotate the bullets sprite to the correct direction, from north facing I guess
- projectile.Transform.LocalRotation = angle.Theta;
-
- // Sound!
- Owner.GetComponent().Play(_soundGunshot);
- }
-
- ///
- /// Try to get a projectile for firing. If null, nothing will be fired.
- ///
- protected abstract IEntity GetFiredProjectile();
- }
-
- public enum BallisticCaliber
- {
- Unspecified = 0,
- // .32
- A32,
- // .357
- A357,
- // .44
- A44,
- // .45mm
- A45mm,
- // .50 cal
- A50,
- // 5.56mm
- A556mm,
- // 6.5mm
- A65mm,
- // 7.62mm
- A762mm,
- // 9mm
- A9mm,
- // 10mm
- A10mm,
- // 20mm
- A20mm,
- // 24mm
- A24mm,
- }
-}
diff --git a/Resources/Prototypes/Entities/Weapons/Ammunition/12g/ammunition.yml b/Resources/Prototypes/Entities/Weapons/Ammunition/12g/ammunition.yml
new file mode 100644
index 0000000000..fcc8c63695
--- /dev/null
+++ b/Resources/Prototypes/Entities/Weapons/Ammunition/12g/ammunition.yml
@@ -0,0 +1,54 @@
+# Empty mags
+- type: entity
+ id: magazine_12g_empty
+ name: "12g magazine - empty"
+ parent: BaseItem
+ abstract: true
+ components:
+ - type: BallisticMagazine
+ caliber: A12g
+ magazine: A12g
+ capacity: 8
+ - type: Sprite
+ netsync: false
+
+#Magazines
+- type: entity
+ id: magazine_12g_shotgun
+ name: "12g Magazine"
+ parent: magazine_12g_empty
+ components:
+ - type: BallisticMagazine
+ fill: ammo_casing_12g
+ caliber: A12g
+ magazine: A12g
+ capacity: 20
+ - type: Icon
+ sprite: Objects/Guns/Ammunition/Magazine/10mm/12mml.rsi
+ state: 12mml-1
+ - type: Sprite
+ sprite: Objects/Guns/Ammunition/Magazine/10mm/12mml.rsi
+ state: 12mml-1
+ - type: Appearance
+ visuals:
+ - type: BallisticMagazineVisualizer2D
+ base_state: 12mml
+ steps: 2
+
+# Casings
+- type: entity
+ id: ammo_casing_12g
+ name: "12g casing"
+ parent: BaseItem
+ components:
+ - type: BallisticBullet
+ caliber: A12g
+ projectile: pellet_12g
+ projectilesfired : 6
+ - type: Sprite
+ sprite: Objects/Guns/Ammunition/ammo_casing.rsi
+ state: s-casing
+ drawdepth: FloorObjects
+ - type: Icon
+ sprite: Objects/Guns/Ammunition/ammo_casing.rsi
+ state: s-casing
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Weapons/Ammunition/12g/boxes.yml b/Resources/Prototypes/Entities/Weapons/Ammunition/12g/boxes.yml
new file mode 100644
index 0000000000..090417ae78
--- /dev/null
+++ b/Resources/Prototypes/Entities/Weapons/Ammunition/12g/boxes.yml
@@ -0,0 +1,34 @@
+# Empty boxes
+- type: entity
+ id: box_12g_empty
+ name: "12g box - empty"
+ parent: BaseItem
+ abstract: true
+ components:
+ - type: AmmoBox
+ caliber: A12g
+ capacity: 30
+ - type: Sprite
+ netsync: false
+
+# Ammo boxes
+- type: entity
+ id: box_12g
+ name: "12g box"
+ parent: box_12g_empty
+ components:
+ - type: AmmoBox
+ fill: ammo_casing_12g
+ caliber: A12g
+ capacity: 30
+ - type: Icon
+ sprite: Objects/Guns/Ammunition/Boxes/10mm/box10mm.rsi
+ state: box10mm-1
+ - type: Sprite
+ sprite: Objects/Guns/Ammunition/Boxes/10mm/box10mm.rsi
+ state: box10mm-1
+ - type: Appearance
+ visuals:
+ - type: BallisticMagazineVisualizer2D
+ base_state: box10mm
+ steps: 2
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Weapons/Ammunition/12g/projectiles.yml b/Resources/Prototypes/Entities/Weapons/Ammunition/12g/projectiles.yml
new file mode 100644
index 0000000000..3d2172cbfc
--- /dev/null
+++ b/Resources/Prototypes/Entities/Weapons/Ammunition/12g/projectiles.yml
@@ -0,0 +1,10 @@
+- type: entity
+ id: pellet_12g
+ name: 12g Pellet
+ parent: bullet_base
+ description: If you can see this you're dead!
+ components:
+ - type: Projectile
+ velocity: 20
+ damages:
+ Brute: 10
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Weapons/Shotguns/shotguns.yml b/Resources/Prototypes/Entities/Weapons/Shotguns/shotguns.yml
new file mode 100644
index 0000000000..00552cf73c
--- /dev/null
+++ b/Resources/Prototypes/Entities/Weapons/Shotguns/shotguns.yml
@@ -0,0 +1,47 @@
+- type: entity
+ name: Shotgun
+ parent: BaseItem
+ id: BaseShotgun
+ description: A rooty tooty point and shooty.
+ abstract: true
+ components:
+ - type: Sound
+ - type: RangedWeapon
+ automatic: false
+ firerate: 20
+ - type: BallisticMagazineWeapon
+ caliber: A12g
+ evenspread : 40
+ magazines:
+ - A12g
+ default_magazine: magazine_12g_shotgun
+ auto_eject_magazine: false
+ sound_auto_eject: /Audio/Guns/EmptyAlarm/smg_empty_alarm.ogg
+ sound_magazine_in: /Audio/Guns/MagIn/smg_magin.ogg
+ sound_magazine_out: /Audio/Guns/MagOut/smg_magout.ogg
+ sound_empty: /Audio/Guns/Empty/empty.ogg
+ sound_gunshot: /Audio/Guns/Gunshots/smg.ogg
+
+- type: entity
+ name: Magazine Fed Shotgun
+ parent: BaseShotgun
+ id: MagazineFedShotgun
+ components:
+ - type: Sprite
+ netsync: false
+ sprite: Objects/Guns/SMGs/c20r.rsi
+ state: c20r-5
+ - type: Icon
+ sprite: Objects/Guns/SMGs/c20r.rsi
+ state: c20r-5
+ - type: Appearance
+ visuals:
+ - type: BallisticMagazineWeaponVisualizer2D
+ base_state: c20r
+ steps: 6
+ - type: Item
+ Size: 24
+ sprite: Objects/Guns/SMGs/c20r.rsi
+ - type: Item
+ Size: 24
+ sprite: Objects/Guns/SMGs/wt550.rsi