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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user