Add a LOT more dakka (#1033)

* Start adding flashy flash

* Change slop

Might give a smoother decline

* flashy flash

* Add flashbang and flash projectiles

Bang bang bang pull my flash trigger

* Add collision check to area flash

* Flash cleanupo

* flash.ogg mixed to mono
* Adjusted flash curve again

* Enhancing flashes with unshaded and lights and shit

Still a WIP

* Add the other ballistic gun types

Re-organised some of the gun stuff so the powercell guns share the shooting code with the ballistic guns.

* Re-merging branch with master

Also fixed some visualizer bugs

* Last cleanup

Fixed some crashes
Fixed Deckard sprite
Fixed Hitscan effects
Re-applied master changes
Re-factor to using soundsystem
Add some more audio effects

* Cleanup flashes for merge

Can put flashbangs in lockers so you don't get blinded

Fix some bugs

* Fix shotties

Also removed some redundant code

* Bulldoze some legacycode

brrrrrrrrt

* Fix clientignore warnings

* Add the other Stunnable types to StunnableProjectile

* Some gun refactoring

* Removed extra visualizers
* All casing ejections use the same code
* Speed loaders can have their ammo pulled out
* Bolt sound less loud

* Stop ThrowController from throwing

* Fix speed loader visuals

* Update hitscan collision mask and fix typo

* Cleanup

* Fit hitscan and flashbang collisions
* Use the new flags support

* Update taser placeholder description

* Update protonames per style guide

* Add yaml flag support for gun firerates

* Cleanup crew

* Fix Audio up (components, audio file, + remove global sounds)
* Add server-side recoil back-in (forgot that I was testing this client-side)
* Add Flag support for fire-rate selectors

* Wrong int you dolt

* Fix AI conflicts

Haha ranged bulldozer go BRR
(I'll rewrite it after the other AI systems are done).

* Mix bang.ogg from stereo to mono

* Make sure serializer's reading for guns

Fixes integration test

* Change EntitySystem calls to use the static function

Also removed the Pumpbarrel commented-out code

* Change StunnableProjectile defaults to 0

* Fix taser paralyse

Apparently removing defaults means you have to specify the values, whodathunkit

* Add slowdown to stunnableprojectiles and fix tasers

* Remove FlagsFor from gun components

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
Co-authored-by: Víctor Aguilera Puerto <6766154+Zumorica@users.noreply.github.com>
This commit is contained in:
metalgearsloth
2020-06-22 05:47:15 +10:00
committed by GitHub
parent ac19ad7eac
commit 95995b6232
1977 changed files with 13600 additions and 11229 deletions

View File

@@ -0,0 +1,322 @@
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Sound;
using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects;
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
using Content.Shared.Interfaces;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.Container;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
{
/// <summary>
/// Shotguns mostly
/// </summary>
[RegisterComponent]
public sealed class BoltActionBarrelComponent : ServerRangedBarrelComponent, IMapInit
{
// 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 string Name => "BoltActionBarrel";
public override int ShotsLeft
{
get
{
var chamberCount = _chamberContainer.ContainedEntity != null ? 1 : 0;
return chamberCount + _spawnedAmmo.Count + _unspawnedCount;
}
}
public override int Capacity => _capacity;
private int _capacity;
private ContainerSlot _chamberContainer;
private Stack<IEntity> _spawnedAmmo;
private Container _ammoContainer;
private BallisticCaliber _caliber;
private string _fillPrototype;
private int _unspawnedCount;
public bool BoltOpen
{
get => _boltOpen;
set
{
if (_boltOpen == value)
{
return;
}
var soundSystem = EntitySystem.Get<AudioSystem>();
if (value)
{
if (_soundBoltOpen != null)
{
soundSystem.PlayAtCoords(_soundBoltOpen, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
}
}
else
{
if (_soundBoltClosed != null)
{
soundSystem.PlayAtCoords(_soundBoltClosed, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
}
}
_boltOpen = value;
UpdateAppearance();
}
}
private bool _boltOpen;
private bool _autoCycle;
private AppearanceComponent _appearanceComponent;
// Sounds
private string _soundCycle;
private string _soundBoltOpen;
private string _soundBoltClosed;
private string _soundInsert;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
serializer.DataField(ref _capacity, "capacity", 6);
serializer.DataField(ref _fillPrototype, "fillPrototype", null);
serializer.DataField(ref _autoCycle, "autoCycle", false);
serializer.DataField(ref _soundCycle, "soundCycle", "/Audio/Guns/Cock/sf_rifle_cock.ogg");
serializer.DataField(ref _soundBoltOpen, "soundBoltOpen", "/Audio/Guns/Bolt/rifle_bolt_open.ogg");
serializer.DataField(ref _soundBoltClosed, "soundBoltClosed", "/Audio/Guns/Bolt/rifle_bolt_closed.ogg");
serializer.DataField(ref _soundInsert, "soundInsert", "/Audio/Guns/MagIn/bullet_insert.ogg");
}
void IMapInit.MapInit()
{
if (_fillPrototype != null)
{
_unspawnedCount += Capacity - 1;
}
UpdateAppearance();
}
public override void Initialize()
{
// TODO: Add existing ammo support on revolvers
base.Initialize();
_spawnedAmmo = new Stack<IEntity>(_capacity - 1);
_ammoContainer = ContainerManagerComponent.Ensure<Container>($"{Name}-ammo-container", Owner, out var existing);
if (existing)
{
foreach (var entity in _ammoContainer.ContainedEntities)
{
_spawnedAmmo.Push(entity);
_unspawnedCount--;
}
}
_chamberContainer = ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-chamber-container", Owner);
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
{
_appearanceComponent = appearanceComponent;
}
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
UpdateAppearance();
}
private void UpdateAppearance()
{
_appearanceComponent?.SetData(BarrelBoltVisuals.BoltOpen, BoltOpen);
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
}
public override IEntity PeekAmmo()
{
return _chamberContainer.ContainedEntity;
}
public override IEntity TakeProjectile()
{
var chamberEntity = _chamberContainer.ContainedEntity;
if (_autoCycle)
{
Cycle();
}
return chamberEntity?.GetComponent<AmmoComponent>().TakeBullet();
}
protected override bool WeaponCanFire()
{
if (!base.WeaponCanFire())
{
return false;
}
return !BoltOpen && _chamberContainer.ContainedEntity != null;
}
private void Cycle(bool manual = false)
{
var chamberedEntity = _chamberContainer.ContainedEntity;
if (chamberedEntity != null)
{
_chamberContainer.Remove(chamberedEntity);
var ammoComponent = chamberedEntity.GetComponent<AmmoComponent>();
if (!ammoComponent.Caseless)
{
EjectCasing(chamberedEntity);
}
}
if (_spawnedAmmo.TryPop(out var next))
{
_ammoContainer.Remove(next);
_chamberContainer.Insert(next);
}
if (_unspawnedCount > 0)
{
_unspawnedCount--;
var ammoEntity = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.GridPosition);
_chamberContainer.Insert(ammoEntity);
}
if (_chamberContainer.ContainedEntity == null && manual)
{
BoltOpen = true;
if (ContainerHelpers.TryGetContainer(Owner, out var container))
{
Owner.PopupMessage(container.Owner, Loc.GetString("Bolt opened"));
}
}
if (manual)
{
if (_soundCycle != null)
{
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundCycle, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
}
}
Dirty();
UpdateAppearance();
}
public bool TryInsertBullet(IEntity user, IEntity ammo)
{
if (!ammo.TryGetComponent(out AmmoComponent ammoComponent))
{
return false;
}
if (ammoComponent.Caliber != _caliber)
{
Owner.PopupMessage(user, Loc.GetString("Wrong caliber"));
return false;
}
if (!BoltOpen)
{
Owner.PopupMessage(user, Loc.GetString("Bolt isn't open"));
return false;
}
if (_chamberContainer.ContainedEntity == null)
{
_chamberContainer.Insert(ammo);
if (_soundInsert != null)
{
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
}
// Dirty();
UpdateAppearance();
return true;
}
if (_ammoContainer.ContainedEntities.Count < Capacity - 1)
{
_ammoContainer.Insert(ammo);
_spawnedAmmo.Push(ammo);
if (_soundInsert != null)
{
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
}
// Dirty();
UpdateAppearance();
return true;
}
Owner.PopupMessage(user, Loc.GetString("No room"));
return false;
}
public override bool UseEntity(UseEntityEventArgs eventArgs)
{
if (BoltOpen)
{
BoltOpen = false;
Owner.PopupMessage(eventArgs.User, Loc.GetString("Bolt closed"));
// Dirty();
return true;
}
Cycle(true);
return true;
}
public override bool InteractUsing(InteractUsingEventArgs eventArgs)
{
return TryInsertBullet(eventArgs.User, eventArgs.Using);
}
[Verb]
private sealed class OpenBoltVerb : Verb<BoltActionBarrelComponent>
{
protected override void GetData(IEntity user, BoltActionBarrelComponent component, VerbData data)
{
data.Text = Loc.GetString("Open bolt");
data.Visibility = component.BoltOpen ? VerbVisibility.Disabled : VerbVisibility.Visible;
}
protected override void Activate(IEntity user, BoltActionBarrelComponent component)
{
component.BoltOpen = true;
}
}
[Verb]
private sealed class CloseBoltVerb : Verb<BoltActionBarrelComponent>
{
protected override void GetData(IEntity user, BoltActionBarrelComponent component, VerbData data)
{
data.Text = Loc.GetString("Close bolt");
data.Visibility = component.BoltOpen ? VerbVisibility.Visible : VerbVisibility.Disabled;
}
protected override void Activate(IEntity user, BoltActionBarrelComponent component)
{
component.BoltOpen = false;
}
}
}
}

View File

@@ -0,0 +1,215 @@
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Sound;
using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
using Content.Shared.Interfaces;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.Container;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
{
/// <summary>
/// Bolt-action rifles
/// </summary>
[RegisterComponent]
public sealed class PumpBarrelComponent : ServerRangedBarrelComponent, IMapInit
{
public override string Name => "PumpBarrel";
public override int ShotsLeft
{
get
{
var chamberCount = _chamberContainer.ContainedEntity != null ? 1 : 0;
return chamberCount + _spawnedAmmo.Count + _unspawnedCount;
}
}
public override int Capacity => _capacity;
private int _capacity;
// Even a point having a chamber? I guess it makes some of the below code cleaner
private ContainerSlot _chamberContainer;
private Stack<IEntity> _spawnedAmmo;
private Container _ammoContainer;
private BallisticCaliber _caliber;
private string _fillPrototype;
private int _unspawnedCount;
private bool _manualCycle;
private AppearanceComponent _appearanceComponent;
// Sounds
private string _soundCycle;
private string _soundInsert;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
serializer.DataField(ref _capacity, "capacity", 6);
serializer.DataField(ref _fillPrototype, "fillPrototype", null);
serializer.DataField(ref _manualCycle, "manualCycle", true);
serializer.DataField(ref _soundCycle, "soundCycle", "/Audio/Guns/Cock/sf_rifle_cock.ogg");
serializer.DataField(ref _soundInsert, "soundInsert", "/Audio/Guns/MagIn/bullet_insert.ogg");
_spawnedAmmo = new Stack<IEntity>(_capacity - 1);
}
void IMapInit.MapInit()
{
if (_fillPrototype != null)
{
_unspawnedCount += Capacity - 1;
}
UpdateAppearance();
}
public override void Initialize()
{
base.Initialize();
_ammoContainer =
ContainerManagerComponent.Ensure<Container>($"{Name}-ammo-container", Owner, out var existing);
if (existing)
{
foreach (var entity in _ammoContainer.ContainedEntities)
{
_spawnedAmmo.Push(entity);
_unspawnedCount--;
}
}
_chamberContainer =
ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-chamber-container", Owner, out existing);
if (existing)
{
_unspawnedCount--;
}
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
{
_appearanceComponent = appearanceComponent;
}
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
UpdateAppearance();
}
private void UpdateAppearance()
{
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
}
public override IEntity PeekAmmo()
{
return _chamberContainer.ContainedEntity;
}
public override IEntity TakeProjectile()
{
var chamberEntity = _chamberContainer.ContainedEntity;
if (!_manualCycle)
{
Cycle();
}
return chamberEntity?.GetComponent<AmmoComponent>().TakeBullet();
}
private void Cycle(bool manual = false)
{
var chamberedEntity = _chamberContainer.ContainedEntity;
if (chamberedEntity != null)
{
_chamberContainer.Remove(chamberedEntity);
var ammoComponent = chamberedEntity.GetComponent<AmmoComponent>();
if (!ammoComponent.Caseless)
{
EjectCasing(chamberedEntity);
}
}
if (_spawnedAmmo.TryPop(out var next))
{
_ammoContainer.Remove(next);
_chamberContainer.Insert(next);
}
if (_unspawnedCount > 0)
{
_unspawnedCount--;
var ammoEntity = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.GridPosition);
_chamberContainer.Insert(ammoEntity);
}
if (manual)
{
if (_soundCycle != null)
{
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundCycle, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
}
}
// Dirty();
UpdateAppearance();
}
public bool TryInsertBullet(InteractUsingEventArgs eventArgs)
{
if (!eventArgs.Using.TryGetComponent(out AmmoComponent ammoComponent))
{
return false;
}
if (ammoComponent.Caliber != _caliber)
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("Wrong caliber"));
return false;
}
if (_ammoContainer.ContainedEntities.Count < Capacity - 1)
{
_ammoContainer.Insert(eventArgs.Using);
_spawnedAmmo.Push(eventArgs.Using);
// Dirty();
UpdateAppearance();
if (_soundInsert != null)
{
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
}
return true;
}
Owner.PopupMessage(eventArgs.User, Loc.GetString("No room"));
return false;
}
public override bool UseEntity(UseEntityEventArgs eventArgs)
{
Cycle(true);
return true;
}
public override bool InteractUsing(InteractUsingEventArgs eventArgs)
{
return TryInsertBullet(eventArgs);
}
}
}

