Pneumatic cannons (#4560)
* basics & visuals * pneumatic cannon works perf * loc + popups * gas tank does stuff + queue changes * updates * b * forcefeeding * inhand * crafting! * pie cannon now is a pneumatic cannon * oopy * fix for entman + verbs * pie * change for tools * actual * combat mode + better sounds * reviews
@@ -287,9 +287,9 @@ namespace Content.Client.Entry
|
|||||||
"Uplink",
|
"Uplink",
|
||||||
"PDA",
|
"PDA",
|
||||||
"SpawnItemsOnUse",
|
"SpawnItemsOnUse",
|
||||||
|
"AmbientOnPowered",
|
||||||
"Wieldable",
|
"Wieldable",
|
||||||
"IncreaseDamageOnWield",
|
"IncreaseDamageOnWield",
|
||||||
"AmbientOnPowered",
|
|
||||||
"TabletopGame",
|
"TabletopGame",
|
||||||
"LitOnPowered",
|
"LitOnPowered",
|
||||||
"TriggerOnSignalReceived",
|
"TriggerOnSignalReceived",
|
||||||
@@ -304,7 +304,8 @@ namespace Content.Client.Entry
|
|||||||
"HandLabeler",
|
"HandLabeler",
|
||||||
"Label",
|
"Label",
|
||||||
"GhostRadio",
|
"GhostRadio",
|
||||||
"Armor"
|
"Armor",
|
||||||
|
"PneumaticCannon"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
Content.Client/PneumaticCannon/PneumaticCannonVisualizer.cs
Normal file
@@ -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<SpriteComponent>(out var sprite))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (component.TryGetData(PneumaticCannonVisuals.Tank, out bool tank))
|
||||||
|
{
|
||||||
|
sprite.LayerSetVisible(PneumaticCannonVisualLayers.Tank, tank);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -199,7 +199,7 @@ namespace Content.Server.Nutrition.Components
|
|||||||
|
|
||||||
if (string.IsNullOrEmpty(TrashPrototype))
|
if (string.IsNullOrEmpty(TrashPrototype))
|
||||||
{
|
{
|
||||||
Owner.Delete();
|
Owner.QueueDelete();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,8 +208,6 @@ namespace Content.Server.Nutrition.Components
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void DeleteAndSpawnTrash(IEntity user)
|
private void DeleteAndSpawnTrash(IEntity user)
|
||||||
{
|
{
|
||||||
//We're empty. Become trash.
|
//We're empty. Become trash.
|
||||||
|
|||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A food item with this component will be forcefully fed to anyone
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, Friend(typeof(ForcefeedOnCollideSystem))]
|
||||||
|
public class ForcefeedOnCollideComponent : Component
|
||||||
|
{
|
||||||
|
public override string Name => "ForcefeedOnCollide";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("removeOnThrowEnd")]
|
||||||
|
public bool RemoveOnThrowEnd = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ForcefeedOnCollideComponent, ThrowDoHitEvent>(OnThrowDoHit);
|
||||||
|
SubscribeLocalEvent<ForcefeedOnCollideComponent, LandEvent>(OnLand);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnThrowDoHit(EntityUid uid, ForcefeedOnCollideComponent component, ThrowDoHitEvent args)
|
||||||
|
{
|
||||||
|
if (!args.Target.HasComponent<HungerComponent>())
|
||||||
|
return;
|
||||||
|
if (!EntityManager.TryGetComponent<FoodComponent>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
Content.Server/PneumaticCannon/PneumaticCannonComponent.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to fire the pneumatic cannon in intervals rather than all at the same time
|
||||||
|
/// </summary>
|
||||||
|
public float AccumulatedFrametime;
|
||||||
|
|
||||||
|
public Queue<FireData> FireQueue = new();
|
||||||
|
|
||||||
|
[DataField("fireInterval")]
|
||||||
|
public float FireInterval = 0.1f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the pneumatic cannon should instantly fire once, or whether it should wait for the
|
||||||
|
/// fire interval initially.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("instantFire")]
|
||||||
|
public bool InstantFire = true;
|
||||||
|
|
||||||
|
[DataField("toolModifyPower", customTypeSerializer:typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
|
||||||
|
public string ToolModifyPower = "Welding";
|
||||||
|
|
||||||
|
[DataField("toolModifyMode", customTypeSerializer:typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
|
||||||
|
public string ToolModifyMode = "Screwing";
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// If this value is too high it just straight up stops working for some reason
|
||||||
|
/// </remarks>
|
||||||
|
[DataField("throwStrength")]
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public float ThrowStrength = 20.0f;
|
||||||
|
|
||||||
|
[DataField("baseThrowRange")]
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public float BaseThrowRange = 8.0f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How long to stun for if they shoot the pneumatic cannon at high power.
|
||||||
|
/// </summary>
|
||||||
|
[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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public enum PneumaticCannonPower : byte
|
||||||
|
{
|
||||||
|
Low = 0,
|
||||||
|
Medium = 1,
|
||||||
|
High = 2,
|
||||||
|
Len = 3 // used for length calc
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to shoot one random item at a time, or all items at the same time.
|
||||||
|
/// </summary>
|
||||||
|
public enum PneumaticCannonFireMode : byte
|
||||||
|
{
|
||||||
|
Single = 0,
|
||||||
|
All = 1,
|
||||||
|
Len = 2 // used for length calc
|
||||||
|
}
|
||||||
|
}
|
||||||
402
Content.Server/PneumaticCannon/PneumaticCannonSystem.cs
Normal file
@@ -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<PneumaticCannonComponent> _currentlyFiring = new();
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<PneumaticCannonComponent, ComponentInit>(OnComponentInit);
|
||||||
|
SubscribeLocalEvent<PneumaticCannonComponent, InteractUsingEvent>(OnInteractUsing);
|
||||||
|
SubscribeLocalEvent<PneumaticCannonComponent, AfterInteractEvent>(OnAfterInteract);
|
||||||
|
SubscribeLocalEvent<PneumaticCannonComponent, GetAlternativeVerbsEvent>(OnAlternativeVerbs);
|
||||||
|
SubscribeLocalEvent<PneumaticCannonComponent, GetOtherVerbsEvent>(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<ContainerSlot>($"{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<GasTankComponent>()
|
||||||
|
&& 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<ToolComponent>(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<ItemComponent>(out var item)
|
||||||
|
&& component.Owner.TryGetComponent<ServerStorageComponent>(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<SharedCombatModeComponent>(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<ServerStorageComponent>(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<ServerStorageComponent>(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<CameraRecoilComponent>(out var recoil))
|
||||||
|
{
|
||||||
|
recoil.Kick(Vector2.One * data.Strength);
|
||||||
|
}
|
||||||
|
|
||||||
|
ent.TryThrow(data.Direction, data.Strength, data.User, GetPushbackRatioFromPower(comp.Power));
|
||||||
|
|
||||||
|
// lasagna, anybody?
|
||||||
|
ent.EnsureComponent<ForcefeedOnCollideComponent>();
|
||||||
|
|
||||||
|
if(data.User.TryGetComponent<StatusEffectsComponent>(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<GasTankComponent>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether the pneumatic cannon has enough gas to shoot an item.
|
||||||
|
/// </summary>
|
||||||
|
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<GasTankComponent>(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<HandsComponent>(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<ServerStorageComponent>(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<AppearanceComponent>(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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,6 +52,9 @@ namespace Content.Server.Storage.Components
|
|||||||
[DataField("quickInsert")]
|
[DataField("quickInsert")]
|
||||||
private bool _quickInsert = false; // Can insert storables by "attacking" them with the storage entity
|
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")]
|
[DataField("areaInsert")]
|
||||||
private bool _areaInsert = false; // "Attacking" with the storage entity causes it to insert all nearby storables after a delay
|
private bool _areaInsert = false; // "Attacking" with the storage entity causes it to insert all nearby storables after a delay
|
||||||
[DataField("areaInsertRadius")]
|
[DataField("areaInsertRadius")]
|
||||||
@@ -480,6 +483,8 @@ namespace Content.Server.Storage.Components
|
|||||||
/// <returns>true if inserted, false otherwise</returns>
|
/// <returns>true if inserted, false otherwise</returns>
|
||||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
async Task<bool> 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}).");
|
Logger.DebugS(LoggerName, $"Storage (UID {Owner.Uid}) attacked by user (UID {eventArgs.User.Uid}) with entity (UID {eventArgs.Using.Uid}).");
|
||||||
|
|
||||||
if (Owner.HasComponent<PlaceableSurfaceComponent>())
|
if (Owner.HasComponent<PlaceableSurfaceComponent>())
|
||||||
|
|||||||
@@ -179,6 +179,5 @@ namespace Content.Server.Weapon.Ranged.Ammunition.Components
|
|||||||
Dart, // Placeholder
|
Dart, // Placeholder
|
||||||
Grenade,
|
Grenade,
|
||||||
Energy,
|
Energy,
|
||||||
CreamPie, // I can't wait for this enum to be a prototype type...
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
Content.Shared/PneumaticCannon/SharedPneumaticCannon.cs
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Resources/Audio/Effects/thunk.ogg
Normal file
BIN
Resources/Audio/Items/hiss.ogg
Normal file
@@ -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!
|
||||||
|
|
||||||
@@ -104,10 +104,6 @@
|
|||||||
- state: tin
|
- state: tin
|
||||||
- state: plain
|
- state: plain
|
||||||
- type: CreamPie
|
- type: CreamPie
|
||||||
- type: Ammo
|
|
||||||
caliber: CreamPie
|
|
||||||
caseless: true
|
|
||||||
projectile: BulletCreampie
|
|
||||||
# Tastes like pie, cream, banana.
|
# Tastes like pie, cream, banana.
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
- type: Sprite
|
- type: Sprite
|
||||||
sprite: Objects/Misc/handcuffs.rsi
|
sprite: Objects/Misc/handcuffs.rsi
|
||||||
state: handcuff
|
state: handcuff
|
||||||
|
- type: Tag
|
||||||
|
tags:
|
||||||
|
- Handcuffs
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
name: makeshift handcuffs
|
name: makeshift handcuffs
|
||||||
|
|||||||
@@ -82,32 +82,3 @@
|
|||||||
magState: mag
|
magState: mag
|
||||||
steps: 1
|
steps: 1
|
||||||
zeroVisible: true
|
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
|
|
||||||
|
|||||||
@@ -312,30 +312,3 @@
|
|||||||
damage:
|
damage:
|
||||||
types:
|
types:
|
||||||
Piercing: 0
|
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
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -61,6 +61,9 @@
|
|||||||
- type: AtmosUnsafeUnanchor
|
- type: AtmosUnsafeUnanchor
|
||||||
- type: AtmosPipeColor
|
- type: AtmosPipeColor
|
||||||
- type: SubFloorHide
|
- 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)
|
#Note: The PipeDirection of the PipeNode should be the south-facing version, because the entity starts at an angle of 0 (south)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
36
Resources/Prototypes/Recipes/Crafting/improvised.yml
Normal file
@@ -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
|
||||||
@@ -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
|
|
||||||
|
|
||||||
@@ -112,6 +112,9 @@
|
|||||||
- type: Tag
|
- type: Tag
|
||||||
id: GlassBeaker
|
id: GlassBeaker
|
||||||
|
|
||||||
|
- type: Tag
|
||||||
|
id: Handcuffs
|
||||||
|
|
||||||
- type: Tag
|
- type: Tag
|
||||||
id: Hoe
|
id: Hoe
|
||||||
|
|
||||||
@@ -148,6 +151,9 @@
|
|||||||
- type: Tag
|
- type: Tag
|
||||||
id: Pill
|
id: Pill
|
||||||
|
|
||||||
|
- type: Tag
|
||||||
|
id: Pipe
|
||||||
|
|
||||||
- type: Tag
|
- type: Tag
|
||||||
id: Pizza
|
id: Pizza
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 388 B After Width: | Height: | Size: 388 B |
|
Before Width: | Height: | Size: 391 B After Width: | Height: | Size: 391 B |
|
Before Width: | Height: | Size: 670 B After Width: | Height: | Size: 670 B |
|
After Width: | Height: | Size: 388 B |
|
After Width: | Height: | Size: 391 B |
@@ -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"}
|
||||||
|
After Width: | Height: | Size: 799 B |
|
After Width: | Height: | Size: 888 B |