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
This commit is contained in:
mirrorcult
2021-11-03 14:33:36 -07:00
committed by GitHub
parent 6bb1e58e77
commit 3f51ffbd3c
33 changed files with 791 additions and 89 deletions

View File

@@ -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"
}; };
} }
} }

View 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);
}
}
}
}

View File

@@ -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.

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View 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
}
}

View 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
};
}
}
}

View File

@@ -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>())

View File

@@ -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...
} }
} }

View 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
}
}

Binary file not shown.

Binary file not shown.

View 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!

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

View File

@@ -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"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B