View File

@@ -0,0 +1,234 @@
using System;
using Content.Server.GameObjects.Components.Sound;
using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects;
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
using Content.Shared.Interfaces;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.Container;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
{
[RegisterComponent]
public sealed class RevolverBarrelComponent : ServerRangedBarrelComponent
{
public override string Name => "RevolverBarrel";
private BallisticCaliber _caliber;
private Container _ammoContainer;
private int _currentSlot = 0;
public override int Capacity => _ammoSlots.Length;
private IEntity[] _ammoSlots;
public override int ShotsLeft => _ammoContainer.ContainedEntities.Count;
private AppearanceComponent _appearanceComponent;
// Sounds
private string _soundEject;
private string _soundInsert;
private string _soundSpin;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
var capacity = serializer.ReadDataField("capacity", 6);
_ammoSlots = new IEntity[capacity];
// Sounds
serializer.DataField(ref _soundEject, "soundEject", "/Audio/Guns/MagOut/revolver_magout.ogg");
serializer.DataField(ref _soundInsert, "soundInsert", "/Audio/Guns/MagIn/revolver_magin.ogg");
serializer.DataField(ref _soundSpin, "soundSpin", "/Audio/Guns/Misc/revolver_spin.ogg");
}
public override void Initialize()
{
base.Initialize();
_ammoContainer = ContainerManagerComponent.Ensure<Container>($"{Name}-ammoContainer", Owner);
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
{
_appearanceComponent = appearanceComponent;
}
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
}
private void UpdateAppearance()
{
// Placeholder, at this stage it's just here for the RPG
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, ShotsLeft > 0);
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
}
public bool TryInsertBullet(IEntity user, IEntity entity)
{
if (!entity.TryGetComponent(out AmmoComponent ammoComponent))
{
return false;
}
if (ammoComponent.Caliber != _caliber)
{
Owner.PopupMessage(user, Loc.GetString("Wrong caliber"));
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 = _ammoSlots.Length - 1; i >= 0; i--)
{
var slot = _ammoSlots[i];
if (slot == null)
{
_currentSlot = i;
_ammoSlots[i] = entity;
_ammoContainer.Insert(entity);
if (_soundInsert != null)
{
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
}
// Dirty();
UpdateAppearance();
return true;
}
}
Owner.PopupMessage(user, Loc.GetString("Ammo full"));
return false;
}
public void Cycle()
{
// Move up a slot
_currentSlot = (_currentSlot + 1) % _ammoSlots.Length;
// Dirty();
UpdateAppearance();
}
/// <summary>
/// Russian Roulette
/// </summary>
public void Spin()
{
var random = IoCManager.Resolve<IRobustRandom>().Next(_ammoSlots.Length - 1);
_currentSlot = random;
if (_soundSpin != null)
{
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundSpin, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
}
}
public override IEntity PeekAmmo()
{
return _ammoSlots[_currentSlot];
}
/// <summary>
/// Takes a projectile out if possible
/// IEnumerable just to make supporting shotguns saner
/// </summary>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public override IEntity TakeProjectile()
{
var ammo = _ammoSlots[_currentSlot];
IEntity bullet = null;
if (ammo != null)
{
var ammoComponent = ammo.GetComponent<AmmoComponent>();
bullet = ammoComponent.TakeBullet();
if (ammoComponent.Caseless)
{
_ammoSlots[_currentSlot] = null;
_ammoContainer.Remove(ammo);
}
}
Cycle();
UpdateAppearance();
return bullet;
}
private void EjectAllSlots()
{
for (var i = 0; i < _ammoSlots.Length; i++)
{
var entity = _ammoSlots[i];
if (entity == null)
{
continue;
}
_ammoContainer.Remove(entity);
EjectCasing(entity);
_ammoSlots[i] = null;
}
if (_ammoContainer.ContainedEntities.Count > 0)
{
if (_soundEject != null)
{
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundEject, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-1));
}
}
// May as well point back at the end?
_currentSlot = _ammoSlots.Length - 1;
return;
}
/// <summary>
/// Eject all casings
/// </summary>
/// <param name="eventArgs"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public override bool UseEntity(UseEntityEventArgs eventArgs)
{
EjectAllSlots();
//Dirty();
UpdateAppearance();
return true;
}
public override bool InteractUsing(InteractUsingEventArgs eventArgs)
{
return TryInsertBullet(eventArgs.User, eventArgs.Using);
}
[Verb]
private sealed class SpinRevolverVerb : Verb<RevolverBarrelComponent>
{
protected override void GetData(IEntity user, RevolverBarrelComponent component, VerbData data)
{
data.Text = Loc.GetString("Spin");
if (component.Capacity <= 1)
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Visibility = component.ShotsLeft > 0 ? VerbVisibility.Visible : VerbVisibility.Disabled;
}
protected override void Activate(IEntity user, RevolverBarrelComponent component)
{
component.Spin();
component.Owner.PopupMessage(user, Loc.GetString("Spun the cylinder"));
}
}
}
}

