diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs index b31db597b1..720bdcd2e4 100644 --- a/Content.Client/Entry/IgnoredComponents.cs +++ b/Content.Client/Entry/IgnoredComponents.cs @@ -287,9 +287,9 @@ namespace Content.Client.Entry "Uplink", "PDA", "SpawnItemsOnUse", + "AmbientOnPowered", "Wieldable", "IncreaseDamageOnWield", - "AmbientOnPowered", "TabletopGame", "LitOnPowered", "TriggerOnSignalReceived", @@ -304,7 +304,8 @@ namespace Content.Client.Entry "HandLabeler", "Label", "GhostRadio", - "Armor" + "Armor", + "PneumaticCannon" }; } } diff --git a/Content.Client/PneumaticCannon/PneumaticCannonVisualizer.cs b/Content.Client/PneumaticCannon/PneumaticCannonVisualizer.cs new file mode 100644 index 0000000000..de7d6a3068 --- /dev/null +++ b/Content.Client/PneumaticCannon/PneumaticCannonVisualizer.cs @@ -0,0 +1,21 @@ +using Content.Shared.PneumaticCannon; +using Robust.Client.GameObjects; + +namespace Content.Client.PneumaticCannon +{ + public class PneumaticCannonVisualizer : AppearanceVisualizer + { + public override void OnChangeData(AppearanceComponent component) + { + base.OnChangeData(component); + + if (!component.Owner.TryGetComponent(out var sprite)) + return; + + if (component.TryGetData(PneumaticCannonVisuals.Tank, out bool tank)) + { + sprite.LayerSetVisible(PneumaticCannonVisualLayers.Tank, tank); + } + } + } +} diff --git a/Content.Server/Nutrition/Components/FoodComponent.cs b/Content.Server/Nutrition/Components/FoodComponent.cs index d0125d2d34..6e8fcbc22a 100644 --- a/Content.Server/Nutrition/Components/FoodComponent.cs +++ b/Content.Server/Nutrition/Components/FoodComponent.cs @@ -199,7 +199,7 @@ namespace Content.Server.Nutrition.Components if (string.IsNullOrEmpty(TrashPrototype)) { - Owner.Delete(); + Owner.QueueDelete(); return true; } @@ -208,8 +208,6 @@ namespace Content.Server.Nutrition.Components return true; } - - private void DeleteAndSpawnTrash(IEntity user) { //We're empty. Become trash. diff --git a/Content.Server/Nutrition/Components/ForcefeedOnCollideComponent.cs b/Content.Server/Nutrition/Components/ForcefeedOnCollideComponent.cs new file mode 100644 index 0000000000..91d90fcf46 --- /dev/null +++ b/Content.Server/Nutrition/Components/ForcefeedOnCollideComponent.cs @@ -0,0 +1,24 @@ +using Content.Server.Nutrition.EntitySystems; +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Server.Nutrition.Components +{ + /// + /// A food item with this component will be forcefully fed to anyone + /// + [RegisterComponent, Friend(typeof(ForcefeedOnCollideSystem))] + public class ForcefeedOnCollideComponent : Component + { + public override string Name => "ForcefeedOnCollide"; + + /// + /// Since this component is primarily used by the pneumatic cannon, which adds this comp on throw start + /// and wants to remove it on throw end, this is set to false. However, you're free to change it if you want + /// something that can -always- be forcefed on collide, or something. + /// + [DataField("removeOnThrowEnd")] + public bool RemoveOnThrowEnd = true; + } +} diff --git a/Content.Server/Nutrition/EntitySystems/ForcefeedOnCollideSystem.cs b/Content.Server/Nutrition/EntitySystems/ForcefeedOnCollideSystem.cs new file mode 100644 index 0000000000..02c7e30133 --- /dev/null +++ b/Content.Server/Nutrition/EntitySystems/ForcefeedOnCollideSystem.cs @@ -0,0 +1,36 @@ +using Content.Server.Nutrition.Components; +using Content.Shared.Throwing; +using Robust.Shared.GameObjects; + +namespace Content.Server.Nutrition.EntitySystems +{ + public class ForcefeedOnCollideSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnThrowDoHit); + SubscribeLocalEvent(OnLand); + } + + private void OnThrowDoHit(EntityUid uid, ForcefeedOnCollideComponent component, ThrowDoHitEvent args) + { + if (!args.Target.HasComponent()) + return; + if (!EntityManager.TryGetComponent(uid, out var food)) + return; + + // the 'target' isnt really the 'user' per se.. but.. + food.TryUseFood(args.Target, args.Target); + } + + private void OnLand(EntityUid uid, ForcefeedOnCollideComponent component, LandEvent args) + { + if (!component.RemoveOnThrowEnd) + return; + + EntityManager.RemoveComponent(uid, component); + } + } +} diff --git a/Content.Server/PneumaticCannon/PneumaticCannonComponent.cs b/Content.Server/PneumaticCannon/PneumaticCannonComponent.cs new file mode 100644 index 0000000000..319b16170c --- /dev/null +++ b/Content.Server/PneumaticCannon/PneumaticCannonComponent.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using Content.Shared.Sound; +using Content.Shared.Tools; +using Content.Shared.Verbs; +using Robust.Shared.Analyzers; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.ViewVariables; + +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, Friend(typeof(PneumaticCannonSystem))] + public class PneumaticCannonComponent : Component + { + public override string Name { get; } = "PneumaticCannon"; + + [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 IEntity 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 new file mode 100644 index 0000000000..bfa073c4d3 --- /dev/null +++ b/Content.Server/PneumaticCannon/PneumaticCannonSystem.cs @@ -0,0 +1,402 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; +using Content.Server.Camera; +using Content.Server.CombatMode; +using Content.Server.Hands.Components; +using Content.Server.Items; +using Content.Server.Nutrition.Components; +using Content.Server.Storage.Components; +using Content.Server.Stunnable; +using Content.Server.Stunnable.Components; +using Content.Shared.Interaction; +using Content.Shared.PneumaticCannon; +using Robust.Server.GameObjects; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Content.Server.Throwing; +using Content.Server.Tools; +using Content.Server.Tools.Components; +using Content.Shared.CombatMode; +using Content.Shared.Popups; +using Content.Shared.Sound; +using Content.Shared.StatusEffect; +using Content.Shared.Verbs; +using Content.Shared.Weapons.Melee; +using Robust.Shared.Audio; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using Robust.Shared.Player; +using Robust.Shared.Random; + +namespace Content.Server.PneumaticCannon +{ + public class PneumaticCannonSystem : EntitySystem + { + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly StunSystem _stun = default!; + [Dependency] private readonly AtmosphereSystem _atmos = default!; + + private HashSet _currentlyFiring = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnAlternativeVerbs); + SubscribeLocalEvent(OnOtherVerbs); + } + + public override void Update(float frameTime) + { + 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; + } + } + } + + private void OnComponentInit(EntityUid uid, PneumaticCannonComponent component, ComponentInit args) + { + component.GasTankSlot = component.Owner.EnsureContainer($"{component.Name}-gasTank"); + + if (component.InstantFire) + component.AccumulatedFrametime = component.FireInterval; + } + + private void OnInteractUsing(EntityUid uid, PneumaticCannonComponent component, InteractUsingEvent args) + { + args.Handled = true; + if (args.Used.HasComponent() + && 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 (args.Used.TryGetComponent(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 (args.Used.TryGetComponent(out var item) + && component.Owner.TryGetComponent(out var storage)) + { + if (storage.CanInsert(args.Used)) + { + storage.Insert(args.Used); + 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(Filter.Pvs(args.Used.Uid), "/Audio/Items/hiss.ogg"); + return; + } + AddToQueue(component, args.User, args.ClickLocation); + } + + public void AddToQueue(PneumaticCannonComponent comp, IEntity user, EntityCoordinates click) + { + if (!comp.Owner.TryGetComponent(out var storage)) + return; + if (storage.StoredEntities == null) return; + if (storage.StoredEntities.Count == 0) + { + SoundSystem.Play(Filter.Pvs(comp.Owner.Uid), "/Audio/Weapons/click.ogg"); + 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) - user.Transform.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(Filter.Pvs(comp.Owner.Uid), "/Audio/Items/hiss.ogg"); + return; + } + + if (!comp.Owner.TryGetComponent(out var storage)) + return; + + if (data.User.Deleted) + return; + + if (storage.StoredEntities == null) return; + if (storage.StoredEntities.Count == 0) return; // click sound? + + IEntity ent = _random.Pick(storage.StoredEntities); + storage.Remove(ent); + + SoundSystem.Play(Filter.Pvs(data.User), comp.FireSound.GetSound()); + if (data.User.TryGetComponent(out var recoil)) + { + recoil.Kick(Vector2.One * data.Strength); + } + + ent.TryThrow(data.Direction, data.Strength, data.User, GetPushbackRatioFromPower(comp.Power)); + + // lasagna, anybody? + ent.EnsureComponent(); + + if(data.User.TryGetComponent(out var status) + && comp.Power == PneumaticCannonPower.High) + { + _stun.TryParalyze(data.User.Uid, TimeSpan.FromSeconds(comp.HighPowerStunTime), status); + data.User.PopupMessage(Loc.GetString("pneumatic-cannon-component-power-stun", + ("cannon", comp.Owner))); + } + + if (comp.GasTankSlot.ContainedEntity != null && comp.GasTankRequired) + { + // we checked for this earlier in HasGas so a GetComp is okay + var gas = comp.GasTankSlot.ContainedEntity.GetComponent(); + var environment = _atmos.GetTileMixture(comp.Owner.Transform.Coordinates, true); + var removed = gas.RemoveAir(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 == null) + return false; + + // not sure how it wouldnt, but it might not! who knows + if (component.GasTankSlot.ContainedEntity.TryGetComponent(out var tank)) + { + if (tank.Air.TotalMoles < usage) + return false; + + return true; + } + + return false; + } + + private void OnAlternativeVerbs(EntityUid uid, PneumaticCannonComponent component, GetAlternativeVerbsEvent args) + { + if (component.GasTankSlot.ContainedEntities.Count == 0 || !component.GasTankRequired) + return; + if (!args.CanInteract) + return; + + Verb 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, GetOtherVerbsEvent 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"); + args.Verbs.Add(ejectItems); + } + + public void TryRemoveGasTank(PneumaticCannonComponent component, IEntity user) + { + if (component.GasTankSlot.ContainedEntity == null) + { + user.PopupMessage(Loc.GetString("pneumatic-cannon-component-gas-tank-none", + ("cannon", component.Owner))); + return; + } + + var ent = component.GasTankSlot.ContainedEntity; + if (component.GasTankSlot.Remove(ent)) + { + if (user.TryGetComponent(out var hands)) + { + hands.TryPutInActiveHandOrAny(ent); + } + + user.PopupMessage(Loc.GetString("pneumatic-cannon-component-gas-tank-remove", + ("tank", ent), ("cannon", component.Owner))); + UpdateAppearance(component); + } + } + + public void TryEjectAllItems(PneumaticCannonComponent component, IEntity user) + { + if (component.Owner.TryGetComponent(out var storage)) + { + if (storage.StoredEntities == null) return; + foreach (var entity in storage.StoredEntities.ToArray()) + { + storage.Remove(entity); + } + + user.PopupMessage(Loc.GetString("pneumatic-cannon-component-ejected-all", + ("cannon", (component.Owner)))); + } + } + + private void UpdateAppearance(PneumaticCannonComponent component) + { + if (component.Owner.TryGetComponent(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 => 15f, + PneumaticCannonPower.Medium => 10f, + PneumaticCannonPower.Low or _ => 5f, + }; + } + + private float GetPushbackRatioFromPower(PneumaticCannonPower power) + { + return power switch + { + PneumaticCannonPower.Medium => 8.0f, + PneumaticCannonPower.High => 16.0f, + PneumaticCannonPower.Low or _ => 0f + }; + } + } +} diff --git a/Content.Server/Storage/Components/ServerStorageComponent.cs b/Content.Server/Storage/Components/ServerStorageComponent.cs index 1cc99c4516..d07866f856 100644 --- a/Content.Server/Storage/Components/ServerStorageComponent.cs +++ b/Content.Server/Storage/Components/ServerStorageComponent.cs @@ -52,6 +52,9 @@ namespace Content.Server.Storage.Components [DataField("quickInsert")] private bool _quickInsert = false; // Can insert storables by "attacking" them with the storage entity + [DataField("clickInsert")] + private bool _clickInsert = true; // Can insert stuff by clicking the storage entity with it + [DataField("areaInsert")] private bool _areaInsert = false; // "Attacking" with the storage entity causes it to insert all nearby storables after a delay [DataField("areaInsertRadius")] @@ -480,6 +483,8 @@ namespace Content.Server.Storage.Components /// true if inserted, false otherwise async Task IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs) { + if (!_clickInsert) + return false; Logger.DebugS(LoggerName, $"Storage (UID {Owner.Uid}) attacked by user (UID {eventArgs.User.Uid}) with entity (UID {eventArgs.Using.Uid})."); if (Owner.HasComponent()) diff --git a/Content.Server/Weapon/Ranged/Ammunition/Components/AmmoComponent.cs b/Content.Server/Weapon/Ranged/Ammunition/Components/AmmoComponent.cs index ec2cde2300..79377077db 100644 --- a/Content.Server/Weapon/Ranged/Ammunition/Components/AmmoComponent.cs +++ b/Content.Server/Weapon/Ranged/Ammunition/Components/AmmoComponent.cs @@ -179,6 +179,5 @@ namespace Content.Server.Weapon.Ranged.Ammunition.Components Dart, // Placeholder Grenade, Energy, - CreamPie, // I can't wait for this enum to be a prototype type... } } diff --git a/Content.Shared/PneumaticCannon/SharedPneumaticCannon.cs b/Content.Shared/PneumaticCannon/SharedPneumaticCannon.cs new file mode 100644 index 0000000000..be65029f20 --- /dev/null +++ b/Content.Shared/PneumaticCannon/SharedPneumaticCannon.cs @@ -0,0 +1,18 @@ +using System; +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/Resources/Audio/Effects/thunk.ogg b/Resources/Audio/Effects/thunk.ogg new file mode 100644 index 0000000000..c25ff5ce79 Binary files /dev/null and b/Resources/Audio/Effects/thunk.ogg differ diff --git a/Resources/Audio/Items/hiss.ogg b/Resources/Audio/Items/hiss.ogg new file mode 100644 index 0000000000..d589faad7a Binary files /dev/null and b/Resources/Audio/Items/hiss.ogg differ diff --git a/Resources/Locale/en-US/pneumatic-cannon/pneumatic-cannon-component.ftl b/Resources/Locale/en-US/pneumatic-cannon/pneumatic-cannon-component.ftl new file mode 100644 index 0000000000..85fc625a7f --- /dev/null +++ b/Resources/Locale/en-US/pneumatic-cannon/pneumatic-cannon-component.ftl @@ -0,0 +1,41 @@ +### 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) }. + +## 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. +} + +pneumatic-cannon-component-change-power = { $power -> + [High] You set the limiter to maximum power. It feels a little too powerful... + [Medium] You set the limiter to medium 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/Consumable/Food/Baked/pie.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pie.yml index f2c0d7a002..838941e4d6 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pie.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pie.yml @@ -104,10 +104,6 @@ - state: tin - state: plain - type: CreamPie - - type: Ammo - caliber: CreamPie - caseless: true - projectile: BulletCreampie # Tastes like pie, cream, banana. - type: entity diff --git a/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml b/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml index 2fb7397f02..0c883d6e40 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml @@ -14,6 +14,9 @@ - type: Sprite sprite: Objects/Misc/handcuffs.rsi state: handcuff + - type: Tag + tags: + - Handcuffs - type: entity name: makeshift handcuffs diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml index 7edcff9e9d..59f2cfad25 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml @@ -82,32 +82,3 @@ magState: mag steps: 1 zeroVisible: true - -- type: entity - name: pie cannon - parent: LauncherBase - id: LauncherCreamPie - description: Load cream pie for optimal results. - components: - - type: Sprite - sprite: Objects/Weapons/Guns/Launchers/pie_cannon.rsi - state: piecannon - - type: Item - size: 24 - sprite: Objects/Weapons/Guns/Launchers/pie_cannon.rsi - - type: RangedWeapon - clumsyCheck: false - - type: RevolverBarrel - caliber: CreamPie - currentSelector: Single - allSelectors: - - Single - fillPrototype: FoodPieBananaCream - fireRate: 5 - capacity: 5 - soundEmpty: - path: /Audio/Weapons/Guns/Empty/empty.ogg - soundGunshot: - path: /Audio/Effects/bang.ogg - soundInsert: - path: /Audio/Items/bikehorn.ogg diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml index 8fcb595343..c9f2e29fc7 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml @@ -312,30 +312,3 @@ damage: types: Piercing: 0 - -- type: entity - id: BulletCreampie - name: cream pie - parent: BulletBase - description: get creampied, honk!! - abstract: true - components: - - type: Projectile - deleteOnCollide: false # CreamPie component handles this. - damage: - types: - Blunt: 1 - - type: CreamPie - - type: ThrownItem - - type: Sprite - sprite: Objects/Consumable/Food/Baked/pie.rsi - netsync: false - layers: - - state: tin - - state: plain - - type: SolutionContainerManager - solutions: - food: - reagents: - - ReagentId: Nutriment - Quantity: 8 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml new file mode 100644 index 0000000000..8071cf9eac --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml @@ -0,0 +1,56 @@ +- type: entity + name: improvised pneumatic cannon + parent: BaseItem + id: ImprovisedPneumaticCannon + description: Improvised using nothing but a pipe, some zipties, and a pneumatic cannon. + 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" ] + visible: false + - type: Item + size: 40 + sprite: Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi + - type: PneumaticCannon + - type: Storage + # todo mirror pneum replace with ecs/evnts + clickInsert: false + capacity: 30 + - type: Appearance + visuals: + - type: PneumaticCannonVisualizer + - type: Construction + graph: PneumaticCannon + node: cannon + +- type: entity + name: pie cannon + parent: BaseItem + id: LauncherCreamPie + description: Load cream pie for optimal results. + components: + - type: Sprite + sprite: Objects/Weapons/Guns/Cannons/pie_cannon.rsi + layers: + - state: piecannon + - type: Storage + whitelist: + components: + - CreamPie + clickInsert: false + storageSoundCollection: + collection: BikeHorn + capacity: 40 + - type: PneumaticCannon + gasTankRequired: false + throwStrength: 30 + baseThrowRange: 12 + fireInterval: 0.4 + - type: Item + size: 50 + sprite: Objects/Weapons/Guns/Cannons/pie_cannon.rsi diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml index 3cb9ef729d..44cc8d5e11 100644 --- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml +++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml @@ -61,6 +61,9 @@ - type: AtmosUnsafeUnanchor - type: AtmosPipeColor - type: SubFloorHide + - type: Tag + tags: + - Pipe #Note: The PipeDirection of the PipeNode should be the south-facing version, because the entity starts at an angle of 0 (south) diff --git a/Resources/Prototypes/Recipes/Crafting/Graphs/pneumatic_cannon.yml b/Resources/Prototypes/Recipes/Crafting/Graphs/pneumatic_cannon.yml new file mode 100644 index 0000000000..48a526abaa --- /dev/null +++ b/Resources/Prototypes/Recipes/Crafting/Graphs/pneumatic_cannon.yml @@ -0,0 +1,24 @@ +- type: constructionGraph + id: PneumaticCannon + start: start + graph: + - node: start + edges: + - to: cannon + steps: + - tag: Pipe + icon: + sprite: Structures/Piping/Atmospherics/pipe.rsi + state: pipeStraight + name: pipe + - tag: Handcuffs + icon: + sprite: Objects/Misc/cablecuffs.rsi + state: cuff + color: red + name: cuffs + - material: Steel + amount: 6 + doAfter: 10 + - node: cannon + entity: ImprovisedPneumaticCannon diff --git a/Resources/Prototypes/Recipes/Crafting/bat.yml b/Resources/Prototypes/Recipes/Crafting/bat.yml deleted file mode 100644 index dcfc03a8bb..0000000000 --- a/Resources/Prototypes/Recipes/Crafting/bat.yml +++ /dev/null @@ -1,12 +0,0 @@ -- type: construction - name: baseball bat - id: bat - graph: WoodenBat - startNode: start - targetNode: bat - category: Weapons - description: A robust baseball bat. - icon: - sprite: Objects/Weapons/Melee/baseball_bat.rsi - state: icon - objectType: Item diff --git a/Resources/Prototypes/Recipes/Crafting/improvised.yml b/Resources/Prototypes/Recipes/Crafting/improvised.yml new file mode 100644 index 0000000000..74fed82636 --- /dev/null +++ b/Resources/Prototypes/Recipes/Crafting/improvised.yml @@ -0,0 +1,36 @@ +- type: construction + name: baseball bat + id: bat + graph: WoodenBat + startNode: start + targetNode: bat + category: Weapons + description: A robust baseball bat. + icon: + sprite: Objects/Weapons/Melee/baseball_bat.rsi + state: icon + objectType: Item + +- type: construction + name: makeshift handcuffs + id: makeshifthandcuffs + graph: makeshifthandcuffs + startNode: start + targetNode: cuffscable + category: Utility + description: "Homemade handcuffs crafted from spare cables." + icon: Objects/Misc/cablecuffs.rsi/cuff.png + objectType: Item + +- type: construction + name: improvised pneumatic cannon + id: pneumaticcannon + graph: PneumaticCannon + startNode: start + targetNode: cannon + category: Weapons + objectType: Item + 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 diff --git a/Resources/Prototypes/Recipes/Crafting/makeshifthandcuffs.yml b/Resources/Prototypes/Recipes/Crafting/makeshifthandcuffs.yml deleted file mode 100644 index b7b0344f11..0000000000 --- a/Resources/Prototypes/Recipes/Crafting/makeshifthandcuffs.yml +++ /dev/null @@ -1,11 +0,0 @@ -- type: construction - name: makeshift handcuffs - id: makeshifthandcuffs - graph: makeshifthandcuffs - startNode: start - targetNode: cuffscable - category: Utility - description: "Homemade handcuffs crafted from spare cables." - icon: Objects/Misc/cablecuffs.rsi/cuff.png - objectType: Item - diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index fff9c9dff3..e7edf9fede 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -112,6 +112,9 @@ - type: Tag id: GlassBeaker +- type: Tag + id: Handcuffs + - type: Tag id: Hoe @@ -148,6 +151,9 @@ - type: Tag id: Pill +- type: Tag + id: Pipe + - type: Tag id: Pizza diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/pie_cannon.rsi/inhand-left.png b/Resources/Textures/Objects/Weapons/Guns/Cannons/pie_cannon.rsi/inhand-left.png similarity index 100% rename from Resources/Textures/Objects/Weapons/Guns/Launchers/pie_cannon.rsi/inhand-left.png rename to Resources/Textures/Objects/Weapons/Guns/Cannons/pie_cannon.rsi/inhand-left.png diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/pie_cannon.rsi/inhand-right.png b/Resources/Textures/Objects/Weapons/Guns/Cannons/pie_cannon.rsi/inhand-right.png similarity index 100% rename from Resources/Textures/Objects/Weapons/Guns/Launchers/pie_cannon.rsi/inhand-right.png rename to Resources/Textures/Objects/Weapons/Guns/Cannons/pie_cannon.rsi/inhand-right.png diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/pie_cannon.rsi/meta.json b/Resources/Textures/Objects/Weapons/Guns/Cannons/pie_cannon.rsi/meta.json similarity index 100% rename from Resources/Textures/Objects/Weapons/Guns/Launchers/pie_cannon.rsi/meta.json rename to Resources/Textures/Objects/Weapons/Guns/Cannons/pie_cannon.rsi/meta.json diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/pie_cannon.rsi/piecannon.png b/Resources/Textures/Objects/Weapons/Guns/Cannons/pie_cannon.rsi/piecannon.png similarity index 100% rename from Resources/Textures/Objects/Weapons/Guns/Launchers/pie_cannon.rsi/piecannon.png rename to Resources/Textures/Objects/Weapons/Guns/Cannons/pie_cannon.rsi/piecannon.png diff --git a/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/inhand-left.png b/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/inhand-left.png new file mode 100644 index 0000000000..b0d0b96ecc Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/inhand-left.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/inhand-right.png b/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/inhand-right.png new file mode 100644 index 0000000000..d3e9b810db Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/inhand-right.png differ 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 new file mode 100644 index 0000000000..3aa08b8390 --- /dev/null +++ b/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/meta.json @@ -0,0 +1 @@ +{"version":1,"name":1,"size":{"x":32,"y":32},"states":[{"name":"pneumaticCannon","directions":1},{"name":"oxygen","directions":1},{"name":"inhand-left","directions":4},{"name":"inhand-right","directions":4}],"license":"CC-BY-SA-3.0","copyright":"tgstation at b2e5316993806b1524ab81237b1735b0591df2a2"} diff --git a/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/oxygen.png b/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/oxygen.png new file mode 100644 index 0000000000..8dc3dbba2f Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/oxygen.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/pneumaticCannon.png b/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/pneumaticCannon.png new file mode 100644 index 0000000000..c696911b74 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Cannons/pneumatic_cannon.rsi/pneumaticCannon.png differ