From b5724feb275060955d36fc4d36b78cb9d5a0dee0 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Sun, 30 Jan 2022 17:39:46 +1100 Subject: [PATCH] ECS guns (#6229) Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com> --- .../ClientMagazineBarrelComponent.cs | 16 +- .../Ranged/ClientRangedWeaponComponent.cs | 9 +- .../Weapons/Ranged/GunSystem.AmmoCounter.cs | 31 ++ .../Weapons/Ranged/RangedWeaponSystem.cs | 18 +- .../Components/RangedMagazineComponent.cs | 154 +----- .../Components/SpeedLoaderComponent.cs | 205 +------- .../Weapon/Ranged/Barrels/BarrelSystem.cs | 116 ----- .../Components/BatteryBarrelComponent.cs | 59 +++ .../Components/BoltActionBarrelComponent.cs | 291 +---------- .../Components/MagazineBarrelComponent.cs | 136 +++++ .../Barrels/Components/PumpBarrelComponent.cs | 219 +-------- .../Components/RevolverBarrelComponent.cs | 234 +-------- .../ServerBatteryBarrelComponent.cs | 171 ------- .../ServerMagazineBarrelComponent.cs | 465 ------------------ .../Components/ServerRangedBarrelComponent.cs | 328 +----------- .../Weapon/Ranged/GunSystem.AmmoBox.cs | 14 +- .../Weapon/Ranged/GunSystem.Battery.cs | 119 +++++ .../Weapon/Ranged/GunSystem.Bolt.cs | 268 ++++++++++ .../Weapon/Ranged/GunSystem.Guns.cs | 257 ++++++++++ .../Weapon/Ranged/GunSystem.Magazine.cs | 384 +++++++++++++++ .../Weapon/Ranged/GunSystem.Pump.cs | 192 ++++++++ .../Weapon/Ranged/GunSystem.RangedMagazine.cs | 142 ++++++ .../Weapon/Ranged/GunSystem.Revolvers.cs | 228 +++++++++ .../Weapon/Ranged/GunSystem.SpeedLoader.cs | 188 +++++++ Content.Server/Weapon/Ranged/GunSystem.cs | 225 ++++++++- .../Ranged/ServerRangedWeaponComponent.cs | 169 +------ .../Components/SharedRangedWeaponComponent.cs | 28 +- ...ctMessage.cs => MagazineAutoEjectEvent.cs} | 7 +- .../components/revolver-barrel-component.ftl | 6 +- 29 files changed, 2342 insertions(+), 2337 deletions(-) create mode 100644 Content.Client/Weapons/Ranged/GunSystem.AmmoCounter.cs delete mode 100644 Content.Server/Weapon/Ranged/Barrels/BarrelSystem.cs create mode 100644 Content.Server/Weapon/Ranged/Barrels/Components/BatteryBarrelComponent.cs create mode 100644 Content.Server/Weapon/Ranged/Barrels/Components/MagazineBarrelComponent.cs delete mode 100644 Content.Server/Weapon/Ranged/Barrels/Components/ServerBatteryBarrelComponent.cs delete mode 100644 Content.Server/Weapon/Ranged/Barrels/Components/ServerMagazineBarrelComponent.cs create mode 100644 Content.Server/Weapon/Ranged/GunSystem.Battery.cs create mode 100644 Content.Server/Weapon/Ranged/GunSystem.Bolt.cs create mode 100644 Content.Server/Weapon/Ranged/GunSystem.Guns.cs create mode 100644 Content.Server/Weapon/Ranged/GunSystem.Magazine.cs create mode 100644 Content.Server/Weapon/Ranged/GunSystem.Pump.cs create mode 100644 Content.Server/Weapon/Ranged/GunSystem.RangedMagazine.cs create mode 100644 Content.Server/Weapon/Ranged/GunSystem.Revolvers.cs create mode 100644 Content.Server/Weapon/Ranged/GunSystem.SpeedLoader.cs rename Content.Shared/Weapons/Ranged/{MagazineAutoEjectMessage.cs => MagazineAutoEjectEvent.cs} (67%) diff --git a/Content.Client/Weapons/Ranged/Barrels/Components/ClientMagazineBarrelComponent.cs b/Content.Client/Weapons/Ranged/Barrels/Components/ClientMagazineBarrelComponent.cs index cec82d5120..f148ff5359 100644 --- a/Content.Client/Weapons/Ranged/Barrels/Components/ClientMagazineBarrelComponent.cs +++ b/Content.Client/Weapons/Ranged/Barrels/Components/ClientMagazineBarrelComponent.cs @@ -3,7 +3,6 @@ using Content.Client.IoC; using Content.Client.Items.Components; using Content.Client.Resources; using Content.Client.Stylesheets; -using Content.Shared.Weapons.Ranged; using Content.Shared.Weapons.Ranged.Barrels.Components; using Robust.Client.Animations; using Robust.Client.Graphics; @@ -13,8 +12,6 @@ using Robust.Shared.Animations; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.Maths; -using Robust.Shared.Network; -using Robust.Shared.Players; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; using static Robust.Client.UserInterface.Controls.BoxContainer; @@ -104,18 +101,9 @@ namespace Content.Client.Weapons.Ranged.Barrels.Components _statusControl?.Update(); } - [Obsolete("Component Messages are deprecated, use Entity Events instead.")] - public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null) + public void PlayAlarmAnimation() { - base.HandleNetworkMessage(message, channel, session); - - switch (message) - { - - case MagazineAutoEjectMessage _: - _statusControl?.PlayAlarmAnimation(); - return; - } + _statusControl?.PlayAlarmAnimation(); } public Control MakeControl() diff --git a/Content.Client/Weapons/Ranged/ClientRangedWeaponComponent.cs b/Content.Client/Weapons/Ranged/ClientRangedWeaponComponent.cs index 9f21407bf0..8ce2c638ec 100644 --- a/Content.Client/Weapons/Ranged/ClientRangedWeaponComponent.cs +++ b/Content.Client/Weapons/Ranged/ClientRangedWeaponComponent.cs @@ -18,7 +18,7 @@ namespace Content.Client.Weapons.Ranged [RegisterComponent] 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) { @@ -30,12 +30,5 @@ namespace Content.Client.Weapons.Ranged FireRateSelector = rangedState.FireRateSelector; } - - public void SyncFirePos(GridId targetGrid, Vector2 targetPosition) - { -#pragma warning disable 618 - SendNetworkMessage(new FirePosComponentMessage(targetGrid, targetPosition)); -#pragma warning restore 618 - } } } diff --git a/Content.Client/Weapons/Ranged/GunSystem.AmmoCounter.cs b/Content.Client/Weapons/Ranged/GunSystem.AmmoCounter.cs new file mode 100644 index 0000000000..8515d06a47 --- /dev/null +++ b/Content.Client/Weapons/Ranged/GunSystem.AmmoCounter.cs @@ -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(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(); + } +} diff --git a/Content.Client/Weapons/Ranged/RangedWeaponSystem.cs b/Content.Client/Weapons/Ranged/RangedWeaponSystem.cs index 4728dbc3a1..e980ff5758 100644 --- a/Content.Client/Weapons/Ranged/RangedWeaponSystem.cs +++ b/Content.Client/Weapons/Ranged/RangedWeaponSystem.cs @@ -85,20 +85,26 @@ namespace Content.Client.Weapons.Ranged } if (_blocked) - { 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 { - 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)); } } } diff --git a/Content.Server/Weapon/Ranged/Ammunition/Components/RangedMagazineComponent.cs b/Content.Server/Weapon/Ranged/Ammunition/Components/RangedMagazineComponent.cs index 0008758e23..5457fda0fa 100644 --- a/Content.Server/Weapon/Ranged/Ammunition/Components/RangedMagazineComponent.cs +++ b/Content.Server/Weapon/Ranged/Ammunition/Components/RangedMagazineComponent.cs @@ -1,37 +1,22 @@ -using System; using System.Collections.Generic; -using System.Threading.Tasks; -using Content.Server.Hands.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.Weapons.Ranged.Barrels.Components; using Robust.Shared.Containers; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Robust.Shared.Utility; namespace Content.Server.Weapon.Ranged.Ammunition.Components { - [RegisterComponent] -#pragma warning disable 618 - public class RangedMagazineComponent : Component, IMapInit, IInteractUsing, IUse, IExamine -#pragma warning restore 618 + [RegisterComponent, ComponentProtoName("RangedMagazine")] + public class RangedMagazineComponent : Component { - [Dependency] private readonly IEntityManager _entities = default!; + public readonly Stack SpawnedAmmo = new(); + public Container AmmoContainer = default!; - public override string Name => "RangedMagazine"; - - private readonly Stack _spawnedAmmo = new(); - private Container _ammoContainer = default!; - - public int ShotsLeft => _spawnedAmmo.Count + _unspawnedCount; + public int ShotsLeft => SpawnedAmmo.Count + UnspawnedCount; public int Capacity => _capacity; [DataField("capacity")] private int _capacity = 20; @@ -43,137 +28,12 @@ namespace Content.Server.Weapon.Ranged.Ammunition.Components [DataField("caliber")] private BallisticCaliber _caliber = BallisticCaliber.Unspecified; - private AppearanceComponent? _appearanceComponent; - // If there's anything already in the magazine [DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))] - 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 // Generally you probablt don't want to use this - private int _unspawnedCount; - - void IMapInit.MapInit() - { - if (_fillPrototype != null) - { - _unspawnedCount += Capacity; - } - UpdateAppearance(); - } - - protected override void Initialize() - { - base.Initialize(); - _ammoContainer = ContainerHelpers.EnsureContainer(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(Owner).Coordinates); - } - - UpdateAppearance(); - return ammo; - } - - async Task 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(ammo); - if (!handsComponent.CanPutInHand(itemComponent)) - { - _entities.GetComponent(ammo).Coordinates = _entities.GetComponent(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); - } + public int UnspawnedCount; } } diff --git a/Content.Server/Weapon/Ranged/Ammunition/Components/SpeedLoaderComponent.cs b/Content.Server/Weapon/Ranged/Ammunition/Components/SpeedLoaderComponent.cs index 07b12e1af8..e61c88cef8 100644 --- a/Content.Server/Weapon/Ranged/Ammunition/Components/SpeedLoaderComponent.cs +++ b/Content.Server/Weapon/Ranged/Ammunition/Components/SpeedLoaderComponent.cs @@ -1,15 +1,6 @@ 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.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; @@ -19,199 +10,21 @@ namespace Content.Server.Weapon.Ranged.Ammunition.Components /// /// Used to load certain ranged weapons quickly /// - [RegisterComponent] - public class SpeedLoaderComponent : Component, IAfterInteract, IInteractUsing, IMapInit, IUse + [RegisterComponent, ComponentProtoName("SpeedLoader")] + public class SpeedLoaderComponent : Component { - [Dependency] private readonly IEntityManager _entMan = default!; - - public override string Name => "SpeedLoader"; - - [DataField("caliber")] - private BallisticCaliber _caliber = BallisticCaliber.Unspecified; + [DataField("caliber")] public BallisticCaliber Caliber = BallisticCaliber.Unspecified; public int Capacity => _capacity; [DataField("capacity")] private int _capacity = 6; - private Container _ammoContainer = default!; - private Stack _spawnedAmmo = new(); - private int _unspawnedCount; - public int AmmoLeft => _spawnedAmmo.Count + _unspawnedCount; + public Container AmmoContainer = default!; + public Stack SpawnedAmmo = new(); + public int UnspawnedCount; + + public int AmmoLeft => SpawnedAmmo.Count + UnspawnedCount; [DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))] - private string? _fillPrototype; - - protected override void Initialize() - { - base.Initialize(); - _ammoContainer = ContainerHelpers.EnsureContainer(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(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(Owner).Coordinates); - _unspawnedCount--; - } - - return entity; - } - - async Task 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 IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs) - { - return TryInsertAmmo(eventArgs.User, eventArgs.Using); - } - - bool IUse.UseEntity(UseEntityEventArgs eventArgs) - { - return UseEntity(eventArgs.User); - } + public string? FillPrototype; } } diff --git a/Content.Server/Weapon/Ranged/Barrels/BarrelSystem.cs b/Content.Server/Weapon/Ranged/Barrels/BarrelSystem.cs deleted file mode 100644 index 8073ceb929..0000000000 --- a/Content.Server/Weapon/Ranged/Barrels/BarrelSystem.cs +++ /dev/null @@ -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(AddSpinVerb); - - SubscribeLocalEvent(OnCellSlotUpdated); - - SubscribeLocalEvent(AddToggleBoltVerb); - - SubscribeLocalEvent(AddMagazineInteractionVerbs); - SubscribeLocalEvent(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(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(@using).EntityName; - insert.Category = VerbCategory.Insert; - insert.Act = () => component.InsertMagazine(args.User, @using); - args.Verbs.Add(insert); - } - } -} diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/BatteryBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/BatteryBarrelComponent.cs new file mode 100644 index 0000000000..93e44cd215 --- /dev/null +++ b/Content.Server/Weapon/Ranged/Barrels/Components/BatteryBarrelComponent.cs @@ -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))] + [ViewVariables] + public string? AmmoPrototype; + + public ContainerSlot AmmoContainer = default!; + + public override int ShotsLeft + { + get + { + + if (!EntitySystem.Get().TryGetBatteryFromSlot(Owner, out var battery)) + { + return 0; + } + + return (int) Math.Ceiling(battery.CurrentCharge / BaseFireCost); + } + } + + public override int Capacity + { + get + { + if (!EntitySystem.Get().TryGetBatteryFromSlot(Owner, out var battery)) + { + return 0; + } + + return (int) Math.Ceiling(battery.MaxCharge / BaseFireCost); + } + } + } +} diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/BoltActionBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/BoltActionBarrelComponent.cs index c38e9bcabf..39e45bf460 100644 --- a/Content.Server/Weapon/Ranged/Barrels/Components/BoltActionBarrelComponent.cs +++ b/Content.Server/Weapon/Ranged/Barrels/Components/BoltActionBarrelComponent.cs @@ -1,23 +1,14 @@ using System.Collections.Generic; -using System.Threading.Tasks; 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.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 @@ -25,42 +16,39 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components /// /// Shotguns mostly /// - [RegisterComponent] - [NetworkedComponent()] -#pragma warning disable 618 - public sealed class BoltActionBarrelComponent : ServerRangedBarrelComponent, IUse, IInteractUsing, IMapInit -#pragma warning restore 618 + [RegisterComponent, NetworkedComponent, ComponentProtoName("BoltActionBarrel"), ComponentReference(typeof(ServerRangedBarrelComponent))] + public sealed class BoltActionBarrelComponent : ServerRangedBarrelComponent { // Originally I had this logic shared with PumpBarrel and used a couple of variables to control things // but it felt a lot messier to play around with, especially when adding verbs - public override string Name => "BoltActionBarrel"; - public override int ShotsLeft { get { - var chamberCount = _chamberContainer.ContainedEntity != null ? 1 : 0; - return chamberCount + _spawnedAmmo.Count + _unspawnedCount; + var chamberCount = ChamberContainer.ContainedEntity != null ? 1 : 0; + return chamberCount + SpawnedAmmo.Count + UnspawnedCount; } } public override int Capacity => _capacity; - [DataField("capacity")] - private int _capacity = 6; - private ContainerSlot _chamberContainer = default!; - private Stack _spawnedAmmo = default!; - private Container _ammoContainer = default!; + [DataField("capacity")] + internal int _capacity = 6; + + public ContainerSlot ChamberContainer = default!; + public Stack SpawnedAmmo = default!; + public Container AmmoContainer = default!; [ViewVariables] [DataField("caliber")] - private BallisticCaliber _caliber = BallisticCaliber.Unspecified; + public BallisticCaliber Caliber = BallisticCaliber.Unspecified; [ViewVariables] [DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))] - private string? _fillPrototype; + public string? FillPrototype; + [ViewVariables] - private int _unspawnedCount; + public int UnspawnedCount; public bool BoltOpen { @@ -72,269 +60,34 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components return; } + var gunSystem = EntitySystem.Get(); + if (value) { - TryEjectChamber(); + gunSystem.TryEjectChamber(this); SoundSystem.Play(Filter.Pvs(Owner), _soundBoltOpen.GetSound(), Owner, AudioParams.Default.WithVolume(-2)); } else { - TryFeedChamber(); + gunSystem.TryFeedChamber(this); SoundSystem.Play(Filter.Pvs(Owner), _soundBoltClosed.GetSound(), Owner, AudioParams.Default.WithVolume(-2)); } _boltOpen = value; - UpdateAppearance(); + gunSystem.UpdateBoltAppearance(this); Dirty(); } } private bool _boltOpen; - [DataField("autoCycle")] - private bool _autoCycle; - private AppearanceComponent? _appearanceComponent; + [DataField("autoCycle")] public bool AutoCycle; // Sounds - [DataField("soundCycle")] - private SoundSpecifier _soundCycle = new SoundPathSpecifier("/Audio/Weapons/Guns/Cock/sf_rifle_cock.ogg"); + [DataField("soundCycle")] public SoundSpecifier SoundCycle = new SoundPathSpecifier("/Audio/Weapons/Guns/Cock/sf_rifle_cock.ogg"); [DataField("soundBoltOpen")] private SoundSpecifier _soundBoltOpen = new SoundPathSpecifier("/Audio/Weapons/Guns/Bolt/rifle_bolt_open.ogg"); [DataField("soundBoltClosed")] private SoundSpecifier _soundBoltClosed = new SoundPathSpecifier("/Audio/Weapons/Guns/Bolt/rifle_bolt_closed.ogg"); - [DataField("soundInsert")] - 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(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(_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(_capacity - 1); - _ammoContainer = ContainerHelpers.EnsureContainer(Owner, $"{Name}-ammo-container", out var existing); - - if (existing) - { - foreach (var entity in _ammoContainer.ContainedEntities) - { - _spawnedAmmo.Push(entity); - _unspawnedCount--; - } - } - - _chamberContainer = ContainerHelpers.EnsureContainer(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(chamberEntity); - - return ammoComponent == null ? null : EntitySystem.Get().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 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(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(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))); - } + [DataField("soundInsert")] public SoundSpecifier SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/bullet_insert.ogg"); } } diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/MagazineBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/MagazineBarrelComponent.cs new file mode 100644 index 0000000000..db462f9670 --- /dev/null +++ b/Content.Server/Weapon/Ranged/Barrels/Components/MagazineBarrelComponent.cs @@ -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(magazine).ShotsLeft; + } + + return count; + } + } + + public override int Capacity + { + get + { + // Chamber + var count = 1; + if (MagazineContainer.ContainedEntity is {Valid: true} magazine) + { + count += _entities.GetComponent(magazine).Capacity; + } + + return count; + } + } + + [DataField("magFillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))] + public string? MagFillPrototype; + + public bool BoltOpen + { + get => _boltOpen; + set + { + if (_boltOpen == value) + { + return; + } + + var gunSystem = EntitySystem.Get(); + + 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, + } +} diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/PumpBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/PumpBarrelComponent.cs index ebfd57b9ce..5dea7f4210 100644 --- a/Content.Server/Weapon/Ranged/Barrels/Components/PumpBarrelComponent.cs +++ b/Content.Server/Weapon/Ranged/Barrels/Components/PumpBarrelComponent.cs @@ -1,21 +1,13 @@ using System.Collections.Generic; -using System.Threading.Tasks; using Content.Server.Weapon.Ranged.Ammunition.Components; -using Content.Shared.Interaction; -using Content.Shared.Popups; using Content.Shared.Sound; -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; using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Utility; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.ViewVariables; namespace Content.Server.Weapon.Ranged.Barrels.Components @@ -23,18 +15,15 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components /// /// Bolt-action rifles /// - [RegisterComponent] - [NetworkedComponent()] - public sealed class PumpBarrelComponent : ServerRangedBarrelComponent, IUse, IInteractUsing, IMapInit, ISerializationHooks + [RegisterComponent, NetworkedComponent, ComponentProtoName("PumpBarrel"), ComponentReference(typeof(ServerRangedBarrelComponent))] + public sealed class PumpBarrelComponent : ServerRangedBarrelComponent, ISerializationHooks { - public override string Name => "PumpBarrel"; - public override int ShotsLeft { get { - var chamberCount = _chamberContainer.ContainedEntity != null ? 1 : 0; - return chamberCount + _spawnedAmmo.Count + _unspawnedCount; + var chamberCount = ChamberContainer.ContainedEntity != null ? 1 : 0; + return chamberCount + SpawnedAmmo.Count + UnspawnedCount; } } @@ -43,204 +32,30 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components public override int Capacity { get; } = DefaultCapacity; // Even a point having a chamber? I guess it makes some of the below code cleaner - private ContainerSlot _chamberContainer = default!; - private Stack _spawnedAmmo = new(DefaultCapacity - 1); - private Container _ammoContainer = default!; + public ContainerSlot ChamberContainer = default!; + public Stack SpawnedAmmo = new(DefaultCapacity - 1); + public Container AmmoContainer = default!; [ViewVariables] [DataField("caliber")] - private BallisticCaliber _caliber = BallisticCaliber.Unspecified; + public BallisticCaliber Caliber = BallisticCaliber.Unspecified; [ViewVariables] - [DataField("fillPrototype")] - private string? _fillPrototype; - [ViewVariables] - private int _unspawnedCount; + [DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))] + public string? FillPrototype; - [DataField("manualCycle")] - private bool _manualCycle = true; + [ViewVariables] public int UnspawnedCount; - private AppearanceComponent? _appearanceComponent; + [DataField("manualCycle")] public bool ManualCycle = true; // Sounds - [DataField("soundCycle")] - private SoundSpecifier _soundCycle = new SoundPathSpecifier("/Audio/Weapons/Guns/Cock/sf_rifle_cock.ogg"); + [DataField("soundCycle")] public SoundSpecifier SoundCycle = new SoundPathSpecifier("/Audio/Weapons/Guns/Cock/sf_rifle_cock.ogg"); - [DataField("soundInsert")] - 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(_chamberContainer.ContainedEntity!.Value, out var ammo)) - { - chamber.Item2 = ammo.Spent; - } - return new PumpBarrelComponentState( - chamber, - FireRateSelector, - count, - SoundGunshot.GetSound()); - } + [DataField("soundInsert")] public SoundSpecifier SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/bullet_insert.ogg"); void ISerializationHooks.AfterDeserialization() { - _spawnedAmmo = new Stack(Capacity - 1); - } - - protected override void Initialize() - { - base.Initialize(); - - _ammoContainer = - ContainerHelpers.EnsureContainer(Owner, $"{Name}-ammo-container", out var existing); - - if (existing) - { - foreach (var entity in _ammoContainer.ContainedEntities) - { - _spawnedAmmo.Push(entity); - _unspawnedCount--; - } - } - - _chamberContainer = - ContainerHelpers.EnsureContainer(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(chamberEntity); - - return ammoComponent == null ? null : EntitySystem.Get().TakeBullet(ammoComponent, spawnAt); - } - - private void Cycle(bool manual = false) - { - if (_chamberContainer.ContainedEntity is {Valid: true} chamberedEntity) - { - _chamberContainer.Remove(chamberedEntity); - var ammoComponent = Entities.GetComponent(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(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 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))); + SpawnedAmmo = new Stack(Capacity - 1); } } } diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/RevolverBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/RevolverBarrelComponent.cs index d1dcdb3d2b..60aacee87e 100644 --- a/Content.Server/Weapon/Ranged/Barrels/Components/RevolverBarrelComponent.cs +++ b/Content.Server/Weapon/Ranged/Barrels/Components/RevolverBarrelComponent.cs @@ -1,20 +1,13 @@ using System; -using System.Threading.Tasks; using Content.Server.Weapon.Ranged.Ammunition.Components; -using Content.Shared.Interaction; -using Content.Shared.Popups; using Content.Shared.Sound; using Content.Shared.Weapons.Ranged.Barrels.Components; -using Robust.Shared.Audio; +using Robust.Shared.Analyzers; 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.Random; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; @@ -22,248 +15,53 @@ using Robust.Shared.ViewVariables; namespace Content.Server.Weapon.Ranged.Barrels.Components { - [RegisterComponent] - [NetworkedComponent()] - public sealed class RevolverBarrelComponent : ServerRangedBarrelComponent, IUse, IInteractUsing, ISerializationHooks + [RegisterComponent, ComponentProtoName("RevolverBarrel"), NetworkedComponent, ComponentReference(typeof(ServerRangedBarrelComponent))] + public sealed class RevolverBarrelComponent : ServerRangedBarrelComponent, ISerializationHooks { - [Dependency] private readonly IRobustRandom _random = default!; - - public override string Name => "RevolverBarrel"; - [ViewVariables] [DataField("caliber")] - private BallisticCaliber _caliber = BallisticCaliber.Unspecified; + public BallisticCaliber Caliber = BallisticCaliber.Unspecified; - private Container _ammoContainer = default!; + public Container AmmoContainer = default!; [ViewVariables] - private int _currentSlot; + public int CurrentSlot; - public override int Capacity => _ammoSlots.Length; + public override int Capacity => AmmoSlots.Length; [DataField("capacity")] private int _serializedCapacity = 6; [DataField("ammoSlots", readOnly: true)] - private EntityUid[] _ammoSlots = Array.Empty(); + public EntityUid?[] AmmoSlots = Array.Empty(); - public override int ShotsLeft => _ammoContainer.ContainedEntities.Count; + public override int ShotsLeft => AmmoContainer.ContainedEntities.Count; [ViewVariables] [DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))] - private string? _fillPrototype; + public string? FillPrototype; [ViewVariables] - private int _unspawnedCount; + public int UnspawnedCount; // Sounds [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")] - 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")] - 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() { - _serializedCapacity = _ammoSlots.Length; + _serializedCapacity = AmmoSlots.Length; } void ISerializationHooks.AfterDeserialization() { - _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(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(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(); - } - - /// - /// Russian Roulette - /// - 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]; - } - - /// - /// Takes a projectile out if possible - /// IEnumerable just to make supporting shotguns saner - /// - /// - /// - public override EntityUid? TakeProjectile(EntityCoordinates spawnAt) - { - var ammo = _ammoSlots[_currentSlot]; - EntityUid? bullet = null; - if (ammo != default) - { - var ammoComponent = Entities.GetComponent(ammo); - bullet = EntitySystem.Get().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; - } - - /// - /// Eject all casings - /// - /// - /// - /// - public bool UseEntity(UseEntityEventArgs eventArgs) - { - EjectAllSlots(); - Dirty(); - UpdateAppearance(); - return true; - } - - public async Task InteractUsing(InteractUsingEventArgs eventArgs) - { - return TryInsertBullet(eventArgs.User, eventArgs.Using); + AmmoSlots = new EntityUid?[_serializedCapacity]; } } } diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/ServerBatteryBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/ServerBatteryBarrelComponent.cs deleted file mode 100644 index d8cb5ee49b..0000000000 --- a/Content.Server/Weapon/Ranged/Barrels/Components/ServerBatteryBarrelComponent.cs +++ /dev/null @@ -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))] - [ViewVariables] private string? _ammoPrototype; - - private ContainerSlot _ammoContainer = default!; - - public override int ShotsLeft - { - get - { - - if (!EntitySystem.Get().TryGetBatteryFromSlot(Owner, out var battery)) - { - return 0; - } - - return (int) Math.Ceiling(battery.CurrentCharge / _baseFireCost); - } - } - - public override int Capacity - { - get - { - if (!EntitySystem.Get().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($"{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().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(Owner).Coordinates); - _ammoContainer.Insert(ammo.Value); - } - - return ammo.Value; - } - - public override EntityUid? TakeProjectile(EntityCoordinates spawnAt) - { - if (!EntitySystem.Get().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(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; - } - } -} diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/ServerMagazineBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/ServerMagazineBarrelComponent.cs deleted file mode 100644 index 411c6615de..0000000000 --- a/Content.Server/Weapon/Ranged/Barrels/Components/ServerMagazineBarrelComponent.cs +++ /dev/null @@ -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(magazine).ShotsLeft; - } - - return count; - } - } - - public override int Capacity - { - get - { - // Chamber - var count = 1; - if (MagazineContainer.ContainedEntity is {Valid: true} magazine) - { - count += _entities.GetComponent(magazine).Capacity; - } - - return count; - } - } - - [DataField("magFillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))] - 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 GetMagazineTypes() - { - var types = new List(); - - 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($"{Name}-chamber"); - MagazineContainer = Owner.EnsureContainer($"{Name}-magazine", out var existing); - - if (!existing && _magFillPrototype != null) - { - var magEntity = _entities.SpawnEntity(_magFillPrototype, _entities.GetComponent(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().TakeBullet(_entities.GetComponent(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(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(magazine)?.TakeAmmo() is not {Valid: true} nextRound) - { - return false; - } - - _chamberContainer.Insert(nextRound); - - if (_autoEjectMag && magazine != null && _entities.GetComponent(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(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 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, - } -} diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/ServerRangedBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/ServerRangedBarrelComponent.cs index 2391bcd43f..166d9bc307 100644 --- a/Content.Server/Weapon/Ranged/Barrels/Components/ServerRangedBarrelComponent.cs +++ b/Content.Server/Weapon/Ranged/Barrels/Components/ServerRangedBarrelComponent.cs @@ -1,32 +1,12 @@ 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.Weapons.Ranged.Components; -using Robust.Shared.Audio; +using Robust.Shared.Analyzers; 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.Physics; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Timing; -using Robust.Shared.Utility; 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. /// Only difference between them is how they retrieve a projectile to shoot (battery, magazine, etc.) /// -#pragma warning disable 618 - public abstract class ServerRangedBarrelComponent : SharedRangedBarrelComponent, IExamine, ISerializationHooks -#pragma warning restore 618 + [Friend(typeof(GunSystem))] + public abstract class ServerRangedBarrelComponent : SharedRangedBarrelComponent, ISerializationHooks { - // 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; [DataField("currentSelector")] @@ -55,10 +28,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components 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) - private TimeSpan _lastFire; - - public abstract EntityUid? PeekAmmo(); - public abstract EntityUid? TakeProjectile(EntityCoordinates spawnAt); + public TimeSpan LastFire; // Recoil / spray control [DataField("minAngle")] @@ -71,7 +41,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components public Angle MaxAngle { get; private set; } - private Angle _currentAngle = Angle.Zero; + public Angle CurrentAngle = Angle.Zero; [DataField("angleDecay")] private float _angleDecayDegrees = 20; @@ -132,294 +102,6 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components 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; - } - - /// - /// Fires a round of ammo out of the weapon. - /// - /// Entity that is operating the weapon, usually the player. - /// Target position on the map to shoot at. - 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(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(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(shooter)) - { - var kick = -angle.ToVec() * 0.15f; - EntitySystem.Get().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(projectile) && - Entities.TryGetComponent(ammo, out AmmoComponent? ammoComponent)) - { - FireProjectiles(shooter, projectile, ammoComponent.ProjectilesFired, ammoComponent.EvenSpreadAngle, angle, ammoComponent.Velocity, ammo.Value); - - if (CanMuzzleFlash) - { - EntitySystem.Get().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; - } - - /// - /// Drops a single cartridge / shell - /// Made as a static function just because multiple places need it - /// - /// - /// - /// - /// - /// - 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(entity); - var offsetPos = ((robustRandom.NextFloat() - 0.5f) * ejectOffset, (robustRandom.NextFloat() - 0.5f) * ejectOffset); - entities.GetComponent(entity).Coordinates = entities.GetComponent(entity).Coordinates.Offset(offsetPos); - entities.GetComponent(entity).LocalRotation = robustRandom.Pick(ejectDirections).ToAngle(); - - var coordinates = entities.GetComponent(entity).Coordinates; - SoundSystem.Play(Filter.Broadcast(), ammo.SoundCollectionEject.GetSound(), coordinates, AudioParams.Default.WithVolume(-1)); - } - - /// - /// Drops multiple cartridges / shells on the floor - /// Wraps EjectCasing to make it less toxic for bulk ejections - /// - /// - public static void EjectCasings(IEnumerable entities) - { - var robustRandom = IoCManager.Resolve(); - var prototypeManager = IoCManager.Resolve(); - 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 - /// - /// Handles firing one or many projectiles - /// - private void FireProjectiles(EntityUid shooter, EntityUid baseProjectile, int count, float evenSpreadAngle, Angle angle, float velocity, EntityUid ammo) - { - List? 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(baseProjectile).EntityPrototype?.ID, - Entities.GetComponent(baseProjectile).Coordinates); - } - - firedProjectiles[i] = projectile; - - Angle projectileAngle; - - if (sprayAngleChange != null) - { - projectileAngle = angle + sprayAngleChange[i]; - } - else - { - projectileAngle = angle; - } - - var physics = Entities.GetComponent(projectile); - physics.BodyStatus = BodyStatus.InAir; - - var projectileComponent = Entities.GetComponent(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(projectile) - .LinearVelocity = projectileAngle.ToVec() * velocity; - }); - - - Entities.GetComponent(projectile).WorldRotation = projectileAngle + MathHelper.PiOver2; - } - - Entities.EventBus.RaiseLocalEvent(Owner, new GunShotEvent(firedProjectiles)); - Entities.EventBus.RaiseLocalEvent(ammo, new AmmoShotEvent(firedProjectiles)); - } - - /// - /// Returns a list of numbers that form a set of equal intervals between the start and end value. Used to calculate shotgun spread angles. - /// - private List Linspace(double start, double end, int intervals) - { - DebugTools.Assert(intervals > 1); - - var linspace = new List(intervals); - - for (var i = 0; i <= intervals - 1; i++) - { - linspace.Add(Angle.FromDegrees(start + (end - start) * i / (intervals - 1))); - } - return linspace; - } - - /// - /// Fires hitscan entities and then displays their effects - /// - private void FireHitscan(EntityUid shooter, HitscanComponent hitscan, Angle angle) - { - var ray = new CollisionRay(Entities.GetComponent(Owner).Coordinates.ToMapPos(Entities), angle.ToVec(), (int) hitscan.CollisionMask); - var physicsManager = EntitySystem.Get(); - var rayCastResults = physicsManager.IntersectRay(Entities.GetComponent(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().TryChangeDamage(result.HitEntity, hitscan.Damage); - if (dmg != null) - EntitySystem.Get().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); - } } /// diff --git a/Content.Server/Weapon/Ranged/GunSystem.AmmoBox.cs b/Content.Server/Weapon/Ranged/GunSystem.AmmoBox.cs index fd00331f88..0ed0d18646 100644 --- a/Content.Server/Weapon/Ranged/GunSystem.AmmoBox.cs +++ b/Content.Server/Weapon/Ranged/GunSystem.AmmoBox.cs @@ -54,7 +54,7 @@ public sealed partial class GunSystem { 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)) { @@ -64,18 +64,18 @@ public sealed partial class GunSystem 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++) { - if (rangedMagazine.TakeAmmo() is not {Valid: true} ammo) + if (TakeAmmo(rangedMagazine) is not {Valid: true} ammo) { continue; } if (!TryInsertAmmo(args.User, ammo, component)) { - rangedMagazine.TryInsertAmmo(args.User, ammo); + TryInsertAmmo(args.User, ammo, rangedMagazine); args.Handled = true; return; } @@ -135,13 +135,13 @@ public sealed partial class GunSystem ejectAmmo.Add(ammo); } - ServerRangedBarrelComponent.EjectCasings(ejectAmmo); + EjectCasings(ejectAmmo); UpdateAmmoBoxAppearance(ammoBox.Owner, ammoBox); } private bool TryUse(EntityUid user, AmmoBoxComponent ammoBox) { - if (!EntityManager.TryGetComponent(user, out HandsComponent? handsComponent)) + if (!TryComp(user, out HandsComponent? handsComponent)) { return false; } @@ -151,7 +151,7 @@ public sealed partial class GunSystem return false; } - if (EntityManager.TryGetComponent(ammo, out ItemComponent? item)) + if (TryComp(ammo, out ItemComponent? item)) { if (!handsComponent.CanPutInHand(item)) { diff --git a/Content.Server/Weapon/Ranged/GunSystem.Battery.cs b/Content.Server/Weapon/Ranged/GunSystem.Battery.cs new file mode 100644 index 0000000000..676cca5157 --- /dev/null +++ b/Content.Server/Weapon/Ranged/GunSystem.Battery.cs @@ -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($"{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; + } +} diff --git a/Content.Server/Weapon/Ranged/GunSystem.Bolt.cs b/Content.Server/Weapon/Ranged/GunSystem.Bolt.cs new file mode 100644 index 0000000000..de87a62234 --- /dev/null +++ b/Content.Server/Weapon/Ranged/GunSystem.Bolt.cs @@ -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(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(component.Capacity - 1); + component.AmmoContainer = uid.EnsureContainer($"{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($"{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(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(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(chamberEntity); + + return ammoComponent == null ? null : TakeBullet(ammoComponent, spawnAt); + } +} diff --git a/Content.Server/Weapon/Ranged/GunSystem.Guns.cs b/Content.Server/Weapon/Ranged/GunSystem.Guns.cs new file mode 100644 index 0000000000..2477d10644 --- /dev/null +++ b/Content.Server/Weapon/Ranged/GunSystem.Guns.cs @@ -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 +{ + /// + /// Tries to fire a round of ammo out of the weapon. + /// + 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); + } + + /// + /// Fires a round of ammo out of the weapon. + /// + 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(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(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 + /// + /// Handles firing one or many projectiles + /// + private void FireProjectiles(EntityUid shooter, EntityUid baseProjectile, ServerRangedBarrelComponent component, int count, float evenSpreadAngle, Angle angle, float velocity, EntityUid ammo) + { + List? 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(projectile); + physics.BodyStatus = BodyStatus.InAir; + + var projectileComponent = EntityManager.GetComponent(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(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)); + } + + /// + /// Returns a list of numbers that form a set of equal intervals between the start and end value. Used to calculate shotgun spread angles. + /// + private List Linspace(double start, double end, int intervals) + { + DebugTools.Assert(intervals > 1); + + var linspace = new List(intervals); + + for (var i = 0; i <= intervals - 1; i++) + { + linspace.Add(Angle.FromDegrees(start + (end - start) * i / (intervals - 1))); + } + return linspace; + } + + /// + /// Fires hitscan entities and then displays their effects + /// + 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 +} diff --git a/Content.Server/Weapon/Ranged/GunSystem.Magazine.cs b/Content.Server/Weapon/Ranged/GunSystem.Magazine.cs new file mode 100644 index 0000000000..4d43720c57 --- /dev/null +++ b/Content.Server/Weapon/Ranged/GunSystem.Magazine.cs @@ -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($"{component.GetType()}-chamber"); + component.MagazineContainer = uid.EnsureContainer($"{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(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(magazine); + + if (magComp == null || TakeAmmo(magComp) is not {Valid: true} nextRound) + { + return false; + } + + component.ChamberContainer.Insert(nextRound); + + if (component.AutoEjectMag && magazine != null && EntityManager.GetComponent(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(entity.Value), spawnAt) : null; + } + + public List GetMagazineTypes(MagazineBarrelComponent component) + { + var types = new List(); + + 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(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); + } +} diff --git a/Content.Server/Weapon/Ranged/GunSystem.Pump.cs b/Content.Server/Weapon/Ranged/GunSystem.Pump.cs new file mode 100644 index 0000000000..a1917c3869 --- /dev/null +++ b/Content.Server/Weapon/Ranged/GunSystem.Pump.cs @@ -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(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($"{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($"{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(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(chamberEntity); + + return ammoComponent == null ? null : TakeBullet(ammoComponent, spawnAt); + } +} diff --git a/Content.Server/Weapon/Ranged/GunSystem.RangedMagazine.cs b/Content.Server/Weapon/Ranged/GunSystem.RangedMagazine.cs new file mode 100644 index 0000000000..a0ae702a6e --- /dev/null +++ b/Content.Server/Weapon/Ranged/GunSystem.RangedMagazine.cs @@ -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($"{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(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; + } +} diff --git a/Content.Server/Weapon/Ranged/GunSystem.Revolvers.cs b/Content.Server/Weapon/Ranged/GunSystem.Revolvers.cs new file mode 100644 index 0000000000..9bd5f8bbe4 --- /dev/null +++ b/Content.Server/Weapon/Ranged/GunSystem.Revolvers.cs @@ -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; + } + + /// + /// Russian Roulette + /// + 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($"{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(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]; + } + + /// + /// Takes a projectile out if possible + /// IEnumerable just to make supporting shotguns saner + /// + /// + /// + public EntityUid? TakeProjectile(RevolverBarrelComponent component, EntityCoordinates spawnAt) + { + var ammo = component.AmmoSlots[component.CurrentSlot]; + EntityUid? bullet = null; + if (ammo != null) + { + var ammoComponent = EntityManager.GetComponent(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; + } +} diff --git a/Content.Server/Weapon/Ranged/GunSystem.SpeedLoader.cs b/Content.Server/Weapon/Ranged/GunSystem.SpeedLoader.cs new file mode 100644 index 0000000000..ce19a83953 --- /dev/null +++ b/Content.Server/Weapon/Ranged/GunSystem.SpeedLoader.cs @@ -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($"{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(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; + } +} diff --git a/Content.Server/Weapon/Ranged/GunSystem.cs b/Content.Server/Weapon/Ranged/GunSystem.cs index 56e0c495b0..40e92c8273 100644 --- a/Content.Server/Weapon/Ranged/GunSystem.cs +++ b/Content.Server/Weapon/Ranged/GunSystem.cs @@ -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.Barrels.Components; +using Content.Shared.ActionBlocker; +using Content.Shared.Camera; +using Content.Shared.Damage; using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.Popups; +using Content.Shared.PowerCell.Components; using Content.Shared.Verbs; +using Content.Shared.Weapons.Ranged.Components; using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Containers; using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; 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; namespace Content.Server.Weapon.Ranged; @@ -13,22 +34,224 @@ namespace Content.Server.Weapon.Ranged; public sealed partial class GunSystem : EntitySystem { [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 PowerCellSystem _cell = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly StunSystem _stun = default!; + + /// + /// How many sounds are allowed to be played on ejecting multiple casings. + /// + private const int EjectionSoundMax = 3; public override void 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(OnAmmoExamine); SubscribeLocalEvent(OnAmmoBoxInit); SubscribeLocalEvent(OnAmmoBoxMapInit); SubscribeLocalEvent(OnAmmoBoxExamine); - SubscribeLocalEvent(OnAmmoBoxInteractUsing); SubscribeLocalEvent(OnAmmoBoxUse); SubscribeLocalEvent(OnAmmoBoxInteractHand); SubscribeLocalEvent(OnAmmoBoxAltVerbs); + + SubscribeLocalEvent(OnRangedMagInit); + SubscribeLocalEvent(OnRangedMagMapInit); + SubscribeLocalEvent(OnRangedMagUse); + SubscribeLocalEvent(OnRangedMagExamine); + SubscribeLocalEvent(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(OnBatteryInit); + SubscribeLocalEvent(OnBatteryMapInit); + SubscribeLocalEvent(OnBatteryGetState); + SubscribeLocalEvent(OnCellSlotUpdated); + + SubscribeLocalEvent(OnBoltInit); + SubscribeLocalEvent(OnBoltMapInit); + SubscribeLocalEvent(OnBoltFireAttempt); + SubscribeLocalEvent(OnBoltUse); + SubscribeLocalEvent(OnBoltInteractUsing); + SubscribeLocalEvent(OnBoltGetState); + SubscribeLocalEvent(OnBoltExamine); + SubscribeLocalEvent(AddToggleBoltVerb); + + SubscribeLocalEvent(OnMagazineInit); + SubscribeLocalEvent(OnMagazineMapInit); + SubscribeLocalEvent(OnMagazineExamine); + SubscribeLocalEvent(OnMagazineUse); + SubscribeLocalEvent(OnMagazineInteractUsing); + SubscribeLocalEvent(OnMagazineGetState); + SubscribeLocalEvent(AddMagazineInteractionVerbs); + SubscribeLocalEvent(AddEjectMagazineVerb); + + SubscribeLocalEvent(OnPumpGetState); + SubscribeLocalEvent(OnPumpInit); + SubscribeLocalEvent(OnPumpMapInit); + SubscribeLocalEvent(OnPumpExamine); + SubscribeLocalEvent(OnPumpUse); + SubscribeLocalEvent(OnPumpInteractUsing); + + SubscribeLocalEvent(OnRevolverMapInit); + SubscribeLocalEvent(OnRevolverUse); + SubscribeLocalEvent(OnRevolverInteractUsing); + SubscribeLocalEvent(OnRevolverGetState); + SubscribeLocalEvent(AddSpinVerb); + + SubscribeLocalEvent(OnSpeedLoaderInit); + SubscribeLocalEvent(OnSpeedLoaderMapInit); + SubscribeLocalEvent(OnSpeedLoaderUse); + SubscribeLocalEvent(OnSpeedLoaderAfterInteract); + SubscribeLocalEvent(OnSpeedLoaderInteractUsing); + + // SubscribeLocalEvent(OnGunExamine); + SubscribeNetworkEvent(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() + }; + } + + /// + /// Drops multiple cartridges / shells on the floor + /// Wraps EjectCasing to make it less toxic for bulk ejections + /// + public void EjectCasings(IEnumerable entities) + { + var soundPlayCount = 0; + var playSound = true; + + foreach (var entity in entities) + { + EjectCasing(entity, playSound); + soundPlayCount++; + if (soundPlayCount > EjectionSoundMax) + { + playSound = false; + } + } + } + + /// + /// Drops a single cartridge / shell + /// + 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; + } + + /// + /// Raised on a gun when it fires. + /// + 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; + } } } diff --git a/Content.Server/Weapon/Ranged/ServerRangedWeaponComponent.cs b/Content.Server/Weapon/Ranged/ServerRangedWeaponComponent.cs index c18a0fed80..9b18038976 100644 --- a/Content.Server/Weapon/Ranged/ServerRangedWeaponComponent.cs +++ b/Content.Server/Weapon/Ranged/ServerRangedWeaponComponent.cs @@ -1,28 +1,9 @@ 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.Hands; -using Content.Shared.Popups; using Content.Shared.Sound; using Content.Shared.Weapons.Ranged.Components; -using Robust.Shared.Audio; 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.Timing; using Robust.Shared.ViewVariables; namespace Content.Server.Weapon.Ranged @@ -30,11 +11,7 @@ namespace Content.Server.Weapon.Ranged [RegisterComponent] public sealed class ServerRangedWeaponComponent : SharedRangedWeaponComponent { - [Dependency] private readonly IEntityManager _entMan = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; - - private TimeSpan _lastFireTime; + public TimeSpan LastFireTime; [ViewVariables(VVAccess.ReadWrite)] [DataField("clumsyCheck")] @@ -46,154 +23,16 @@ namespace Content.Server.Weapon.Ranged [ViewVariables(VVAccess.ReadWrite)] [DataField("canHotspot")] - private bool _canHotspot = true; + public bool CanHotspot = true; [DataField("clumsyWeaponHandlingSound")] - private SoundSpecifier _clumsyWeaponHandlingSound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg"); + public SoundSpecifier ClumsyWeaponHandlingSound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg"); [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)] [DataField("clumsyDamage")] public DamageSpecifier? ClumsyDamage; - - public Func? WeaponCanFireHandler; - public Func? UserCanFireHandler; - public Action? 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().CanInteract(user); - } - - /// - [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); - } - - /// - /// Tries to fire a round of ammo out of the weapon. - /// - /// Entity that is operating the weapon, usually the player. - /// Target position on the map to shoot at. - 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().TryChangeDamage(user, ClumsyDamage); - EntitySystem.Get().TryParalyze(user, TimeSpan.FromSeconds(3f), true); - - // Apply salt to the wound ("Honk!") - SoundSystem.Play( - Filter.Pvs(Owner), _clumsyWeaponHandlingSound.GetSound(), - _entMan.GetComponent(Owner).Coordinates, AudioParams.Default.WithMaxDistance(5)); - - SoundSystem.Play( - Filter.Pvs(Owner), _clumsyWeaponShotSound.GetSound(), - _entMan.GetComponent(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().HotspotExpose(_entMan.GetComponent(user).Coordinates, 700, 50); - } - FireHandler?.Invoke(user, targetPos); - } } } diff --git a/Content.Shared/Weapons/Ranged/Components/SharedRangedWeaponComponent.cs b/Content.Shared/Weapons/Ranged/Components/SharedRangedWeaponComponent.cs index 55465a6dcb..83e85e97ff 100644 --- a/Content.Shared/Weapons/Ranged/Components/SharedRangedWeaponComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/SharedRangedWeaponComponent.cs @@ -29,34 +29,16 @@ namespace Content.Shared.Weapons.Ranged.Components } /// - /// 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. /// [Serializable, NetSerializable] -#pragma warning disable 618 - public sealed class FirePosComponentMessage : ComponentMessage -#pragma warning restore 618 + public sealed class FirePosEvent : EntityEventArgs { - /// - /// If this is not invalid, the target position is relative to the grid. - /// Otherwise, it is a map position. - /// - public GridId TargetGrid { get; } + public EntityCoordinates Coordinates; - /// - /// If Target Grid is not invalid, this is relative to the grid, otherwise - /// it is a map position. - /// - public Vector2 TargetPosition { get; } - - /// - /// Constructs a new instance of . - /// - /// The grid that the target position is on, if any. - /// Target position relative to the grid, or a map position if the grid is invalid. - public FirePosComponentMessage(GridId targetGrid, Vector2 targetPosition) + public FirePosEvent(EntityCoordinates coordinates) { - TargetGrid = targetGrid; - TargetPosition = targetPosition; + Coordinates = coordinates; } } } diff --git a/Content.Shared/Weapons/Ranged/MagazineAutoEjectMessage.cs b/Content.Shared/Weapons/Ranged/MagazineAutoEjectEvent.cs similarity index 67% rename from Content.Shared/Weapons/Ranged/MagazineAutoEjectMessage.cs rename to Content.Shared/Weapons/Ranged/MagazineAutoEjectEvent.cs index 089b6f5af2..ceca33c3ed 100644 --- a/Content.Shared/Weapons/Ranged/MagazineAutoEjectMessage.cs +++ b/Content.Shared/Weapons/Ranged/MagazineAutoEjectEvent.cs @@ -8,7 +8,8 @@ namespace Content.Shared.Weapons.Ranged /// This is sent if the MagazineBarrel AutoEjects the magazine /// [Serializable, NetSerializable] -#pragma warning disable 618 - public sealed class MagazineAutoEjectMessage : ComponentMessage {} -#pragma warning restore 618 + public sealed class MagazineAutoEjectEvent : EntityEventArgs + { + public EntityUid Uid; + } } diff --git a/Resources/Locale/en-US/weapons/ranged/barrels/components/revolver-barrel-component.ftl b/Resources/Locale/en-US/weapons/ranged/barrels/components/revolver-barrel-component.ftl index 17af63039a..3854023b17 100644 --- a/Resources/Locale/en-US/weapons/ranged/barrels/components/revolver-barrel-component.ftl +++ b/Resources/Locale/en-US/weapons/ranged/barrels/components/revolver-barrel-component.ftl @@ -1,9 +1,9 @@ ## RevolverBarrelComponent -revolver-barrel-component-try-inser-bullet-wrong-caliber = Wrong caliber -revolver-barrel-component-try-inser-bullet-ammo-full = Ammo full +revolver-barrel-component-try-insert-bullet-wrong-caliber = Wrong caliber +revolver-barrel-component-try-insert-bullet-ammo-full = Ammo full ## SpinRevolverVerb spin-revolver-verb-get-data-text = Spin -spin-revolver-verb-on-activate = Spun the cylinder \ No newline at end of file +spin-revolver-verb-on-activate = Spun the cylinder