View File

@@ -0,0 +1,274 @@
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Power;
using Content.Server.GameObjects.Components.Projectiles;
using Content.Server.GameObjects.Components.Sound;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects;
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.Container;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
using Logger = Robust.Shared.Log.Logger;
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
{
[RegisterComponent]
public sealed class ServerBatteryBarrelComponent : ServerRangedBarrelComponent
{
public override string Name => "BatteryBarrel";
// The minimum change we need before we can fire
[ViewVariables] private float _lowerChargeLimit;
[ViewVariables] private int _baseFireCost;
// What gets fired
[ViewVariables] private string _ammoPrototype;
[ViewVariables] public IEntity PowerCellEntity => _powerCellContainer.ContainedEntity;
public PowerCellComponent PowerCell => _powerCellContainer.ContainedEntity.GetComponent<PowerCellComponent>();
private ContainerSlot _powerCellContainer;
private ContainerSlot _ammoContainer;
private string _powerCellPrototype;
[ViewVariables] private bool _powerCellRemovable;
public override int ShotsLeft
{
get
{
var powerCell = _powerCellContainer.ContainedEntity;
if (powerCell == null)
{
return 0;
}
return (int) Math.Ceiling(powerCell.GetComponent<PowerCellComponent>().Charge / _baseFireCost);
}
}
public override int Capacity
{
get
{
var powerCell = _powerCellContainer.ContainedEntity;
if (powerCell == null)
{
return 0;
}
return (int) Math.Ceiling(powerCell.GetComponent<PowerCellComponent>().Capacity / _baseFireCost);
}
}
private AppearanceComponent _appearanceComponent;
// Sounds
private string _soundPowerCellInsert;
private string _soundPowerCellEject;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
if (serializer.Reading)
{
_powerCellPrototype = serializer.ReadDataField<string>("powerCellPrototype", null);
}
serializer.DataField(ref _powerCellRemovable, "powerCellRemovable", false);
serializer.DataField(ref _baseFireCost, "fireCost", 300);
serializer.DataField(ref _ammoPrototype, "ammoPrototype", null);
serializer.DataField(ref _lowerChargeLimit, "lowerChargeLimit", 10);
serializer.DataField(ref _soundPowerCellInsert, "soundPowerCellInsert", null);
serializer.DataField(ref _soundPowerCellEject, "soundPowerCellEject", null);
}
public override void Initialize()
{
base.Initialize();
_powerCellContainer = ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-powercell-container", Owner, out var existing);
if (!existing && _powerCellPrototype != null)
{
var powerCellEntity = Owner.EntityManager.SpawnEntity(_powerCellPrototype, Owner.Transform.GridPosition);
_powerCellContainer.Insert(powerCellEntity);
}
if (_ammoPrototype != null)
{
_ammoContainer = ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-ammo-container", Owner);
}
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
{
_appearanceComponent = appearanceComponent;
}
UpdateAppearance();
}
public void UpdateAppearance()
{
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, _powerCellContainer.ContainedEntity != null);
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
}
public override IEntity PeekAmmo()
{
// Spawn a dummy entity because it's easier to work with I guess
// This will get re-used for the projectile
var ammo = _ammoContainer.ContainedEntity;
if (ammo == null)
{
ammo = Owner.EntityManager.SpawnEntity(_ammoPrototype, Owner.Transform.GridPosition);
_ammoContainer.Insert(ammo);
}
return ammo;
}
public override IEntity TakeProjectile()
{
var powerCellEntity = _powerCellContainer.ContainedEntity;
if (powerCellEntity == null)
{
return null;
}
var capacitor = powerCellEntity.GetComponent<PowerCellComponent>();
if (capacitor.Charge < _lowerChargeLimit)
{
return null;
}
// Can fire confirmed
// Multiply the entity's damage / whatever by the percentage of charge the shot has.
IEntity entity;
var chargeChange = Math.Min(capacitor.Charge, _baseFireCost);
capacitor.DeductCharge(chargeChange);
var energyRatio = chargeChange / _baseFireCost;
if (_ammoContainer.ContainedEntity != null)
{
entity = _ammoContainer.ContainedEntity;
_ammoContainer.Remove(entity);
}
else
{
entity = Owner.EntityManager.SpawnEntity(_ammoPrototype, Owner.Transform.GridPosition);
}
if (entity.TryGetComponent(out ProjectileComponent projectileComponent))
{
if (energyRatio < 1.0)
{
var newDamages = new Dictionary<DamageType, int>(projectileComponent.Damages);
foreach (var (damageType, damage) in projectileComponent.Damages)
{
newDamages.Add(damageType, (int) (damage * energyRatio));
}
projectileComponent.Damages = newDamages;
}
} else if (entity.TryGetComponent(out HitscanComponent hitscanComponent))
{
hitscanComponent.Damage *= energyRatio;
hitscanComponent.ColorModifier = energyRatio;
}
else
{
throw new InvalidOperationException("Ammo doesn't have hitscan or projectile?");
}
UpdateAppearance();
//Dirty();
return entity;
}
public bool TryInsertPowerCell(IEntity entity)
{
if (_powerCellContainer.ContainedEntity != null)
{
return false;
}
if (!entity.HasComponent<PowerCellComponent>())
{
return false;
}
if (_soundPowerCellInsert != null)
{
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundPowerCellInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
}
_powerCellContainer.Insert(entity);
UpdateAppearance();
//Dirty();
return true;
}
private IEntity RemovePowerCell()
{
if (!_powerCellRemovable || _powerCellContainer.ContainedEntity == null)
{
return null;
}
var entity = _powerCellContainer.ContainedEntity;
_powerCellContainer.Remove(entity);
if (_soundPowerCellEject != null)
{
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundPowerCellEject, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
}
UpdateAppearance();
//Dirty();
return entity;
}
public override bool UseEntity(UseEntityEventArgs eventArgs)
{
if (!_powerCellRemovable)
{
return false;
}
if (!eventArgs.User.TryGetComponent(out HandsComponent handsComponent) ||
PowerCellEntity == null)
{
return false;
}
var itemComponent = PowerCellEntity.GetComponent<ItemComponent>();
if (!handsComponent.CanPutInHand(itemComponent))
{
return false;
}
var powerCell = RemovePowerCell();
handsComponent.PutInHand(itemComponent);
powerCell.Transform.GridPosition = eventArgs.User.Transform.GridPosition;
return true;
}
public override bool InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!eventArgs.Using.HasComponent<PowerStorageComponent>())
{
return false;
}
return TryInsertPowerCell(eventArgs.Using);
}
}
}

