ECS guns (#6229)
Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>
This commit is contained in:
@@ -3,7 +3,6 @@ using Content.Client.IoC;
|
|||||||
using Content.Client.Items.Components;
|
using Content.Client.Items.Components;
|
||||||
using Content.Client.Resources;
|
using Content.Client.Resources;
|
||||||
using Content.Client.Stylesheets;
|
using Content.Client.Stylesheets;
|
||||||
using Content.Shared.Weapons.Ranged;
|
|
||||||
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
||||||
using Robust.Client.Animations;
|
using Robust.Client.Animations;
|
||||||
using Robust.Client.Graphics;
|
using Robust.Client.Graphics;
|
||||||
@@ -13,8 +12,6 @@ using Robust.Shared.Animations;
|
|||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.GameStates;
|
using Robust.Shared.GameStates;
|
||||||
using Robust.Shared.Maths;
|
using Robust.Shared.Maths;
|
||||||
using Robust.Shared.Network;
|
|
||||||
using Robust.Shared.Players;
|
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.ViewVariables;
|
using Robust.Shared.ViewVariables;
|
||||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||||
@@ -104,18 +101,9 @@ namespace Content.Client.Weapons.Ranged.Barrels.Components
|
|||||||
_statusControl?.Update();
|
_statusControl?.Update();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
|
public void PlayAlarmAnimation()
|
||||||
public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null)
|
|
||||||
{
|
{
|
||||||
base.HandleNetworkMessage(message, channel, session);
|
_statusControl?.PlayAlarmAnimation();
|
||||||
|
|
||||||
switch (message)
|
|
||||||
{
|
|
||||||
|
|
||||||
case MagazineAutoEjectMessage _:
|
|
||||||
_statusControl?.PlayAlarmAnimation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Control MakeControl()
|
public Control MakeControl()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace Content.Client.Weapons.Ranged
|
|||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
public sealed class ClientRangedWeaponComponent : SharedRangedWeaponComponent
|
public sealed class ClientRangedWeaponComponent : SharedRangedWeaponComponent
|
||||||
{
|
{
|
||||||
public FireRateSelector FireRateSelector { get; private set; } = FireRateSelector.Safety;
|
public FireRateSelector FireRateSelector { get; private set; } = FireRateSelector.Automatic;
|
||||||
|
|
||||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||||
{
|
{
|
||||||
@@ -30,12 +30,5 @@ namespace Content.Client.Weapons.Ranged
|
|||||||
|
|
||||||
FireRateSelector = rangedState.FireRateSelector;
|
FireRateSelector = rangedState.FireRateSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SyncFirePos(GridId targetGrid, Vector2 targetPosition)
|
|
||||||
{
|
|
||||||
#pragma warning disable 618
|
|
||||||
SendNetworkMessage(new FirePosComponentMessage(targetGrid, targetPosition));
|
|
||||||
#pragma warning restore 618
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
Content.Client/Weapons/Ranged/GunSystem.AmmoCounter.cs
Normal file
31
Content.Client/Weapons/Ranged/GunSystem.AmmoCounter.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using Content.Client.Weapons.Ranged.Barrels.Components;
|
||||||
|
using Content.Shared.Weapons.Ranged;
|
||||||
|
using Robust.Client.Player;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
|
||||||
|
namespace Content.Client.Weapons.Ranged;
|
||||||
|
|
||||||
|
public sealed class GunSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||||
|
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
SubscribeNetworkEvent<MagazineAutoEjectEvent>(OnMagAutoEject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMagAutoEject(MagazineAutoEjectEvent ev)
|
||||||
|
{
|
||||||
|
var player = _playerManager.LocalPlayer?.ControlledEntity;
|
||||||
|
|
||||||
|
if (!TryComp(ev.Uid, out ClientMagazineBarrelComponent? mag) ||
|
||||||
|
!_container.TryGetContainingContainer(ev.Uid, out var container) ||
|
||||||
|
container.Owner != player) return;
|
||||||
|
|
||||||
|
mag.PlayAlarmAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,20 +85,26 @@ namespace Content.Client.Weapons.Ranged
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_blocked)
|
if (_blocked)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
var worldPos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition);
|
var mapCoordinates = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition);
|
||||||
|
EntityCoordinates coordinates;
|
||||||
|
|
||||||
if (!_mapManager.TryFindGridAt(worldPos, out var grid))
|
if (_mapManager.TryFindGridAt(mapCoordinates, out var grid))
|
||||||
{
|
{
|
||||||
weapon.SyncFirePos(GridId.Invalid, worldPos.Position);
|
coordinates = EntityCoordinates.FromMap(grid.GridEntityId, mapCoordinates);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
weapon.SyncFirePos(grid.Index, grid.MapToGrid(worldPos).Position);
|
coordinates = EntityCoordinates.FromMap(_mapManager.GetMapEntityId(mapCoordinates.MapId), mapCoordinates);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SyncFirePos(coordinates);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SyncFirePos(EntityCoordinates coordinates)
|
||||||
|
{
|
||||||
|
RaiseNetworkEvent(new FirePosEvent(coordinates));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,22 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Content.Server.Hands.Components;
|
|
||||||
using Content.Server.Weapon.Ranged.Barrels.Components;
|
using Content.Server.Weapon.Ranged.Barrels.Components;
|
||||||
using Content.Shared.Examine;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Item;
|
|
||||||
using Content.Shared.Popups;
|
using Content.Shared.Popups;
|
||||||
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Containers;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.IoC;
|
|
||||||
using Robust.Shared.Localization;
|
using Robust.Shared.Localization;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
using Robust.Shared.Utility;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Ammunition.Components
|
namespace Content.Server.Weapon.Ranged.Ammunition.Components
|
||||||
{
|
{
|
||||||
[RegisterComponent]
|
[RegisterComponent, ComponentProtoName("RangedMagazine")]
|
||||||
#pragma warning disable 618
|
public class RangedMagazineComponent : Component
|
||||||
public class RangedMagazineComponent : Component, IMapInit, IInteractUsing, IUse, IExamine
|
|
||||||
#pragma warning restore 618
|
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IEntityManager _entities = default!;
|
public readonly Stack<EntityUid> SpawnedAmmo = new();
|
||||||
|
public Container AmmoContainer = default!;
|
||||||
|
|
||||||
public override string Name => "RangedMagazine";
|
public int ShotsLeft => SpawnedAmmo.Count + UnspawnedCount;
|
||||||
|
|
||||||
private readonly Stack<EntityUid> _spawnedAmmo = new();
|
|
||||||
private Container _ammoContainer = default!;
|
|
||||||
|
|
||||||
public int ShotsLeft => _spawnedAmmo.Count + _unspawnedCount;
|
|
||||||
public int Capacity => _capacity;
|
public int Capacity => _capacity;
|
||||||
[DataField("capacity")]
|
[DataField("capacity")]
|
||||||
private int _capacity = 20;
|
private int _capacity = 20;
|
||||||
@@ -43,137 +28,12 @@ namespace Content.Server.Weapon.Ranged.Ammunition.Components
|
|||||||
[DataField("caliber")]
|
[DataField("caliber")]
|
||||||
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
|
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
|
||||||
|
|
||||||
private AppearanceComponent? _appearanceComponent;
|
|
||||||
|
|
||||||
// If there's anything already in the magazine
|
// If there's anything already in the magazine
|
||||||
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
private string? _fillPrototype;
|
public string? FillPrototype;
|
||||||
|
|
||||||
// By default the magazine won't spawn the entity until needed so we need to keep track of how many left we can spawn
|
// By default the magazine won't spawn the entity until needed so we need to keep track of how many left we can spawn
|
||||||
// Generally you probablt don't want to use this
|
// Generally you probablt don't want to use this
|
||||||
private int _unspawnedCount;
|
public int UnspawnedCount;
|
||||||
|
|
||||||
void IMapInit.MapInit()
|
|
||||||
{
|
|
||||||
if (_fillPrototype != null)
|
|
||||||
{
|
|
||||||
_unspawnedCount += Capacity;
|
|
||||||
}
|
|
||||||
UpdateAppearance();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
_ammoContainer = ContainerHelpers.EnsureContainer<Container>(Owner, $"{Name}-magazine", out var existing);
|
|
||||||
|
|
||||||
if (existing)
|
|
||||||
{
|
|
||||||
if (_ammoContainer.ContainedEntities.Count > Capacity)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Initialized capacity of magazine higher than its actual capacity");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var entity in _ammoContainer.ContainedEntities)
|
|
||||||
{
|
|
||||||
_spawnedAmmo.Push(entity);
|
|
||||||
_unspawnedCount--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_entities.TryGetComponent(Owner, out AppearanceComponent? appearanceComponent))
|
|
||||||
{
|
|
||||||
_appearanceComponent = appearanceComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateAppearance()
|
|
||||||
{
|
|
||||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
|
|
||||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryInsertAmmo(EntityUid user, EntityUid ammo)
|
|
||||||
{
|
|
||||||
if (!_entities.TryGetComponent(ammo, out AmmoComponent? ammoComponent))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ammoComponent.Caliber != _caliber)
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(user, Loc.GetString("ranged-magazine-component-try-insert-ammo-wrong-caliber"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ShotsLeft >= Capacity)
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(user, Loc.GetString("ranged-magazine-component-try-insert-ammo-is-full "));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ammoContainer.Insert(ammo);
|
|
||||||
_spawnedAmmo.Push(ammo);
|
|
||||||
UpdateAppearance();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityUid? TakeAmmo()
|
|
||||||
{
|
|
||||||
EntityUid? ammo = null;
|
|
||||||
// If anything's spawned use that first, otherwise use the fill prototype as a fallback (if we have spawn count left)
|
|
||||||
if (_spawnedAmmo.TryPop(out var entity))
|
|
||||||
{
|
|
||||||
ammo = entity;
|
|
||||||
_ammoContainer.Remove(entity);
|
|
||||||
}
|
|
||||||
else if (_unspawnedCount > 0)
|
|
||||||
{
|
|
||||||
_unspawnedCount--;
|
|
||||||
ammo = _entities.SpawnEntity(_fillPrototype, _entities.GetComponent<TransformComponent>(Owner).Coordinates);
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateAppearance();
|
|
||||||
return ammo;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
return TryInsertAmmo(eventArgs.User, eventArgs.Using);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
if (!_entities.TryGetComponent(eventArgs.User, out HandsComponent? handsComponent))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TakeAmmo() is not {Valid: true} ammo)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var itemComponent = _entities.GetComponent<SharedItemComponent>(ammo);
|
|
||||||
if (!handsComponent.CanPutInHand(itemComponent))
|
|
||||||
{
|
|
||||||
_entities.GetComponent<TransformComponent>(ammo).Coordinates = _entities.GetComponent<TransformComponent>(eventArgs.User).Coordinates;
|
|
||||||
ServerRangedBarrelComponent.EjectCasing(ammo);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
handsComponent.PutInHand(itemComponent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Examine(FormattedMessage message, bool inDetailsRange)
|
|
||||||
{
|
|
||||||
var text = Loc.GetString("ranged-magazine-component-on-examine", ("magazineType", MagazineType),("caliber", Caliber));
|
|
||||||
message.AddMarkup(text);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Content.Server.Hands.Components;
|
|
||||||
using Content.Server.Weapon.Ranged.Barrels.Components;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Item;
|
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Containers;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.IoC;
|
|
||||||
using Robust.Shared.Localization;
|
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
@@ -19,199 +10,21 @@ namespace Content.Server.Weapon.Ranged.Ammunition.Components
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used to load certain ranged weapons quickly
|
/// Used to load certain ranged weapons quickly
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RegisterComponent]
|
[RegisterComponent, ComponentProtoName("SpeedLoader")]
|
||||||
public class SpeedLoaderComponent : Component, IAfterInteract, IInteractUsing, IMapInit, IUse
|
public class SpeedLoaderComponent : Component
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
[DataField("caliber")] public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
|
||||||
|
|
||||||
public override string Name => "SpeedLoader";
|
|
||||||
|
|
||||||
[DataField("caliber")]
|
|
||||||
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
|
|
||||||
public int Capacity => _capacity;
|
public int Capacity => _capacity;
|
||||||
[DataField("capacity")]
|
[DataField("capacity")]
|
||||||
private int _capacity = 6;
|
private int _capacity = 6;
|
||||||
private Container _ammoContainer = default!;
|
|
||||||
private Stack<EntityUid> _spawnedAmmo = new();
|
|
||||||
private int _unspawnedCount;
|
|
||||||
|
|
||||||
public int AmmoLeft => _spawnedAmmo.Count + _unspawnedCount;
|
public Container AmmoContainer = default!;
|
||||||
|
public Stack<EntityUid> SpawnedAmmo = new();
|
||||||
|
public int UnspawnedCount;
|
||||||
|
|
||||||
|
public int AmmoLeft => SpawnedAmmo.Count + UnspawnedCount;
|
||||||
|
|
||||||
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
private string? _fillPrototype;
|
public string? FillPrototype;
|
||||||
|
|
||||||
protected override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
_ammoContainer = ContainerHelpers.EnsureContainer<Container>(Owner, $"{Name}-container", out var existing);
|
|
||||||
|
|
||||||
if (existing)
|
|
||||||
{
|
|
||||||
foreach (var ammo in _ammoContainer.ContainedEntities)
|
|
||||||
{
|
|
||||||
_unspawnedCount--;
|
|
||||||
_spawnedAmmo.Push(ammo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void IMapInit.MapInit()
|
|
||||||
{
|
|
||||||
_unspawnedCount += _capacity;
|
|
||||||
UpdateAppearance();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateAppearance()
|
|
||||||
{
|
|
||||||
if (_entMan.TryGetComponent(Owner, out AppearanceComponent? appearanceComponent))
|
|
||||||
{
|
|
||||||
appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
|
||||||
appearanceComponent?.SetData(AmmoVisuals.AmmoCount, AmmoLeft);
|
|
||||||
appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryInsertAmmo(EntityUid user, EntityUid entity)
|
|
||||||
{
|
|
||||||
if (!_entMan.TryGetComponent(entity, out AmmoComponent? ammoComponent))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ammoComponent.Caliber != _caliber)
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(user, Loc.GetString("speed-loader-component-try-insert-ammo-wrong-caliber"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AmmoLeft >= Capacity)
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(user, Loc.GetString("speed-loader-component-try-insert-ammo-no-room"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_spawnedAmmo.Push(entity);
|
|
||||||
_ammoContainer.Insert(entity);
|
|
||||||
UpdateAppearance();
|
|
||||||
return true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool UseEntity(EntityUid user)
|
|
||||||
{
|
|
||||||
if (!_entMan.TryGetComponent(user, out HandsComponent? handsComponent))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ammo = TakeAmmo();
|
|
||||||
if (ammo == default)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var itemComponent = _entMan.GetComponent<SharedItemComponent>(ammo);
|
|
||||||
if (!handsComponent.CanPutInHand(itemComponent))
|
|
||||||
{
|
|
||||||
ServerRangedBarrelComponent.EjectCasing(ammo);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
handsComponent.PutInHand(itemComponent);
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateAppearance();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private EntityUid TakeAmmo()
|
|
||||||
{
|
|
||||||
if (_spawnedAmmo.TryPop(out var entity))
|
|
||||||
{
|
|
||||||
_ammoContainer.Remove(entity);
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_unspawnedCount > 0)
|
|
||||||
{
|
|
||||||
entity = _entMan.SpawnEntity(_fillPrototype, _entMan.GetComponent<TransformComponent>(Owner).Coordinates);
|
|
||||||
_unspawnedCount--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
if (eventArgs.Target == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This area is dirty but not sure of an easier way to do it besides add an interface or somethin
|
|
||||||
var changed = false;
|
|
||||||
|
|
||||||
var entities = _entMan;
|
|
||||||
if (entities.TryGetComponent(eventArgs.Target.Value, out RevolverBarrelComponent? revolverBarrel))
|
|
||||||
{
|
|
||||||
for (var i = 0; i < Capacity; i++)
|
|
||||||
{
|
|
||||||
var ammo = TakeAmmo();
|
|
||||||
if (ammo == default)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (revolverBarrel.TryInsertBullet(eventArgs.User, ammo))
|
|
||||||
{
|
|
||||||
changed = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take the ammo back
|
|
||||||
TryInsertAmmo(eventArgs.User, ammo);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (_entMan.TryGetComponent(eventArgs.Target.Value, out BoltActionBarrelComponent? boltActionBarrel))
|
|
||||||
{
|
|
||||||
for (var i = 0; i < Capacity; i++)
|
|
||||||
{
|
|
||||||
var ammo = TakeAmmo();
|
|
||||||
if (ammo == default)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (boltActionBarrel.TryInsertBullet(eventArgs.User, ammo))
|
|
||||||
{
|
|
||||||
changed = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take the ammo back
|
|
||||||
TryInsertAmmo(eventArgs.User, ammo);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed)
|
|
||||||
{
|
|
||||||
UpdateAppearance();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
return TryInsertAmmo(eventArgs.User, eventArgs.Using);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
return UseEntity(eventArgs.User);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
using Content.Server.Weapon.Ranged.Barrels.Components;
|
|
||||||
using Content.Shared.ActionBlocker;
|
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Content.Shared.PowerCell.Components;
|
|
||||||
using Content.Shared.Verbs;
|
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
using Robust.Shared.IoC;
|
|
||||||
using Robust.Shared.Localization;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Barrels
|
|
||||||
{
|
|
||||||
public sealed class BarrelSystem : EntitySystem
|
|
||||||
{
|
|
||||||
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
|
|
||||||
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
SubscribeLocalEvent<RevolverBarrelComponent, GetAlternativeVerbsEvent>(AddSpinVerb);
|
|
||||||
|
|
||||||
SubscribeLocalEvent<ServerBatteryBarrelComponent, PowerCellChangedEvent>(OnCellSlotUpdated);
|
|
||||||
|
|
||||||
SubscribeLocalEvent<BoltActionBarrelComponent, GetInteractionVerbsEvent>(AddToggleBoltVerb);
|
|
||||||
|
|
||||||
SubscribeLocalEvent<ServerMagazineBarrelComponent, GetInteractionVerbsEvent>(AddMagazineInteractionVerbs);
|
|
||||||
SubscribeLocalEvent<ServerMagazineBarrelComponent, GetAlternativeVerbsEvent>(AddEjectMagazineVerb);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCellSlotUpdated(EntityUid uid, ServerBatteryBarrelComponent component, PowerCellChangedEvent args)
|
|
||||||
{
|
|
||||||
component.UpdateAppearance();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddSpinVerb(EntityUid uid, RevolverBarrelComponent component, GetAlternativeVerbsEvent args)
|
|
||||||
{
|
|
||||||
if (args.Hands == null || !args.CanAccess || !args.CanInteract)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (component.Capacity <= 1 || component.ShotsLeft == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Verb verb = new();
|
|
||||||
verb.Text = Loc.GetString("spin-revolver-verb-get-data-text");
|
|
||||||
verb.IconTexture = "/Textures/Interface/VerbIcons/refresh.svg.192dpi.png";
|
|
||||||
verb.Act = () =>
|
|
||||||
{
|
|
||||||
component.Spin();
|
|
||||||
component.Owner.PopupMessage(args.User, Loc.GetString("spin-revolver-verb-on-activate"));
|
|
||||||
};
|
|
||||||
args.Verbs.Add(verb);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddToggleBoltVerb(EntityUid uid, BoltActionBarrelComponent component, GetInteractionVerbsEvent args)
|
|
||||||
{
|
|
||||||
if (args.Hands == null ||
|
|
||||||
!args.CanAccess ||
|
|
||||||
!args.CanInteract)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Verb verb = new();
|
|
||||||
verb.Text = component.BoltOpen
|
|
||||||
? Loc.GetString("close-bolt-verb-get-data-text")
|
|
||||||
: Loc.GetString("open-bolt-verb-get-data-text");
|
|
||||||
verb.Act = () => component.BoltOpen = !component.BoltOpen;
|
|
||||||
args.Verbs.Add(verb);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddEjectMagazineVerb(EntityUid uid, ServerMagazineBarrelComponent component, GetAlternativeVerbsEvent args)
|
|
||||||
{
|
|
||||||
if (args.Hands == null ||
|
|
||||||
!args.CanAccess ||
|
|
||||||
!args.CanInteract ||
|
|
||||||
!component.HasMagazine ||
|
|
||||||
!_actionBlockerSystem.CanPickup(args.User))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (component.MagNeedsOpenBolt && !component.BoltOpen)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Verb verb = new();
|
|
||||||
verb.Text = EntityManager.GetComponent<MetaDataComponent>(component.MagazineContainer.ContainedEntity!.Value).EntityName;
|
|
||||||
verb.Category = VerbCategory.Eject;
|
|
||||||
verb.Act = () => component.RemoveMagazine(args.User);
|
|
||||||
args.Verbs.Add(verb);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddMagazineInteractionVerbs(EntityUid uid, ServerMagazineBarrelComponent component, GetInteractionVerbsEvent args)
|
|
||||||
{
|
|
||||||
if (args.Hands == null ||
|
|
||||||
!args.CanAccess ||
|
|
||||||
!args.CanInteract)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Toggle bolt verb
|
|
||||||
Verb toggleBolt = new();
|
|
||||||
toggleBolt.Text = component.BoltOpen
|
|
||||||
? Loc.GetString("close-bolt-verb-get-data-text")
|
|
||||||
: Loc.GetString("open-bolt-verb-get-data-text");
|
|
||||||
toggleBolt.Act = () => component.BoltOpen = !component.BoltOpen;
|
|
||||||
args.Verbs.Add(toggleBolt);
|
|
||||||
|
|
||||||
// Are we holding a mag that we can insert?
|
|
||||||
if (args.Using is not {Valid: true} @using ||
|
|
||||||
!component.CanInsertMagazine(args.User, @using) ||
|
|
||||||
!_actionBlockerSystem.CanDrop(args.User))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Insert mag verb
|
|
||||||
Verb insert = new();
|
|
||||||
insert.Text = EntityManager.GetComponent<MetaDataComponent>(@using).EntityName;
|
|
||||||
insert.Category = VerbCategory.Insert;
|
|
||||||
insert.Act = () => component.InsertMagazine(args.User, @using);
|
|
||||||
args.Verbs.Add(insert);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
using Content.Server.PowerCell;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
using Robust.Shared.ViewVariables;
|
||||||
|
|
||||||
|
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
||||||
|
{
|
||||||
|
[RegisterComponent, NetworkedComponent, ComponentProtoName("BatteryBarrel"), ComponentReference(typeof(ServerRangedBarrelComponent))]
|
||||||
|
public sealed class BatteryBarrelComponent : ServerRangedBarrelComponent
|
||||||
|
{
|
||||||
|
// The minimum change we need before we can fire
|
||||||
|
[DataField("lowerChargeLimit")]
|
||||||
|
[ViewVariables]
|
||||||
|
public float LowerChargeLimit = 10;
|
||||||
|
|
||||||
|
[DataField("fireCost")]
|
||||||
|
[ViewVariables]
|
||||||
|
public int BaseFireCost = 300;
|
||||||
|
|
||||||
|
// What gets fired
|
||||||
|
[DataField("ammoPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
|
[ViewVariables]
|
||||||
|
public string? AmmoPrototype;
|
||||||
|
|
||||||
|
public ContainerSlot AmmoContainer = default!;
|
||||||
|
|
||||||
|
public override int ShotsLeft
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
|
||||||
|
if (!EntitySystem.Get<PowerCellSystem>().TryGetBatteryFromSlot(Owner, out var battery))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) Math.Ceiling(battery.CurrentCharge / BaseFireCost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Capacity
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!EntitySystem.Get<PowerCellSystem>().TryGetBatteryFromSlot(Owner, out var battery))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) Math.Ceiling(battery.MaxCharge / BaseFireCost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,14 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
||||||
using Content.Shared.Examine;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Content.Shared.Sound;
|
using Content.Shared.Sound;
|
||||||
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Containers;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.GameStates;
|
using Robust.Shared.GameStates;
|
||||||
using Robust.Shared.IoC;
|
|
||||||
using Robust.Shared.Localization;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
using Robust.Shared.Utility;
|
|
||||||
using Robust.Shared.ViewVariables;
|
using Robust.Shared.ViewVariables;
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
||||||
@@ -25,42 +16,39 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Shotguns mostly
|
/// Shotguns mostly
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RegisterComponent]
|
[RegisterComponent, NetworkedComponent, ComponentProtoName("BoltActionBarrel"), ComponentReference(typeof(ServerRangedBarrelComponent))]
|
||||||
[NetworkedComponent()]
|
public sealed class BoltActionBarrelComponent : ServerRangedBarrelComponent
|
||||||
#pragma warning disable 618
|
|
||||||
public sealed class BoltActionBarrelComponent : ServerRangedBarrelComponent, IUse, IInteractUsing, IMapInit
|
|
||||||
#pragma warning restore 618
|
|
||||||
{
|
{
|
||||||
// Originally I had this logic shared with PumpBarrel and used a couple of variables to control things
|
// 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
|
// but it felt a lot messier to play around with, especially when adding verbs
|
||||||
|
|
||||||
public override string Name => "BoltActionBarrel";
|
|
||||||
|
|
||||||
public override int ShotsLeft
|
public override int ShotsLeft
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var chamberCount = _chamberContainer.ContainedEntity != null ? 1 : 0;
|
var chamberCount = ChamberContainer.ContainedEntity != null ? 1 : 0;
|
||||||
return chamberCount + _spawnedAmmo.Count + _unspawnedCount;
|
return chamberCount + SpawnedAmmo.Count + UnspawnedCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public override int Capacity => _capacity;
|
public override int Capacity => _capacity;
|
||||||
[DataField("capacity")]
|
|
||||||
private int _capacity = 6;
|
|
||||||
|
|
||||||
private ContainerSlot _chamberContainer = default!;
|
[DataField("capacity")]
|
||||||
private Stack<EntityUid> _spawnedAmmo = default!;
|
internal int _capacity = 6;
|
||||||
private Container _ammoContainer = default!;
|
|
||||||
|
public ContainerSlot ChamberContainer = default!;
|
||||||
|
public Stack<EntityUid> SpawnedAmmo = default!;
|
||||||
|
public Container AmmoContainer = default!;
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
[DataField("caliber")]
|
[DataField("caliber")]
|
||||||
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
|
public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
private string? _fillPrototype;
|
public string? FillPrototype;
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
private int _unspawnedCount;
|
public int UnspawnedCount;
|
||||||
|
|
||||||
public bool BoltOpen
|
public bool BoltOpen
|
||||||
{
|
{
|
||||||
@@ -72,269 +60,34 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var gunSystem = EntitySystem.Get<GunSystem>();
|
||||||
|
|
||||||
if (value)
|
if (value)
|
||||||
{
|
{
|
||||||
TryEjectChamber();
|
gunSystem.TryEjectChamber(this);
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltOpen.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltOpen.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
TryFeedChamber();
|
gunSystem.TryFeedChamber(this);
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltClosed.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltClosed.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
||||||
}
|
}
|
||||||
|
|
||||||
_boltOpen = value;
|
_boltOpen = value;
|
||||||
UpdateAppearance();
|
gunSystem.UpdateBoltAppearance(this);
|
||||||
Dirty();
|
Dirty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private bool _boltOpen;
|
private bool _boltOpen;
|
||||||
[DataField("autoCycle")]
|
|
||||||
private bool _autoCycle;
|
|
||||||
|
|
||||||
private AppearanceComponent? _appearanceComponent;
|
[DataField("autoCycle")] public bool AutoCycle;
|
||||||
|
|
||||||
// Sounds
|
// Sounds
|
||||||
[DataField("soundCycle")]
|
[DataField("soundCycle")] public SoundSpecifier SoundCycle = new SoundPathSpecifier("/Audio/Weapons/Guns/Cock/sf_rifle_cock.ogg");
|
||||||
private SoundSpecifier _soundCycle = new SoundPathSpecifier("/Audio/Weapons/Guns/Cock/sf_rifle_cock.ogg");
|
|
||||||
[DataField("soundBoltOpen")]
|
[DataField("soundBoltOpen")]
|
||||||
private SoundSpecifier _soundBoltOpen = new SoundPathSpecifier("/Audio/Weapons/Guns/Bolt/rifle_bolt_open.ogg");
|
private SoundSpecifier _soundBoltOpen = new SoundPathSpecifier("/Audio/Weapons/Guns/Bolt/rifle_bolt_open.ogg");
|
||||||
[DataField("soundBoltClosed")]
|
[DataField("soundBoltClosed")]
|
||||||
private SoundSpecifier _soundBoltClosed = new SoundPathSpecifier("/Audio/Weapons/Guns/Bolt/rifle_bolt_closed.ogg");
|
private SoundSpecifier _soundBoltClosed = new SoundPathSpecifier("/Audio/Weapons/Guns/Bolt/rifle_bolt_closed.ogg");
|
||||||
[DataField("soundInsert")]
|
[DataField("soundInsert")] public SoundSpecifier SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/bullet_insert.ogg");
|
||||||
private SoundSpecifier _soundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/bullet_insert.ogg");
|
|
||||||
|
|
||||||
void IMapInit.MapInit()
|
|
||||||
{
|
|
||||||
if (_fillPrototype != null)
|
|
||||||
{
|
|
||||||
_unspawnedCount += Capacity;
|
|
||||||
if (_unspawnedCount > 0)
|
|
||||||
{
|
|
||||||
_unspawnedCount--;
|
|
||||||
var chamberEntity = Entities.SpawnEntity(_fillPrototype, Entities.GetComponent<TransformComponent>(Owner).Coordinates);
|
|
||||||
_chamberContainer.Insert(chamberEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
UpdateAppearance();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ComponentState GetComponentState()
|
|
||||||
{
|
|
||||||
(int, int)? count = (ShotsLeft, Capacity);
|
|
||||||
var chamberedExists = _chamberContainer.ContainedEntity != null;
|
|
||||||
// (Is one chambered?, is the bullet spend)
|
|
||||||
var chamber = (chamberedExists, false);
|
|
||||||
|
|
||||||
if (chamberedExists && Entities.TryGetComponent<AmmoComponent?>(_chamberContainer.ContainedEntity!.Value, out var ammo))
|
|
||||||
{
|
|
||||||
chamber.Item2 = ammo.Spent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new BoltActionBarrelComponentState(
|
|
||||||
chamber,
|
|
||||||
FireRateSelector,
|
|
||||||
count,
|
|
||||||
SoundGunshot.GetSound());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Initialize()
|
|
||||||
{
|
|
||||||
// TODO: Add existing ammo support on revolvers
|
|
||||||
base.Initialize();
|
|
||||||
_spawnedAmmo = new Stack<EntityUid>(_capacity - 1);
|
|
||||||
_ammoContainer = ContainerHelpers.EnsureContainer<Container>(Owner, $"{Name}-ammo-container", out var existing);
|
|
||||||
|
|
||||||
if (existing)
|
|
||||||
{
|
|
||||||
foreach (var entity in _ammoContainer.ContainedEntities)
|
|
||||||
{
|
|
||||||
_spawnedAmmo.Push(entity);
|
|
||||||
_unspawnedCount--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_chamberContainer = ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-chamber-container");
|
|
||||||
|
|
||||||
if (Entities.TryGetComponent(Owner, out AppearanceComponent? appearanceComponent))
|
|
||||||
{
|
|
||||||
_appearanceComponent = appearanceComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
|
||||||
Dirty();
|
|
||||||
UpdateAppearance();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateAppearance()
|
|
||||||
{
|
|
||||||
_appearanceComponent?.SetData(BarrelBoltVisuals.BoltOpen, BoltOpen);
|
|
||||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
|
|
||||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override EntityUid? PeekAmmo()
|
|
||||||
{
|
|
||||||
return _chamberContainer.ContainedEntity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override EntityUid? TakeProjectile(EntityCoordinates spawnAt)
|
|
||||||
{
|
|
||||||
if (_autoCycle)
|
|
||||||
{
|
|
||||||
Cycle();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Dirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_chamberContainer.ContainedEntity is not {Valid: true} chamberEntity) return null;
|
|
||||||
|
|
||||||
var ammoComponent = Entities.GetComponentOrNull<AmmoComponent>(chamberEntity);
|
|
||||||
|
|
||||||
return ammoComponent == null ? null : EntitySystem.Get<GunSystem>().TakeBullet(ammoComponent, spawnAt);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool WeaponCanFire()
|
|
||||||
{
|
|
||||||
if (!base.WeaponCanFire())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !BoltOpen && _chamberContainer.ContainedEntity != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Cycle(bool manual = false)
|
|
||||||
{
|
|
||||||
TryEjectChamber();
|
|
||||||
TryFeedChamber();
|
|
||||||
|
|
||||||
if (_chamberContainer.ContainedEntity == null && manual)
|
|
||||||
{
|
|
||||||
BoltOpen = true;
|
|
||||||
if (Owner.TryGetContainer(out var container))
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(container.Owner, Loc.GetString("bolt-action-barrel-component-bolt-opened"));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundCycle.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
}
|
|
||||||
|
|
||||||
Dirty();
|
|
||||||
UpdateAppearance();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryInsertBullet(EntityUid user, EntityUid ammo)
|
|
||||||
{
|
|
||||||
if (!Entities.TryGetComponent(ammo, out AmmoComponent? ammoComponent))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ammoComponent.Caliber != _caliber)
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(user, Loc.GetString("bolt-action-barrel-component-try-insert-bullet-wrong-caliber"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!BoltOpen)
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(user, Loc.GetString("bolt-action-barrel-component-try-insert-bullet-bolt-closed"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_chamberContainer.ContainedEntity == null)
|
|
||||||
{
|
|
||||||
_chamberContainer.Insert(ammo);
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundInsert.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
Dirty();
|
|
||||||
UpdateAppearance();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_ammoContainer.ContainedEntities.Count < Capacity - 1)
|
|
||||||
{
|
|
||||||
_ammoContainer.Insert(ammo);
|
|
||||||
_spawnedAmmo.Push(ammo);
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundInsert.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
Dirty();
|
|
||||||
UpdateAppearance();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Owner.PopupMessage(user, Loc.GetString("bolt-action-barrel-component-try-insert-bullet-no-room"));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool UseEntity(UseEntityEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
if (BoltOpen)
|
|
||||||
{
|
|
||||||
BoltOpen = false;
|
|
||||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("bolt-action-barrel-component-bolt-closed"));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Cycle(true);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
return TryInsertBullet(eventArgs.User, eventArgs.Using);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryEjectChamber()
|
|
||||||
{
|
|
||||||
if (_chamberContainer.ContainedEntity is {Valid: true} chambered)
|
|
||||||
{
|
|
||||||
if (!_chamberContainer.Remove(chambered))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!Entities.GetComponent<AmmoComponent>(chambered).Caseless)
|
|
||||||
{
|
|
||||||
EjectCasing(chambered);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryFeedChamber()
|
|
||||||
{
|
|
||||||
if (_chamberContainer.ContainedEntity != null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (_spawnedAmmo.TryPop(out var next))
|
|
||||||
{
|
|
||||||
_ammoContainer.Remove(next);
|
|
||||||
_chamberContainer.Insert(next);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (_unspawnedCount > 0)
|
|
||||||
{
|
|
||||||
_unspawnedCount--;
|
|
||||||
var ammoEntity = Entities.SpawnEntity(_fillPrototype, Entities.GetComponent<TransformComponent>(Owner).Coordinates);
|
|
||||||
_chamberContainer.Insert(ammoEntity);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Examine(FormattedMessage message, bool inDetailsRange)
|
|
||||||
{
|
|
||||||
base.Examine(message, inDetailsRange);
|
|
||||||
|
|
||||||
message.AddMarkup("\n" + Loc.GetString("bolt-action-barrel-component-on-examine", ("caliber", _caliber)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
using System;
|
||||||
|
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
||||||
|
using Content.Shared.Sound;
|
||||||
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
using Robust.Shared.ViewVariables;
|
||||||
|
|
||||||
|
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
||||||
|
{
|
||||||
|
[RegisterComponent, NetworkedComponent, ComponentProtoName("MagazineBarrel"), ComponentReference(typeof(ServerRangedBarrelComponent))]
|
||||||
|
public sealed class MagazineBarrelComponent : ServerRangedBarrelComponent
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IEntityManager _entities = default!;
|
||||||
|
|
||||||
|
[ViewVariables] public ContainerSlot ChamberContainer = default!;
|
||||||
|
[ViewVariables] public bool HasMagazine => MagazineContainer.ContainedEntity != null;
|
||||||
|
public ContainerSlot MagazineContainer = default!;
|
||||||
|
|
||||||
|
[ViewVariables] public MagazineType MagazineTypes => _magazineTypes;
|
||||||
|
[DataField("magazineTypes")]
|
||||||
|
private MagazineType _magazineTypes = default;
|
||||||
|
[ViewVariables] public BallisticCaliber Caliber => _caliber;
|
||||||
|
[DataField("caliber")]
|
||||||
|
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
|
||||||
|
|
||||||
|
public override int ShotsLeft
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
if (ChamberContainer.ContainedEntity != null)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MagazineContainer.ContainedEntity is {Valid: true} magazine)
|
||||||
|
{
|
||||||
|
count += _entities.GetComponent<RangedMagazineComponent>(magazine).ShotsLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Capacity
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// Chamber
|
||||||
|
var count = 1;
|
||||||
|
if (MagazineContainer.ContainedEntity is {Valid: true} magazine)
|
||||||
|
{
|
||||||
|
count += _entities.GetComponent<RangedMagazineComponent>(magazine).Capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataField("magFillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
|
public string? MagFillPrototype;
|
||||||
|
|
||||||
|
public bool BoltOpen
|
||||||
|
{
|
||||||
|
get => _boltOpen;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_boltOpen == value)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gunSystem = EntitySystem.Get<GunSystem>();
|
||||||
|
|
||||||
|
if (value)
|
||||||
|
{
|
||||||
|
gunSystem.TryEjectChamber(this);
|
||||||
|
SoundSystem.Play(Filter.Pvs(Owner), SoundBoltOpen.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
gunSystem.TryFeedChamber(this);
|
||||||
|
SoundSystem.Play(Filter.Pvs(Owner), SoundBoltClosed.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
_boltOpen = value;
|
||||||
|
gunSystem.UpdateMagazineAppearance(this);
|
||||||
|
Dirty(_entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private bool _boltOpen = true;
|
||||||
|
|
||||||
|
[DataField("autoEjectMag")] public bool AutoEjectMag;
|
||||||
|
// If the bolt needs to be open before we can insert / remove the mag (i.e. for LMGs)
|
||||||
|
public bool MagNeedsOpenBolt => _magNeedsOpenBolt;
|
||||||
|
[DataField("magNeedsOpenBolt")]
|
||||||
|
private bool _magNeedsOpenBolt = default;
|
||||||
|
|
||||||
|
// Sounds
|
||||||
|
[DataField("soundBoltOpen", required: true)]
|
||||||
|
public SoundSpecifier SoundBoltOpen = default!;
|
||||||
|
[DataField("soundBoltClosed", required: true)]
|
||||||
|
public SoundSpecifier SoundBoltClosed = default!;
|
||||||
|
[DataField("soundRack", required: true)]
|
||||||
|
public SoundSpecifier SoundRack = default!;
|
||||||
|
[DataField("soundMagInsert", required: true)]
|
||||||
|
public SoundSpecifier SoundMagInsert = default!;
|
||||||
|
[DataField("soundMagEject", required: true)]
|
||||||
|
public SoundSpecifier SoundMagEject = default!;
|
||||||
|
[DataField("soundAutoEject")] public SoundSpecifier SoundAutoEject = new SoundPathSpecifier("/Audio/Weapons/Guns/EmptyAlarm/smg_empty_alarm.ogg");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum MagazineType
|
||||||
|
{
|
||||||
|
Unspecified = 0,
|
||||||
|
LPistol = 1 << 0, // Placeholder?
|
||||||
|
Pistol = 1 << 1,
|
||||||
|
HCPistol = 1 << 2,
|
||||||
|
Smg = 1 << 3,
|
||||||
|
SmgTopMounted = 1 << 4,
|
||||||
|
Rifle = 1 << 5,
|
||||||
|
IH = 1 << 6, // Placeholder?
|
||||||
|
Box = 1 << 7,
|
||||||
|
Pan = 1 << 8,
|
||||||
|
Dart = 1 << 9, // Placeholder
|
||||||
|
CalicoTopMounted = 1 << 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,13 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Content.Shared.Sound;
|
using Content.Shared.Sound;
|
||||||
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Containers;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.GameStates;
|
using Robust.Shared.GameStates;
|
||||||
using Robust.Shared.IoC;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Localization;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
using Robust.Shared.ViewVariables;
|
using Robust.Shared.ViewVariables;
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
||||||
@@ -23,18 +15,15 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Bolt-action rifles
|
/// Bolt-action rifles
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RegisterComponent]
|
[RegisterComponent, NetworkedComponent, ComponentProtoName("PumpBarrel"), ComponentReference(typeof(ServerRangedBarrelComponent))]
|
||||||
[NetworkedComponent()]
|
public sealed class PumpBarrelComponent : ServerRangedBarrelComponent, ISerializationHooks
|
||||||
public sealed class PumpBarrelComponent : ServerRangedBarrelComponent, IUse, IInteractUsing, IMapInit, ISerializationHooks
|
|
||||||
{
|
{
|
||||||
public override string Name => "PumpBarrel";
|
|
||||||
|
|
||||||
public override int ShotsLeft
|
public override int ShotsLeft
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var chamberCount = _chamberContainer.ContainedEntity != null ? 1 : 0;
|
var chamberCount = ChamberContainer.ContainedEntity != null ? 1 : 0;
|
||||||
return chamberCount + _spawnedAmmo.Count + _unspawnedCount;
|
return chamberCount + SpawnedAmmo.Count + UnspawnedCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,204 +32,30 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
|
|||||||
public override int Capacity { get; } = DefaultCapacity;
|
public override int Capacity { get; } = DefaultCapacity;
|
||||||
|
|
||||||
// Even a point having a chamber? I guess it makes some of the below code cleaner
|
// Even a point having a chamber? I guess it makes some of the below code cleaner
|
||||||
private ContainerSlot _chamberContainer = default!;
|
public ContainerSlot ChamberContainer = default!;
|
||||||
private Stack<EntityUid> _spawnedAmmo = new(DefaultCapacity - 1);
|
public Stack<EntityUid> SpawnedAmmo = new(DefaultCapacity - 1);
|
||||||
private Container _ammoContainer = default!;
|
public Container AmmoContainer = default!;
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
[DataField("caliber")]
|
[DataField("caliber")]
|
||||||
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
|
public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
[DataField("fillPrototype")]
|
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
private string? _fillPrototype;
|
public string? FillPrototype;
|
||||||
[ViewVariables]
|
|
||||||
private int _unspawnedCount;
|
|
||||||
|
|
||||||
[DataField("manualCycle")]
|
[ViewVariables] public int UnspawnedCount;
|
||||||
private bool _manualCycle = true;
|
|
||||||
|
|
||||||
private AppearanceComponent? _appearanceComponent;
|
[DataField("manualCycle")] public bool ManualCycle = true;
|
||||||
|
|
||||||
// Sounds
|
// Sounds
|
||||||
[DataField("soundCycle")]
|
[DataField("soundCycle")] public SoundSpecifier SoundCycle = new SoundPathSpecifier("/Audio/Weapons/Guns/Cock/sf_rifle_cock.ogg");
|
||||||
private SoundSpecifier _soundCycle = new SoundPathSpecifier("/Audio/Weapons/Guns/Cock/sf_rifle_cock.ogg");
|
|
||||||
|
|
||||||
[DataField("soundInsert")]
|
[DataField("soundInsert")] public SoundSpecifier SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/bullet_insert.ogg");
|
||||||
private SoundSpecifier _soundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/bullet_insert.ogg");
|
|
||||||
|
|
||||||
void IMapInit.MapInit()
|
|
||||||
{
|
|
||||||
if (_fillPrototype != null)
|
|
||||||
{
|
|
||||||
_unspawnedCount += Capacity - 1;
|
|
||||||
}
|
|
||||||
UpdateAppearance();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ComponentState GetComponentState()
|
|
||||||
{
|
|
||||||
(int, int)? count = (ShotsLeft, Capacity);
|
|
||||||
var chamberedExists = _chamberContainer.ContainedEntity != null;
|
|
||||||
// (Is one chambered?, is the bullet spend)
|
|
||||||
var chamber = (chamberedExists, false);
|
|
||||||
|
|
||||||
if (chamberedExists && Entities.TryGetComponent<AmmoComponent?>(_chamberContainer.ContainedEntity!.Value, out var ammo))
|
|
||||||
{
|
|
||||||
chamber.Item2 = ammo.Spent;
|
|
||||||
}
|
|
||||||
return new PumpBarrelComponentState(
|
|
||||||
chamber,
|
|
||||||
FireRateSelector,
|
|
||||||
count,
|
|
||||||
SoundGunshot.GetSound());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ISerializationHooks.AfterDeserialization()
|
void ISerializationHooks.AfterDeserialization()
|
||||||
{
|
{
|
||||||
_spawnedAmmo = new Stack<EntityUid>(Capacity - 1);
|
SpawnedAmmo = new Stack<EntityUid>(Capacity - 1);
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
|
|
||||||
_ammoContainer =
|
|
||||||
ContainerHelpers.EnsureContainer<Container>(Owner, $"{Name}-ammo-container", out var existing);
|
|
||||||
|
|
||||||
if (existing)
|
|
||||||
{
|
|
||||||
foreach (var entity in _ammoContainer.ContainedEntities)
|
|
||||||
{
|
|
||||||
_spawnedAmmo.Push(entity);
|
|
||||||
_unspawnedCount--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_chamberContainer =
|
|
||||||
ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-chamber-container", out existing);
|
|
||||||
if (existing)
|
|
||||||
{
|
|
||||||
_unspawnedCount--;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Entities.TryGetComponent(Owner, out AppearanceComponent? appearanceComponent))
|
|
||||||
{
|
|
||||||
_appearanceComponent = appearanceComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
|
||||||
Dirty();
|
|
||||||
UpdateAppearance();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateAppearance()
|
|
||||||
{
|
|
||||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
|
|
||||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override EntityUid? PeekAmmo()
|
|
||||||
{
|
|
||||||
return _chamberContainer.ContainedEntity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override EntityUid? TakeProjectile(EntityCoordinates spawnAt)
|
|
||||||
{
|
|
||||||
if (!_manualCycle)
|
|
||||||
{
|
|
||||||
Cycle();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Dirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_chamberContainer.ContainedEntity is not {Valid: true} chamberEntity) return null;
|
|
||||||
|
|
||||||
var ammoComponent = Entities.GetComponentOrNull<AmmoComponent>(chamberEntity);
|
|
||||||
|
|
||||||
return ammoComponent == null ? null : EntitySystem.Get<GunSystem>().TakeBullet(ammoComponent, spawnAt);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Cycle(bool manual = false)
|
|
||||||
{
|
|
||||||
if (_chamberContainer.ContainedEntity is {Valid: true} chamberedEntity)
|
|
||||||
{
|
|
||||||
_chamberContainer.Remove(chamberedEntity);
|
|
||||||
var ammoComponent = Entities.GetComponent<AmmoComponent>(chamberedEntity);
|
|
||||||
if (!ammoComponent.Caseless)
|
|
||||||
{
|
|
||||||
EjectCasing(chamberedEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_spawnedAmmo.TryPop(out var next))
|
|
||||||
{
|
|
||||||
_ammoContainer.Remove(next);
|
|
||||||
_chamberContainer.Insert(next);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_unspawnedCount > 0)
|
|
||||||
{
|
|
||||||
_unspawnedCount--;
|
|
||||||
var ammoEntity = Entities.SpawnEntity(_fillPrototype, Entities.GetComponent<TransformComponent>(Owner).Coordinates);
|
|
||||||
_chamberContainer.Insert(ammoEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (manual)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundCycle.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
}
|
|
||||||
|
|
||||||
Dirty();
|
|
||||||
UpdateAppearance();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryInsertBullet(InteractUsingEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
if (!Entities.TryGetComponent(eventArgs.Using, out AmmoComponent? ammoComponent))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ammoComponent.Caliber != _caliber)
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("pump-barrel-component-try-insert-bullet-wrong-caliber"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_ammoContainer.ContainedEntities.Count < Capacity - 1)
|
|
||||||
{
|
|
||||||
_ammoContainer.Insert(eventArgs.Using);
|
|
||||||
_spawnedAmmo.Push(eventArgs.Using);
|
|
||||||
Dirty();
|
|
||||||
UpdateAppearance();
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundInsert.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("pump-barrel-component-try-insert-bullet-no-room"));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool UseEntity(UseEntityEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
Cycle(true);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
return TryInsertBullet(eventArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Examine(FormattedMessage message, bool inDetailsRange)
|
|
||||||
{
|
|
||||||
base.Examine(message, inDetailsRange);
|
|
||||||
|
|
||||||
message.AddMarkup("\n" + Loc.GetString("pump-barrel-component-on-examine", ("caliber", _caliber)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Content.Shared.Sound;
|
using Content.Shared.Sound;
|
||||||
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Analyzers;
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Containers;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.GameStates;
|
using Robust.Shared.GameStates;
|
||||||
using Robust.Shared.IoC;
|
|
||||||
using Robust.Shared.Localization;
|
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Player;
|
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
|
||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
@@ -22,248 +15,53 @@ using Robust.Shared.ViewVariables;
|
|||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
||||||
{
|
{
|
||||||
[RegisterComponent]
|
[RegisterComponent, ComponentProtoName("RevolverBarrel"), NetworkedComponent, ComponentReference(typeof(ServerRangedBarrelComponent))]
|
||||||
[NetworkedComponent()]
|
public sealed class RevolverBarrelComponent : ServerRangedBarrelComponent, ISerializationHooks
|
||||||
public sealed class RevolverBarrelComponent : ServerRangedBarrelComponent, IUse, IInteractUsing, ISerializationHooks
|
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
|
||||||
|
|
||||||
public override string Name => "RevolverBarrel";
|
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
[DataField("caliber")]
|
[DataField("caliber")]
|
||||||
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
|
public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
|
||||||
|
|
||||||
private Container _ammoContainer = default!;
|
public Container AmmoContainer = default!;
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
private int _currentSlot;
|
public int CurrentSlot;
|
||||||
|
|
||||||
public override int Capacity => _ammoSlots.Length;
|
public override int Capacity => AmmoSlots.Length;
|
||||||
|
|
||||||
[DataField("capacity")]
|
[DataField("capacity")]
|
||||||
private int _serializedCapacity = 6;
|
private int _serializedCapacity = 6;
|
||||||
|
|
||||||
[DataField("ammoSlots", readOnly: true)]
|
[DataField("ammoSlots", readOnly: true)]
|
||||||
private EntityUid[] _ammoSlots = Array.Empty<EntityUid>();
|
public EntityUid?[] AmmoSlots = Array.Empty<EntityUid?>();
|
||||||
|
|
||||||
public override int ShotsLeft => _ammoContainer.ContainedEntities.Count;
|
public override int ShotsLeft => AmmoContainer.ContainedEntities.Count;
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
private string? _fillPrototype;
|
public string? FillPrototype;
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
private int _unspawnedCount;
|
public int UnspawnedCount;
|
||||||
|
|
||||||
// Sounds
|
// Sounds
|
||||||
[DataField("soundEject")]
|
[DataField("soundEject")]
|
||||||
private SoundSpecifier _soundEject = new SoundPathSpecifier("/Audio/Weapons/Guns/MagOut/revolver_magout.ogg");
|
public SoundSpecifier SoundEject = new SoundPathSpecifier("/Audio/Weapons/Guns/MagOut/revolver_magout.ogg");
|
||||||
|
|
||||||
[DataField("soundInsert")]
|
[DataField("soundInsert")]
|
||||||
private SoundSpecifier _soundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/revolver_magin.ogg");
|
public SoundSpecifier SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/revolver_magin.ogg");
|
||||||
|
|
||||||
[DataField("soundSpin")]
|
[DataField("soundSpin")]
|
||||||
private SoundSpecifier _soundSpin = new SoundPathSpecifier("/Audio/Weapons/Guns/Misc/revolver_spin.ogg");
|
public SoundSpecifier SoundSpin = new SoundPathSpecifier("/Audio/Weapons/Guns/Misc/revolver_spin.ogg");
|
||||||
|
|
||||||
void ISerializationHooks.BeforeSerialization()
|
void ISerializationHooks.BeforeSerialization()
|
||||||
{
|
{
|
||||||
_serializedCapacity = _ammoSlots.Length;
|
_serializedCapacity = AmmoSlots.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ISerializationHooks.AfterDeserialization()
|
void ISerializationHooks.AfterDeserialization()
|
||||||
{
|
{
|
||||||
_ammoSlots = new EntityUid[_serializedCapacity];
|
AmmoSlots = new EntityUid?[_serializedCapacity];
|
||||||
}
|
|
||||||
|
|
||||||
public override ComponentState GetComponentState()
|
|
||||||
{
|
|
||||||
var slotsSpent = new bool?[Capacity];
|
|
||||||
for (var i = 0; i < Capacity; i++)
|
|
||||||
{
|
|
||||||
slotsSpent[i] = null;
|
|
||||||
var ammoEntity = _ammoSlots[i];
|
|
||||||
if (ammoEntity != default && Entities.TryGetComponent(ammoEntity, out AmmoComponent? ammo))
|
|
||||||
{
|
|
||||||
slotsSpent[i] = ammo.Spent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: make yaml var to not sent currentSlot/UI? (for russian roulette)
|
|
||||||
return new RevolverBarrelComponentState(
|
|
||||||
_currentSlot,
|
|
||||||
FireRateSelector,
|
|
||||||
slotsSpent,
|
|
||||||
SoundGunshot.GetSound());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
_unspawnedCount = Capacity;
|
|
||||||
int idx = 0;
|
|
||||||
_ammoContainer = ContainerHelpers.EnsureContainer<Container>(Owner, $"{Name}-ammoContainer", out var existing);
|
|
||||||
if (existing)
|
|
||||||
{
|
|
||||||
foreach (var entity in _ammoContainer.ContainedEntities)
|
|
||||||
{
|
|
||||||
_unspawnedCount--;
|
|
||||||
_ammoSlots[idx] = entity;
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < _unspawnedCount; i++)
|
|
||||||
{
|
|
||||||
var entity = Entities.SpawnEntity(_fillPrototype, Entities.GetComponent<TransformComponent>(Owner).Coordinates);
|
|
||||||
_ammoSlots[idx] = entity;
|
|
||||||
_ammoContainer.Insert(entity);
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateAppearance();
|
|
||||||
Dirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateAppearance()
|
|
||||||
{
|
|
||||||
if (!Entities.TryGetComponent(Owner, out AppearanceComponent? appearance))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Placeholder, at this stage it's just here for the RPG
|
|
||||||
appearance.SetData(MagazineBarrelVisuals.MagLoaded, ShotsLeft > 0);
|
|
||||||
appearance.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
|
|
||||||
appearance.SetData(AmmoVisuals.AmmoMax, Capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryInsertBullet(EntityUid user, EntityUid entity)
|
|
||||||
{
|
|
||||||
if (!Entities.TryGetComponent(entity, out AmmoComponent? ammoComponent))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ammoComponent.Caliber != _caliber)
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(user, Loc.GetString("revolver-barrel-component-try-inser-bullet-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 == default)
|
|
||||||
{
|
|
||||||
_currentSlot = i;
|
|
||||||
_ammoSlots[i] = entity;
|
|
||||||
_ammoContainer.Insert(entity);
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundInsert.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
|
|
||||||
Dirty();
|
|
||||||
UpdateAppearance();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Owner.PopupMessage(user, Loc.GetString("revolver-barrel-component-try-inser-bullet-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 = _random.Next(_ammoSlots.Length - 1);
|
|
||||||
_currentSlot = random;
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundSpin.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
Dirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override EntityUid? 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 EntityUid? TakeProjectile(EntityCoordinates spawnAt)
|
|
||||||
{
|
|
||||||
var ammo = _ammoSlots[_currentSlot];
|
|
||||||
EntityUid? bullet = null;
|
|
||||||
if (ammo != default)
|
|
||||||
{
|
|
||||||
var ammoComponent = Entities.GetComponent<AmmoComponent>(ammo);
|
|
||||||
bullet = EntitySystem.Get<GunSystem>().TakeBullet(ammoComponent, spawnAt);
|
|
||||||
if (ammoComponent.Caseless)
|
|
||||||
{
|
|
||||||
_ammoSlots[_currentSlot] = default;
|
|
||||||
_ammoContainer.Remove(ammo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Cycle();
|
|
||||||
UpdateAppearance();
|
|
||||||
return bullet;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EjectAllSlots()
|
|
||||||
{
|
|
||||||
for (var i = 0; i < _ammoSlots.Length; i++)
|
|
||||||
{
|
|
||||||
var entity = _ammoSlots[i];
|
|
||||||
if (entity == default)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ammoContainer.Remove(entity);
|
|
||||||
EjectCasing(entity);
|
|
||||||
_ammoSlots[i] = default;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_ammoContainer.ContainedEntities.Count > 0)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundEject.GetSound(), Owner, AudioParams.Default.WithVolume(-1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// May as well point back at the end?
|
|
||||||
_currentSlot = _ammoSlots.Length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Eject all casings
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="eventArgs"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <exception cref="NotImplementedException"></exception>
|
|
||||||
public bool UseEntity(UseEntityEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
EjectAllSlots();
|
|
||||||
Dirty();
|
|
||||||
UpdateAppearance();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
return TryInsertBullet(eventArgs.User, eventArgs.Using);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,171 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Content.Server.PowerCell;
|
|
||||||
using Content.Server.Projectiles.Components;
|
|
||||||
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
using Robust.Shared.GameStates;
|
|
||||||
using Robust.Shared.IoC;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
using Robust.Shared.ViewVariables;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
|
||||||
{
|
|
||||||
[RegisterComponent]
|
|
||||||
[NetworkedComponent()]
|
|
||||||
public sealed class ServerBatteryBarrelComponent : ServerRangedBarrelComponent
|
|
||||||
{
|
|
||||||
[Dependency] private readonly IEntityManager _entities = default!;
|
|
||||||
|
|
||||||
public override string Name => "BatteryBarrel";
|
|
||||||
|
|
||||||
// The minimum change we need before we can fire
|
|
||||||
[DataField("lowerChargeLimit")]
|
|
||||||
[ViewVariables] private float _lowerChargeLimit = 10;
|
|
||||||
[DataField("fireCost")]
|
|
||||||
[ViewVariables] private int _baseFireCost = 300;
|
|
||||||
// What gets fired
|
|
||||||
[DataField("ammoPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
|
||||||
[ViewVariables] private string? _ammoPrototype;
|
|
||||||
|
|
||||||
private ContainerSlot _ammoContainer = default!;
|
|
||||||
|
|
||||||
public override int ShotsLeft
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
|
|
||||||
if (!EntitySystem.Get<PowerCellSystem>().TryGetBatteryFromSlot(Owner, out var battery))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int) Math.Ceiling(battery.CurrentCharge / _baseFireCost);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int Capacity
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (!EntitySystem.Get<PowerCellSystem>().TryGetBatteryFromSlot(Owner, out var battery))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int) Math.Ceiling(battery.MaxCharge / _baseFireCost);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private AppearanceComponent? _appearanceComponent;
|
|
||||||
|
|
||||||
public override ComponentState GetComponentState()
|
|
||||||
{
|
|
||||||
(int, int)? count = (ShotsLeft, Capacity);
|
|
||||||
|
|
||||||
return new BatteryBarrelComponentState(
|
|
||||||
FireRateSelector,
|
|
||||||
count);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
|
|
||||||
if (_ammoPrototype != null)
|
|
||||||
{
|
|
||||||
_ammoContainer = Owner.EnsureContainer<ContainerSlot>($"{Name}-ammo-container");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_entities.TryGetComponent(Owner, out AppearanceComponent? appearanceComponent))
|
|
||||||
{
|
|
||||||
_appearanceComponent = appearanceComponent;
|
|
||||||
}
|
|
||||||
Dirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Startup()
|
|
||||||
{
|
|
||||||
base.Startup();
|
|
||||||
UpdateAppearance();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateAppearance()
|
|
||||||
{
|
|
||||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, EntitySystem.Get<PowerCellSystem>().TryGetBatteryFromSlot(Owner, out _));
|
|
||||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
|
|
||||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
|
||||||
Dirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override EntityUid? 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 = _entities.SpawnEntity(_ammoPrototype, _entities.GetComponent<TransformComponent>(Owner).Coordinates);
|
|
||||||
_ammoContainer.Insert(ammo.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ammo.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override EntityUid? TakeProjectile(EntityCoordinates spawnAt)
|
|
||||||
{
|
|
||||||
if (!EntitySystem.Get<PowerCellSystem>().TryGetBatteryFromSlot(Owner, out var capacitor))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (capacitor.CurrentCharge < _lowerChargeLimit)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Can fire confirmed
|
|
||||||
// Multiply the entity's damage / whatever by the percentage of charge the shot has.
|
|
||||||
EntityUid? entity;
|
|
||||||
var chargeChange = Math.Min(capacitor.CurrentCharge, _baseFireCost);
|
|
||||||
if (capacitor.UseCharge(chargeChange) < _lowerChargeLimit)
|
|
||||||
{
|
|
||||||
// Handling of funny exploding cells.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var energyRatio = chargeChange / _baseFireCost;
|
|
||||||
|
|
||||||
if (_ammoContainer.ContainedEntity != null)
|
|
||||||
{
|
|
||||||
entity = _ammoContainer.ContainedEntity;
|
|
||||||
_ammoContainer.Remove(entity.Value);
|
|
||||||
_entities.GetComponent<TransformComponent>(entity.Value).Coordinates = spawnAt;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
entity = _entities.SpawnEntity(_ammoPrototype, spawnAt);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_entities.TryGetComponent(entity.Value, out ProjectileComponent? projectileComponent))
|
|
||||||
{
|
|
||||||
if (energyRatio < 1.0)
|
|
||||||
{
|
|
||||||
projectileComponent.Damage *= energyRatio;
|
|
||||||
}
|
|
||||||
} else if (_entities.TryGetComponent(entity.Value, out HitscanComponent? hitscanComponent))
|
|
||||||
{
|
|
||||||
hitscanComponent.Damage *= energyRatio;
|
|
||||||
hitscanComponent.ColorModifier = energyRatio;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Ammo doesn't have hitscan or projectile?");
|
|
||||||
}
|
|
||||||
|
|
||||||
// capacitor.UseCharge() triggers a PowerCellChangedEvent which will cause appearance to be updated.
|
|
||||||
// So let's not double-call UpdateAppearance() here.
|
|
||||||
return entity.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,465 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Content.Server.Hands.Components;
|
|
||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
|
||||||
using Content.Shared.Examine;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Item;
|
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Content.Shared.Sound;
|
|
||||||
using Content.Shared.Weapons.Ranged;
|
|
||||||
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
using Robust.Shared.GameStates;
|
|
||||||
using Robust.Shared.IoC;
|
|
||||||
using Robust.Shared.Localization;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
using Robust.Shared.Utility;
|
|
||||||
using Robust.Shared.ViewVariables;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
|
||||||
{
|
|
||||||
[RegisterComponent]
|
|
||||||
[NetworkedComponent()]
|
|
||||||
#pragma warning disable 618
|
|
||||||
public sealed class ServerMagazineBarrelComponent : ServerRangedBarrelComponent, IUse, IInteractUsing, IExamine
|
|
||||||
#pragma warning restore 618
|
|
||||||
{
|
|
||||||
[Dependency] private readonly IEntityManager _entities = default!;
|
|
||||||
|
|
||||||
public override string Name => "MagazineBarrel";
|
|
||||||
|
|
||||||
[ViewVariables]
|
|
||||||
private ContainerSlot _chamberContainer = default!;
|
|
||||||
[ViewVariables] public bool HasMagazine => MagazineContainer.ContainedEntity != null;
|
|
||||||
public ContainerSlot MagazineContainer = default!;
|
|
||||||
|
|
||||||
[ViewVariables] public MagazineType MagazineTypes => _magazineTypes;
|
|
||||||
[DataField("magazineTypes")]
|
|
||||||
private MagazineType _magazineTypes = default;
|
|
||||||
[ViewVariables] public BallisticCaliber Caliber => _caliber;
|
|
||||||
[DataField("caliber")]
|
|
||||||
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
|
|
||||||
|
|
||||||
public override int ShotsLeft
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var count = 0;
|
|
||||||
if (_chamberContainer.ContainedEntity != null)
|
|
||||||
{
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MagazineContainer.ContainedEntity is {Valid: true} magazine)
|
|
||||||
{
|
|
||||||
count += _entities.GetComponent<RangedMagazineComponent>(magazine).ShotsLeft;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int Capacity
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
// Chamber
|
|
||||||
var count = 1;
|
|
||||||
if (MagazineContainer.ContainedEntity is {Valid: true} magazine)
|
|
||||||
{
|
|
||||||
count += _entities.GetComponent<RangedMagazineComponent>(magazine).Capacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataField("magFillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
|
||||||
private string? _magFillPrototype;
|
|
||||||
|
|
||||||
public bool BoltOpen
|
|
||||||
{
|
|
||||||
get => _boltOpen;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_boltOpen == value)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value)
|
|
||||||
{
|
|
||||||
TryEjectChamber();
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltOpen.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TryFeedChamber();
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltClosed.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
}
|
|
||||||
|
|
||||||
_boltOpen = value;
|
|
||||||
UpdateAppearance();
|
|
||||||
Dirty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private bool _boltOpen = true;
|
|
||||||
|
|
||||||
[DataField("autoEjectMag")]
|
|
||||||
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;
|
|
||||||
[DataField("magNeedsOpenBolt")]
|
|
||||||
private bool _magNeedsOpenBolt = default;
|
|
||||||
|
|
||||||
private AppearanceComponent? _appearanceComponent;
|
|
||||||
|
|
||||||
// Sounds
|
|
||||||
[DataField("soundBoltOpen", required: true)]
|
|
||||||
private SoundSpecifier _soundBoltOpen = default!;
|
|
||||||
[DataField("soundBoltClosed", required: true)]
|
|
||||||
private SoundSpecifier _soundBoltClosed = default!;
|
|
||||||
[DataField("soundRack", required: true)]
|
|
||||||
private SoundSpecifier _soundRack = default!;
|
|
||||||
[DataField("soundMagInsert", required: true)]
|
|
||||||
private SoundSpecifier _soundMagInsert = default!;
|
|
||||||
[DataField("soundMagEject", required: true)]
|
|
||||||
private SoundSpecifier _soundMagEject = default!;
|
|
||||||
[DataField("soundAutoEject")]
|
|
||||||
private SoundSpecifier _soundAutoEject = new SoundPathSpecifier("/Audio/Weapons/Guns/EmptyAlarm/smg_empty_alarm.ogg");
|
|
||||||
|
|
||||||
private List<MagazineType> GetMagazineTypes()
|
|
||||||
{
|
|
||||||
var types = new List<MagazineType>();
|
|
||||||
|
|
||||||
foreach (MagazineType mag in Enum.GetValues(typeof(MagazineType)))
|
|
||||||
{
|
|
||||||
if ((_magazineTypes & mag) != 0)
|
|
||||||
{
|
|
||||||
types.Add(mag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return types;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ComponentState GetComponentState()
|
|
||||||
{
|
|
||||||
(int, int)? count = null;
|
|
||||||
if (MagazineContainer.ContainedEntity is {Valid: true} magazine &&
|
|
||||||
_entities.TryGetComponent(magazine, out RangedMagazineComponent? rangedMagazineComponent))
|
|
||||||
{
|
|
||||||
count = (rangedMagazineComponent.ShotsLeft, rangedMagazineComponent.Capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new MagazineBarrelComponentState(
|
|
||||||
_chamberContainer.ContainedEntity != null,
|
|
||||||
FireRateSelector,
|
|
||||||
count,
|
|
||||||
SoundGunshot.GetSound());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
|
|
||||||
if (_entities.TryGetComponent(Owner, out AppearanceComponent? appearanceComponent))
|
|
||||||
{
|
|
||||||
_appearanceComponent = appearanceComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
_chamberContainer = Owner.EnsureContainer<ContainerSlot>($"{Name}-chamber");
|
|
||||||
MagazineContainer = Owner.EnsureContainer<ContainerSlot>($"{Name}-magazine", out var existing);
|
|
||||||
|
|
||||||
if (!existing && _magFillPrototype != null)
|
|
||||||
{
|
|
||||||
var magEntity = _entities.SpawnEntity(_magFillPrototype, _entities.GetComponent<TransformComponent>(Owner).Coordinates);
|
|
||||||
MagazineContainer.Insert(magEntity);
|
|
||||||
}
|
|
||||||
Dirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Startup()
|
|
||||||
{
|
|
||||||
base.Startup();
|
|
||||||
UpdateAppearance();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override EntityUid? PeekAmmo()
|
|
||||||
{
|
|
||||||
return BoltOpen ? null : _chamberContainer.ContainedEntity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override EntityUid? TakeProjectile(EntityCoordinates spawnAt)
|
|
||||||
{
|
|
||||||
if (BoltOpen)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var entity = _chamberContainer.ContainedEntity;
|
|
||||||
|
|
||||||
Cycle();
|
|
||||||
|
|
||||||
return entity != null ? EntitySystem.Get<GunSystem>().TakeBullet(_entities.GetComponent<AmmoComponent>(entity.Value), spawnAt) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Cycle(bool manual = false)
|
|
||||||
{
|
|
||||||
if (BoltOpen)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TryEjectChamber();
|
|
||||||
|
|
||||||
TryFeedChamber();
|
|
||||||
|
|
||||||
if (_chamberContainer.ContainedEntity == null && !BoltOpen)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltOpen.GetSound(), Owner, AudioParams.Default.WithVolume(-5));
|
|
||||||
|
|
||||||
if (Owner.TryGetContainer(out var container))
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(container.Owner, Loc.GetString("server-magazine-barrel-component-cycle-bolt-open"));
|
|
||||||
}
|
|
||||||
BoltOpen = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (manual)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundRack.GetSound(), Owner, 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 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)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltClosed.GetSound(), Owner, AudioParams.Default.WithVolume(-5));
|
|
||||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("server-magazine-barrel-component-use-entity-bolt-closed"));
|
|
||||||
BoltOpen = false;
|
|
||||||
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 bool TryEjectChamber()
|
|
||||||
{
|
|
||||||
if (_chamberContainer.ContainedEntity is {Valid: true} chamberEntity)
|
|
||||||
{
|
|
||||||
if (!_chamberContainer.Remove(chamberEntity))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var ammoComponent = _entities.GetComponent<AmmoComponent>(chamberEntity);
|
|
||||||
if (!ammoComponent.Caseless)
|
|
||||||
{
|
|
||||||
EjectCasing(chamberEntity);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryFeedChamber()
|
|
||||||
{
|
|
||||||
if (_chamberContainer.ContainedEntity != null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try and pull a round from the magazine to replace the chamber if possible
|
|
||||||
var magazine = MagazineContainer.ContainedEntity;
|
|
||||||
|
|
||||||
if (_entities.GetComponentOrNull<RangedMagazineComponent>(magazine)?.TakeAmmo() is not {Valid: true} nextRound)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_chamberContainer.Insert(nextRound);
|
|
||||||
|
|
||||||
if (_autoEjectMag && magazine != null && _entities.GetComponent<RangedMagazineComponent>(magazine.Value).ShotsLeft == 0)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundAutoEject.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
|
|
||||||
MagazineContainer.Remove(magazine.Value);
|
|
||||||
#pragma warning disable 618
|
|
||||||
SendNetworkMessage(new MagazineAutoEjectMessage());
|
|
||||||
#pragma warning restore 618
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveMagazine(EntityUid user)
|
|
||||||
{
|
|
||||||
var mag = MagazineContainer.ContainedEntity;
|
|
||||||
|
|
||||||
if (mag == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MagNeedsOpenBolt && !BoltOpen)
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(user, Loc.GetString("server-magazine-barrel-component-remove-magazine-bolt-closed"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MagazineContainer.Remove(mag.Value);
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundMagEject.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
|
|
||||||
if (_entities.TryGetComponent(user, out HandsComponent? handsComponent))
|
|
||||||
{
|
|
||||||
handsComponent.PutInHandOrDrop(_entities.GetComponent<SharedItemComponent>(mag.Value));
|
|
||||||
}
|
|
||||||
|
|
||||||
Dirty();
|
|
||||||
UpdateAppearance();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanInsertMagazine(EntityUid user, EntityUid magazine, bool quiet = true)
|
|
||||||
{
|
|
||||||
if (!_entities.TryGetComponent(magazine, out RangedMagazineComponent? magazineComponent))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((MagazineTypes & magazineComponent.MagazineType) == 0)
|
|
||||||
{
|
|
||||||
if (!quiet)
|
|
||||||
Owner.PopupMessage(user, Loc.GetString("server-magazine-barrel-component-interact-using-wrong-magazine-type"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (magazineComponent.Caliber != _caliber)
|
|
||||||
{
|
|
||||||
if (!quiet)
|
|
||||||
Owner.PopupMessage(user, Loc.GetString("server-magazine-barrel-component-interact-using-wrong-caliber"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_magNeedsOpenBolt && !BoltOpen)
|
|
||||||
{
|
|
||||||
if (!quiet)
|
|
||||||
Owner.PopupMessage(user, Loc.GetString("server-magazine-barrel-component-interact-using-bolt-closed"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MagazineContainer.ContainedEntity == null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!quiet)
|
|
||||||
Owner.PopupMessage(user, Loc.GetString("server-magazine-barrel-component-interact-using-already-holding-magazine"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void InsertMagazine(EntityUid user, EntityUid magazine)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundMagInsert.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
Owner.PopupMessage(user, Loc.GetString("server-magazine-barrel-component-interact-using-success"));
|
|
||||||
MagazineContainer.Insert(magazine);
|
|
||||||
Dirty();
|
|
||||||
UpdateAppearance();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
|
|
||||||
{
|
|
||||||
if (CanInsertMagazine(eventArgs.User, eventArgs.Using, quiet: false))
|
|
||||||
{
|
|
||||||
InsertMagazine(eventArgs.User, eventArgs.Using);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert 1 ammo
|
|
||||||
if (_entities.TryGetComponent(eventArgs.Using, out AmmoComponent? ammoComponent))
|
|
||||||
{
|
|
||||||
if (!BoltOpen)
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("server-magazine-barrel-component-interact-using-ammo-bolt-closed"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ammoComponent.Caliber != _caliber)
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("server-magazine-barrel-component-interact-using-wrong-caliber"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_chamberContainer.ContainedEntity == null)
|
|
||||||
{
|
|
||||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("server-magazine-barrel-component-interact-using-ammo-success"));
|
|
||||||
_chamberContainer.Insert(eventArgs.Using);
|
|
||||||
Dirty();
|
|
||||||
UpdateAppearance();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("server-magazine-barrel-component-interact-using-ammo-full"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Examine(FormattedMessage message, bool inDetailsRange)
|
|
||||||
{
|
|
||||||
base.Examine(message, inDetailsRange);
|
|
||||||
|
|
||||||
message.AddMarkup("\n" + Loc.GetString("server-magazine-barrel-component-on-examine", ("caliber", Caliber)));
|
|
||||||
|
|
||||||
foreach (var magazineType in GetMagazineTypes())
|
|
||||||
{
|
|
||||||
message.AddMarkup("\n" + Loc.GetString("server-magazine-barrel-component-on-examine-magazine-type", ("magazineType", magazineType)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
public enum MagazineType
|
|
||||||
{
|
|
||||||
Unspecified = 0,
|
|
||||||
LPistol = 1 << 0, // Placeholder?
|
|
||||||
Pistol = 1 << 1,
|
|
||||||
HCPistol = 1 << 2,
|
|
||||||
Smg = 1 << 3,
|
|
||||||
SmgTopMounted = 1 << 4,
|
|
||||||
Rifle = 1 << 5,
|
|
||||||
IH = 1 << 6, // Placeholder?
|
|
||||||
Box = 1 << 7,
|
|
||||||
Pan = 1 << 8,
|
|
||||||
Dart = 1 << 9, // Placeholder
|
|
||||||
CalicoTopMounted = 1 << 10,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Content.Server.Administration.Logs;
|
|
||||||
using Content.Server.Projectiles.Components;
|
|
||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
|
||||||
using Content.Shared.Camera;
|
|
||||||
using Content.Shared.Damage;
|
|
||||||
using Content.Shared.Database;
|
|
||||||
using Content.Shared.Examine;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Sound;
|
using Content.Shared.Sound;
|
||||||
using Content.Shared.Weapons.Ranged.Components;
|
using Content.Shared.Weapons.Ranged.Components;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Analyzers;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.IoC;
|
|
||||||
using Robust.Shared.Localization;
|
|
||||||
using Robust.Shared.Log;
|
using Robust.Shared.Log;
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Maths;
|
using Robust.Shared.Maths;
|
||||||
using Robust.Shared.Physics;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Random;
|
|
||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.Timing;
|
|
||||||
using Robust.Shared.Utility;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
||||||
{
|
{
|
||||||
@@ -34,16 +14,9 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
|
|||||||
/// All of the ranged weapon components inherit from this to share mechanics like shooting etc.
|
/// 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.)
|
/// Only difference between them is how they retrieve a projectile to shoot (battery, magazine, etc.)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
#pragma warning disable 618
|
[Friend(typeof(GunSystem))]
|
||||||
public abstract class ServerRangedBarrelComponent : SharedRangedBarrelComponent, IExamine, ISerializationHooks
|
public abstract class ServerRangedBarrelComponent : SharedRangedBarrelComponent, ISerializationHooks
|
||||||
#pragma warning restore 618
|
|
||||||
{
|
{
|
||||||
// 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
|
|
||||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
|
||||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
|
||||||
[Dependency] protected readonly IEntityManager Entities = default!;
|
|
||||||
|
|
||||||
public override FireRateSelector FireRateSelector => _fireRateSelector;
|
public override FireRateSelector FireRateSelector => _fireRateSelector;
|
||||||
|
|
||||||
[DataField("currentSelector")]
|
[DataField("currentSelector")]
|
||||||
@@ -55,10 +28,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
|
|||||||
public override float FireRate { get; } = 2f;
|
public override float FireRate { get; } = 2f;
|
||||||
|
|
||||||
// _lastFire is when we actually fired (so if we hold the button then recoil doesn't build up if we're not firing)
|
// _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 TimeSpan LastFire;
|
||||||
|
|
||||||
public abstract EntityUid? PeekAmmo();
|
|
||||||
public abstract EntityUid? TakeProjectile(EntityCoordinates spawnAt);
|
|
||||||
|
|
||||||
// Recoil / spray control
|
// Recoil / spray control
|
||||||
[DataField("minAngle")]
|
[DataField("minAngle")]
|
||||||
@@ -71,7 +41,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
|
|||||||
|
|
||||||
public Angle MaxAngle { get; private set; }
|
public Angle MaxAngle { get; private set; }
|
||||||
|
|
||||||
private Angle _currentAngle = Angle.Zero;
|
public Angle CurrentAngle = Angle.Zero;
|
||||||
|
|
||||||
[DataField("angleDecay")]
|
[DataField("angleDecay")]
|
||||||
private float _angleDecayDegrees = 20;
|
private float _angleDecayDegrees = 20;
|
||||||
@@ -132,294 +102,6 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
|
|||||||
throw new InvalidOperationException();
|
throw new InvalidOperationException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
|
|
||||||
Owner.EnsureComponentWarn(out ServerRangedWeaponComponent rangedWeaponComponent);
|
|
||||||
|
|
||||||
rangedWeaponComponent.Barrel ??= this;
|
|
||||||
rangedWeaponComponent.FireHandler += Fire;
|
|
||||||
rangedWeaponComponent.WeaponCanFireHandler += WeaponCanFire;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnRemove()
|
|
||||||
{
|
|
||||||
base.OnRemove();
|
|
||||||
if (Entities.TryGetComponent(Owner, out ServerRangedWeaponComponent? rangedWeaponComponent))
|
|
||||||
{
|
|
||||||
rangedWeaponComponent.Barrel = null;
|
|
||||||
rangedWeaponComponent.FireHandler -= Fire;
|
|
||||||
rangedWeaponComponent.WeaponCanFireHandler -= WeaponCanFire;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Angle GetRecoilAngle(Angle direction)
|
|
||||||
{
|
|
||||||
var currentTime = _gameTiming.CurTime;
|
|
||||||
var timeSinceLastFire = (currentTime - _lastFire).TotalSeconds;
|
|
||||||
var newTheta = MathHelper.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 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fires a round of ammo out of the weapon.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="shooter">Entity that is operating the weapon, usually the player.</param>
|
|
||||||
/// <param name="targetPos">Target position on the map to shoot at.</param>
|
|
||||||
private void Fire(EntityUid shooter, Vector2 targetPos)
|
|
||||||
{
|
|
||||||
if (ShotsLeft == 0)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Broadcast(), SoundEmpty.GetSound(), Owner);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ammo = PeekAmmo();
|
|
||||||
if (TakeProjectile(Entities.GetComponent<TransformComponent>(shooter).Coordinates) is not {Valid: true} projectile)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Broadcast(), SoundEmpty.GetSound(), Owner);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// At this point firing is confirmed
|
|
||||||
var direction = (targetPos - Entities.GetComponent<TransformComponent>(shooter).WorldPosition).ToAngle();
|
|
||||||
var angle = GetRecoilAngle(direction);
|
|
||||||
// This should really be client-side but for now we'll just leave it here
|
|
||||||
if (Entities.HasComponent<CameraRecoilComponent>(shooter))
|
|
||||||
{
|
|
||||||
var kick = -angle.ToVec() * 0.15f;
|
|
||||||
EntitySystem.Get<CameraRecoilSystem>().KickCamera(shooter, kick);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This section probably needs tweaking so there can be caseless hitscan etc.
|
|
||||||
if (Entities.TryGetComponent(projectile, out HitscanComponent? hitscan))
|
|
||||||
{
|
|
||||||
FireHitscan(shooter, hitscan, angle);
|
|
||||||
}
|
|
||||||
else if (Entities.HasComponent<ProjectileComponent>(projectile) &&
|
|
||||||
Entities.TryGetComponent(ammo, out AmmoComponent? ammoComponent))
|
|
||||||
{
|
|
||||||
FireProjectiles(shooter, projectile, ammoComponent.ProjectilesFired, ammoComponent.EvenSpreadAngle, angle, ammoComponent.Velocity, ammo.Value);
|
|
||||||
|
|
||||||
if (CanMuzzleFlash)
|
|
||||||
{
|
|
||||||
EntitySystem.Get<GunSystem>().MuzzleFlash(Owner, ammoComponent, angle);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ammoComponent.Caseless)
|
|
||||||
{
|
|
||||||
Entities.DeleteEntity(ammo.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Invalid types
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
SoundSystem.Play(Filter.Broadcast(), SoundGunshot.GetSound(), Owner);
|
|
||||||
|
|
||||||
_lastFire = _gameTiming.CurTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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(
|
|
||||||
EntityUid entity,
|
|
||||||
bool playSound = true,
|
|
||||||
Direction[]? ejectDirections = null,
|
|
||||||
IRobustRandom? robustRandom = null,
|
|
||||||
IPrototypeManager? prototypeManager = null,
|
|
||||||
IEntityManager? entities = null)
|
|
||||||
{
|
|
||||||
IoCManager.Resolve(ref robustRandom, ref prototypeManager, ref entities);
|
|
||||||
|
|
||||||
ejectDirections ??= new[]
|
|
||||||
{Direction.East, Direction.North, Direction.NorthWest, Direction.South, Direction.SouthEast, Direction.West};
|
|
||||||
|
|
||||||
const float ejectOffset = 1.8f;
|
|
||||||
var ammo = entities.GetComponent<AmmoComponent>(entity);
|
|
||||||
var offsetPos = ((robustRandom.NextFloat() - 0.5f) * ejectOffset, (robustRandom.NextFloat() - 0.5f) * ejectOffset);
|
|
||||||
entities.GetComponent<TransformComponent>(entity).Coordinates = entities.GetComponent<TransformComponent>(entity).Coordinates.Offset(offsetPos);
|
|
||||||
entities.GetComponent<TransformComponent>(entity).LocalRotation = robustRandom.Pick(ejectDirections).ToAngle();
|
|
||||||
|
|
||||||
var coordinates = entities.GetComponent<TransformComponent>(entity).Coordinates;
|
|
||||||
SoundSystem.Play(Filter.Broadcast(), ammo.SoundCollectionEject.GetSound(), coordinates, 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<EntityUid> entities)
|
|
||||||
{
|
|
||||||
var robustRandom = IoCManager.Resolve<IRobustRandom>();
|
|
||||||
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
|
||||||
var ejectDirections = new[] {Direction.East, Direction.North, Direction.NorthWest, Direction.South, Direction.SouthEast, Direction.West};
|
|
||||||
var soundPlayCount = 0;
|
|
||||||
var playSound = true;
|
|
||||||
|
|
||||||
foreach (var entity in entities)
|
|
||||||
{
|
|
||||||
EjectCasing(entity, playSound, ejectDirections, robustRandom, prototypeManager);
|
|
||||||
soundPlayCount++;
|
|
||||||
if (soundPlayCount > 3)
|
|
||||||
{
|
|
||||||
playSound = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Firing
|
|
||||||
/// <summary>
|
|
||||||
/// Handles firing one or many projectiles
|
|
||||||
/// </summary>
|
|
||||||
private void FireProjectiles(EntityUid shooter, EntityUid baseProjectile, int count, float evenSpreadAngle, Angle angle, float velocity, EntityUid ammo)
|
|
||||||
{
|
|
||||||
List<Angle>? sprayAngleChange = null;
|
|
||||||
if (count > 1)
|
|
||||||
{
|
|
||||||
evenSpreadAngle *= SpreadRatio;
|
|
||||||
sprayAngleChange = Linspace(-evenSpreadAngle / 2, evenSpreadAngle / 2, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
var firedProjectiles = new EntityUid[count];
|
|
||||||
for (var i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
EntityUid projectile;
|
|
||||||
|
|
||||||
if (i == 0)
|
|
||||||
{
|
|
||||||
projectile = baseProjectile;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
projectile = Entities.SpawnEntity(
|
|
||||||
Entities.GetComponent<MetaDataComponent>(baseProjectile).EntityPrototype?.ID,
|
|
||||||
Entities.GetComponent<TransformComponent>(baseProjectile).Coordinates);
|
|
||||||
}
|
|
||||||
|
|
||||||
firedProjectiles[i] = projectile;
|
|
||||||
|
|
||||||
Angle projectileAngle;
|
|
||||||
|
|
||||||
if (sprayAngleChange != null)
|
|
||||||
{
|
|
||||||
projectileAngle = angle + sprayAngleChange[i];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
projectileAngle = angle;
|
|
||||||
}
|
|
||||||
|
|
||||||
var physics = Entities.GetComponent<IPhysBody>(projectile);
|
|
||||||
physics.BodyStatus = BodyStatus.InAir;
|
|
||||||
|
|
||||||
var projectileComponent = Entities.GetComponent<ProjectileComponent>(projectile);
|
|
||||||
projectileComponent.IgnoreEntity(shooter);
|
|
||||||
|
|
||||||
// FIXME: Work around issue where inserting and removing an entity from a container,
|
|
||||||
// then setting its linear velocity in the same tick resets velocity back to zero.
|
|
||||||
// See SharedBroadphaseSystem.HandleContainerInsert()... It sets Awake to false, which causes this.
|
|
||||||
projectile.SpawnTimer(TimeSpan.FromMilliseconds(25), () =>
|
|
||||||
{
|
|
||||||
Entities.GetComponent<IPhysBody>(projectile)
|
|
||||||
.LinearVelocity = projectileAngle.ToVec() * velocity;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
Entities.GetComponent<TransformComponent>(projectile).WorldRotation = projectileAngle + MathHelper.PiOver2;
|
|
||||||
}
|
|
||||||
|
|
||||||
Entities.EventBus.RaiseLocalEvent(Owner, new GunShotEvent(firedProjectiles));
|
|
||||||
Entities.EventBus.RaiseLocalEvent(ammo, new AmmoShotEvent(firedProjectiles));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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(EntityUid shooter, HitscanComponent hitscan, Angle angle)
|
|
||||||
{
|
|
||||||
var ray = new CollisionRay(Entities.GetComponent<TransformComponent>(Owner).Coordinates.ToMapPos(Entities), angle.ToVec(), (int) hitscan.CollisionMask);
|
|
||||||
var physicsManager = EntitySystem.Get<SharedPhysicsSystem>();
|
|
||||||
var rayCastResults = physicsManager.IntersectRay(Entities.GetComponent<TransformComponent>(Owner).MapID, ray, hitscan.MaxLength, shooter, false).ToList();
|
|
||||||
|
|
||||||
if (rayCastResults.Count >= 1)
|
|
||||||
{
|
|
||||||
var result = rayCastResults[0];
|
|
||||||
var distance = result.Distance;
|
|
||||||
hitscan.FireEffects(shooter, distance, angle, result.HitEntity);
|
|
||||||
var dmg = EntitySystem.Get<DamageableSystem>().TryChangeDamage(result.HitEntity, hitscan.Damage);
|
|
||||||
if (dmg != null)
|
|
||||||
EntitySystem.Get<AdminLogSystem>().Add(LogType.HitScanHit,
|
|
||||||
$"{Entities.ToPrettyString(shooter):user} hit {Entities.ToPrettyString(result.HitEntity):target} using {Entities.ToPrettyString(hitscan.Owner):used} and dealt {dmg.Total:damage} damage");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
hitscan.FireEffects(shooter, hitscan.MaxLength, angle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
public virtual void Examine(FormattedMessage message, bool inDetailsRange)
|
|
||||||
{
|
|
||||||
var fireRateMessage = Loc.GetString(FireRateSelector switch
|
|
||||||
{
|
|
||||||
FireRateSelector.Safety => "server-ranged-barrel-component-on-examine-fire-rate-safety-description",
|
|
||||||
FireRateSelector.Single => "server-ranged-barrel-component-on-examine-fire-rate-single-description",
|
|
||||||
FireRateSelector.Automatic => "server-ranged-barrel-component-on-examine-fire-rate-automatic-description",
|
|
||||||
_ => throw new IndexOutOfRangeException()
|
|
||||||
});
|
|
||||||
|
|
||||||
message.AddText(fireRateMessage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ public sealed partial class GunSystem
|
|||||||
{
|
{
|
||||||
if (args.Handled) return;
|
if (args.Handled) return;
|
||||||
|
|
||||||
if (EntityManager.TryGetComponent(args.Used, out AmmoComponent? ammoComponent))
|
if (TryComp(args.Used, out AmmoComponent? ammoComponent))
|
||||||
{
|
{
|
||||||
if (TryInsertAmmo(args.User, args.Used, component, ammoComponent))
|
if (TryInsertAmmo(args.User, args.Used, component, ammoComponent))
|
||||||
{
|
{
|
||||||
@@ -64,18 +64,18 @@ public sealed partial class GunSystem
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!EntityManager.TryGetComponent(args.Used, out RangedMagazineComponent? rangedMagazine)) return;
|
if (!TryComp(args.Used, out RangedMagazineComponent? rangedMagazine)) return;
|
||||||
|
|
||||||
for (var i = 0; i < Math.Max(10, rangedMagazine.ShotsLeft); i++)
|
for (var i = 0; i < Math.Max(10, rangedMagazine.ShotsLeft); i++)
|
||||||
{
|
{
|
||||||
if (rangedMagazine.TakeAmmo() is not {Valid: true} ammo)
|
if (TakeAmmo(rangedMagazine) is not {Valid: true} ammo)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryInsertAmmo(args.User, ammo, component))
|
if (!TryInsertAmmo(args.User, ammo, component))
|
||||||
{
|
{
|
||||||
rangedMagazine.TryInsertAmmo(args.User, ammo);
|
TryInsertAmmo(args.User, ammo, rangedMagazine);
|
||||||
args.Handled = true;
|
args.Handled = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -135,13 +135,13 @@ public sealed partial class GunSystem
|
|||||||
ejectAmmo.Add(ammo);
|
ejectAmmo.Add(ammo);
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerRangedBarrelComponent.EjectCasings(ejectAmmo);
|
EjectCasings(ejectAmmo);
|
||||||
UpdateAmmoBoxAppearance(ammoBox.Owner, ammoBox);
|
UpdateAmmoBoxAppearance(ammoBox.Owner, ammoBox);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryUse(EntityUid user, AmmoBoxComponent ammoBox)
|
private bool TryUse(EntityUid user, AmmoBoxComponent ammoBox)
|
||||||
{
|
{
|
||||||
if (!EntityManager.TryGetComponent(user, out HandsComponent? handsComponent))
|
if (!TryComp(user, out HandsComponent? handsComponent))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -151,7 +151,7 @@ public sealed partial class GunSystem
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (EntityManager.TryGetComponent(ammo, out ItemComponent? item))
|
if (TryComp(ammo, out ItemComponent? item))
|
||||||
{
|
{
|
||||||
if (!handsComponent.CanPutInHand(item))
|
if (!handsComponent.CanPutInHand(item))
|
||||||
{
|
{
|
||||||
|
|||||||
119
Content.Server/Weapon/Ranged/GunSystem.Battery.cs
Normal file
119
Content.Server/Weapon/Ranged/GunSystem.Battery.cs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
using System;
|
||||||
|
using Content.Server.PowerCell;
|
||||||
|
using Content.Server.Projectiles.Components;
|
||||||
|
using Content.Server.Weapon.Ranged.Barrels.Components;
|
||||||
|
using Content.Shared.PowerCell.Components;
|
||||||
|
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Server.Weapon.Ranged;
|
||||||
|
|
||||||
|
public sealed partial class GunSystem
|
||||||
|
{
|
||||||
|
private void OnBatteryInit(EntityUid uid, BatteryBarrelComponent component, ComponentInit args)
|
||||||
|
{
|
||||||
|
if (component.AmmoPrototype != null)
|
||||||
|
{
|
||||||
|
component.AmmoContainer = uid.EnsureContainer<ContainerSlot>($"{component.GetType()}-ammo-container");
|
||||||
|
}
|
||||||
|
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBatteryMapInit(EntityUid uid, BatteryBarrelComponent component, MapInitEvent args)
|
||||||
|
{
|
||||||
|
UpdateBatteryAppearance(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBatteryGetState(EntityUid uid, BatteryBarrelComponent component, ref ComponentGetState args)
|
||||||
|
{
|
||||||
|
(int, int)? count = (component.ShotsLeft, component.Capacity);
|
||||||
|
|
||||||
|
args.State = new BatteryBarrelComponentState(
|
||||||
|
component.FireRateSelector,
|
||||||
|
count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCellSlotUpdated(EntityUid uid, BatteryBarrelComponent component, PowerCellChangedEvent args)
|
||||||
|
{
|
||||||
|
UpdateBatteryAppearance(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateBatteryAppearance(BatteryBarrelComponent component)
|
||||||
|
{
|
||||||
|
if (!EntityManager.TryGetComponent(component.Owner, out AppearanceComponent? appearanceComponent)) return;
|
||||||
|
|
||||||
|
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, _cell.TryGetBatteryFromSlot(component.Owner, out _));
|
||||||
|
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
|
||||||
|
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntityUid? PeekAmmo(BatteryBarrelComponent component)
|
||||||
|
{
|
||||||
|
// Spawn a dummy entity because it's easier to work with I guess
|
||||||
|
// This will get re-used for the projectile
|
||||||
|
var ammo = component.AmmoContainer.ContainedEntity;
|
||||||
|
if (ammo == null)
|
||||||
|
{
|
||||||
|
ammo = EntityManager.SpawnEntity(component.AmmoPrototype, Transform(component.Owner).Coordinates);
|
||||||
|
component.AmmoContainer.Insert(ammo.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ammo.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntityUid? TakeProjectile(BatteryBarrelComponent component, EntityCoordinates spawnAt)
|
||||||
|
{
|
||||||
|
if (!_cell.TryGetBatteryFromSlot(component.Owner, out var capacitor))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (capacitor.CurrentCharge < component.LowerChargeLimit)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Can fire confirmed
|
||||||
|
// Multiply the entity's damage / whatever by the percentage of charge the shot has.
|
||||||
|
EntityUid? entity;
|
||||||
|
var chargeChange = Math.Min(capacitor.CurrentCharge, component.BaseFireCost);
|
||||||
|
if (capacitor.UseCharge(chargeChange) < component.LowerChargeLimit)
|
||||||
|
{
|
||||||
|
// Handling of funny exploding cells.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var energyRatio = chargeChange / component.BaseFireCost;
|
||||||
|
|
||||||
|
if (component.AmmoContainer.ContainedEntity != null)
|
||||||
|
{
|
||||||
|
entity = component.AmmoContainer.ContainedEntity;
|
||||||
|
component.AmmoContainer.Remove(entity.Value);
|
||||||
|
Transform(entity.Value).Coordinates = spawnAt;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entity = EntityManager.SpawnEntity(component.AmmoPrototype, spawnAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryComp(entity.Value, out ProjectileComponent? projectileComponent))
|
||||||
|
{
|
||||||
|
if (energyRatio < 1.0)
|
||||||
|
{
|
||||||
|
projectileComponent.Damage *= energyRatio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (TryComp(entity.Value, out HitscanComponent? hitscanComponent))
|
||||||
|
{
|
||||||
|
hitscanComponent.Damage *= energyRatio;
|
||||||
|
hitscanComponent.ColorModifier = energyRatio;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Ammo doesn't have hitscan or projectile?");
|
||||||
|
}
|
||||||
|
|
||||||
|
// capacitor.UseCharge() triggers a PowerCellChangedEvent which will cause appearance to be updated.
|
||||||
|
// So let's not double-call UpdateAppearance() here.
|
||||||
|
return entity.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
268
Content.Server/Weapon/Ranged/GunSystem.Bolt.cs
Normal file
268
Content.Server/Weapon/Ranged/GunSystem.Bolt.cs
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
||||||
|
using Content.Server.Weapon.Ranged.Barrels.Components;
|
||||||
|
using Content.Shared.Examine;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
|
using Content.Shared.Verbs;
|
||||||
|
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
||||||
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
|
||||||
|
namespace Content.Server.Weapon.Ranged;
|
||||||
|
|
||||||
|
public sealed partial class GunSystem
|
||||||
|
{
|
||||||
|
private void AddToggleBoltVerb(EntityUid uid, BoltActionBarrelComponent component, GetInteractionVerbsEvent args)
|
||||||
|
{
|
||||||
|
if (args.Hands == null ||
|
||||||
|
!args.CanAccess ||
|
||||||
|
!args.CanInteract)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Verb verb = new()
|
||||||
|
{
|
||||||
|
Text = component.BoltOpen
|
||||||
|
? Loc.GetString("close-bolt-verb-get-data-text")
|
||||||
|
: Loc.GetString("open-bolt-verb-get-data-text"),
|
||||||
|
Act = () => component.BoltOpen = !component.BoltOpen
|
||||||
|
};
|
||||||
|
args.Verbs.Add(verb);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBoltExamine(EntityUid uid, BoltActionBarrelComponent component, ExaminedEvent args)
|
||||||
|
{
|
||||||
|
args.PushMarkup(Loc.GetString("bolt-action-barrel-component-on-examine", ("caliber", component.Caliber)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBoltFireAttempt(EntityUid uid, BoltActionBarrelComponent component, GunFireAttemptEvent args)
|
||||||
|
{
|
||||||
|
if (args.Cancelled) return;
|
||||||
|
|
||||||
|
if (component.BoltOpen || component.ChamberContainer.ContainedEntity == null)
|
||||||
|
args.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBoltMapInit(EntityUid uid, BoltActionBarrelComponent component, MapInitEvent args)
|
||||||
|
{
|
||||||
|
if (component.FillPrototype != null)
|
||||||
|
{
|
||||||
|
component.UnspawnedCount += component.Capacity;
|
||||||
|
if (component.UnspawnedCount > 0)
|
||||||
|
{
|
||||||
|
component.UnspawnedCount--;
|
||||||
|
var chamberEntity = EntityManager.SpawnEntity(component.FillPrototype, EntityManager.GetComponent<TransformComponent>(uid).Coordinates);
|
||||||
|
component.ChamberContainer.Insert(chamberEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateBoltAppearance(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateBoltAppearance(BoltActionBarrelComponent component)
|
||||||
|
{
|
||||||
|
if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
|
||||||
|
|
||||||
|
appearanceComponent.SetData(BarrelBoltVisuals.BoltOpen, component.BoltOpen);
|
||||||
|
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
|
||||||
|
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBoltInit(EntityUid uid, BoltActionBarrelComponent component, ComponentInit args)
|
||||||
|
{
|
||||||
|
component.SpawnedAmmo = new Stack<EntityUid>(component.Capacity - 1);
|
||||||
|
component.AmmoContainer = uid.EnsureContainer<Container>($"{component.GetType()}-ammo-container", out var existing);
|
||||||
|
|
||||||
|
if (existing)
|
||||||
|
{
|
||||||
|
foreach (var entity in component.AmmoContainer.ContainedEntities)
|
||||||
|
{
|
||||||
|
component.SpawnedAmmo.Push(entity);
|
||||||
|
component.UnspawnedCount--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component.ChamberContainer = uid.EnsureContainer<ContainerSlot>($"{component.GetType()}-chamber-container");
|
||||||
|
|
||||||
|
if (TryComp(uid, out AppearanceComponent? appearanceComponent))
|
||||||
|
{
|
||||||
|
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
UpdateBoltAppearance(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBoltUse(EntityUid uid, BoltActionBarrelComponent component, UseInHandEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled) return;
|
||||||
|
|
||||||
|
args.Handled = true;
|
||||||
|
|
||||||
|
if (component.BoltOpen)
|
||||||
|
{
|
||||||
|
component.BoltOpen = false;
|
||||||
|
_popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-bolt-closed"), uid, Filter.Entities(args.User));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CycleBolt(component, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CycleBolt(BoltActionBarrelComponent component, bool manual = false)
|
||||||
|
{
|
||||||
|
TryEjectChamber(component);
|
||||||
|
TryFeedChamber(component);
|
||||||
|
|
||||||
|
if (component.ChamberContainer.ContainedEntity == null && manual)
|
||||||
|
{
|
||||||
|
component.BoltOpen = true;
|
||||||
|
|
||||||
|
if (_container.TryGetContainingContainer(component.Owner, out var container))
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-bolt-opened"), container.Owner, Filter.Entities(container.Owner));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundCycle.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
UpdateBoltAppearance(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryEjectChamber(BoltActionBarrelComponent component)
|
||||||
|
{
|
||||||
|
if (component.ChamberContainer.ContainedEntity is {Valid: true} chambered)
|
||||||
|
{
|
||||||
|
if (!component.ChamberContainer.Remove(chambered))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (TryComp(chambered, out AmmoComponent? ammoComponent) && !ammoComponent.Caseless)
|
||||||
|
EjectCasing(chambered);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryFeedChamber(BoltActionBarrelComponent component)
|
||||||
|
{
|
||||||
|
if (component.ChamberContainer.ContainedEntity != null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (component.SpawnedAmmo.TryPop(out var next))
|
||||||
|
{
|
||||||
|
component.AmmoContainer.Remove(next);
|
||||||
|
component.ChamberContainer.Insert(next);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (component.UnspawnedCount > 0)
|
||||||
|
{
|
||||||
|
component.UnspawnedCount--;
|
||||||
|
var ammoEntity = EntityManager.SpawnEntity(component.FillPrototype, EntityManager.GetComponent<TransformComponent>(component.Owner).Coordinates);
|
||||||
|
component.ChamberContainer.Insert(ammoEntity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBoltInteractUsing(EntityUid uid, BoltActionBarrelComponent component, InteractUsingEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled) return;
|
||||||
|
|
||||||
|
if (TryInsertBullet(args.User, args.Used, component))
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryInsertBullet(EntityUid user, EntityUid ammo, BoltActionBarrelComponent component)
|
||||||
|
{
|
||||||
|
if (!TryComp(ammo, out AmmoComponent? ammoComponent))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (ammoComponent.Caliber != component.Caliber)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-try-insert-bullet-wrong-caliber"), component.Owner, Filter.Entities(user));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!component.BoltOpen)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-try-insert-bullet-bolt-closed"), component.Owner, Filter.Entities(user));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.ChamberContainer.ContainedEntity == null)
|
||||||
|
{
|
||||||
|
component.ChamberContainer.Insert(ammo);
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
UpdateBoltAppearance(component);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.AmmoContainer.ContainedEntities.Count < component.Capacity - 1)
|
||||||
|
{
|
||||||
|
component.AmmoContainer.Insert(ammo);
|
||||||
|
component.SpawnedAmmo.Push(ammo);
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
UpdateBoltAppearance(component);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-try-insert-bullet-no-room"), component.Owner, Filter.Entities(user));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBoltGetState(EntityUid uid, BoltActionBarrelComponent component, ref ComponentGetState args)
|
||||||
|
{
|
||||||
|
(int, int)? count = (component.ShotsLeft, component.Capacity);
|
||||||
|
var chamberedExists = component.ChamberContainer.ContainedEntity != null;
|
||||||
|
// (Is one chambered?, is the bullet spend)
|
||||||
|
var chamber = (chamberedExists, false);
|
||||||
|
|
||||||
|
if (chamberedExists && TryComp<AmmoComponent?>(component.ChamberContainer.ContainedEntity!.Value, out var ammo))
|
||||||
|
{
|
||||||
|
chamber.Item2 = ammo.Spent;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.State = new BoltActionBarrelComponentState(
|
||||||
|
chamber,
|
||||||
|
component.FireRateSelector,
|
||||||
|
count,
|
||||||
|
component.SoundGunshot.GetSound());
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntityUid? PeekAmmo(BoltActionBarrelComponent component)
|
||||||
|
{
|
||||||
|
return component.ChamberContainer.ContainedEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntityUid? TakeProjectile(BoltActionBarrelComponent component, EntityCoordinates spawnAt)
|
||||||
|
{
|
||||||
|
if (component.AutoCycle)
|
||||||
|
{
|
||||||
|
CycleBolt(component);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.ChamberContainer.ContainedEntity is not {Valid: true} chamberEntity) return null;
|
||||||
|
|
||||||
|
var ammoComponent = EntityManager.GetComponentOrNull<AmmoComponent>(chamberEntity);
|
||||||
|
|
||||||
|
return ammoComponent == null ? null : TakeBullet(ammoComponent, spawnAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
257
Content.Server/Weapon/Ranged/GunSystem.Guns.cs
Normal file
257
Content.Server/Weapon/Ranged/GunSystem.Guns.cs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Content.Server.Administration.Logs;
|
||||||
|
using Content.Server.Atmos.EntitySystems;
|
||||||
|
using Content.Server.CombatMode;
|
||||||
|
using Content.Server.Hands.Components;
|
||||||
|
using Content.Server.Interaction.Components;
|
||||||
|
using Content.Server.Projectiles.Components;
|
||||||
|
using Content.Server.Stunnable;
|
||||||
|
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
||||||
|
using Content.Server.Weapon.Ranged.Barrels.Components;
|
||||||
|
using Content.Shared.Camera;
|
||||||
|
using Content.Shared.Damage;
|
||||||
|
using Content.Shared.Database;
|
||||||
|
using Content.Shared.Examine;
|
||||||
|
using Content.Shared.Popups;
|
||||||
|
using Content.Shared.Weapons.Ranged.Components;
|
||||||
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
using Robust.Shared.Physics;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Server.Weapon.Ranged;
|
||||||
|
|
||||||
|
public sealed partial class GunSystem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to fire a round of ammo out of the weapon.
|
||||||
|
/// </summary>
|
||||||
|
private void TryFire(EntityUid user, EntityCoordinates targetCoords, ServerRangedWeaponComponent gun)
|
||||||
|
{
|
||||||
|
if (!TryComp(gun.Owner, out ServerRangedBarrelComponent? barrel)) return;
|
||||||
|
|
||||||
|
if (!TryComp(user, out HandsComponent? hands) || hands.GetActiveHand()?.HeldEntity != gun.Owner) return;
|
||||||
|
|
||||||
|
if (!TryComp(user, out CombatModeComponent? combat) ||
|
||||||
|
!combat.IsInCombatMode ||
|
||||||
|
!_blocker.CanInteract(user)) return;
|
||||||
|
|
||||||
|
var fireAttempt = new GunFireAttemptEvent(user, gun);
|
||||||
|
EntityManager.EventBus.RaiseLocalEvent(gun.Owner, fireAttempt);
|
||||||
|
|
||||||
|
if (fireAttempt.Cancelled) return;
|
||||||
|
|
||||||
|
var curTime = _gameTiming.CurTime;
|
||||||
|
var span = curTime - gun.LastFireTime;
|
||||||
|
if (span.TotalSeconds < 1 / barrel.FireRate) return;
|
||||||
|
|
||||||
|
// TODO: Clumsy should be eventbus I think?
|
||||||
|
|
||||||
|
gun.LastFireTime = curTime;
|
||||||
|
var coordinates = Transform(gun.Owner).Coordinates;
|
||||||
|
|
||||||
|
if (gun.ClumsyCheck && gun.ClumsyDamage != null && ClumsyComponent.TryRollClumsy(user, gun.ClumsyExplodeChance))
|
||||||
|
{
|
||||||
|
//Wound them
|
||||||
|
_damageable.TryChangeDamage(user, gun.ClumsyDamage);
|
||||||
|
_stun.TryParalyze(user, TimeSpan.FromSeconds(3f), true);
|
||||||
|
|
||||||
|
// Apply salt to the wound ("Honk!")
|
||||||
|
SoundSystem.Play(
|
||||||
|
Filter.Pvs(gun.Owner), gun.ClumsyWeaponHandlingSound.GetSound(),
|
||||||
|
coordinates, AudioParams.Default.WithMaxDistance(5));
|
||||||
|
|
||||||
|
SoundSystem.Play(
|
||||||
|
Filter.Pvs(gun.Owner), gun.ClumsyWeaponShotSound.GetSound(),
|
||||||
|
coordinates, AudioParams.Default.WithMaxDistance(5));
|
||||||
|
|
||||||
|
user.PopupMessage(Loc.GetString("server-ranged-weapon-component-try-fire-clumsy"));
|
||||||
|
|
||||||
|
EntityManager.DeleteEntity(gun.Owner);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Firing confirmed
|
||||||
|
|
||||||
|
if (gun.CanHotspot)
|
||||||
|
_atmos.HotspotExpose(coordinates, 700, 50);
|
||||||
|
|
||||||
|
EntityManager.EventBus.RaiseLocalEvent(gun.Owner, new GunShotEvent());
|
||||||
|
Fire(user, barrel, targetCoords);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fires a round of ammo out of the weapon.
|
||||||
|
/// </summary>
|
||||||
|
private void Fire(EntityUid shooter, ServerRangedBarrelComponent component, EntityCoordinates coordinates)
|
||||||
|
{
|
||||||
|
if (component.ShotsLeft == 0)
|
||||||
|
{
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundEmpty.GetSound(), component.Owner);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ammo = PeekAtAmmo(component);
|
||||||
|
if (TakeOutProjectile(component, Transform(shooter).Coordinates) is not {Valid: true} projectile)
|
||||||
|
{
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundEmpty.GetSound(), component.Owner);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetPos = coordinates.ToMapPos(EntityManager);
|
||||||
|
|
||||||
|
// At this point firing is confirmed
|
||||||
|
var direction = (targetPos - Transform(shooter).WorldPosition).ToAngle();
|
||||||
|
var angle = GetRecoilAngle(component, direction);
|
||||||
|
// This should really be client-side but for now we'll just leave it here
|
||||||
|
if (HasComp<CameraRecoilComponent>(shooter))
|
||||||
|
{
|
||||||
|
var kick = -angle.ToVec() * 0.15f;
|
||||||
|
_recoil.KickCamera(shooter, kick);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This section probably needs tweaking so there can be caseless hitscan etc.
|
||||||
|
if (TryComp(projectile, out HitscanComponent? hitscan))
|
||||||
|
{
|
||||||
|
FireHitscan(shooter, hitscan, component, angle);
|
||||||
|
}
|
||||||
|
else if (HasComp<ProjectileComponent>(projectile) &&
|
||||||
|
TryComp(ammo, out AmmoComponent? ammoComponent))
|
||||||
|
{
|
||||||
|
FireProjectiles(shooter, projectile, component, ammoComponent.ProjectilesFired, ammoComponent.EvenSpreadAngle, angle, ammoComponent.Velocity, ammo!.Value);
|
||||||
|
|
||||||
|
if (component.CanMuzzleFlash)
|
||||||
|
{
|
||||||
|
MuzzleFlash(component.Owner, ammoComponent, angle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ammoComponent.Caseless)
|
||||||
|
{
|
||||||
|
EntityManager.DeleteEntity(ammo.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Invalid types
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
SoundSystem.Play(Filter.Broadcast(), component.SoundGunshot.GetSound(), component.Owner);
|
||||||
|
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
component.LastFire = _gameTiming.CurTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Firing
|
||||||
|
/// <summary>
|
||||||
|
/// Handles firing one or many projectiles
|
||||||
|
/// </summary>
|
||||||
|
private void FireProjectiles(EntityUid shooter, EntityUid baseProjectile, ServerRangedBarrelComponent component, int count, float evenSpreadAngle, Angle angle, float velocity, EntityUid ammo)
|
||||||
|
{
|
||||||
|
List<Angle>? sprayAngleChange = null;
|
||||||
|
if (count > 1)
|
||||||
|
{
|
||||||
|
evenSpreadAngle *= component.SpreadRatio;
|
||||||
|
sprayAngleChange = Linspace(-evenSpreadAngle / 2, evenSpreadAngle / 2, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
var firedProjectiles = new EntityUid[count];
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
EntityUid projectile;
|
||||||
|
|
||||||
|
if (i == 0)
|
||||||
|
{
|
||||||
|
projectile = baseProjectile;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO: Cursed as bruh
|
||||||
|
projectile = EntityManager.SpawnEntity(
|
||||||
|
MetaData(baseProjectile).EntityPrototype?.ID,
|
||||||
|
Transform(baseProjectile).Coordinates);
|
||||||
|
}
|
||||||
|
|
||||||
|
firedProjectiles[i] = projectile;
|
||||||
|
|
||||||
|
Angle projectileAngle;
|
||||||
|
|
||||||
|
if (sprayAngleChange != null)
|
||||||
|
{
|
||||||
|
projectileAngle = angle + sprayAngleChange[i];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
projectileAngle = angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
var physics = EntityManager.GetComponent<IPhysBody>(projectile);
|
||||||
|
physics.BodyStatus = BodyStatus.InAir;
|
||||||
|
|
||||||
|
var projectileComponent = EntityManager.GetComponent<ProjectileComponent>(projectile);
|
||||||
|
projectileComponent.IgnoreEntity(shooter);
|
||||||
|
|
||||||
|
// FIXME: Work around issue where inserting and removing an entity from a container,
|
||||||
|
// then setting its linear velocity in the same tick resets velocity back to zero.
|
||||||
|
// See SharedBroadphaseSystem.HandleContainerInsert()... It sets Awake to false, which causes this.
|
||||||
|
projectile.SpawnTimer(TimeSpan.FromMilliseconds(25), () =>
|
||||||
|
{
|
||||||
|
EntityManager.GetComponent<IPhysBody>(projectile)
|
||||||
|
.LinearVelocity = projectileAngle.ToVec() * velocity;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Transform(projectile).WorldRotation = projectileAngle + MathHelper.PiOver2;
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityManager.EventBus.RaiseLocalEvent(component.Owner, new Barrels.Components.GunShotEvent(firedProjectiles));
|
||||||
|
EntityManager.EventBus.RaiseLocalEvent(ammo, new AmmoShotEvent(firedProjectiles));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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(EntityUid shooter, HitscanComponent hitscan, ServerRangedBarrelComponent component, Angle angle)
|
||||||
|
{
|
||||||
|
var ray = new CollisionRay(Transform(component.Owner).WorldPosition, angle.ToVec(), (int) hitscan.CollisionMask);
|
||||||
|
var rayCastResults = _physics.IntersectRay(Transform(component.Owner).MapID, ray, hitscan.MaxLength, shooter, false).ToList();
|
||||||
|
|
||||||
|
if (rayCastResults.Count >= 1)
|
||||||
|
{
|
||||||
|
var result = rayCastResults[0];
|
||||||
|
var distance = result.Distance;
|
||||||
|
hitscan.FireEffects(shooter, distance, angle, result.HitEntity);
|
||||||
|
var dmg = _damageable.TryChangeDamage(result.HitEntity, hitscan.Damage);
|
||||||
|
if (dmg != null)
|
||||||
|
_logs.Add(LogType.HitScanHit,
|
||||||
|
$"{EntityManager.ToPrettyString(shooter):user} hit {EntityManager.ToPrettyString(result.HitEntity):target} using {EntityManager.ToPrettyString(hitscan.Owner):used} and dealt {dmg.Total:damage} damage");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hitscan.FireEffects(shooter, hitscan.MaxLength, angle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
384
Content.Server/Weapon/Ranged/GunSystem.Magazine.cs
Normal file
384
Content.Server/Weapon/Ranged/GunSystem.Magazine.cs
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Server.Hands.Components;
|
||||||
|
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
||||||
|
using Content.Server.Weapon.Ranged.Barrels.Components;
|
||||||
|
using Content.Shared.Examine;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
|
using Content.Shared.Item;
|
||||||
|
using Content.Shared.Verbs;
|
||||||
|
using Content.Shared.Weapons.Ranged;
|
||||||
|
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
||||||
|
using Content.Shared.Weapons.Ranged.Components;
|
||||||
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
|
||||||
|
namespace Content.Server.Weapon.Ranged;
|
||||||
|
|
||||||
|
public sealed partial class GunSystem
|
||||||
|
{
|
||||||
|
private void AddEjectMagazineVerb(EntityUid uid, MagazineBarrelComponent component, GetAlternativeVerbsEvent args)
|
||||||
|
{
|
||||||
|
if (args.Hands == null ||
|
||||||
|
!args.CanAccess ||
|
||||||
|
!args.CanInteract ||
|
||||||
|
!component.HasMagazine ||
|
||||||
|
!_blocker.CanPickup(args.User))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (component.MagNeedsOpenBolt && !component.BoltOpen)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Verb verb = new()
|
||||||
|
{
|
||||||
|
Text = MetaData(component.MagazineContainer.ContainedEntity!.Value).EntityName,
|
||||||
|
Category = VerbCategory.Eject,
|
||||||
|
Act = () => RemoveMagazine(args.User, component)
|
||||||
|
};
|
||||||
|
args.Verbs.Add(verb);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddMagazineInteractionVerbs(EntityUid uid, MagazineBarrelComponent component, GetInteractionVerbsEvent args)
|
||||||
|
{
|
||||||
|
if (args.Hands == null ||
|
||||||
|
!args.CanAccess ||
|
||||||
|
!args.CanInteract)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Toggle bolt verb
|
||||||
|
Verb toggleBolt = new()
|
||||||
|
{
|
||||||
|
Text = component.BoltOpen
|
||||||
|
? Loc.GetString("close-bolt-verb-get-data-text")
|
||||||
|
: Loc.GetString("open-bolt-verb-get-data-text"),
|
||||||
|
Act = () => component.BoltOpen = !component.BoltOpen
|
||||||
|
};
|
||||||
|
args.Verbs.Add(toggleBolt);
|
||||||
|
|
||||||
|
// Are we holding a mag that we can insert?
|
||||||
|
if (args.Using is not {Valid: true} @using ||
|
||||||
|
!CanInsertMagazine(args.User, @using, component) ||
|
||||||
|
!_blocker.CanDrop(args.User))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Insert mag verb
|
||||||
|
Verb insert = new()
|
||||||
|
{
|
||||||
|
Text = MetaData(@using).EntityName,
|
||||||
|
Category = VerbCategory.Insert,
|
||||||
|
Act = () => InsertMagazine(args.User, @using, component)
|
||||||
|
};
|
||||||
|
args.Verbs.Add(insert);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMagazineExamine(EntityUid uid, MagazineBarrelComponent component, ExaminedEvent args)
|
||||||
|
{
|
||||||
|
args.PushMarkup(Loc.GetString("server-magazine-barrel-component-on-examine", ("caliber", component.Caliber)));
|
||||||
|
|
||||||
|
foreach (var magazineType in GetMagazineTypes(component))
|
||||||
|
{
|
||||||
|
args.PushMarkup(Loc.GetString("server-magazine-barrel-component-on-examine-magazine-type", ("magazineType", magazineType)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMagazineUse(EntityUid uid, MagazineBarrelComponent component, UseInHandEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled) return;
|
||||||
|
|
||||||
|
// Behavior:
|
||||||
|
// If bolt open just close it
|
||||||
|
// If bolt closed then cycle
|
||||||
|
// If we cycle then get next round
|
||||||
|
// If no more round then open bolt
|
||||||
|
|
||||||
|
args.Handled = true;
|
||||||
|
|
||||||
|
if (component.BoltOpen)
|
||||||
|
{
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundBoltClosed.GetSound(), component.Owner, AudioParams.Default.WithVolume(-5));
|
||||||
|
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-use-entity-bolt-closed"), component.Owner, Filter.Entities(args.User));
|
||||||
|
component.BoltOpen = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Could play a rack-slide specific sound here if you're so inclined (if the chamber is empty but rounds are available)
|
||||||
|
|
||||||
|
CycleMagazine(component, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateMagazineAppearance(MagazineBarrelComponent component)
|
||||||
|
{
|
||||||
|
if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
|
||||||
|
|
||||||
|
appearanceComponent.SetData(BarrelBoltVisuals.BoltOpen, component.BoltOpen);
|
||||||
|
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, component.MagazineContainer.ContainedEntity != null);
|
||||||
|
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
|
||||||
|
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMagazineGetState(EntityUid uid, MagazineBarrelComponent component, ref ComponentGetState args)
|
||||||
|
{
|
||||||
|
(int, int)? count = null;
|
||||||
|
if (component.MagazineContainer.ContainedEntity is {Valid: true} magazine &&
|
||||||
|
TryComp(magazine, out RangedMagazineComponent? rangedMagazineComponent))
|
||||||
|
{
|
||||||
|
count = (rangedMagazineComponent.ShotsLeft, rangedMagazineComponent.Capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.State = new MagazineBarrelComponentState(
|
||||||
|
component.ChamberContainer.ContainedEntity != null,
|
||||||
|
component.FireRateSelector,
|
||||||
|
count,
|
||||||
|
component.SoundGunshot.GetSound());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMagazineInteractUsing(EntityUid uid, MagazineBarrelComponent component, InteractUsingEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled) return;
|
||||||
|
|
||||||
|
if (CanInsertMagazine(args.User, args.Used, component, false))
|
||||||
|
{
|
||||||
|
InsertMagazine(args.User, args.Used, component);
|
||||||
|
args.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 1 ammo
|
||||||
|
if (TryComp(args.Used, out AmmoComponent? ammoComponent))
|
||||||
|
{
|
||||||
|
if (!component.BoltOpen)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-ammo-bolt-closed"), component.Owner, Filter.Entities(args.User));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ammoComponent.Caliber != component.Caliber)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-wrong-caliber"), component.Owner, Filter.Entities(args.User));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.ChamberContainer.ContainedEntity == null)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-ammo-success"), component.Owner, Filter.Entities(args.User));
|
||||||
|
component.ChamberContainer.Insert(args.Used);
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
UpdateMagazineAppearance(component);
|
||||||
|
args.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-ammo-full"), component.Owner, Filter.Entities(args.User));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMagazineInit(EntityUid uid, MagazineBarrelComponent component, ComponentInit args)
|
||||||
|
{
|
||||||
|
component.ChamberContainer = uid.EnsureContainer<ContainerSlot>($"{component.GetType()}-chamber");
|
||||||
|
component.MagazineContainer = uid.EnsureContainer<ContainerSlot>($"{component.GetType()}-magazine", out var existing);
|
||||||
|
|
||||||
|
if (!existing && component.MagFillPrototype != null)
|
||||||
|
{
|
||||||
|
var magEntity = EntityManager.SpawnEntity(component.MagFillPrototype, Transform(uid).Coordinates);
|
||||||
|
component.MagazineContainer.Insert(magEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporary coz client doesn't know about magfill.
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMagazineMapInit(EntityUid uid, MagazineBarrelComponent component, MapInitEvent args)
|
||||||
|
{
|
||||||
|
UpdateMagazineAppearance(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryEjectChamber(MagazineBarrelComponent component)
|
||||||
|
{
|
||||||
|
if (component.ChamberContainer.ContainedEntity is {Valid: true} chamberEntity)
|
||||||
|
{
|
||||||
|
if (!component.ChamberContainer.Remove(chamberEntity))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var ammoComponent = EntityManager.GetComponent<AmmoComponent>(chamberEntity);
|
||||||
|
if (!ammoComponent.Caseless)
|
||||||
|
{
|
||||||
|
EjectCasing(chamberEntity);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryFeedChamber(MagazineBarrelComponent component)
|
||||||
|
{
|
||||||
|
if (component.ChamberContainer.ContainedEntity != null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try and pull a round from the magazine to replace the chamber if possible
|
||||||
|
var magazine = component.MagazineContainer.ContainedEntity;
|
||||||
|
var magComp = EntityManager.GetComponentOrNull<RangedMagazineComponent>(magazine);
|
||||||
|
|
||||||
|
if (magComp == null || TakeAmmo(magComp) is not {Valid: true} nextRound)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
component.ChamberContainer.Insert(nextRound);
|
||||||
|
|
||||||
|
if (component.AutoEjectMag && magazine != null && EntityManager.GetComponent<RangedMagazineComponent>(magazine.Value).ShotsLeft == 0)
|
||||||
|
{
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundAutoEject.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
||||||
|
|
||||||
|
component.MagazineContainer.Remove(magazine.Value);
|
||||||
|
// TODO: Should be a state or something, waste of bandwidth
|
||||||
|
RaiseNetworkEvent(new MagazineAutoEjectEvent {Uid = component.Owner});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CycleMagazine(MagazineBarrelComponent component, bool manual = false)
|
||||||
|
{
|
||||||
|
if (component.BoltOpen)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TryEjectChamber(component);
|
||||||
|
|
||||||
|
TryFeedChamber(component);
|
||||||
|
|
||||||
|
if (component.ChamberContainer.ContainedEntity == null && !component.BoltOpen)
|
||||||
|
{
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundBoltOpen.GetSound(), component.Owner, AudioParams.Default.WithVolume(-5));
|
||||||
|
|
||||||
|
if (_container.TryGetContainingContainer(component.Owner, out var container))
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-cycle-bolt-open"), component.Owner, Filter.Entities(container.Owner));
|
||||||
|
}
|
||||||
|
|
||||||
|
component.BoltOpen = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manual)
|
||||||
|
{
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundRack.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
UpdateMagazineAppearance(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntityUid? PeekAmmo(MagazineBarrelComponent component)
|
||||||
|
{
|
||||||
|
return component.BoltOpen ? null : component.ChamberContainer.ContainedEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntityUid? TakeProjectile(MagazineBarrelComponent component, EntityCoordinates spawnAt)
|
||||||
|
{
|
||||||
|
if (component.BoltOpen)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var entity = component.ChamberContainer.ContainedEntity;
|
||||||
|
|
||||||
|
CycleMagazine(component);
|
||||||
|
|
||||||
|
return entity != null ? TakeBullet(EntityManager.GetComponent<AmmoComponent>(entity.Value), spawnAt) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MagazineType> GetMagazineTypes(MagazineBarrelComponent component)
|
||||||
|
{
|
||||||
|
var types = new List<MagazineType>();
|
||||||
|
|
||||||
|
foreach (MagazineType mag in Enum.GetValues(typeof(MagazineType)))
|
||||||
|
{
|
||||||
|
if ((component.MagazineTypes & mag) != 0)
|
||||||
|
{
|
||||||
|
types.Add(mag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveMagazine(EntityUid user, MagazineBarrelComponent component)
|
||||||
|
{
|
||||||
|
var mag = component.MagazineContainer.ContainedEntity;
|
||||||
|
|
||||||
|
if (mag == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (component.MagNeedsOpenBolt && !component.BoltOpen)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-remove-magazine-bolt-closed"), component.Owner, Filter.Entities(user));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
component.MagazineContainer.Remove(mag.Value);
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundMagEject.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
||||||
|
|
||||||
|
if (TryComp(user, out HandsComponent? handsComponent))
|
||||||
|
{
|
||||||
|
handsComponent.PutInHandOrDrop(EntityManager.GetComponent<SharedItemComponent>(mag.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
UpdateMagazineAppearance(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanInsertMagazine(EntityUid user, EntityUid magazine, MagazineBarrelComponent component, bool quiet = true)
|
||||||
|
{
|
||||||
|
if (!TryComp(magazine, out RangedMagazineComponent? magazineComponent))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((component.MagazineTypes & magazineComponent.MagazineType) == 0)
|
||||||
|
{
|
||||||
|
if (!quiet)
|
||||||
|
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-wrong-magazine-type"), component.Owner, Filter.Entities(user));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (magazineComponent.Caliber != component.Caliber)
|
||||||
|
{
|
||||||
|
if (!quiet)
|
||||||
|
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-wrong-caliber"), component.Owner, Filter.Entities(user));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.MagNeedsOpenBolt && !component.BoltOpen)
|
||||||
|
{
|
||||||
|
if (!quiet)
|
||||||
|
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-bolt-closed"), component.Owner, Filter.Entities(user));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.MagazineContainer.ContainedEntity == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!quiet)
|
||||||
|
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-already-holding-magazine"), component.Owner, Filter.Entities(user));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InsertMagazine(EntityUid user, EntityUid magazine, MagazineBarrelComponent component)
|
||||||
|
{
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundMagInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
||||||
|
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-success"), component.Owner, Filter.Entities(user));
|
||||||
|
component.MagazineContainer.Insert(magazine);
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
UpdateMagazineAppearance(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
192
Content.Server/Weapon/Ranged/GunSystem.Pump.cs
Normal file
192
Content.Server/Weapon/Ranged/GunSystem.Pump.cs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
||||||
|
using Content.Server.Weapon.Ranged.Barrels.Components;
|
||||||
|
using Content.Shared.Examine;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
|
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
||||||
|
using Content.Shared.Weapons.Ranged.Components;
|
||||||
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
|
||||||
|
namespace Content.Server.Weapon.Ranged;
|
||||||
|
|
||||||
|
public sealed partial class GunSystem
|
||||||
|
{
|
||||||
|
private void OnPumpExamine(EntityUid uid, PumpBarrelComponent component, ExaminedEvent args)
|
||||||
|
{
|
||||||
|
args.PushMarkup(Loc.GetString("pump-barrel-component-on-examine", ("caliber", component.Caliber)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPumpGetState(EntityUid uid, PumpBarrelComponent component, ref ComponentGetState args)
|
||||||
|
{
|
||||||
|
(int, int)? count = (component.ShotsLeft, component.Capacity);
|
||||||
|
var chamberedExists = component.ChamberContainer.ContainedEntity != null;
|
||||||
|
// (Is one chambered?, is the bullet spend)
|
||||||
|
var chamber = (chamberedExists, false);
|
||||||
|
|
||||||
|
if (chamberedExists && TryComp<AmmoComponent?>(component.ChamberContainer.ContainedEntity!.Value, out var ammo))
|
||||||
|
{
|
||||||
|
chamber.Item2 = ammo.Spent;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.State = new PumpBarrelComponentState(
|
||||||
|
chamber,
|
||||||
|
component.FireRateSelector,
|
||||||
|
count,
|
||||||
|
component.SoundGunshot.GetSound());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPumpMapInit(EntityUid uid, PumpBarrelComponent component, MapInitEvent args)
|
||||||
|
{
|
||||||
|
if (component.FillPrototype != null)
|
||||||
|
{
|
||||||
|
component.UnspawnedCount += component.Capacity - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdatePumpAppearance(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePumpAppearance(PumpBarrelComponent component)
|
||||||
|
{
|
||||||
|
if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
|
||||||
|
|
||||||
|
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
|
||||||
|
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPumpInit(EntityUid uid, PumpBarrelComponent component, ComponentInit args)
|
||||||
|
{
|
||||||
|
component.AmmoContainer =
|
||||||
|
uid.EnsureContainer<Container>($"{component.GetType()}-ammo-container", out var existing);
|
||||||
|
|
||||||
|
if (existing)
|
||||||
|
{
|
||||||
|
foreach (var entity in component.AmmoContainer.ContainedEntities)
|
||||||
|
{
|
||||||
|
component.SpawnedAmmo.Push(entity);
|
||||||
|
component.UnspawnedCount--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component.ChamberContainer =
|
||||||
|
uid.EnsureContainer<ContainerSlot>($"{component.GetType()}-chamber-container", out existing);
|
||||||
|
|
||||||
|
if (existing)
|
||||||
|
{
|
||||||
|
component.UnspawnedCount--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryComp(uid, out AppearanceComponent? appearanceComponent))
|
||||||
|
{
|
||||||
|
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
UpdatePumpAppearance(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPumpUse(EntityUid uid, PumpBarrelComponent component, UseInHandEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled) return;
|
||||||
|
|
||||||
|
args.Handled = true;
|
||||||
|
CyclePump(component, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPumpInteractUsing(EntityUid uid, PumpBarrelComponent component, InteractUsingEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled) return;
|
||||||
|
|
||||||
|
if (TryInsertBullet(component, args))
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryInsertBullet(PumpBarrelComponent component, InteractUsingEvent args)
|
||||||
|
{
|
||||||
|
if (!TryComp(args.Used, out AmmoComponent? ammoComponent))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ammoComponent.Caliber != component.Caliber)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("pump-barrel-component-try-insert-bullet-wrong-caliber"), component.Owner, Filter.Entities(args.User));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.AmmoContainer.ContainedEntities.Count < component.Capacity - 1)
|
||||||
|
{
|
||||||
|
component.AmmoContainer.Insert(args.Used);
|
||||||
|
component.SpawnedAmmo.Push(args.Used);
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
UpdatePumpAppearance(component);
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_popup.PopupEntity(Loc.GetString("pump-barrel-component-try-insert-bullet-no-room"), component.Owner, Filter.Entities(args.User));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CyclePump(PumpBarrelComponent component, bool manual = false)
|
||||||
|
{
|
||||||
|
if (component.ChamberContainer.ContainedEntity is {Valid: true} chamberedEntity)
|
||||||
|
{
|
||||||
|
component.ChamberContainer.Remove(chamberedEntity);
|
||||||
|
var ammoComponent = EntityManager.GetComponent<AmmoComponent>(chamberedEntity);
|
||||||
|
if (!ammoComponent.Caseless)
|
||||||
|
{
|
||||||
|
EjectCasing(chamberedEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.SpawnedAmmo.TryPop(out var next))
|
||||||
|
{
|
||||||
|
component.AmmoContainer.Remove(next);
|
||||||
|
component.ChamberContainer.Insert(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.UnspawnedCount > 0)
|
||||||
|
{
|
||||||
|
component.UnspawnedCount--;
|
||||||
|
var ammoEntity = EntityManager.SpawnEntity(component.FillPrototype, Transform(component.Owner).Coordinates);
|
||||||
|
component.ChamberContainer.Insert(ammoEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manual)
|
||||||
|
{
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundCycle.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
UpdatePumpAppearance(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntityUid? PeekAmmo(PumpBarrelComponent component)
|
||||||
|
{
|
||||||
|
return component.ChamberContainer.ContainedEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntityUid? TakeProjectile(PumpBarrelComponent component, EntityCoordinates spawnAt)
|
||||||
|
{
|
||||||
|
if (!component.ManualCycle)
|
||||||
|
{
|
||||||
|
CyclePump(component);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.ChamberContainer.ContainedEntity is not {Valid: true} chamberEntity) return null;
|
||||||
|
|
||||||
|
var ammoComponent = EntityManager.GetComponentOrNull<AmmoComponent>(chamberEntity);
|
||||||
|
|
||||||
|
return ammoComponent == null ? null : TakeBullet(ammoComponent, spawnAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
142
Content.Server/Weapon/Ranged/GunSystem.RangedMagazine.cs
Normal file
142
Content.Server/Weapon/Ranged/GunSystem.RangedMagazine.cs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
using System;
|
||||||
|
using Content.Server.Hands.Components;
|
||||||
|
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
||||||
|
using Content.Server.Weapon.Ranged.Barrels.Components;
|
||||||
|
using Content.Shared.Examine;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
|
using Content.Shared.Item;
|
||||||
|
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
|
||||||
|
namespace Content.Server.Weapon.Ranged;
|
||||||
|
|
||||||
|
public sealed partial class GunSystem
|
||||||
|
{
|
||||||
|
private void OnRangedMagMapInit(EntityUid uid, RangedMagazineComponent component, MapInitEvent args)
|
||||||
|
{
|
||||||
|
if (component.FillPrototype != null)
|
||||||
|
{
|
||||||
|
component.UnspawnedCount += component.Capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateRangedMagAppearance(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRangedMagInit(EntityUid uid, RangedMagazineComponent component, ComponentInit args)
|
||||||
|
{
|
||||||
|
component.AmmoContainer = uid.EnsureContainer<Container>($"{component.GetType()}-magazine", out var existing);
|
||||||
|
|
||||||
|
if (existing)
|
||||||
|
{
|
||||||
|
if (component.AmmoContainer.ContainedEntities.Count > component.Capacity)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Initialized capacity of magazine higher than its actual capacity");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entity in component.AmmoContainer.ContainedEntities)
|
||||||
|
{
|
||||||
|
component.SpawnedAmmo.Push(entity);
|
||||||
|
component.UnspawnedCount--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryComp(component.Owner, out AppearanceComponent? appearanceComponent))
|
||||||
|
{
|
||||||
|
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateRangedMagAppearance(RangedMagazineComponent component)
|
||||||
|
{
|
||||||
|
if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
|
||||||
|
|
||||||
|
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
|
||||||
|
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRangedMagUse(EntityUid uid, RangedMagazineComponent component, UseInHandEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled) return;
|
||||||
|
|
||||||
|
if (!TryComp(args.User, out HandsComponent? handsComponent))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TakeAmmo(component) is not {Valid: true} ammo)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var itemComponent = EntityManager.GetComponent<SharedItemComponent>(ammo);
|
||||||
|
if (!handsComponent.CanPutInHand(itemComponent))
|
||||||
|
{
|
||||||
|
Transform(ammo).Coordinates = Transform(args.User).Coordinates;
|
||||||
|
EjectCasing(ammo);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
handsComponent.PutInHand(itemComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRangedMagExamine(EntityUid uid, RangedMagazineComponent component, ExaminedEvent args)
|
||||||
|
{
|
||||||
|
args.PushMarkup(Loc.GetString("ranged-magazine-component-on-examine", ("magazineType", component.MagazineType),("caliber", component.Caliber)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRangedMagInteractUsing(EntityUid uid, RangedMagazineComponent component, InteractUsingEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled) return;
|
||||||
|
|
||||||
|
if (TryInsertAmmo(args.User, args.Used, component))
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryInsertAmmo(EntityUid user, EntityUid ammo, RangedMagazineComponent component)
|
||||||
|
{
|
||||||
|
if (!TryComp(ammo, out AmmoComponent? ammoComponent))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ammoComponent.Caliber != component.Caliber)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("ranged-magazine-component-try-insert-ammo-wrong-caliber"), component.Owner, Filter.Entities(user));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.ShotsLeft >= component.Capacity)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("ranged-magazine-component-try-insert-ammo-is-full "), component.Owner, Filter.Entities(user));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
component.AmmoContainer.Insert(ammo);
|
||||||
|
component.SpawnedAmmo.Push(ammo);
|
||||||
|
UpdateRangedMagAppearance(component);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntityUid? TakeAmmo(RangedMagazineComponent component)
|
||||||
|
{
|
||||||
|
EntityUid? ammo = null;
|
||||||
|
// If anything's spawned use that first, otherwise use the fill prototype as a fallback (if we have spawn count left)
|
||||||
|
if (component.SpawnedAmmo.TryPop(out var entity))
|
||||||
|
{
|
||||||
|
ammo = entity;
|
||||||
|
component.AmmoContainer.Remove(entity);
|
||||||
|
}
|
||||||
|
else if (component.UnspawnedCount > 0)
|
||||||
|
{
|
||||||
|
component.UnspawnedCount--;
|
||||||
|
ammo = EntityManager.SpawnEntity(component.FillPrototype, Transform(component.Owner).Coordinates);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateRangedMagAppearance(component);
|
||||||
|
return ammo;
|
||||||
|
}
|
||||||
|
}
|
||||||
228
Content.Server/Weapon/Ranged/GunSystem.Revolvers.cs
Normal file
228
Content.Server/Weapon/Ranged/GunSystem.Revolvers.cs
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
using System;
|
||||||
|
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
||||||
|
using Content.Server.Weapon.Ranged.Barrels.Components;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
|
using Content.Shared.Popups;
|
||||||
|
using Content.Shared.Verbs;
|
||||||
|
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
||||||
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
|
||||||
|
namespace Content.Server.Weapon.Ranged;
|
||||||
|
|
||||||
|
public sealed partial class GunSystem
|
||||||
|
{
|
||||||
|
private void OnRevolverUse(EntityUid uid, RevolverBarrelComponent component, UseInHandEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled) return;
|
||||||
|
|
||||||
|
EjectAllSlots(component);
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
UpdateRevolverAppearance(component);
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRevolverInteractUsing(EntityUid uid, RevolverBarrelComponent component, InteractUsingEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled) return;
|
||||||
|
|
||||||
|
if (TryInsertBullet(args.User, args.Used, component))
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryInsertBullet(EntityUid user, EntityUid entity, RevolverBarrelComponent component)
|
||||||
|
{
|
||||||
|
if (!TryComp(entity, out AmmoComponent? ammoComponent))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ammoComponent.Caliber != component.Caliber)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("revolver-barrel-component-try-insert-bullet-wrong-caliber"), component.Owner, Filter.Entities(user));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Functions like a stack
|
||||||
|
// These are inserted in reverse order but then when fired Cycle will go through in order
|
||||||
|
// The reason we don't just use an actual stack is because spin can select a random slot to point at
|
||||||
|
for (var i = component.AmmoSlots.Length - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var slot = component.AmmoSlots[i];
|
||||||
|
if (slot == default)
|
||||||
|
{
|
||||||
|
component.CurrentSlot = i;
|
||||||
|
component.AmmoSlots[i] = entity;
|
||||||
|
component.AmmoContainer.Insert(entity);
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
||||||
|
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
UpdateRevolverAppearance(component);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_popup.PopupEntity(Loc.GetString("revolver-barrel-component-try-insert-bullet-ammo-full"), ammoComponent.Owner, Filter.Entities(user));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Russian Roulette
|
||||||
|
/// </summary>
|
||||||
|
public void SpinRevolver(RevolverBarrelComponent component)
|
||||||
|
{
|
||||||
|
var random = _random.Next(component.AmmoSlots.Length - 1);
|
||||||
|
component.CurrentSlot = random;
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundSpin.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CycleRevolver(RevolverBarrelComponent component)
|
||||||
|
{
|
||||||
|
// Move up a slot
|
||||||
|
component.CurrentSlot = (component.CurrentSlot + 1) % component.AmmoSlots.Length;
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
UpdateRevolverAppearance(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EjectAllSlots(RevolverBarrelComponent component)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < component.AmmoSlots.Length; i++)
|
||||||
|
{
|
||||||
|
var entity = component.AmmoSlots[i];
|
||||||
|
if (entity == null) continue;
|
||||||
|
|
||||||
|
component.AmmoContainer.Remove(entity.Value);
|
||||||
|
EjectCasing(entity.Value);
|
||||||
|
component.AmmoSlots[i] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.AmmoContainer.ContainedEntities.Count > 0)
|
||||||
|
{
|
||||||
|
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundEject.GetSound(), component.Owner, AudioParams.Default.WithVolume(-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// May as well point back at the end?
|
||||||
|
component.CurrentSlot = component.AmmoSlots.Length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRevolverGetState(EntityUid uid, RevolverBarrelComponent component, ref ComponentGetState args)
|
||||||
|
{
|
||||||
|
var slotsSpent = new bool?[component.Capacity];
|
||||||
|
for (var i = 0; i < component.Capacity; i++)
|
||||||
|
{
|
||||||
|
slotsSpent[i] = null;
|
||||||
|
var ammoEntity = component.AmmoSlots[i];
|
||||||
|
if (ammoEntity != default && TryComp(ammoEntity, out AmmoComponent? ammo))
|
||||||
|
{
|
||||||
|
slotsSpent[i] = ammo.Spent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: make yaml var to not sent currentSlot/UI? (for russian roulette)
|
||||||
|
args.State = new RevolverBarrelComponentState(
|
||||||
|
component.CurrentSlot,
|
||||||
|
component.FireRateSelector,
|
||||||
|
slotsSpent,
|
||||||
|
component.SoundGunshot.GetSound());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRevolverMapInit(EntityUid uid, RevolverBarrelComponent component, MapInitEvent args)
|
||||||
|
{
|
||||||
|
component.UnspawnedCount = component.Capacity;
|
||||||
|
var idx = 0;
|
||||||
|
component.AmmoContainer = component.Owner.EnsureContainer<Container>($"{component.GetType()}-ammoContainer", out var existing);
|
||||||
|
if (existing)
|
||||||
|
{
|
||||||
|
foreach (var entity in component.AmmoContainer.ContainedEntities)
|
||||||
|
{
|
||||||
|
component.UnspawnedCount--;
|
||||||
|
component.AmmoSlots[idx] = entity;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Revolvers should also defer spawning T B H
|
||||||
|
var xform = EntityManager.GetComponent<TransformComponent>(uid);
|
||||||
|
|
||||||
|
for (var i = 0; i < component.UnspawnedCount; i++)
|
||||||
|
{
|
||||||
|
var entity = EntityManager.SpawnEntity(component.FillPrototype, xform.Coordinates);
|
||||||
|
component.AmmoSlots[idx] = entity;
|
||||||
|
component.AmmoContainer.Insert(entity);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateRevolverAppearance(component);
|
||||||
|
component.Dirty(EntityManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateRevolverAppearance(RevolverBarrelComponent component)
|
||||||
|
{
|
||||||
|
if (!TryComp(component.Owner, out AppearanceComponent? appearance))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder, at this stage it's just here for the RPG
|
||||||
|
appearance.SetData(MagazineBarrelVisuals.MagLoaded, component.ShotsLeft > 0);
|
||||||
|
appearance.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
|
||||||
|
appearance.SetData(AmmoVisuals.AmmoMax, component.Capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddSpinVerb(EntityUid uid, RevolverBarrelComponent component, GetAlternativeVerbsEvent args)
|
||||||
|
{
|
||||||
|
if (args.Hands == null || !args.CanAccess || !args.CanInteract)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (component.Capacity <= 1 || component.ShotsLeft == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Verb verb = new()
|
||||||
|
{
|
||||||
|
Text = Loc.GetString("spin-revolver-verb-get-data-text"),
|
||||||
|
IconTexture = "/Textures/Interface/VerbIcons/refresh.svg.192dpi.png",
|
||||||
|
Act = () =>
|
||||||
|
{
|
||||||
|
SpinRevolver(component);
|
||||||
|
component.Owner.PopupMessage(args.User, Loc.GetString("spin-revolver-verb-on-activate"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
args.Verbs.Add(verb);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntityUid? PeekAmmo(RevolverBarrelComponent component)
|
||||||
|
{
|
||||||
|
return component.AmmoSlots[component.CurrentSlot];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Takes a projectile out if possible
|
||||||
|
/// IEnumerable just to make supporting shotguns saner
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="NotImplementedException"></exception>
|
||||||
|
public EntityUid? TakeProjectile(RevolverBarrelComponent component, EntityCoordinates spawnAt)
|
||||||
|
{
|
||||||
|
var ammo = component.AmmoSlots[component.CurrentSlot];
|
||||||
|
EntityUid? bullet = null;
|
||||||
|
if (ammo != null)
|
||||||
|
{
|
||||||
|
var ammoComponent = EntityManager.GetComponent<AmmoComponent>(ammo.Value);
|
||||||
|
bullet = TakeBullet(ammoComponent, spawnAt);
|
||||||
|
if (ammoComponent.Caseless)
|
||||||
|
{
|
||||||
|
component.AmmoSlots[component.CurrentSlot] = null;
|
||||||
|
component.AmmoContainer.Remove(ammo.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CycleRevolver(component);
|
||||||
|
UpdateRevolverAppearance(component);
|
||||||
|
return bullet;
|
||||||
|
}
|
||||||
|
}
|
||||||
188
Content.Server/Weapon/Ranged/GunSystem.SpeedLoader.cs
Normal file
188
Content.Server/Weapon/Ranged/GunSystem.SpeedLoader.cs
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
using Content.Server.Hands.Components;
|
||||||
|
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
||||||
|
using Content.Server.Weapon.Ranged.Barrels.Components;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
|
using Content.Shared.Item;
|
||||||
|
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
|
||||||
|
namespace Content.Server.Weapon.Ranged;
|
||||||
|
|
||||||
|
public sealed partial class GunSystem
|
||||||
|
{
|
||||||
|
private void OnSpeedLoaderInit(EntityUid uid, SpeedLoaderComponent component, ComponentInit args)
|
||||||
|
{
|
||||||
|
component.AmmoContainer = uid.EnsureContainer<Container>($"{component.GetType()}-container", out var existing);
|
||||||
|
|
||||||
|
if (existing)
|
||||||
|
{
|
||||||
|
foreach (var ammo in component.AmmoContainer.ContainedEntities)
|
||||||
|
{
|
||||||
|
component.UnspawnedCount--;
|
||||||
|
component.SpawnedAmmo.Push(ammo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSpeedLoaderMapInit(EntityUid uid, SpeedLoaderComponent component, MapInitEvent args)
|
||||||
|
{
|
||||||
|
component.UnspawnedCount += component.Capacity;
|
||||||
|
UpdateSpeedLoaderAppearance(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSpeedLoaderAppearance(SpeedLoaderComponent component)
|
||||||
|
{
|
||||||
|
if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
|
||||||
|
|
||||||
|
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
||||||
|
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.AmmoLeft);
|
||||||
|
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EntityUid? TakeAmmo(SpeedLoaderComponent component)
|
||||||
|
{
|
||||||
|
if (component.SpawnedAmmo.TryPop(out var entity))
|
||||||
|
{
|
||||||
|
component.AmmoContainer.Remove(entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.UnspawnedCount > 0)
|
||||||
|
{
|
||||||
|
component.UnspawnedCount--;
|
||||||
|
return EntityManager.SpawnEntity(component.FillPrototype, Transform(component.Owner).Coordinates);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSpeedLoaderUse(EntityUid uid, SpeedLoaderComponent component, UseInHandEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled) return;
|
||||||
|
|
||||||
|
if (!TryComp(args.User, out HandsComponent? handsComponent))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ammo = TakeAmmo(component);
|
||||||
|
if (ammo == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemComponent = EntityManager.GetComponent<SharedItemComponent>(ammo.Value);
|
||||||
|
if (!handsComponent.CanPutInHand(itemComponent))
|
||||||
|
{
|
||||||
|
EjectCasing(ammo.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
handsComponent.PutInHand(itemComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateSpeedLoaderAppearance(component);
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSpeedLoaderAfterInteract(EntityUid uid, SpeedLoaderComponent component, AfterInteractEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled) return;
|
||||||
|
|
||||||
|
if (args.Target == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This area is dirty but not sure of an easier way to do it besides add an interface or somethin
|
||||||
|
var changed = false;
|
||||||
|
|
||||||
|
if (TryComp(args.Target.Value, out RevolverBarrelComponent? revolverBarrel))
|
||||||
|
{
|
||||||
|
for (var i = 0; i < component.Capacity; i++)
|
||||||
|
{
|
||||||
|
var ammo = TakeAmmo(component);
|
||||||
|
if (ammo == null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryInsertBullet(args.User, ammo.Value, revolverBarrel))
|
||||||
|
{
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the ammo back
|
||||||
|
TryInsertAmmo(args.User, ammo.Value, component);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (TryComp(args.Target.Value, out BoltActionBarrelComponent? boltActionBarrel))
|
||||||
|
{
|
||||||
|
for (var i = 0; i < component.Capacity; i++)
|
||||||
|
{
|
||||||
|
var ammo = TakeAmmo(component);
|
||||||
|
if (ammo == null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryInsertBullet(args.User, ammo.Value, boltActionBarrel))
|
||||||
|
{
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the ammo back
|
||||||
|
TryInsertAmmo(args.User, ammo.Value, component);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
UpdateSpeedLoaderAppearance(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryInsertAmmo(EntityUid user, EntityUid entity, SpeedLoaderComponent component)
|
||||||
|
{
|
||||||
|
if (!TryComp(entity, out AmmoComponent? ammoComponent))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ammoComponent.Caliber != component.Caliber)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("speed-loader-component-try-insert-ammo-wrong-caliber"), component.Owner, Filter.Entities(user));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.AmmoLeft >= component.Capacity)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("speed-loader-component-try-insert-ammo-no-room"), component.Owner, Filter.Entities(user));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
component.SpawnedAmmo.Push(entity);
|
||||||
|
component.AmmoContainer.Insert(entity);
|
||||||
|
UpdateSpeedLoaderAppearance(component);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSpeedLoaderInteractUsing(EntityUid uid, SpeedLoaderComponent component, InteractUsingEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled) return;
|
||||||
|
|
||||||
|
if (TryInsertAmmo(args.User, args.Used, component))
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Server.Administration.Logs;
|
||||||
|
using Content.Server.Atmos.EntitySystems;
|
||||||
|
using Content.Server.Hands.Components;
|
||||||
|
using Content.Server.PowerCell;
|
||||||
|
using Content.Server.Stunnable;
|
||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
||||||
|
using Content.Server.Weapon.Ranged.Barrels.Components;
|
||||||
|
using Content.Shared.ActionBlocker;
|
||||||
|
using Content.Shared.Camera;
|
||||||
|
using Content.Shared.Damage;
|
||||||
using Content.Shared.Examine;
|
using Content.Shared.Examine;
|
||||||
using Content.Shared.Interaction;
|
using Content.Shared.Interaction;
|
||||||
using Content.Shared.Popups;
|
using Content.Shared.Popups;
|
||||||
|
using Content.Shared.PowerCell.Components;
|
||||||
using Content.Shared.Verbs;
|
using Content.Shared.Verbs;
|
||||||
|
using Content.Shared.Weapons.Ranged.Components;
|
||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
using Robust.Shared.IoC;
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Random;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged;
|
namespace Content.Server.Weapon.Ranged;
|
||||||
@@ -13,22 +34,224 @@ namespace Content.Server.Weapon.Ranged;
|
|||||||
public sealed partial class GunSystem : EntitySystem
|
public sealed partial class GunSystem : EntitySystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||||
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
|
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
|
||||||
|
[Dependency] private readonly AdminLogSystem _logs = default!;
|
||||||
|
[Dependency] private readonly AtmosphereSystem _atmos = default!;
|
||||||
|
[Dependency] private readonly CameraRecoilSystem _recoil = default!;
|
||||||
|
[Dependency] private readonly DamageableSystem _damageable = default!;
|
||||||
[Dependency] private readonly EffectSystem _effects = default!;
|
[Dependency] private readonly EffectSystem _effects = default!;
|
||||||
|
[Dependency] private readonly PowerCellSystem _cell = default!;
|
||||||
|
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||||
|
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||||
|
[Dependency] private readonly StunSystem _stun = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How many sounds are allowed to be played on ejecting multiple casings.
|
||||||
|
/// </summary>
|
||||||
|
private const int EjectionSoundMax = 3;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
|
// TODO: So at the time I thought there might've been a need to keep magazines
|
||||||
|
// and ammo boxes separate.
|
||||||
|
// There isn't.
|
||||||
|
// They should be combined.
|
||||||
|
|
||||||
SubscribeLocalEvent<AmmoComponent, ExaminedEvent>(OnAmmoExamine);
|
SubscribeLocalEvent<AmmoComponent, ExaminedEvent>(OnAmmoExamine);
|
||||||
|
|
||||||
SubscribeLocalEvent<AmmoBoxComponent, ComponentInit>(OnAmmoBoxInit);
|
SubscribeLocalEvent<AmmoBoxComponent, ComponentInit>(OnAmmoBoxInit);
|
||||||
SubscribeLocalEvent<AmmoBoxComponent, MapInitEvent>(OnAmmoBoxMapInit);
|
SubscribeLocalEvent<AmmoBoxComponent, MapInitEvent>(OnAmmoBoxMapInit);
|
||||||
SubscribeLocalEvent<AmmoBoxComponent, ExaminedEvent>(OnAmmoBoxExamine);
|
SubscribeLocalEvent<AmmoBoxComponent, ExaminedEvent>(OnAmmoBoxExamine);
|
||||||
|
|
||||||
SubscribeLocalEvent<AmmoBoxComponent, InteractUsingEvent>(OnAmmoBoxInteractUsing);
|
SubscribeLocalEvent<AmmoBoxComponent, InteractUsingEvent>(OnAmmoBoxInteractUsing);
|
||||||
SubscribeLocalEvent<AmmoBoxComponent, UseInHandEvent>(OnAmmoBoxUse);
|
SubscribeLocalEvent<AmmoBoxComponent, UseInHandEvent>(OnAmmoBoxUse);
|
||||||
SubscribeLocalEvent<AmmoBoxComponent, InteractHandEvent>(OnAmmoBoxInteractHand);
|
SubscribeLocalEvent<AmmoBoxComponent, InteractHandEvent>(OnAmmoBoxInteractHand);
|
||||||
SubscribeLocalEvent<AmmoBoxComponent, GetAlternativeVerbsEvent>(OnAmmoBoxAltVerbs);
|
SubscribeLocalEvent<AmmoBoxComponent, GetAlternativeVerbsEvent>(OnAmmoBoxAltVerbs);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<RangedMagazineComponent, ComponentInit>(OnRangedMagInit);
|
||||||
|
SubscribeLocalEvent<RangedMagazineComponent, MapInitEvent>(OnRangedMagMapInit);
|
||||||
|
SubscribeLocalEvent<RangedMagazineComponent, UseInHandEvent>(OnRangedMagUse);
|
||||||
|
SubscribeLocalEvent<RangedMagazineComponent, ExaminedEvent>(OnRangedMagExamine);
|
||||||
|
SubscribeLocalEvent<RangedMagazineComponent, InteractUsingEvent>(OnRangedMagInteractUsing);
|
||||||
|
|
||||||
|
// Whenever I get around to refactoring guns this is all going to change.
|
||||||
|
// Essentially the idea is
|
||||||
|
// You have GunComponent and ChamberedGunComponent (which is just guncomp + containerslot for chamber)
|
||||||
|
// GunComponent has a component for an ammo provider on it (e.g. battery) and asks it for ammo to shoot
|
||||||
|
// ALTERNATIVELY, it has a "MagazineAmmoProvider" that has its own containerslot that it can ask
|
||||||
|
// (All of these would be comp references so max you only ever have 2 components on the gun).
|
||||||
|
SubscribeLocalEvent<BatteryBarrelComponent, ComponentInit>(OnBatteryInit);
|
||||||
|
SubscribeLocalEvent<BatteryBarrelComponent, MapInitEvent>(OnBatteryMapInit);
|
||||||
|
SubscribeLocalEvent<BatteryBarrelComponent, ComponentGetState>(OnBatteryGetState);
|
||||||
|
SubscribeLocalEvent<BatteryBarrelComponent, PowerCellChangedEvent>(OnCellSlotUpdated);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<BoltActionBarrelComponent, ComponentInit>(OnBoltInit);
|
||||||
|
SubscribeLocalEvent<BoltActionBarrelComponent, MapInitEvent>(OnBoltMapInit);
|
||||||
|
SubscribeLocalEvent<BoltActionBarrelComponent, GunFireAttemptEvent>(OnBoltFireAttempt);
|
||||||
|
SubscribeLocalEvent<BoltActionBarrelComponent, UseInHandEvent>(OnBoltUse);
|
||||||
|
SubscribeLocalEvent<BoltActionBarrelComponent, InteractUsingEvent>(OnBoltInteractUsing);
|
||||||
|
SubscribeLocalEvent<BoltActionBarrelComponent, ComponentGetState>(OnBoltGetState);
|
||||||
|
SubscribeLocalEvent<BoltActionBarrelComponent, ExaminedEvent>(OnBoltExamine);
|
||||||
|
SubscribeLocalEvent<BoltActionBarrelComponent, GetInteractionVerbsEvent>(AddToggleBoltVerb);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<MagazineBarrelComponent, ComponentInit>(OnMagazineInit);
|
||||||
|
SubscribeLocalEvent<MagazineBarrelComponent, MapInitEvent>(OnMagazineMapInit);
|
||||||
|
SubscribeLocalEvent<MagazineBarrelComponent, ExaminedEvent>(OnMagazineExamine);
|
||||||
|
SubscribeLocalEvent<MagazineBarrelComponent, UseInHandEvent>(OnMagazineUse);
|
||||||
|
SubscribeLocalEvent<MagazineBarrelComponent, InteractUsingEvent>(OnMagazineInteractUsing);
|
||||||
|
SubscribeLocalEvent<MagazineBarrelComponent, ComponentGetState>(OnMagazineGetState);
|
||||||
|
SubscribeLocalEvent<MagazineBarrelComponent, GetInteractionVerbsEvent>(AddMagazineInteractionVerbs);
|
||||||
|
SubscribeLocalEvent<MagazineBarrelComponent, GetAlternativeVerbsEvent>(AddEjectMagazineVerb);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<PumpBarrelComponent, ComponentGetState>(OnPumpGetState);
|
||||||
|
SubscribeLocalEvent<PumpBarrelComponent, ComponentInit>(OnPumpInit);
|
||||||
|
SubscribeLocalEvent<PumpBarrelComponent, MapInitEvent>(OnPumpMapInit);
|
||||||
|
SubscribeLocalEvent<PumpBarrelComponent, ExaminedEvent>(OnPumpExamine);
|
||||||
|
SubscribeLocalEvent<PumpBarrelComponent, UseInHandEvent>(OnPumpUse);
|
||||||
|
SubscribeLocalEvent<PumpBarrelComponent, InteractUsingEvent>(OnPumpInteractUsing);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<RevolverBarrelComponent, MapInitEvent>(OnRevolverMapInit);
|
||||||
|
SubscribeLocalEvent<RevolverBarrelComponent, UseInHandEvent>(OnRevolverUse);
|
||||||
|
SubscribeLocalEvent<RevolverBarrelComponent, InteractUsingEvent>(OnRevolverInteractUsing);
|
||||||
|
SubscribeLocalEvent<RevolverBarrelComponent, ComponentGetState>(OnRevolverGetState);
|
||||||
|
SubscribeLocalEvent<RevolverBarrelComponent, GetAlternativeVerbsEvent>(AddSpinVerb);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<SpeedLoaderComponent, ComponentInit>(OnSpeedLoaderInit);
|
||||||
|
SubscribeLocalEvent<SpeedLoaderComponent, MapInitEvent>(OnSpeedLoaderMapInit);
|
||||||
|
SubscribeLocalEvent<SpeedLoaderComponent, UseInHandEvent>(OnSpeedLoaderUse);
|
||||||
|
SubscribeLocalEvent<SpeedLoaderComponent, AfterInteractEvent>(OnSpeedLoaderAfterInteract);
|
||||||
|
SubscribeLocalEvent<SpeedLoaderComponent, InteractUsingEvent>(OnSpeedLoaderInteractUsing);
|
||||||
|
|
||||||
|
// SubscribeLocalEvent<ServerRangedWeaponComponent, ExaminedEvent>(OnGunExamine);
|
||||||
|
SubscribeNetworkEvent<FirePosEvent>(OnFirePos);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFirePos(FirePosEvent msg, EntitySessionEventArgs args)
|
||||||
|
{
|
||||||
|
if (args.SenderSession.AttachedEntity is not {Valid: true} user)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!msg.Coordinates.IsValid(EntityManager))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!TryComp(user, out HandsComponent? handsComponent))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// TODO: Not exactly robust
|
||||||
|
var gun = handsComponent.GetActiveHand()?.HeldEntity;
|
||||||
|
|
||||||
|
if (gun == null || !TryComp(gun, out ServerRangedWeaponComponent? weapon))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// map pos
|
||||||
|
TryFire(user, msg.Coordinates, weapon);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntityUid? PeekAtAmmo(ServerRangedBarrelComponent component)
|
||||||
|
{
|
||||||
|
return component switch
|
||||||
|
{
|
||||||
|
BatteryBarrelComponent battery => PeekAmmo(battery),
|
||||||
|
BoltActionBarrelComponent bolt => PeekAmmo(bolt),
|
||||||
|
MagazineBarrelComponent mag => PeekAmmo(mag),
|
||||||
|
PumpBarrelComponent pump => PeekAmmo(pump),
|
||||||
|
RevolverBarrelComponent revolver => PeekAmmo(revolver),
|
||||||
|
_ => throw new NotImplementedException()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntityUid? TakeOutProjectile(ServerRangedBarrelComponent component, EntityCoordinates spawnAt)
|
||||||
|
{
|
||||||
|
return component switch
|
||||||
|
{
|
||||||
|
BatteryBarrelComponent battery => TakeProjectile(battery, spawnAt),
|
||||||
|
BoltActionBarrelComponent bolt => TakeProjectile(bolt, spawnAt),
|
||||||
|
MagazineBarrelComponent mag => TakeProjectile(mag, spawnAt),
|
||||||
|
PumpBarrelComponent pump => TakeProjectile(pump, spawnAt),
|
||||||
|
RevolverBarrelComponent revolver => TakeProjectile(revolver, spawnAt),
|
||||||
|
_ => throw new NotImplementedException()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drops multiple cartridges / shells on the floor
|
||||||
|
/// Wraps EjectCasing to make it less toxic for bulk ejections
|
||||||
|
/// </summary>
|
||||||
|
public void EjectCasings(IEnumerable<EntityUid> entities)
|
||||||
|
{
|
||||||
|
var soundPlayCount = 0;
|
||||||
|
var playSound = true;
|
||||||
|
|
||||||
|
foreach (var entity in entities)
|
||||||
|
{
|
||||||
|
EjectCasing(entity, playSound);
|
||||||
|
soundPlayCount++;
|
||||||
|
if (soundPlayCount > EjectionSoundMax)
|
||||||
|
{
|
||||||
|
playSound = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drops a single cartridge / shell
|
||||||
|
/// </summary>
|
||||||
|
public void EjectCasing(
|
||||||
|
EntityUid entity,
|
||||||
|
bool playSound = true,
|
||||||
|
AmmoComponent? ammoComponent = null)
|
||||||
|
{
|
||||||
|
const float ejectOffset = 0.4f;
|
||||||
|
|
||||||
|
if (!Resolve(entity, ref ammoComponent)) return;
|
||||||
|
|
||||||
|
var offsetPos = (_random.NextFloat(-ejectOffset, ejectOffset), _random.NextFloat(-ejectOffset, ejectOffset));
|
||||||
|
|
||||||
|
var xform = Transform(entity);
|
||||||
|
|
||||||
|
var coordinates = xform.Coordinates;
|
||||||
|
coordinates = coordinates.Offset(offsetPos);
|
||||||
|
|
||||||
|
xform.LocalRotation = _random.NextFloat(MathF.Tau);
|
||||||
|
xform.Coordinates = coordinates;
|
||||||
|
|
||||||
|
if (playSound)
|
||||||
|
SoundSystem.Play(Filter.Pvs(entity), ammoComponent.SoundCollectionEject.GetSound(), coordinates, AudioParams.Default.WithVolume(-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Angle GetRecoilAngle(ServerRangedBarrelComponent component, Angle direction)
|
||||||
|
{
|
||||||
|
var currentTime = _gameTiming.CurTime;
|
||||||
|
var timeSinceLastFire = (currentTime - component.LastFire).TotalSeconds;
|
||||||
|
var newTheta = MathHelper.Clamp(component.CurrentAngle.Theta + component.AngleIncrease - component.AngleDecay * timeSinceLastFire, component.MinAngle.Theta, component.MaxAngle.Theta);
|
||||||
|
component.CurrentAngle = new Angle(newTheta);
|
||||||
|
|
||||||
|
var random = (_random.NextDouble(-1, 1));
|
||||||
|
var angle = Angle.FromDegrees(direction.Degrees + component.CurrentAngle.Degrees * random);
|
||||||
|
return angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised on a gun when it fires.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GunShotEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GunFireAttemptEvent : CancellableEntityEventArgs
|
||||||
|
{
|
||||||
|
public EntityUid? User = null;
|
||||||
|
public ServerRangedWeaponComponent Weapon;
|
||||||
|
|
||||||
|
public GunFireAttemptEvent(EntityUid? user, ServerRangedWeaponComponent weapon)
|
||||||
|
{
|
||||||
|
User = user;
|
||||||
|
Weapon = weapon;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using Content.Server.Atmos.EntitySystems;
|
|
||||||
using Content.Server.CombatMode;
|
|
||||||
using Content.Server.Hands.Components;
|
|
||||||
using Content.Server.Interaction.Components;
|
|
||||||
using Content.Server.Stunnable;
|
|
||||||
using Content.Server.Weapon.Ranged.Barrels.Components;
|
|
||||||
using Content.Shared.ActionBlocker;
|
|
||||||
using Content.Shared.Damage;
|
using Content.Shared.Damage;
|
||||||
using Content.Shared.Hands;
|
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Content.Shared.Sound;
|
using Content.Shared.Sound;
|
||||||
using Content.Shared.Weapons.Ranged.Components;
|
using Content.Shared.Weapons.Ranged.Components;
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.IoC;
|
|
||||||
using Robust.Shared.Localization;
|
|
||||||
using Robust.Shared.Log;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Maths;
|
|
||||||
using Robust.Shared.Network;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
using Robust.Shared.Players;
|
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.Timing;
|
|
||||||
using Robust.Shared.ViewVariables;
|
using Robust.Shared.ViewVariables;
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged
|
namespace Content.Server.Weapon.Ranged
|
||||||
@@ -30,11 +11,7 @@ namespace Content.Server.Weapon.Ranged
|
|||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
public sealed class ServerRangedWeaponComponent : SharedRangedWeaponComponent
|
public sealed class ServerRangedWeaponComponent : SharedRangedWeaponComponent
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
public TimeSpan LastFireTime;
|
||||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
|
||||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
|
||||||
|
|
||||||
private TimeSpan _lastFireTime;
|
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
[DataField("clumsyCheck")]
|
[DataField("clumsyCheck")]
|
||||||
@@ -46,154 +23,16 @@ namespace Content.Server.Weapon.Ranged
|
|||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
[DataField("canHotspot")]
|
[DataField("canHotspot")]
|
||||||
private bool _canHotspot = true;
|
public bool CanHotspot = true;
|
||||||
|
|
||||||
[DataField("clumsyWeaponHandlingSound")]
|
[DataField("clumsyWeaponHandlingSound")]
|
||||||
private SoundSpecifier _clumsyWeaponHandlingSound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg");
|
public SoundSpecifier ClumsyWeaponHandlingSound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg");
|
||||||
|
|
||||||
[DataField("clumsyWeaponShotSound")]
|
[DataField("clumsyWeaponShotSound")]
|
||||||
private SoundSpecifier _clumsyWeaponShotSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/bang.ogg");
|
public SoundSpecifier ClumsyWeaponShotSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/bang.ogg");
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
[DataField("clumsyDamage")]
|
[DataField("clumsyDamage")]
|
||||||
public DamageSpecifier? ClumsyDamage;
|
public DamageSpecifier? ClumsyDamage;
|
||||||
|
|
||||||
public Func<bool>? WeaponCanFireHandler;
|
|
||||||
public Func<EntityUid, bool>? UserCanFireHandler;
|
|
||||||
public Action<EntityUid, Vector2>? FireHandler;
|
|
||||||
|
|
||||||
public ServerRangedBarrelComponent? Barrel
|
|
||||||
{
|
|
||||||
get => _barrel;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_barrel != null && value != null)
|
|
||||||
{
|
|
||||||
Logger.Error("Tried setting Barrel on RangedWeapon that already has one");
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
_barrel = value;
|
|
||||||
Dirty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private ServerRangedBarrelComponent? _barrel;
|
|
||||||
|
|
||||||
private FireRateSelector FireRateSelector => _barrel?.FireRateSelector ?? FireRateSelector.Safety;
|
|
||||||
|
|
||||||
private bool WeaponCanFire()
|
|
||||||
{
|
|
||||||
return WeaponCanFireHandler == null || WeaponCanFireHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool UserCanFire(EntityUid user)
|
|
||||||
{
|
|
||||||
return (UserCanFireHandler == null || UserCanFireHandler(user)) && EntitySystem.Get<ActionBlockerSystem>().CanInteract(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
|
|
||||||
public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null)
|
|
||||||
{
|
|
||||||
base.HandleNetworkMessage(message, channel, session);
|
|
||||||
|
|
||||||
if (session == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(session));
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (message)
|
|
||||||
{
|
|
||||||
case FirePosComponentMessage msg:
|
|
||||||
if (session.AttachedEntity is not {Valid: true} user)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.TargetGrid != GridId.Invalid)
|
|
||||||
{
|
|
||||||
// grid pos
|
|
||||||
if (!_mapManager.TryGetGrid(msg.TargetGrid, out var grid))
|
|
||||||
{
|
|
||||||
// Client sent us a message with an invalid grid.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetPos = grid.LocalToWorld(msg.TargetPosition);
|
|
||||||
TryFire(user, targetPos);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// map pos
|
|
||||||
TryFire(user, msg.TargetPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ComponentState GetComponentState()
|
|
||||||
{
|
|
||||||
return new RangedWeaponComponentState(FireRateSelector);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to fire a round of ammo out of the weapon.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="user">Entity that is operating the weapon, usually the player.</param>
|
|
||||||
/// <param name="targetPos">Target position on the map to shoot at.</param>
|
|
||||||
private void TryFire(EntityUid user, Vector2 targetPos)
|
|
||||||
{
|
|
||||||
if (!_entMan.TryGetComponent(user, out HandsComponent? hands) || hands.GetActiveHandItem?.Owner != Owner)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_entMan.TryGetComponent(user, out CombatModeComponent? combat) || !combat.IsInCombatMode)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!UserCanFire(user) || !WeaponCanFire())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var curTime = _gameTiming.CurTime;
|
|
||||||
var span = curTime - _lastFireTime;
|
|
||||||
if (span.TotalSeconds < 1 / _barrel?.FireRate)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastFireTime = curTime;
|
|
||||||
|
|
||||||
if (ClumsyCheck && ClumsyDamage != null && ClumsyComponent.TryRollClumsy(user, ClumsyExplodeChance))
|
|
||||||
{
|
|
||||||
//Wound them
|
|
||||||
EntitySystem.Get<DamageableSystem>().TryChangeDamage(user, ClumsyDamage);
|
|
||||||
EntitySystem.Get<StunSystem>().TryParalyze(user, TimeSpan.FromSeconds(3f), true);
|
|
||||||
|
|
||||||
// Apply salt to the wound ("Honk!")
|
|
||||||
SoundSystem.Play(
|
|
||||||
Filter.Pvs(Owner), _clumsyWeaponHandlingSound.GetSound(),
|
|
||||||
_entMan.GetComponent<TransformComponent>(Owner).Coordinates, AudioParams.Default.WithMaxDistance(5));
|
|
||||||
|
|
||||||
SoundSystem.Play(
|
|
||||||
Filter.Pvs(Owner), _clumsyWeaponShotSound.GetSound(),
|
|
||||||
_entMan.GetComponent<TransformComponent>(Owner).Coordinates, AudioParams.Default.WithMaxDistance(5));
|
|
||||||
|
|
||||||
user.PopupMessage(Loc.GetString("server-ranged-weapon-component-try-fire-clumsy"));
|
|
||||||
|
|
||||||
_entMan.DeleteEntity(Owner);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_canHotspot)
|
|
||||||
{
|
|
||||||
EntitySystem.Get<AtmosphereSystem>().HotspotExpose(_entMan.GetComponent<TransformComponent>(user).Coordinates, 700, 50);
|
|
||||||
}
|
|
||||||
FireHandler?.Invoke(user, targetPos);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,34 +29,16 @@ namespace Content.Shared.Weapons.Ranged.Components
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A component message raised when the weapon is fired at a position on the map.
|
/// An event raised when the weapon is fired at a position on the map by a client.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Serializable, NetSerializable]
|
[Serializable, NetSerializable]
|
||||||
#pragma warning disable 618
|
public sealed class FirePosEvent : EntityEventArgs
|
||||||
public sealed class FirePosComponentMessage : ComponentMessage
|
|
||||||
#pragma warning restore 618
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
public EntityCoordinates Coordinates;
|
||||||
/// If this is not invalid, the target position is relative to the grid.
|
|
||||||
/// Otherwise, it is a map position.
|
|
||||||
/// </summary>
|
|
||||||
public GridId TargetGrid { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
public FirePosEvent(EntityCoordinates coordinates)
|
||||||
/// If Target Grid is not invalid, this is relative to the grid, otherwise
|
|
||||||
/// it is a map position.
|
|
||||||
/// </summary>
|
|
||||||
public Vector2 TargetPosition { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constructs a new instance of <see cref="FirePosComponentMessage"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="targetGrid">The grid that the target position is on, if any.</param>
|
|
||||||
/// <param name="targetPosition">Target position relative to the grid, or a map position if the grid is invalid.</param>
|
|
||||||
public FirePosComponentMessage(GridId targetGrid, Vector2 targetPosition)
|
|
||||||
{
|
{
|
||||||
TargetGrid = targetGrid;
|
Coordinates = coordinates;
|
||||||
TargetPosition = targetPosition;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ namespace Content.Shared.Weapons.Ranged
|
|||||||
/// This is sent if the MagazineBarrel AutoEjects the magazine
|
/// This is sent if the MagazineBarrel AutoEjects the magazine
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Serializable, NetSerializable]
|
[Serializable, NetSerializable]
|
||||||
#pragma warning disable 618
|
public sealed class MagazineAutoEjectEvent : EntityEventArgs
|
||||||
public sealed class MagazineAutoEjectMessage : ComponentMessage {}
|
{
|
||||||
#pragma warning restore 618
|
public EntityUid Uid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
## RevolverBarrelComponent
|
## RevolverBarrelComponent
|
||||||
|
|
||||||
revolver-barrel-component-try-inser-bullet-wrong-caliber = Wrong caliber
|
revolver-barrel-component-try-insert-bullet-wrong-caliber = Wrong caliber
|
||||||
revolver-barrel-component-try-inser-bullet-ammo-full = Ammo full
|
revolver-barrel-component-try-insert-bullet-ammo-full = Ammo full
|
||||||
|
|
||||||
## SpinRevolverVerb
|
## SpinRevolverVerb
|
||||||
|
|
||||||
spin-revolver-verb-get-data-text = Spin
|
spin-revolver-verb-get-data-text = Spin
|
||||||
spin-revolver-verb-on-activate = Spun the cylinder
|
spin-revolver-verb-on-activate = Spun the cylinder
|
||||||
|
|||||||
Reference in New Issue
Block a user