From 72535921269fae70d7ab9b000cc379731fb19cf1 Mon Sep 17 00:00:00 2001 From: Kara Date: Mon, 16 Jan 2023 10:56:09 -0600 Subject: [PATCH] Gunify pneumatic cannon (#13296) --- .../PneumaticCannon/PneumaticCannonSystem.cs | 7 + .../PneumaticCannonVisualizer.cs | 25 - .../PneumaticCannonComponent.cs | 101 ---- .../PneumaticCannon/PneumaticCannonSystem.cs | 462 ++++-------------- .../Storage/EntitySystems/StorageSystem.cs | 3 +- .../PneumaticCannonComponent.cs | 53 ++ .../PneumaticCannon/SharedPneumaticCannon.cs | 17 - .../SharedPneumaticCannonSystem.cs | 32 ++ .../Storage/Components/ItemMapperComponent.cs | 6 + .../EntitySystems/SharedItemMapperSystem.cs | 8 +- .../ContainerAmmoProviderComponent.cs | 13 + .../Systems/SharedGunSystem.Container.cs | 51 ++ .../Weapons/Ranged/Systems/SharedGunSystem.cs | 36 +- .../pneumatic-cannon-component.ftl | 25 +- .../Objects/Weapons/Guns/pneumatic_cannon.yml | 65 ++- .../Recipes/Crafting/improvised.yml | 2 +- .../{pneumaticCannon.png => icon.png} | Bin .../Cannons/pneumatic_cannon.rsi/meta.json | 6 +- .../{oxygen.png => tank.png} | Bin 19 files changed, 351 insertions(+), 561 deletions(-) create mode 100644 Content.Client/PneumaticCannon/PneumaticCannonSystem.cs delete mode 100644 Content.Client/PneumaticCannon/PneumaticCannonVisualizer.cs delete mode 100644 Content.Server/PneumaticCannon/PneumaticCannonComponent.cs create mode 100644 Content.Shared/PneumaticCannon/PneumaticCannonComponent.cs delete mode 100644 Content.Shared/PneumaticCannon/SharedPneumaticCannon.cs create mode 100644 Content.Shared/PneumaticCannon/SharedPneumaticCannonSystem.cs create mode 100644 Content.Shared/Weapons/Ranged/Components/ContainerAmmoProviderComponent.cs create mode 100644 Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Container.cs rename Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/{pneumaticCannon.png => icon.png} (100%) rename Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/{oxygen.png => tank.png} (100%) diff --git a/Content.Client/PneumaticCannon/PneumaticCannonSystem.cs b/Content.Client/PneumaticCannon/PneumaticCannonSystem.cs new file mode 100644 index 0000000000..0dcee1c69d --- /dev/null +++ b/Content.Client/PneumaticCannon/PneumaticCannonSystem.cs @@ -0,0 +1,7 @@ +using Content.Shared.PneumaticCannon; + +namespace Content.Client.PneumaticCannon; + +public sealed class PneumaticCannonSystem : SharedPneumaticCannonSystem +{ +} diff --git a/Content.Client/PneumaticCannon/PneumaticCannonVisualizer.cs b/Content.Client/PneumaticCannon/PneumaticCannonVisualizer.cs deleted file mode 100644 index 4e893f30f5..0000000000 --- a/Content.Client/PneumaticCannon/PneumaticCannonVisualizer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Content.Shared.PneumaticCannon; -using Robust.Client.GameObjects; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; - -namespace Content.Client.PneumaticCannon -{ - public sealed class PneumaticCannonVisualizer : AppearanceVisualizer - { - [Obsolete("Subscribe to AppearanceChangeEvent instead.")] - public override void OnChangeData(AppearanceComponent component) - { - base.OnChangeData(component); - - var entities = IoCManager.Resolve(); - if (!entities.TryGetComponent(component.Owner, out SpriteComponent? sprite)) - return; - - if (component.TryGetData(PneumaticCannonVisuals.Tank, out bool tank)) - { - sprite.LayerSetVisible(PneumaticCannonVisualLayers.Tank, tank); - } - } - } -} diff --git a/Content.Server/PneumaticCannon/PneumaticCannonComponent.cs b/Content.Server/PneumaticCannon/PneumaticCannonComponent.cs deleted file mode 100644 index 5916772c00..0000000000 --- a/Content.Server/PneumaticCannon/PneumaticCannonComponent.cs +++ /dev/null @@ -1,101 +0,0 @@ -using Content.Shared.Tools; -using Robust.Shared.Audio; -using Robust.Shared.Containers; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.PneumaticCannon -{ - // TODO: ideally, this and most of the actual firing code doesn't need to exist, and guns can be flexible enough - // to handle shooting things that aren't ammo (just firing any entity) - [RegisterComponent, Access(typeof(PneumaticCannonSystem))] - public sealed class PneumaticCannonComponent : Component - { - [ViewVariables] - public ContainerSlot GasTankSlot = default!; - - [ViewVariables(VVAccess.ReadWrite)] - public PneumaticCannonPower Power = PneumaticCannonPower.Low; - - [ViewVariables(VVAccess.ReadWrite)] - public PneumaticCannonFireMode Mode = PneumaticCannonFireMode.Single; - - /// - /// Used to fire the pneumatic cannon in intervals rather than all at the same time - /// - public float AccumulatedFrametime; - - public Queue FireQueue = new(); - - [DataField("fireInterval")] - public float FireInterval = 0.1f; - - /// - /// Whether the pneumatic cannon should instantly fire once, or whether it should wait for the - /// fire interval initially. - /// - [DataField("instantFire")] - public bool InstantFire = true; - - [DataField("toolModifyPower", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string ToolModifyPower = "Welding"; - - [DataField("toolModifyMode", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string ToolModifyMode = "Screwing"; - - /// - /// If this value is too high it just straight up stops working for some reason - /// - [DataField("throwStrength")] - [ViewVariables(VVAccess.ReadWrite)] - public float ThrowStrength = 20.0f; - - [DataField("baseThrowRange")] - [ViewVariables(VVAccess.ReadWrite)] - public float BaseThrowRange = 8.0f; - - /// - /// How long to stun for if they shoot the pneumatic cannon at high power. - /// - [DataField("highPowerStunTime")] - [ViewVariables(VVAccess.ReadWrite)] - public float HighPowerStunTime = 3.0f; - - [DataField("gasTankRequired")] - [ViewVariables(VVAccess.ReadWrite)] - public bool GasTankRequired = true; - - [DataField("fireSound")] - [ViewVariables(VVAccess.ReadWrite)] - public SoundSpecifier FireSound = new SoundPathSpecifier("/Audio/Effects/thunk.ogg"); - - public struct FireData - { - public EntityUid User; - public float Strength; - public Vector2 Direction; - } - } - - /// - /// How strong the pneumatic cannon should be. - /// Each tier throws items farther and with more speed, but has drawbacks. - /// The highest power knocks the player down for a considerable amount of time. - /// - public enum PneumaticCannonPower : byte - { - Low = 0, - Medium = 1, - High = 2, - Len = 3 // used for length calc - } - - /// - /// Whether to shoot one random item at a time, or all items at the same time. - /// - public enum PneumaticCannonFireMode : byte - { - Single = 0, - All = 1, - Len = 2 // used for length calc - } -} diff --git a/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs b/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs index 02bbae9316..b7c3ad609b 100644 --- a/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs +++ b/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs @@ -1,393 +1,121 @@ -using System.Linq; using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; -using Content.Server.Camera; -using Content.Server.Nutrition.Components; using Content.Server.Storage.EntitySystems; -using Content.Server.Storage.Components; using Content.Server.Stunnable; -using Content.Shared.Camera; -using Content.Shared.CombatMode; -using Content.Shared.Hands.EntitySystems; +using Content.Shared.Containers.ItemSlots; using Content.Shared.Interaction; -using Content.Shared.Item; using Content.Shared.PneumaticCannon; -using Content.Shared.Popups; using Content.Shared.StatusEffect; -using Content.Shared.Throwing; using Content.Shared.Tools.Components; -using Content.Shared.Verbs; -using Robust.Shared.Audio; +using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Containers; -using Robust.Shared.Map; -using Robust.Shared.Player; -using Robust.Shared.Random; -namespace Content.Server.PneumaticCannon +namespace Content.Server.PneumaticCannon; + +public sealed class PneumaticCannonSystem : SharedPneumaticCannonSystem { - public sealed class PneumaticCannonSystem : EntitySystem + [Dependency] private readonly AtmosphereSystem _atmos = default!; + [Dependency] private readonly GasTankSystem _gasTank = default!; + [Dependency] private readonly StunSystem _stun = default!; + [Dependency] private readonly ItemSlotsSystem _slots = default!; + + public override void Initialize() { - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly AtmosphereSystem _atmos = default!; - [Dependency] private readonly CameraRecoilSystem _cameraRecoil = default!; - [Dependency] private readonly GasTankSystem _gasTank = default!; - [Dependency] private readonly SharedHandsSystem _handsSystem = default!; - [Dependency] private readonly StorageSystem _storageSystem = default!; - [Dependency] private readonly StunSystem _stun = default!; - [Dependency] private readonly ThrowingSystem _throwingSystem = default!; + base.Initialize(); - private HashSet _currentlyFiring = new(); + SubscribeLocalEvent(OnInteractUsing, before: new []{ typeof(StorageSystem) }); + SubscribeLocalEvent(OnShoot); + SubscribeLocalEvent(OnContainerInserting); + } - public override void Initialize() + private void OnInteractUsing(EntityUid uid, PneumaticCannonComponent component, InteractUsingEvent args) + { + if (args.Handled) + return; + + if (!TryComp(args.Used, out var tool)) + return; + + if (!tool.Qualities.Contains(component.ToolModifyPower)) + return; + + var val = (int) component.Power; + val = (val + 1) % (int) PneumaticCannonPower.Len; + component.Power = (PneumaticCannonPower) val; + + Popup.PopupEntity(Loc.GetString("pneumatic-cannon-component-change-power", + ("power", component.Power.ToString())), uid, args.User); + + if (TryComp(uid, out var gun)) { - base.Initialize(); - - SubscribeLocalEvent(OnComponentInit); - SubscribeLocalEvent(OnInteractUsing); - SubscribeLocalEvent(OnAfterInteract); - SubscribeLocalEvent>(OnAlternativeVerbs); - SubscribeLocalEvent>(OnOtherVerbs); + gun.ProjectileSpeed = GetProjectileSpeedFromPower(component); } - public override void Update(float frameTime) + args.Handled = true; + } + + private void OnContainerInserting(EntityUid uid, PneumaticCannonComponent component, ContainerIsInsertingAttemptEvent args) + { + if (args.Container.ID != PneumaticCannonComponent.TankSlotId) + return; + + if (!TryComp(args.EntityUid, out var gas)) + return; + + if (gas.Air.TotalMoles >= component.GasUsage) + return; + + args.Cancel(); + } + + private void OnShoot(EntityUid uid, PneumaticCannonComponent component, ref GunShotEvent args) + { + if (GetGas(uid) is not { } gas) + return; + + if(TryComp(args.User, out var status) + && component.Power == PneumaticCannonPower.High) { - base.Update(frameTime); - - if (_currentlyFiring.Count == 0) - return; - - foreach (var comp in _currentlyFiring.ToArray()) - { - if (comp.FireQueue.Count == 0) - { - _currentlyFiring.Remove(comp); - // reset acc frametime to the fire interval if we're instant firing - if (comp.InstantFire) - { - comp.AccumulatedFrametime = comp.FireInterval; - } - else - { - comp.AccumulatedFrametime = 0f; - } - return; - } - - comp.AccumulatedFrametime += frameTime; - if (comp.AccumulatedFrametime > comp.FireInterval) - { - var dat = comp.FireQueue.Dequeue(); - Fire(comp, dat); - comp.AccumulatedFrametime -= comp.FireInterval; - } - } + _stun.TryParalyze(args.User, TimeSpan.FromSeconds(component.HighPowerStunTime), true, status); + Popup.PopupEntity(Loc.GetString("pneumatic-cannon-component-power-stun", + ("cannon", component.Owner)), uid, args.User); } - private void OnComponentInit(EntityUid uid, PneumaticCannonComponent component, ComponentInit args) + // this should always be possible, as we'll eject the gas tank when it no longer is + var environment = _atmos.GetContainingMixture(component.Owner, false, true); + var removed = _gasTank.RemoveAir(gas, component.GasUsage); + if (environment != null && removed != null) { - component.GasTankSlot = component.Owner.EnsureContainer($"{component.Name}-gasTank"); - - if (component.InstantFire) - component.AccumulatedFrametime = component.FireInterval; + _atmos.Merge(environment, removed); } - private void OnInteractUsing(EntityUid uid, PneumaticCannonComponent component, InteractUsingEvent args) + if (gas.Air.TotalMoles >= component.GasUsage) + return; + + // eject gas tank + _slots.TryEject(uid, PneumaticCannonComponent.TankSlotId, args.User, out _); + } + + /// + /// Returns whether the pneumatic cannon has enough gas to shoot an item, as well as the tank itself. + /// + private GasTankComponent? GetGas(EntityUid uid) + { + if (!Container.TryGetContainer(uid, PneumaticCannonComponent.TankSlotId, out var container) || + container is not ContainerSlot slot || slot.ContainedEntity is not {} contained) + return null; + + return TryComp(contained, out var gasTank) ? gasTank : null; + } + + private float GetProjectileSpeedFromPower(PneumaticCannonComponent component) + { + return component.Power switch { - args.Handled = true; - if (EntityManager.HasComponent(args.Used) - && component.GasTankSlot.CanInsert(args.Used) - && component.GasTankRequired) - { - component.GasTankSlot.Insert(args.Used); - args.User.PopupMessage(Loc.GetString("pneumatic-cannon-component-gas-tank-insert", - ("tank", args.Used), ("cannon", component.Owner))); - UpdateAppearance(component); - return; - } - - if (EntityManager.TryGetComponent(args.Used, out var tool)) - { - if (tool.Qualities.Contains(component.ToolModifyMode)) - { - // this is kind of ugly but it just cycles the enum - var val = (int) component.Mode; - val = (val + 1) % (int) PneumaticCannonFireMode.Len; - component.Mode = (PneumaticCannonFireMode) val; - args.User.PopupMessage(Loc.GetString("pneumatic-cannon-component-change-fire-mode", - ("mode", component.Mode.ToString()))); - // sound - return; - } - - if (tool.Qualities.Contains(component.ToolModifyPower)) - { - var val = (int) component.Power; - val = (val + 1) % (int) PneumaticCannonPower.Len; - component.Power = (PneumaticCannonPower) val; - args.User.PopupMessage(Loc.GetString("pneumatic-cannon-component-change-power", - ("power", component.Power.ToString()))); - // sound - return; - } - } - - // this overrides the ServerStorageComponent's insertion stuff because - // it's not event-based yet and I can't cancel it, so tools and stuff - // will modify mode/power then get put in anyway - if (EntityManager.TryGetComponent(args.Used, out var item) - && EntityManager.TryGetComponent(component.Owner, out var storage)) - { - if (_storageSystem.CanInsert(component.Owner, args.Used, out _, storage)) - { - _storageSystem.Insert(component.Owner, args.Used, storage); - args.User.PopupMessage(Loc.GetString("pneumatic-cannon-component-insert-item-success", - ("item", args.Used), ("cannon", component.Owner))); - } - else - { - args.User.PopupMessage(Loc.GetString("pneumatic-cannon-component-insert-item-failure", - ("item", args.Used), ("cannon", component.Owner))); - } - } - } - - private void OnAfterInteract(EntityUid uid, PneumaticCannonComponent component, AfterInteractEvent args) - { - if (EntityManager.TryGetComponent(uid, out var combat) - && !combat.IsInCombatMode) - return; - - args.Handled = true; - - if (!HasGas(component) && component.GasTankRequired) - { - args.User.PopupMessage(Loc.GetString("pneumatic-cannon-component-fire-no-gas", - ("cannon", component.Owner))); - SoundSystem.Play("/Audio/Items/hiss.ogg", Filter.Pvs(args.Used), args.Used, AudioParams.Default); - return; - } - AddToQueue(component, args.User, args.ClickLocation); - } - - public void AddToQueue(PneumaticCannonComponent comp, EntityUid user, EntityCoordinates click) - { - if (!EntityManager.TryGetComponent(comp.Owner, out var storage)) - return; - if (storage.StoredEntities == null) return; - if (storage.StoredEntities.Count == 0) - { - SoundSystem.Play("/Audio/Weapons/click.ogg", Filter.Pvs((comp).Owner), ((IComponent) comp).Owner, AudioParams.Default); - return; - } - - _currentlyFiring.Add(comp); - - int entCounts = comp.Mode switch - { - PneumaticCannonFireMode.All => storage.StoredEntities.Count, - PneumaticCannonFireMode.Single => 1, - _ => 0 - }; - - for (int i = 0; i < entCounts; i++) - { - var dir = (click.ToMapPos(EntityManager) - EntityManager.GetComponent(user).WorldPosition).Normalized; - - var randomAngle = GetRandomFireAngleFromPower(comp.Power).RotateVec(dir); - var randomStrengthMult = _random.NextFloat(0.75f, 1.25f); - var throwMult = GetRangeMultFromPower(comp.Power); - - var data = new PneumaticCannonComponent.FireData - { - User = user, - Strength = comp.ThrowStrength * randomStrengthMult, - Direction = (dir + randomAngle).Normalized * comp.BaseThrowRange * throwMult, - }; - comp.FireQueue.Enqueue(data); - } - } - - public void Fire(PneumaticCannonComponent comp, PneumaticCannonComponent.FireData data) - { - if (!HasGas(comp) && comp.GasTankRequired) - { - data.User.PopupMessage(Loc.GetString("pneumatic-cannon-component-fire-no-gas", - ("cannon", comp.Owner))); - SoundSystem.Play("/Audio/Items/hiss.ogg", Filter.Pvs(comp.Owner), comp.Owner, AudioParams.Default); - return; - } - - if (!EntityManager.TryGetComponent(comp.Owner, out var storage)) - return; - - if (Deleted(data.User)) - return; - - if (storage.StoredEntities == null) return; - if (storage.StoredEntities.Count == 0) return; // click sound? - - var ent = _random.Pick(storage.StoredEntities); - _storageSystem.RemoveAndDrop(comp.Owner, ent, storage); - - SoundSystem.Play(comp.FireSound.GetSound(), Filter.Pvs(data.User), comp.Owner, AudioParams.Default); - if (EntityManager.HasComponent(data.User)) - { - var kick = Vector2.One * data.Strength; - _cameraRecoil.KickCamera(data.User, kick); - } - - _throwingSystem.TryThrow(ent, data.Direction, data.Strength, data.User, GetPushbackRatioFromPower(comp.Power)); - - if(EntityManager.TryGetComponent(data.User, out var status) - && comp.Power == PneumaticCannonPower.High) - { - _stun.TryParalyze(data.User, TimeSpan.FromSeconds(comp.HighPowerStunTime), true, status); - - data.User.PopupMessage(Loc.GetString("pneumatic-cannon-component-power-stun", - ("cannon", comp.Owner))); - } - - if (comp.GasTankSlot.ContainedEntity is {Valid: true} contained && comp.GasTankRequired) - { - // we checked for this earlier in HasGas so a GetComp is okay - var gas = EntityManager.GetComponent(contained); - var environment = _atmos.GetContainingMixture(comp.Owner, false, true); - var removed = _gasTank.RemoveAir(gas, GetMoleUsageFromPower(comp.Power)); - if (environment != null && removed != null) - { - _atmos.Merge(environment, removed); - } - } - } - - /// - /// Returns whether the pneumatic cannon has enough gas to shoot an item. - /// - public bool HasGas(PneumaticCannonComponent component) - { - var usage = GetMoleUsageFromPower(component.Power); - - if (component.GasTankSlot.ContainedEntity is not {Valid: true } contained) - return false; - - // not sure how it wouldnt, but it might not! who knows - if (EntityManager.TryGetComponent(contained, out var tank)) - { - if (tank.Air.TotalMoles < usage) - return false; - - return true; - } - - return false; - } - - private void OnAlternativeVerbs(EntityUid uid, PneumaticCannonComponent component, GetVerbsEvent args) - { - if (component.GasTankSlot.ContainedEntities.Count == 0 || !component.GasTankRequired) - return; - if (!args.CanInteract) - return; - - AlternativeVerb ejectTank = new(); - ejectTank.Act = () => TryRemoveGasTank(component, args.User); - ejectTank.Text = Loc.GetString("pneumatic-cannon-component-verb-gas-tank-name"); - args.Verbs.Add(ejectTank); - } - - private void OnOtherVerbs(EntityUid uid, PneumaticCannonComponent component, GetVerbsEvent args) - { - if (!args.CanInteract) - return; - - Verb ejectItems = new(); - ejectItems.Act = () => TryEjectAllItems(component, args.User); - ejectItems.Text = Loc.GetString("pneumatic-cannon-component-verb-eject-items-name"); - ejectItems.DoContactInteraction = true; - args.Verbs.Add(ejectItems); - } - - public void TryRemoveGasTank(PneumaticCannonComponent component, EntityUid user) - { - if (component.GasTankSlot.ContainedEntity is not {Valid: true} contained) - { - user.PopupMessage(Loc.GetString("pneumatic-cannon-component-gas-tank-none", - ("cannon", component.Owner))); - return; - } - - if (component.GasTankSlot.Remove(contained)) - { - _handsSystem.TryPickupAnyHand(user, contained); - - user.PopupMessage(Loc.GetString("pneumatic-cannon-component-gas-tank-remove", - ("tank", contained), ("cannon", component.Owner))); - UpdateAppearance(component); - } - } - - public void TryEjectAllItems(PneumaticCannonComponent component, EntityUid user) - { - if (EntityManager.TryGetComponent(component.Owner, out var storage)) - { - if (storage.StoredEntities == null) return; - foreach (var entity in storage.StoredEntities.ToArray()) - { - _storageSystem.RemoveAndDrop(component.Owner, entity, storage); - } - - user.PopupMessage(Loc.GetString("pneumatic-cannon-component-ejected-all", - ("cannon", (component.Owner)))); - } - } - - private void UpdateAppearance(PneumaticCannonComponent component) - { - if (EntityManager.TryGetComponent(component.Owner, out var appearance)) - { - appearance.SetData(PneumaticCannonVisuals.Tank, - component.GasTankSlot.ContainedEntities.Count != 0); - } - } - - private Angle GetRandomFireAngleFromPower(PneumaticCannonPower power) - { - return power switch - { - PneumaticCannonPower.High => _random.NextAngle(-0.3, 0.3), - PneumaticCannonPower.Medium => _random.NextAngle(-0.2, 0.2), - PneumaticCannonPower.Low or _ => _random.NextAngle(-0.1, 0.1), - }; - } - - private float GetRangeMultFromPower(PneumaticCannonPower power) - { - return power switch - { - PneumaticCannonPower.High => 1.6f, - PneumaticCannonPower.Medium => 1.3f, - PneumaticCannonPower.Low or _ => 1.0f, - }; - } - - private float GetMoleUsageFromPower(PneumaticCannonPower power) - { - return power switch - { - PneumaticCannonPower.High => 9f, - PneumaticCannonPower.Medium => 6f, - PneumaticCannonPower.Low or _ => 3f, - }; - } - - private float GetPushbackRatioFromPower(PneumaticCannonPower power) - { - return power switch - { - PneumaticCannonPower.Medium => 8.0f, - PneumaticCannonPower.High => 16.0f, - PneumaticCannonPower.Low or _ => 0f - }; - } + PneumaticCannonPower.High => component.BaseProjectileSpeed * 4f, + PneumaticCannonPower.Medium => component.BaseProjectileSpeed, + PneumaticCannonPower.Low or _ => component.BaseProjectileSpeed * 0.5f, + }; } } diff --git a/Content.Server/Storage/EntitySystems/StorageSystem.cs b/Content.Server/Storage/EntitySystems/StorageSystem.cs index f54aa25434..3951d15f4d 100644 --- a/Content.Server/Storage/EntitySystems/StorageSystem.cs +++ b/Content.Server/Storage/EntitySystems/StorageSystem.cs @@ -29,6 +29,7 @@ using Content.Shared.Destructible; using static Content.Shared.Storage.SharedStorageComponent; using Content.Shared.ActionBlocker; using Content.Shared.CombatMode; +using Content.Shared.Containers.ItemSlots; using Content.Shared.Implants.Components; using Content.Shared.Movement.Events; @@ -61,7 +62,7 @@ namespace Content.Server.Storage.EntitySystems SubscribeLocalEvent(OnComponentInit); SubscribeLocalEvent>(AddOpenUiVerb); SubscribeLocalEvent>(AddTransferVerbs); - SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnInteractUsing, after: new []{ typeof(ItemSlotsSystem)} ); SubscribeLocalEvent(OnActivate); SubscribeLocalEvent(OnImplantActivate); SubscribeLocalEvent(AfterInteract); diff --git a/Content.Shared/PneumaticCannon/PneumaticCannonComponent.cs b/Content.Shared/PneumaticCannon/PneumaticCannonComponent.cs new file mode 100644 index 0000000000..11407f1d3f --- /dev/null +++ b/Content.Shared/PneumaticCannon/PneumaticCannonComponent.cs @@ -0,0 +1,53 @@ +using Content.Shared.Tools; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.PneumaticCannon; + +/// +/// Handles gas powered guns--cancels shooting if no gas is available, and takes gas from the given container slot. +/// +[RegisterComponent, NetworkedComponent] +public sealed class PneumaticCannonComponent : Component +{ + public const string TankSlotId = "gas_tank"; + + [ViewVariables(VVAccess.ReadWrite)] + public PneumaticCannonPower Power = PneumaticCannonPower.Medium; + + [DataField("toolModifyPower", customTypeSerializer:typeof(PrototypeIdSerializer))] + public string ToolModifyPower = "Anchoring"; + + /// + /// How long to stun for if they shoot the pneumatic cannon at high power. + /// + [DataField("highPowerStunTime")] + [ViewVariables(VVAccess.ReadWrite)] + public float HighPowerStunTime = 3.0f; + + /// + /// Amount of moles to consume for each shot at any power. + /// + [DataField("gasUsage")] + [ViewVariables(VVAccess.ReadWrite)] + public float GasUsage = 2f; + + /// + /// Base projectile speed at default power. + /// + [DataField("baseProjectileSpeed")] + public float BaseProjectileSpeed = 20f; +} + +/// +/// How strong the pneumatic cannon should be. +/// Each tier throws items farther and with more speed, but has drawbacks. +/// The highest power knocks the player down for a considerable amount of time. +/// +public enum PneumaticCannonPower : byte +{ + Low = 0, + Medium = 1, + High = 2, + Len = 3 // used for length calc +} diff --git a/Content.Shared/PneumaticCannon/SharedPneumaticCannon.cs b/Content.Shared/PneumaticCannon/SharedPneumaticCannon.cs deleted file mode 100644 index c5d6bba31d..0000000000 --- a/Content.Shared/PneumaticCannon/SharedPneumaticCannon.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Robust.Shared.Serialization; - -namespace Content.Shared.PneumaticCannon -{ - [Serializable, NetSerializable] - public enum PneumaticCannonVisualLayers : byte - { - Base, - Tank - } - - [Serializable, NetSerializable] - public enum PneumaticCannonVisuals - { - Tank - } -} diff --git a/Content.Shared/PneumaticCannon/SharedPneumaticCannonSystem.cs b/Content.Shared/PneumaticCannon/SharedPneumaticCannonSystem.cs new file mode 100644 index 0000000000..7071676b2c --- /dev/null +++ b/Content.Shared/PneumaticCannon/SharedPneumaticCannonSystem.cs @@ -0,0 +1,32 @@ +using Content.Shared.Popups; +using Content.Shared.Weapons.Ranged.Systems; +using Robust.Shared.Containers; +using Robust.Shared.Serialization; + +namespace Content.Shared.PneumaticCannon; + +public abstract class SharedPneumaticCannonSystem : EntitySystem +{ + [Dependency] protected readonly SharedContainerSystem Container = default!; + [Dependency] protected readonly SharedPopupSystem Popup = default!; + + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAttemptShoot); + } + + private void OnAttemptShoot(EntityUid uid, PneumaticCannonComponent component, ref AttemptShootEvent args) + { + // we don't have atmos on shared, so just predict by the existence of a slot item + // server will handle auto ejecting/not adding the slot item if it doesnt have enough gas, + // so this won't mispredict + if (!Container.TryGetContainer(uid, PneumaticCannonComponent.TankSlotId, out var container) || + container is not ContainerSlot slot || slot.ContainedEntity is null) + { + args.Cancelled = true; + } + } +} diff --git a/Content.Shared/Storage/Components/ItemMapperComponent.cs b/Content.Shared/Storage/Components/ItemMapperComponent.cs index 86d1d879cc..c5c9e55664 100644 --- a/Content.Shared/Storage/Components/ItemMapperComponent.cs +++ b/Content.Shared/Storage/Components/ItemMapperComponent.cs @@ -60,6 +60,12 @@ namespace Content.Shared.Storage.Components [DataField("sprite")] public ResourcePath? RSIPath; + /// + /// If this exists, shown layers will only consider entities in the given containers. + /// + [DataField("containerWhitelist")] + public HashSet? ContainerWhitelist; + public readonly List SpriteLayers = new(); } } diff --git a/Content.Shared/Storage/EntitySystems/SharedItemMapperSystem.cs b/Content.Shared/Storage/EntitySystems/SharedItemMapperSystem.cs index 808e1e0b42..fcd76e1515 100644 --- a/Content.Shared/Storage/EntitySystems/SharedItemMapperSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SharedItemMapperSystem.cs @@ -42,12 +42,18 @@ namespace Content.Shared.Storage.EntitySystems private void MapperEntityRemoved(EntityUid uid, ItemMapperComponent itemMapper, EntRemovedFromContainerMessage args) { + if (itemMapper.ContainerWhitelist != null && !itemMapper.ContainerWhitelist.Contains(args.Container.ID)) + return; + UpdateAppearance(uid, itemMapper, args); } private void MapperEntityInserted(EntityUid uid, ItemMapperComponent itemMapper, EntInsertedIntoContainerMessage args) { + if (itemMapper.ContainerWhitelist != null && !itemMapper.ContainerWhitelist.Contains(args.Container.ID)) + return; + UpdateAppearance(uid, itemMapper, args); } @@ -76,7 +82,7 @@ namespace Content.Shared.Storage.EntitySystems out IReadOnlyList showLayers) { var containedLayers = _container.GetAllContainers(msg.Container.Owner) - .SelectMany(cont => cont.ContainedEntities).ToArray(); + .Where(c => itemMapper.ContainerWhitelist?.Contains(c.ID) ?? true).SelectMany(cont => cont.ContainedEntities).ToArray(); var list = new List(); foreach (var mapLayerData in itemMapper.MapLayers.Values) diff --git a/Content.Shared/Weapons/Ranged/Components/ContainerAmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/ContainerAmmoProviderComponent.cs new file mode 100644 index 0000000000..ee9158a6eb --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Components/ContainerAmmoProviderComponent.cs @@ -0,0 +1,13 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Weapons.Ranged.Components; + +/// +/// Handles pulling entities from the given container to use as ammunition. +/// +[RegisterComponent] +public sealed class ContainerAmmoProviderComponent : AmmoProviderComponent +{ + [DataField("container", required: true)] + public string Container = default!; +} diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Container.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Container.cs new file mode 100644 index 0000000000..453daded30 --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Container.cs @@ -0,0 +1,51 @@ +using System.Linq; +using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Events; +using Robust.Shared.Containers; +using Robust.Shared.Network; + +namespace Content.Shared.Weapons.Ranged.Systems; + +public partial class SharedGunSystem +{ + [Dependency] private readonly INetManager _netMan = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + + public void InitializeContainer() + { + SubscribeLocalEvent(OnContainerTakeAmmo); + SubscribeLocalEvent(OnContainerAmmoCount); + } + + private void OnContainerTakeAmmo(EntityUid uid, ContainerAmmoProviderComponent component, TakeAmmoEvent args) + { + if (!_container.TryGetContainer(uid, component.Container, out var container)) + return; + + for (int i = 0; i < args.Shots; i++) + { + if (!container.ContainedEntities.Any()) + break; + + var ent = container.ContainedEntities[0]; + + if (_netMan.IsServer) + container.Remove(ent); + + args.Ammo.Add(EnsureComp(ent)); + } + } + + private void OnContainerAmmoCount(EntityUid uid, ContainerAmmoProviderComponent component, ref GetAmmoCountEvent args) + { + if (!_container.TryGetContainer(uid, component.Container, out var container)) + { + args.Capacity = 0; + args.Count = 0; + return; + } + + args.Capacity = int.MaxValue; + args.Count = container.ContainedEntities.Count; + } +} diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs index 736fa6ee74..056ee83168 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs @@ -81,6 +81,7 @@ public abstract partial class SharedGunSystem : EntitySystem InitializeMagazine(); InitializeRevolver(); InitializeBasicEntity(); + InitializeContainer(); // Interactions SubscribeLocalEvent>(OnAltVerb); @@ -205,11 +206,13 @@ public abstract partial class SharedGunSystem : EntitySystem private void AttemptShoot(EntityUid user, GunComponent gun) { - if (gun.FireRate <= 0f) return; + if (gun.FireRate <= 0f) + return; var toCoordinates = gun.ShootCoordinates; - if (toCoordinates == null) return; + if (toCoordinates == null) + return; if (TagSystem.HasTag(user, "GunsDisabled")) { @@ -217,11 +220,13 @@ public abstract partial class SharedGunSystem : EntitySystem return; } + var curTime = Timing.CurTime; // Need to do this to play the clicking sound for empty automatic weapons // but not play anything for burst fire. - if (gun.NextFire > curTime) return; + if (gun.NextFire > curTime) + return; // First shot if (gun.ShotCounter == 0 && gun.NextFire < curTime) @@ -269,7 +274,10 @@ public abstract partial class SharedGunSystem : EntitySystem // where the gun may be SemiAuto or Burst. gun.ShotCounter += shots; - if (ev.Ammo.Count <= 0) + var attemptEv = new AttemptShootEvent(user); + RaiseLocalEvent(gun.Owner, ref attemptEv); + + if (ev.Ammo.Count <= 0 || attemptEv.Cancelled) { // Play empty gun sounds if relevant // If they're firing an existing clip then don't play anything. @@ -288,6 +296,8 @@ public abstract partial class SharedGunSystem : EntitySystem // Shoot confirmed - sounds also played here in case it's invalid (e.g. cartridge already spent). Shoot(gun, ev.Ammo, fromCoordinates, toCoordinates.Value, user); + var shotEv = new GunShotEvent(user); + RaiseLocalEvent(gun.Owner, ref shotEv); // Projectiles cause impulses especially important in non gravity environments if (TryComp(user, out var userPhysics)) { @@ -410,6 +420,24 @@ public abstract partial class SharedGunSystem : EntitySystem } } +/// +/// Raised directed on the gun before firing to see if the shot should go through. +/// +/// +/// Handling this in server exclusively will lead to mispredicts. +/// +/// The user that attempted to fire this gun. +/// Set this to true if the shot should be cancelled. +[ByRefEvent] +public record struct AttemptShootEvent(EntityUid User, bool Cancelled=false); + +/// +/// Raised directed on the gun after firing. +/// +/// The user that fired this gun. +[ByRefEvent] +public record struct GunShotEvent(EntityUid User); + public enum EffectLayers : byte { Unshaded, diff --git a/Resources/Locale/en-US/pneumatic-cannon/pneumatic-cannon-component.ftl b/Resources/Locale/en-US/pneumatic-cannon/pneumatic-cannon-component.ftl index 85fc625a7f..4b925969ef 100644 --- a/Resources/Locale/en-US/pneumatic-cannon/pneumatic-cannon-component.ftl +++ b/Resources/Locale/en-US/pneumatic-cannon/pneumatic-cannon-component.ftl @@ -1,23 +1,12 @@ ### Loc for the pneumatic cannon. -pneumatic-cannon-component-verb-gas-tank-name = Eject gas tank -pneumatic-cannon-component-verb-eject-items-name = Eject all items - -## Shown when inserting items into it - -pneumatic-cannon-component-insert-item-success = You insert { THE($item) } into { THE($cannon) }. -pneumatic-cannon-component-insert-item-failure = You can't seem to fit { THE($item) } in { THE($cannon) }. +pneumatic-cannon-component-itemslot-name = Gas Tank ## Shown when trying to fire, but no gas pneumatic-cannon-component-fire-no-gas = { CAPITALIZE(THE($cannon)) } clicks, but no gas comes out. -## Shown when changing the fire mode or power. - -pneumatic-cannon-component-change-fire-mode = { $mode -> - [All] You loosen the valves to fire everything at once. - *[Single] You tighten the valves to fire one item at a time. -} +## Shown when changing power. pneumatic-cannon-component-change-power = { $power -> [High] You set the limiter to maximum power. It feels a little too powerful... @@ -25,16 +14,6 @@ pneumatic-cannon-component-change-power = { $power -> *[Low] You set the limiter to low power. } -## Shown when inserting/removing the gas tank. - -pneumatic-cannon-component-gas-tank-insert = You fit { THE($tank) } onto { THE($cannon) }. -pneumatic-cannon-component-gas-tank-remove = You take { THE($tank) } off of { THE($cannon) }. -pneumatic-cannon-component-gas-tank-none = There is no gas tank on { THE($cannon) }! - -## Shown when ejecting every item from the cannon using a verb. - -pneumatic-cannon-component-ejected-all = You eject everything from { THE($cannon) }. - ## Shown when being stunned by having the power too high. pneumatic-cannon-component-power-stun = The pure force of { THE($cannon) } knocks you over! diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml index b8a925e402..01a6ff8db7 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml @@ -2,40 +2,64 @@ name: improvised pneumatic cannon parent: BaseStorageItem id: WeaponImprovisedPneumaticCannon - description: Improvised using nothing but a pipe, some zipties, and a pneumatic cannon. + description: Improvised using nothing but a pipe, some zipties, and a pneumatic cannon. Doesn't accept tanks without enough gas. components: - type: Sprite sprite: Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi netsync: false layers: - - state: pneumaticCannon - map: [ "enum.PneumaticCannonVisualLayers.Base" ] - - state: oxygen - map: [ "enum.PneumaticCannonVisualLayers.Tank" ] + - state: icon + - state: tank + map: [ "tank" ] visible: false - type: Item size: 50 - type: Clothing - sprite: Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi quickEquip: false slots: - Back + - type: Gun + fireRate: 2 + selectedMode: SemiAuto + availableModes: + - SemiAuto + - FullAuto + soundGunshot: + path: /Audio/Effects/thunk.ogg + soundEmpty: + path: /Audio/Items/hiss.ogg + - type: ContainerAmmoProvider + container: storagebase - type: PneumaticCannon - type: Storage - # todo mirror pneum replace with ecs/evnts - clickInsert: false capacity: 30 - type: Appearance - visuals: - - type: PneumaticCannonVisualizer + - type: ItemMapper + containerWhitelist: [gas_tank] + mapLayers: + tank: + whitelist: + components: + - GasTank - type: Construction graph: PneumaticCannon node: cannon + - type: ItemSlots + slots: + gas_tank: + name: pneumatic-cannon-component-itemslot-name + whitelist: + components: + - GasTank + insertSound: + path: /Audio/Weapons/click.ogg + params: + volume: -3 - type: ContainerContainer containers: storagebase: !type:Container ents: [] - PneumaticCannon-gasTank: !type:ContainerSlot + gas_tank: !type:ContainerSlot - type: entity name: pie cannon @@ -51,13 +75,19 @@ whitelist: components: - CreamPie - clickInsert: false capacity: 40 - - type: PneumaticCannon - gasTankRequired: false - throwStrength: 30 - baseThrowRange: 12 - fireInterval: 0.4 + - type: Gun + fireRate: 1 + selectedMode: SemiAuto + availableModes: + - SemiAuto + - FullAuto + soundGunshot: + path: /Audio/Effects/thunk.ogg + soundEmpty: + path: /Audio/Items/hiss.ogg + - type: ContainerAmmoProvider + container: storagebase - type: Item size: 50 - type: Clothing @@ -69,4 +99,3 @@ containers: storagebase: !type:Container ents: [] - PneumaticCannon-gasTank: !type:ContainerSlot diff --git a/Resources/Prototypes/Recipes/Crafting/improvised.yml b/Resources/Prototypes/Recipes/Crafting/improvised.yml index 62bf382885..5229290a29 100644 --- a/Resources/Prototypes/Recipes/Crafting/improvised.yml +++ b/Resources/Prototypes/Recipes/Crafting/improvised.yml @@ -46,7 +46,7 @@ description: This son of a gun can fire anything that fits in it using just a little gas. icon: sprite: Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi - state: pneumaticCannon + state: icon - type: construction name: gauze diff --git a/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/pneumaticCannon.png b/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/icon.png similarity index 100% rename from Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/pneumaticCannon.png rename to Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/icon.png diff --git a/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/meta.json b/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/meta.json index 38b327f302..7bd3c1f786 100644 --- a/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/meta.json +++ b/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/meta.json @@ -8,10 +8,10 @@ }, "states": [ { - "name": "pneumaticCannon" + "name": "icon" }, { - "name": "oxygen" + "name": "tank" }, { "name": "inhand-left", @@ -26,4 +26,4 @@ "directions": 4 } ] -} \ No newline at end of file +} diff --git a/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/oxygen.png b/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/tank.png similarity index 100% rename from Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/oxygen.png rename to Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/tank.png