View File

@@ -0,0 +1,457 @@
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects;
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
using Content.Shared.Interfaces;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.Container;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
{
[RegisterComponent]
public sealed class ServerMagazineBarrelComponent : ServerRangedBarrelComponent
{
public override string Name => "MagazineBarrel";
public override uint? NetID => ContentNetIDs.MAGAZINE_BARREL;
private ContainerSlot _chamberContainer;
[ViewVariables] public bool HasMagazine => _magazineContainer.ContainedEntity != null;
private ContainerSlot _magazineContainer;
[ViewVariables] public MagazineType MagazineTypes => _magazineTypes;
private MagazineType _magazineTypes;
[ViewVariables] public BallisticCaliber Caliber => _caliber;
private BallisticCaliber _caliber;
public override int ShotsLeft
{
get
{
var count = 0;
if (_chamberContainer.ContainedEntity != null)
{
count++;
}
var magazine = _magazineContainer.ContainedEntity;
if (magazine != null)
{
count += magazine.GetComponent<RangedMagazineComponent>().ShotsLeft;
}
return count;
}
}
public override int Capacity
{
get
{
// Chamber
var count = 1;
var magazine = _magazineContainer.ContainedEntity;
if (magazine != null)
{
count += magazine.GetComponent<RangedMagazineComponent>().Capacity;
}
return count;
}
}
public bool BoltOpen { get; private set; } = true;
private bool _autoEjectMag;
// If the bolt needs to be open before we can insert / remove the mag (i.e. for LMGs)
public bool MagNeedsOpenBolt => _magNeedsOpenBolt;
private bool _magNeedsOpenBolt;
private AppearanceComponent _appearanceComponent;
// Sounds
private string _soundBoltOpen;
private string _soundBoltClosed;
private string _soundRack;
private string _soundMagInsert;
private string _soundMagEject;
private string _soundAutoEject;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
if (serializer.Reading)
{
var magTypes = serializer.ReadDataField("magazineTypes", new List<MagazineType>());
foreach (var mag in magTypes)
{
_magazineTypes |= mag;
}
}
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
serializer.DataField(ref _autoEjectMag, "autoEjectMag", false);
serializer.DataField(ref _magNeedsOpenBolt, "magNeedsOpenBolt", false);
serializer.DataField(ref _soundBoltOpen, "soundBoltOpen", null);
serializer.DataField(ref _soundBoltClosed, "soundBoltClosed", null);
serializer.DataField(ref _soundRack, "soundRack", null);
serializer.DataField(ref _soundMagInsert, "soundMagInsert", null);
serializer.DataField(ref _soundMagEject, "soundMagEject", null);
serializer.DataField(ref _soundAutoEject, "soundAutoEject", "/Audio/Guns/EmptyAlarm/smg_empty_alarm.ogg");
}
public override ComponentState GetComponentState()
{
(int, int)? count = null;
var magazine = _magazineContainer.ContainedEntity;
if (magazine != null && magazine.TryGetComponent(out RangedMagazineComponent rangedMagazineComponent))
{
count = (rangedMagazineComponent.ShotsLeft, rangedMagazineComponent.Capacity);
}
return new MagazineBarrelComponentState(
_chamberContainer.ContainedEntity != null,
FireRateSelector,
count,
SoundGunshot);
}
public override void Initialize()
{
base.Initialize();
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
{
_appearanceComponent = appearanceComponent;
}
_chamberContainer = ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-chamber", Owner);
_magazineContainer = ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-magazine", Owner);
}
public void ToggleBolt()
{
// For magazines only when we normally set BoltOpen we'll defer the UpdateAppearance until everything is done
// Whereas this will just call it straight up.
BoltOpen = !BoltOpen;
var soundSystem = EntitySystem.Get<AudioSystem>();
if (BoltOpen)
{
if (_soundBoltOpen != null)
{
soundSystem.PlayAtCoords(_soundBoltOpen, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-5));
}
}
else
{
if (_soundBoltClosed != null)
{
soundSystem.PlayAtCoords(_soundBoltClosed, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-5));
}
}
Dirty();
UpdateAppearance();
}
public override IEntity PeekAmmo()
{
return BoltOpen ? null : _chamberContainer.ContainedEntity;
}
public override IEntity TakeProjectile()
{
if (BoltOpen)
{
return null;
}
var entity = _chamberContainer.ContainedEntity;
Cycle();
return entity?.GetComponent<AmmoComponent>().TakeBullet();
}
private void Cycle(bool manual = false)
{
if (BoltOpen)
{
return;
}
var chamberEntity = _chamberContainer.ContainedEntity;
if (chamberEntity != null)
{
_chamberContainer.Remove(chamberEntity);
var ammoComponent = chamberEntity.GetComponent<AmmoComponent>();
if (!ammoComponent.Caseless)
{
EjectCasing(chamberEntity);
}
}
// Try and pull a round from the magazine to replace the chamber if possible
var magazine = _magazineContainer.ContainedEntity;
var nextRound = magazine?.GetComponent<RangedMagazineComponent>().TakeAmmo();
if (nextRound != null)
{
// If you're really into gunporn you could put a sound here
_chamberContainer.Insert(nextRound);
}
var soundSystem = EntitySystem.Get<AudioSystem>();
if (_autoEjectMag && magazine != null && magazine.GetComponent<RangedMagazineComponent>().ShotsLeft == 0)
{
if (_soundAutoEject != null)
{
soundSystem.PlayAtCoords(_soundAutoEject, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
}
_magazineContainer.Remove(magazine);
}
if (nextRound == null && !BoltOpen)
{
if (_soundBoltOpen != null)
{
soundSystem.PlayAtCoords(_soundBoltOpen, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-5));
}
if (ContainerHelpers.TryGetContainer(Owner, out var container))
{
Owner.PopupMessage(container.Owner, Loc.GetString("Bolt open"));
}
BoltOpen = true;
Dirty();
UpdateAppearance();
return;
}
if (manual)
{
if (_soundRack != null)
{
soundSystem.PlayAtCoords(_soundRack, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
}
}
Dirty();
UpdateAppearance();
}
private void UpdateAppearance()
{
_appearanceComponent?.SetData(BarrelBoltVisuals.BoltOpen, BoltOpen);
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, _magazineContainer.ContainedEntity != null);
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
}
public override bool UseEntity(UseEntityEventArgs eventArgs)
{
// 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
if (BoltOpen)
{
if (_soundBoltClosed != null)
{
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundBoltClosed, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-5));
}
Owner.PopupMessage(eventArgs.User, Loc.GetString("Bolt closed"));
BoltOpen = false;
Dirty();
UpdateAppearance();
return true;
}
// Could play a rack-slide specific sound here if you're so inclined (if the chamber is empty but rounds are available)
Cycle(true);
return true;
}
public void RemoveMagazine(IEntity user)
{
var mag = _magazineContainer.ContainedEntity;
if (mag == null)
{
return;
}
if (MagNeedsOpenBolt && !BoltOpen)
{
Owner.PopupMessage(user, Loc.GetString("Bolt needs to be open"));
return;
}
_magazineContainer.Remove(mag);
if (_soundMagEject != null)
{
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundMagEject, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
}
if (user.TryGetComponent(out HandsComponent handsComponent))
{
handsComponent.PutInHandOrDrop(mag.GetComponent<ItemComponent>());
}
Dirty();
UpdateAppearance();
}
public override bool InteractUsing(InteractUsingEventArgs eventArgs)
{
// Insert magazine
if (eventArgs.Using.TryGetComponent(out RangedMagazineComponent magazineComponent))
{
if ((MagazineTypes & magazineComponent.MagazineType) == 0)
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("Wrong magazine type"));
return false;
}
if (magazineComponent.Caliber != _caliber)
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("Wrong caliber"));
return false;
}
if (_magNeedsOpenBolt && !BoltOpen)
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("Need to open bolt first"));
return false;
}
if (_magazineContainer.ContainedEntity == null)
{
if (_soundMagInsert != null)
{
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundMagInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
}
Owner.PopupMessage(eventArgs.User, Loc.GetString("Magazine inserted"));
_magazineContainer.Insert(eventArgs.Using);
Dirty();
UpdateAppearance();
return true;
}
Owner.PopupMessage(eventArgs.User, Loc.GetString("Already holding a magazine"));
return false;
}
// Insert 1 ammo
if (eventArgs.Using.TryGetComponent(out AmmoComponent ammoComponent))
{
if (!BoltOpen)
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("Cannot insert ammo while bolt is closed"));
return false;
}
if (ammoComponent.Caliber != _caliber)
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("Wrong caliber"));
return false;
}
if (_chamberContainer.ContainedEntity == null)
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("Ammo inserted"));
_chamberContainer.Insert(eventArgs.Using);
Dirty();
UpdateAppearance();
return true;
}
Owner.PopupMessage(eventArgs.User, Loc.GetString("Chamber full"));
return false;
}
return false;
}
[Verb]
private sealed class EjectMagazineVerb : Verb<ServerMagazineBarrelComponent>
{
protected override void GetData(IEntity user, ServerMagazineBarrelComponent component, VerbData data)
{
data.Text = Loc.GetString("Eject magazine");
if (component.MagNeedsOpenBolt)
{
data.Visibility = component.HasMagazine && component.BoltOpen
? VerbVisibility.Visible
: VerbVisibility.Disabled;
return;
}
data.Visibility = component.HasMagazine ? VerbVisibility.Visible : VerbVisibility.Disabled;
}
protected override void Activate(IEntity user, ServerMagazineBarrelComponent component)
{
component.RemoveMagazine(user);
}
}
[Verb]
private sealed class OpenBoltVerb : Verb<ServerMagazineBarrelComponent>
{
protected override void GetData(IEntity user, ServerMagazineBarrelComponent component, VerbData data)
{
data.Text = Loc.GetString("Open bolt");
data.Visibility = component.BoltOpen ? VerbVisibility.Disabled : VerbVisibility.Visible;
}
protected override void Activate(IEntity user, ServerMagazineBarrelComponent component)
{
component.ToggleBolt();
}
}
[Verb]
private sealed class CloseBoltVerb : Verb<ServerMagazineBarrelComponent>
{
protected override void GetData(IEntity user, ServerMagazineBarrelComponent component, VerbData data)
{
data.Text = Loc.GetString("Close bolt");
data.Visibility = component.BoltOpen ? VerbVisibility.Visible : VerbVisibility.Disabled;
}
protected override void Activate(IEntity user, ServerMagazineBarrelComponent component)
{
component.ToggleBolt();
}
}
}
[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
}
}

