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