View File

@@ -0,0 +1,415 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Projectiles;
using Content.Server.GameObjects.Components.Sound;
using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Audio;
using Content.Shared.GameObjects.Components.Weapons.Ranged;
using Content.Shared.Physics;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.EntitySystemMessages;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.Interfaces.Physics;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
{
/// <summary>
/// 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.)
/// </summary>
public abstract class ServerRangedBarrelComponent : SharedRangedBarrelComponent, IUse, IInteractUsing
{
// There's still some of py01 and PJB's work left over, especially in underlying shooting logic,
// it's just when I re-organised it changed me as the contributor
#pragma warning disable 649
[Dependency] private IGameTiming _gameTiming;
[Dependency] private IRobustRandom _robustRandom;
#pragma warning restore 649
public override FireRateSelector FireRateSelector => _fireRateSelector;
private FireRateSelector _fireRateSelector;
public override FireRateSelector AllRateSelectors => _fireRateSelector;
private FireRateSelector _allRateSelectors;
public override float FireRate => _fireRate;
private float _fireRate;
// _lastFire is when we actually fired (so if we hold the button then recoil doesn't build up if we're not firing)
private TimeSpan _lastFire;
public abstract IEntity PeekAmmo();
public abstract IEntity TakeProjectile();
// Recoil / spray control
private Angle _minAngle;
private Angle _maxAngle;
private Angle _currentAngle = Angle.Zero;
/// <summary>
/// How slowly the angle's theta decays per second in radians
/// </summary>
private float _angleDecay;
/// <summary>
/// How quickly the angle's theta builds for every shot fired in radians
/// </summary>
private float _angleIncrease;
// Multiplies the ammo spread to get the final spread of each pellet
private float _spreadRatio;
public bool CanMuzzleFlash => _canMuzzleFlash;
private bool _canMuzzleFlash = true;
// Sounds
public string SoundGunshot
{
get => _soundGunshot;
set => _soundGunshot = value;
}
private string _soundGunshot;
public string SoundEmpty => _soundEmpty;
private string _soundEmpty;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _fireRateSelector, "currentSelector", FireRateSelector.Safety);
serializer.DataField(ref _fireRate, "fireRate", 2.0f);
// This hard-to-read area's dealing with recoil
// Use degrees in yaml as it's easier to read compared to "0.0125f"
if (serializer.Reading)
{
var minAngle = serializer.ReadDataField("minAngle", 0) / 2;
_minAngle = Angle.FromDegrees(minAngle);
// Random doubles it as it's +/- so uhh we'll just half it here for readability
var maxAngle = serializer.ReadDataField("maxAngle", 45) / 2;
_maxAngle = Angle.FromDegrees(maxAngle);
var angleIncrease = serializer.ReadDataField("angleIncrease", (40 / _fireRate));
_angleIncrease = angleIncrease * (float) Math.PI / 180;
var angleDecay = serializer.ReadDataField("angleDecay", (float) 20);
_angleDecay = angleDecay * (float) Math.PI / 180;
serializer.DataField(ref _spreadRatio, "ammoSpreadRatio", 1.0f);
// FireRate options
var allFireRates = serializer.ReadDataField("allSelectors", new List<FireRateSelector>());
foreach (var fireRate in allFireRates)
{
_allRateSelectors |= fireRate;
}
}
// 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();
}
serializer.DataField(ref _canMuzzleFlash, "canMuzzleFlash", true);
// Sounds
serializer.DataField(ref _soundGunshot, "soundGunshot", null);
serializer.DataField(ref _soundEmpty, "soundEmpty", "/Audio/Guns/Empty/empty.ogg");
}
public override void OnAdd()
{
base.OnAdd();
var rangedWeapon = Owner.GetComponent<ServerRangedWeaponComponent>();
rangedWeapon.Barrel = this;
rangedWeapon.FireHandler += Fire;
rangedWeapon.WeaponCanFireHandler += WeaponCanFire;
}
public override void OnRemove()
{
base.OnRemove();
var rangedWeapon = Owner.GetComponent<ServerRangedWeaponComponent>();
rangedWeapon.Barrel = null;
rangedWeapon.FireHandler -= Fire;
rangedWeapon.WeaponCanFireHandler -= WeaponCanFire;
}
private Angle GetRecoilAngle(Angle direction)
{
var currentTime = _gameTiming.CurTime;
var timeSinceLastFire = (currentTime - _lastFire).TotalSeconds;
var newTheta = Math.Clamp(_currentAngle.Theta + _angleIncrease - _angleDecay * timeSinceLastFire, _minAngle.Theta, _maxAngle.Theta);
_currentAngle = new Angle(newTheta);
var random = (_robustRandom.NextDouble() - 0.5) * 2;
var angle = Angle.FromDegrees(direction.Degrees + _currentAngle.Degrees * random);
return angle;
}
public abstract bool UseEntity(UseEntityEventArgs eventArgs);
public abstract bool InteractUsing(InteractUsingEventArgs eventArgs);
public void ChangeFireSelector(FireRateSelector rateSelector)
{
if ((rateSelector & AllRateSelectors) != 0)
{
_fireRateSelector = rateSelector;
return;
}
throw new InvalidOperationException();
}
protected virtual bool WeaponCanFire()
{
// If the ServerRangedWeaponComponent gets re-done probably need to add the checks here
return true;
}
private void Fire(IEntity shooter, GridCoordinates target)
{
var soundSystem = EntitySystem.Get<AudioSystem>();
if (ShotsLeft == 0)
{
if (_soundEmpty != null)
{
soundSystem.PlayAtCoords(_soundEmpty, Owner.Transform.GridPosition);
}
return;
}
var ammo = PeekAmmo();
var projectile = TakeProjectile();
if (projectile == null)
{
soundSystem.PlayAtCoords(_soundEmpty, Owner.Transform.GridPosition);
return;
}
// At this point firing is confirmed
var worldPosition = IoCManager.Resolve<IMapManager>().GetGrid(target.GridID).LocalToWorld(target).Position;
var direction = (worldPosition - shooter.Transform.WorldPosition).ToAngle();
var angle = GetRecoilAngle(direction);
// This should really be client-side but for now we'll just leave it here
if (shooter.TryGetComponent(out CameraRecoilComponent recoilComponent))
{
recoilComponent.Kick(-angle.ToVec() * 0.15f);
}
// This section probably needs tweaking so there can be caseless hitscan etc.
if (projectile.TryGetComponent(out HitscanComponent hitscan))
{
FireHitscan(shooter, hitscan, angle);
}
else if (projectile.HasComponent<ProjectileComponent>())
{
var ammoComponent = ammo.GetComponent<AmmoComponent>();
FireProjectiles(shooter, projectile, ammoComponent.ProjectilesFired, ammoComponent.EvenSpreadAngle, angle, ammoComponent.Velocity);
if (CanMuzzleFlash)
{
ammoComponent.MuzzleFlash(Owner.Transform.GridPosition, angle);
}
if (ammoComponent.Caseless)
{
ammo.Delete();
}
}
else
{
// Invalid types
throw new InvalidOperationException();
}
soundSystem.PlayAtCoords(_soundGunshot, Owner.Transform.GridPosition);
_lastFire = _gameTiming.CurTime;
return;
}
/// <summary>
/// Drops a single cartridge / shell
/// Made as a static function just because multiple places need it
/// </summary>
/// <param name="entity"></param>
/// <param name="playSound"></param>
/// <param name="robustRandom"></param>
/// <param name="prototypeManager"></param>
/// <param name="ejectDirections"></param>
public static void EjectCasing(
IEntity entity,
bool playSound = true,
IRobustRandom robustRandom = null,
IPrototypeManager prototypeManager = null,
Direction[] ejectDirections = null)
{
if (robustRandom == null)
{
robustRandom = IoCManager.Resolve<IRobustRandom>();
}
if (ejectDirections == null)
{
ejectDirections = new[] {Direction.East, Direction.North, Direction.South, Direction.West};
}
const float ejectOffset = 0.2f;
var ammo = entity.GetComponent<AmmoComponent>();
var offsetPos = (robustRandom.NextFloat() * ejectOffset, robustRandom.NextFloat() * ejectOffset);
entity.Transform.GridPosition = entity.Transform.GridPosition.Offset(offsetPos);
entity.Transform.LocalRotation = robustRandom.Pick(ejectDirections).ToAngle();
if (ammo.SoundCollectionEject == null || !playSound)
{
return;
}
if (prototypeManager == null)
{
prototypeManager = IoCManager.Resolve<IPrototypeManager>();
}
var soundCollection = prototypeManager.Index<SoundCollectionPrototype>(ammo.SoundCollectionEject);
var randomFile = robustRandom.Pick(soundCollection.PickFiles);
EntitySystem.Get<AudioSystem>().PlayAtCoords(randomFile, entity.Transform.GridPosition, AudioParams.Default.WithVolume(-1));
}
/// <summary>
/// Drops multiple cartridges / shells on the floor
/// Wraps EjectCasing to make it less toxic for bulk ejections
/// </summary>
/// <param name="entities"></param>
public static void EjectCasings(IEnumerable<IEntity> entities)
{
var robustRandom = IoCManager.Resolve<IRobustRandom>();
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var ejectDirections = new[] {Direction.East, Direction.North, Direction.South, Direction.West};
var soundPlayCount = 0;
var playSound = true;
foreach (var entity in entities)
{
EjectCasing(entity, playSound, robustRandom, prototypeManager, ejectDirections);
soundPlayCount++;
if (soundPlayCount > 3)
{
playSound = false;
}
}
}
#region Firing
/// <summary>
/// Handles firing one or many projectiles
/// </summary>
private void FireProjectiles(IEntity shooter, IEntity baseProjectile, int count, float evenSpreadAngle, Angle angle, float velocity)
{
List<Angle> sprayAngleChange = null;
if (count > 1)
{
evenSpreadAngle *= _spreadRatio;
sprayAngleChange = Linspace(-evenSpreadAngle / 2, evenSpreadAngle / 2, count);
}
for (var i = 0; i < count; i++)
{
IEntity projectile;
if (i == 0)
{
projectile = baseProjectile;
}
else
{
projectile =
Owner.EntityManager.SpawnEntity(baseProjectile.Prototype.ID, Owner.Transform.GridPosition);
}
Angle projectileAngle;
if (sprayAngleChange != null)
{
projectileAngle = angle + sprayAngleChange[i];
}
else
{
projectileAngle = angle;
}
var physicsComponent = projectile.GetComponent<PhysicsComponent>();
physicsComponent.Status = BodyStatus.InAir;
projectile.Transform.GridPosition = Owner.Transform.GridPosition;
var projectileComponent = projectile.GetComponent<ProjectileComponent>();
projectileComponent.IgnoreEntity(shooter);
projectile.GetComponent<PhysicsComponent>().LinearVelocity = projectileAngle.ToVec() * velocity;
projectile.Transform.LocalRotation = projectileAngle.Theta;
}
}
/// <summary>
/// Returns a list of numbers that form a set of equal intervals between the start and end value. Used to calculate shotgun spread angles.
/// </summary>
private List<Angle> Linspace(double start, double end, int intervals)
{
DebugTools.Assert(intervals > 1);
var linspace = new List<Angle>(intervals);
for (var i = 0; i <= intervals - 1; i++)
{
linspace.Add(Angle.FromDegrees(start + (end - start) * i / (intervals - 1)));
}
return linspace;
}
/// <summary>
/// Fires hitscan entities and then displays their effects
/// </summary>
private void FireHitscan(IEntity shooter, HitscanComponent hitscan, Angle angle)
{
var ray = new CollisionRay(Owner.Transform.GridPosition.Position, angle.ToVec(), (int) hitscan.CollisionMask);
var physicsManager = IoCManager.Resolve<IPhysicsManager>();
var rayCastResults = physicsManager.IntersectRay(Owner.Transform.MapID, ray, hitscan.MaxLength, shooter, false).ToList();
if (rayCastResults.Count >= 1)
{
var result = rayCastResults[0];
var distance = result.HitEntity != null ? result.Distance : hitscan.MaxLength;
hitscan.FireEffects(shooter, distance, angle, result.HitEntity);
if (result.HitEntity == null || !result.HitEntity.TryGetComponent(out DamageableComponent damageable))
{
return;
}
damageable.TakeDamage(
hitscan.DamageType,
(int)Math.Round(hitscan.Damage, MidpointRounding.AwayFromZero),
Owner,
shooter);
//I used Math.Round over Convert.toInt32, as toInt32 always rounds to
//even numbers if halfway between two numbers, rather than rounding to nearest
}
else
{
hitscan.FireEffects(shooter, hitscan.MaxLength, angle);
}
}
#endregion
}
}