diff --git a/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs index f20e47b6a1..53cc8faa10 100644 --- a/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs +++ b/Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs @@ -70,7 +70,7 @@ public void SetWhitelisted(bool? whitelisted) else { Whitelisted.Text = Loc.GetString("player-panel-whitelisted"); - WhitelistToggle.Text = whitelisted.Value.ToString(); + WhitelistToggle.Text = whitelisted.Value ? Loc.GetString("player-panel-true") : Loc.GetString("player-panel-false"); WhitelistToggle.Visible = true; _isWhitelisted = whitelisted.Value; } diff --git a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs index b54af3a587..bb24da44e1 100644 --- a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs +++ b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs @@ -16,6 +16,8 @@ namespace Content.Client.Atmos.UI [GenerateTypedNameReferences] public sealed partial class GasAnalyzerWindow : DefaultWindow { + private NetEntity _currentEntity = NetEntity.Invalid; + public GasAnalyzerWindow() { RobustXamlLoader.Load(this); @@ -55,6 +57,13 @@ public void Populate(GasAnalyzerUserMessage msg) // Device Tab if (msg.NodeGasMixes.Length > 1) { + if (_currentEntity != msg.DeviceUid) + { + // when we get new device data switch to the device tab + CTabContainer.CurrentTab = 0; + _currentEntity = msg.DeviceUid; + } + CTabContainer.SetTabVisible(0, true); CTabContainer.SetTabTitle(0, Loc.GetString("gas-analyzer-window-tab-title-capitalized", ("title", msg.DeviceName))); // Set up Grid @@ -143,6 +152,7 @@ public void Populate(GasAnalyzerUserMessage msg) CTabContainer.SetTabVisible(0, false); CTabContainer.CurrentTab = 1; minSize = new Vector2(CEnvironmentMix.DesiredSize.X + 40, MinSize.Y); + _currentEntity = NetEntity.Invalid; } MinSize = minSize; diff --git a/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs b/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs index 0f4490cd7e..81f0b96d02 100644 --- a/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs +++ b/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs @@ -1,5 +1,4 @@ using System.Linq; -using Content.Server.Atmos; using Content.Server.Atmos.Components; using Content.Server.NodeContainer; using Content.Server.NodeContainer.Nodes; @@ -10,274 +9,266 @@ using Content.Shared.Interaction.Events; using JetBrains.Annotations; using Robust.Server.GameObjects; -using Robust.Shared.Player; using static Content.Shared.Atmos.Components.GasAnalyzerComponent; -namespace Content.Server.Atmos.EntitySystems +namespace Content.Server.Atmos.EntitySystems; + +[UsedImplicitly] +public sealed class GasAnalyzerSystem : EntitySystem { - [UsedImplicitly] - public sealed class GasAnalyzerSystem : EntitySystem + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly AtmosphereSystem _atmo = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly UserInterfaceSystem _userInterface = default!; + [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; + + /// + /// Minimum moles of a gas to be sent to the client. + /// + private const float UIMinMoles = 0.01f; + + public override void Initialize() { - [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly AtmosphereSystem _atmo = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly UserInterfaceSystem _userInterface = default!; - [Dependency] private readonly TransformSystem _transform = default!; - - /// - /// Minimum moles of a gas to be sent to the client. - /// - private const float UIMinMoles = 0.01f; - - public override void Initialize() - { - base.Initialize(); + base.Initialize(); - SubscribeLocalEvent(OnAfterInteract); - SubscribeLocalEvent(OnDisabledMessage); - SubscribeLocalEvent(OnDropped); - SubscribeLocalEvent(OnUseInHand); - } + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnDisabledMessage); + SubscribeLocalEvent(OnDropped); + SubscribeLocalEvent(OnUseInHand); + } - public override void Update(float frameTime) + public override void Update(float frameTime) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var analyzer)) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var analyzer)) - { - // Don't update every tick - analyzer.AccumulatedFrametime += frameTime; + // Don't update every tick + analyzer.AccumulatedFrametime += frameTime; - if (analyzer.AccumulatedFrametime < analyzer.UpdateInterval) - continue; + if (analyzer.AccumulatedFrametime < analyzer.UpdateInterval) + continue; - analyzer.AccumulatedFrametime -= analyzer.UpdateInterval; + analyzer.AccumulatedFrametime -= analyzer.UpdateInterval; - if (!UpdateAnalyzer(uid)) - RemCompDeferred(uid); - } + if (!UpdateAnalyzer(uid)) + RemCompDeferred(uid); } + } - /// - /// Activates the analyzer when used in the world, scanning either the target entity or the tile clicked - /// - private void OnAfterInteract(EntityUid uid, GasAnalyzerComponent component, AfterInteractEvent args) + /// + /// Activates the analyzer when used in the world, scanning the target entity (if it exists) and the tile the analyzer is in + /// + private void OnAfterInteract(Entity entity, ref AfterInteractEvent args) + { + var target = args.Target; + if (target != null && !_interactionSystem.InRangeUnobstructed((args.User, null), (target.Value, null))) { - if (!args.CanReach) - { - _popup.PopupEntity(Loc.GetString("gas-analyzer-component-player-cannot-reach-message"), args.User, args.User); - return; - } - ActivateAnalyzer(uid, component, args.User, args.Target); - args.Handled = true; + target = null; // if the target is out of reach, invalidate it } + // always run the analyzer, regardless of weather or not there is a target + // since we can always show the local environment. + ActivateAnalyzer(entity, args.User, target); + args.Handled = true; + } - /// - /// Activates the analyzer with no target, so it only scans the tile the user was on when activated - /// - private void OnUseInHand(EntityUid uid, GasAnalyzerComponent component, UseInHandEvent args) + /// + /// Activates the analyzer with no target, so it only scans the tile the user was on when activated + /// + private void OnUseInHand(Entity entity, ref UseInHandEvent args) + { + if (!entity.Comp.Enabled) { - ActivateAnalyzer(uid, component, args.User); - args.Handled = true; + ActivateAnalyzer(entity, args.User); } - - /// - /// Handles analyzer activation logic - /// - private void ActivateAnalyzer(EntityUid uid, GasAnalyzerComponent component, EntityUid user, EntityUid? target = null) + else { - if (!TryOpenUserInterface(uid, user, component)) - return; - - component.Target = target; - component.User = user; - if (target != null) - component.LastPosition = Transform(target.Value).Coordinates; - else - component.LastPosition = null; - component.Enabled = true; - Dirty(uid, component); - UpdateAppearance(uid, component); - EnsureComp(uid); - UpdateAnalyzer(uid, component); + DisableAnalyzer(entity, args.User); } + args.Handled = true; + } - /// - /// Close the UI, turn the analyzer off, and don't update when it's dropped - /// - private void OnDropped(EntityUid uid, GasAnalyzerComponent component, DroppedEvent args) - { - if (args.User is var userId && component.Enabled) - _popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId); - DisableAnalyzer(uid, component, args.User); - } + /// + /// Handles analyzer activation logic + /// + private void ActivateAnalyzer(Entity entity, EntityUid user, EntityUid? target = null) + { + if (!_userInterface.TryOpenUi(entity.Owner, GasAnalyzerUiKey.Key, user)) + return; + + entity.Comp.Target = target; + entity.Comp.User = user; + entity.Comp.Enabled = true; + Dirty(entity); + _appearance.SetData(entity.Owner, GasAnalyzerVisuals.Enabled, entity.Comp.Enabled); + EnsureComp(entity.Owner); + UpdateAnalyzer(entity.Owner, entity.Comp); + } - /// - /// Closes the UI, sets the icon to off, and removes it from the update list - /// - private void DisableAnalyzer(EntityUid uid, GasAnalyzerComponent? component = null, EntityUid? user = null) - { - if (!Resolve(uid, ref component)) - return; + /// + /// Close the UI, turn the analyzer off, and don't update when it's dropped + /// + private void OnDropped(Entity entity, ref DroppedEvent args) + { + if (args.User is var userId && entity.Comp.Enabled) + _popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId); + DisableAnalyzer(entity, args.User); + } + + /// + /// Closes the UI, sets the icon to off, and removes it from the update list + /// + private void DisableAnalyzer(Entity entity, EntityUid? user = null) + { + _userInterface.CloseUi(entity.Owner, GasAnalyzerUiKey.Key, user); - _userInterface.CloseUi(uid, GasAnalyzerUiKey.Key, user); + entity.Comp.Enabled = false; + Dirty(entity); + _appearance.SetData(entity.Owner, GasAnalyzerVisuals.Enabled, entity.Comp.Enabled); + RemCompDeferred(entity.Owner); + } - component.Enabled = false; - Dirty(uid, component); - UpdateAppearance(uid, component); - RemCompDeferred(uid); - } + /// + /// Disables the analyzer when the user closes the UI + /// + private void OnDisabledMessage(Entity entity, ref GasAnalyzerDisableMessage message) + { + DisableAnalyzer(entity); + } - /// - /// Disables the analyzer when the user closes the UI - /// - private void OnDisabledMessage(EntityUid uid, GasAnalyzerComponent component, GasAnalyzerDisableMessage message) - { - DisableAnalyzer(uid, component); - } + /// + /// Fetches fresh data for the analyzer. Should only be called by Update or when the user requests an update via refresh button + /// + private bool UpdateAnalyzer(EntityUid uid, GasAnalyzerComponent? component = null) + { + if (!Resolve(uid, ref component)) + return false; - private bool TryOpenUserInterface(EntityUid uid, EntityUid user, GasAnalyzerComponent? component = null) + // check if the user has walked away from what they scanned + if (component.Target.HasValue) { - if (!Resolve(uid, ref component, false)) - return false; + // Listen! Even if you don't want the Gas Analyzer to work on moving targets, you should use + // this code to determine if the object is still generally in range so that the check is consistent with the code + // in OnAfterInteract() and also consistent with interaction code in general. + if (!_interactionSystem.InRangeUnobstructed((component.User, null), (component.Target.Value, null))) + { + if (component.User is { } userId && component.Enabled) + _popup.PopupEntity(Loc.GetString("gas-analyzer-object-out-of-range"), userId, userId); - return _userInterface.TryOpenUi(uid, GasAnalyzerUiKey.Key, user); + component.Target = null; + } } - /// - /// Fetches fresh data for the analyzer. Should only be called by Update or when the user requests an update via refresh button - /// - private bool UpdateAnalyzer(EntityUid uid, GasAnalyzerComponent? component = null) + var gasMixList = new List(); + + // Fetch the environmental atmosphere around the scanner. This must be the first entry + var tileMixture = _atmo.GetContainingMixture(uid, true); + if (tileMixture != null) { - if (!Resolve(uid, ref component)) - return false; + gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), tileMixture.Volume, tileMixture.Pressure, tileMixture.Temperature, + GenerateGasEntryArray(tileMixture))); + } + else + { + // No gases were found + gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), 0f, 0f, 0f)); + } - if (!TryComp(component.User, out TransformComponent? xform)) + var deviceFlipped = false; + if (component.Target != null) + { + if (Deleted(component.Target)) { - DisableAnalyzer(uid, component); + component.Target = null; + DisableAnalyzer((uid, component), component.User); return false; } - // check if the user has walked away from what they scanned - var userPos = xform.Coordinates; - if (component.LastPosition.HasValue) - { - // Check if position is out of range => don't update and disable - if (!_transform.InRange(component.LastPosition.Value, userPos, SharedInteractionSystem.InteractionRange)) - { - if (component.User is { } userId && component.Enabled) - _popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId); - DisableAnalyzer(uid, component, component.User); - return false; - } - } - - var gasMixList = new List(); + var validTarget = false; - // Fetch the environmental atmosphere around the scanner. This must be the first entry - var tileMixture = _atmo.GetContainingMixture(uid, true); - if (tileMixture != null) - { - gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), tileMixture.Volume, tileMixture.Pressure, tileMixture.Temperature, - GenerateGasEntryArray(tileMixture))); - } - else - { - // No gases were found - gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), 0f, 0f, 0f)); - } + // gas analyzed was used on an entity, try to request gas data via event for override + var ev = new GasAnalyzerScanEvent(); + RaiseLocalEvent(component.Target.Value, ev); - var deviceFlipped = false; - if (component.Target != null) + if (ev.GasMixtures != null) { - if (Deleted(component.Target)) - { - component.Target = null; - DisableAnalyzer(uid, component, component.User); - return false; - } - - // gas analyzed was used on an entity, try to request gas data via event for override - var ev = new GasAnalyzerScanEvent(); - RaiseLocalEvent(component.Target.Value, ev); - - if (ev.GasMixtures != null) + foreach (var mixes in ev.GasMixtures) { - foreach (var mixes in ev.GasMixtures) + if (mixes.Item2 != null) { - if (mixes.Item2 != null) - gasMixList.Add(new GasMixEntry(mixes.Item1, mixes.Item2.Volume, mixes.Item2.Pressure, mixes.Item2.Temperature, GenerateGasEntryArray(mixes.Item2))); + gasMixList.Add(new GasMixEntry(mixes.Item1, mixes.Item2.Volume, mixes.Item2.Pressure, mixes.Item2.Temperature, GenerateGasEntryArray(mixes.Item2))); + validTarget = true; } - - deviceFlipped = ev.DeviceFlipped; } - else + + deviceFlipped = ev.DeviceFlipped; + } + else + { + // No override, fetch manually, to handle flippable devices you must subscribe to GasAnalyzerScanEvent + if (TryComp(component.Target, out NodeContainerComponent? node)) { - // No override, fetch manually, to handle flippable devices you must subscribe to GasAnalyzerScanEvent - if (TryComp(component.Target, out NodeContainerComponent? node)) + foreach (var pair in node.Nodes) { - foreach (var pair in node.Nodes) + if (pair.Value is PipeNode pipeNode) { - if (pair.Value is PipeNode pipeNode) - { - // check if the volume is zero for some reason so we don't divide by zero - if (pipeNode.Air.Volume == 0f) - continue; - // only display the gas in the analyzed pipe element, not the whole system - var pipeAir = pipeNode.Air.Clone(); - pipeAir.Multiply(pipeNode.Volume / pipeNode.Air.Volume); - pipeAir.Volume = pipeNode.Volume; - gasMixList.Add(new GasMixEntry(pair.Key, pipeAir.Volume, pipeAir.Pressure, pipeAir.Temperature, GenerateGasEntryArray(pipeAir))); - } + // check if the volume is zero for some reason so we don't divide by zero + if (pipeNode.Air.Volume == 0f) + continue; + // only display the gas in the analyzed pipe element, not the whole system + var pipeAir = pipeNode.Air.Clone(); + pipeAir.Multiply(pipeNode.Volume / pipeNode.Air.Volume); + pipeAir.Volume = pipeNode.Volume; + gasMixList.Add(new GasMixEntry(pair.Key, pipeAir.Volume, pipeAir.Pressure, pipeAir.Temperature, GenerateGasEntryArray(pipeAir))); + validTarget = true; } } } } - // Don't bother sending a UI message with no content, and stop updating I guess? - if (gasMixList.Count == 0) - return false; - - _userInterface.ServerSendUiMessage(uid, GasAnalyzerUiKey.Key, - new GasAnalyzerUserMessage(gasMixList.ToArray(), - component.Target != null ? Name(component.Target.Value) : string.Empty, - GetNetEntity(component.Target) ?? NetEntity.Invalid, - deviceFlipped)); - return true; + // If the target doesn't actually have any gas mixes to add, + // invalidate it as the target + if (!validTarget) + { + component.Target = null; + } } - /// - /// Sets the appearance based on the analyzers Enabled state - /// - private void UpdateAppearance(EntityUid uid, GasAnalyzerComponent analyzer) - { - _appearance.SetData(uid, GasAnalyzerVisuals.Enabled, analyzer.Enabled); - } + // Don't bother sending a UI message with no content, and stop updating I guess? + if (gasMixList.Count == 0) + return false; - /// - /// Generates a GasEntry array for a given GasMixture - /// - private GasEntry[] GenerateGasEntryArray(GasMixture? mixture) - { - var gases = new List(); + _userInterface.ServerSendUiMessage(uid, GasAnalyzerUiKey.Key, + new GasAnalyzerUserMessage(gasMixList.ToArray(), + component.Target != null ? Name(component.Target.Value) : string.Empty, + GetNetEntity(component.Target) ?? NetEntity.Invalid, + deviceFlipped)); + return true; + } - for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) - { - var gas = _atmo.GetGas(i); + /// + /// Generates a GasEntry array for a given GasMixture + /// + private GasEntry[] GenerateGasEntryArray(GasMixture? mixture) + { + var gases = new List(); + + for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) + { + var gas = _atmo.GetGas(i); - if (mixture?[i] <= UIMinMoles) - continue; + if (mixture?[i] <= UIMinMoles) + continue; - if (mixture != null) - { - var gasName = Loc.GetString(gas.Name); - gases.Add(new GasEntry(gasName, mixture[i], gas.Color)); - } + if (mixture != null) + { + var gasName = Loc.GetString(gas.Name); + gases.Add(new GasEntry(gasName, mixture[i], gas.Color)); } + } - var gasesOrdered = gases.OrderByDescending(gas => gas.Amount); + var gasesOrdered = gases.OrderByDescending(gas => gas.Amount); - return gasesOrdered.ToArray(); - } + return gasesOrdered.ToArray(); } } diff --git a/Content.Server/Explosion/Components/ShockOnTriggerComponent.cs b/Content.Server/Explosion/Components/ShockOnTriggerComponent.cs new file mode 100644 index 0000000000..a553cc047a --- /dev/null +++ b/Content.Server/Explosion/Components/ShockOnTriggerComponent.cs @@ -0,0 +1,37 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Content.Server.Explosion.EntitySystems; + +namespace Content.Server.Explosion.Components; + +/// +/// A component that electrocutes an entity having this component when a trigger is triggered. +/// +[RegisterComponent, AutoGenerateComponentPause] +[Access(typeof(TriggerSystem))] +public sealed partial class ShockOnTriggerComponent : Component +{ + /// + /// The force of an electric shock when the trigger is triggered. + /// + [DataField] + public int Damage = 5; + + /// + /// Duration of electric shock when the trigger is triggered. + /// + [DataField] + public TimeSpan Duration = TimeSpan.FromSeconds(2); + + /// + /// The minimum delay between repeating triggers. + /// + [DataField] + public TimeSpan Cooldown = TimeSpan.FromSeconds(4); + + /// + /// When can the trigger run again? + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField] + public TimeSpan NextTrigger = TimeSpan.Zero; +} diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs index 92e065bf4c..1208cd1771 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Chemistry.Containers.EntitySystems; using Content.Server.Explosion.Components; using Content.Server.Flash; +using Content.Server.Electrocution; using Content.Server.Pinpointer; using Content.Shared.Flash.Components; using Content.Server.Radio.EntitySystems; @@ -33,6 +34,7 @@ using Robust.Shared.Player; using Content.Shared.Coordinates; using Robust.Shared.Utility; +using Robust.Shared.Timing; namespace Content.Server.Explosion.EntitySystems { @@ -75,6 +77,7 @@ public sealed partial class TriggerSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly ElectrocutionSystem _electrocution = default!; public override void Initialize() { @@ -104,6 +107,7 @@ public override void Initialize() SubscribeLocalEvent(OnAnchorTrigger); SubscribeLocalEvent(OnSoundTrigger); + SubscribeLocalEvent(HandleShockTrigger); SubscribeLocalEvent(HandleRattleTrigger); } @@ -120,6 +124,24 @@ private void OnSoundTrigger(EntityUid uid, SoundOnTriggerComponent component, Tr } } + private void HandleShockTrigger(Entity shockOnTrigger, ref TriggerEvent args) + { + if (!_container.TryGetContainingContainer(shockOnTrigger, out var container)) + return; + + var containerEnt = container.Owner; + var curTime = _timing.CurTime; + + if (curTime < shockOnTrigger.Comp.NextTrigger) + { + // The trigger's on cooldown. + return; + } + + _electrocution.TryDoElectrocution(containerEnt, null, shockOnTrigger.Comp.Damage, shockOnTrigger.Comp.Duration, true); + shockOnTrigger.Comp.NextTrigger = curTime + shockOnTrigger.Comp.Cooldown; + } + private void OnAnchorTrigger(EntityUid uid, AnchorOnTriggerComponent component, TriggerEvent args) { var xform = Transform(uid); diff --git a/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs b/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs index c262988c86..3fab18b1b7 100644 --- a/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs +++ b/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; -using Content.Server.Atmos; +using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.Components; using Content.Server.Popups; using Content.Server.Power.Components; diff --git a/Content.Server/Strip/StrippableSystem.cs b/Content.Server/Strip/StrippableSystem.cs index 194df7b3d0..6d728df9d6 100644 --- a/Content.Server/Strip/StrippableSystem.cs +++ b/Content.Server/Strip/StrippableSystem.cs @@ -218,7 +218,7 @@ private void StartStripInsertInventory( return; } - var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime); + var (time, stealth) = GetStripTimeModifiers(user, target, held, slotDef.StripTime); if (!stealth) _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert", ("user", Identity.Entity(user, EntityManager)), ("item", user.Comp.ActiveHandEntity!.Value)), target, target, PopupType.Large); @@ -306,7 +306,7 @@ private void StartStripRemoveInventory( return; } - var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime); + var (time, stealth) = GetStripTimeModifiers(user, target, item, slotDef.StripTime); if (!stealth) { @@ -411,7 +411,7 @@ private void StartStripInsertHand( if (!CanStripInsertHand(user, target, held, handName)) return; - var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay); + var (time, stealth) = GetStripTimeModifiers(user, target, null, targetStrippable.HandStripDelay); if (!stealth) _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert-hand", ("user", Identity.Entity(user, EntityManager)), ("item", user.Comp.ActiveHandEntity!.Value)), target, target, PopupType.Large); @@ -510,7 +510,7 @@ private void StartStripRemoveHand( if (!CanStripRemoveHand(user, target, item, handName)) return; - var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay); + var (time, stealth) = GetStripTimeModifiers(user, target, null, targetStrippable.HandStripDelay); if (!stealth) _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target); diff --git a/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs b/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs index dec9516c01..c143e8cf85 100644 --- a/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs +++ b/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs @@ -13,9 +13,6 @@ public sealed partial class GasAnalyzerComponent : Component [ViewVariables] public EntityUid User; - [ViewVariables(VVAccess.ReadWrite)] - public EntityCoordinates? LastPosition; - [DataField("enabled"), ViewVariables(VVAccess.ReadWrite)] public bool Enabled; diff --git a/Content.Shared/Clothing/Components/ClothingComponent.cs b/Content.Shared/Clothing/Components/ClothingComponent.cs index 581125d4fe..4f8058dbf5 100644 --- a/Content.Shared/Clothing/Components/ClothingComponent.cs +++ b/Content.Shared/Clothing/Components/ClothingComponent.cs @@ -69,6 +69,13 @@ public sealed partial class ClothingComponent : Component [DataField, ViewVariables(VVAccess.ReadWrite)] public TimeSpan UnequipDelay = TimeSpan.Zero; + + /// + /// Offset for the strip time for an entity with this component. + /// Only applied when it is being equipped or removed by another player. + /// + [DataField] + public TimeSpan StripDelay = TimeSpan.Zero; } [Serializable, NetSerializable] diff --git a/Content.Shared/Clothing/Components/SelfUnremovableClothingComponent.cs b/Content.Shared/Clothing/Components/SelfUnremovableClothingComponent.cs new file mode 100644 index 0000000000..1d624516ec --- /dev/null +++ b/Content.Shared/Clothing/Components/SelfUnremovableClothingComponent.cs @@ -0,0 +1,18 @@ +using Content.Shared.Clothing.EntitySystems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Clothing.Components; + +/// +/// The component prohibits the player from taking off clothes on them that have this component. +/// +/// +/// See also ClothingComponent.EquipDelay if you want the clothes that the player cannot take off by himself to be put on by the player with a delay. +/// +[NetworkedComponent] +[RegisterComponent] +[Access(typeof(SelfUnremovableClothingSystem))] +public sealed partial class SelfUnremovableClothingComponent : Component +{ + +} diff --git a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs index 082b040a32..3b26360f10 100644 --- a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs @@ -6,6 +6,7 @@ using Content.Shared.Inventory; using Content.Shared.Inventory.Events; using Content.Shared.Item; +using Content.Shared.Strip.Components; using Robust.Shared.Containers; using Robust.Shared.GameStates; @@ -32,6 +33,8 @@ public override void Initialize() SubscribeLocalEvent(OnEquipDoAfter); SubscribeLocalEvent(OnUnequipDoAfter); + + SubscribeLocalEvent(OnItemStripped); } private void OnUseInHand(Entity ent, ref UseInHandEvent args) @@ -192,6 +195,11 @@ private void OnUnequipDoAfter(Entity ent, ref ClothingUnequip _handsSystem.TryPickup(args.User, ent); } + private void OnItemStripped(Entity ent, ref BeforeItemStrippedEvent args) + { + args.Additive += ent.Comp.StripDelay; + } + private void CheckEquipmentForLayerHide(EntityUid equipment, EntityUid equipee) { if (TryComp(equipment, out HideLayerClothingComponent? clothesComp) && TryComp(equipee, out HumanoidAppearanceComponent? appearanceComp)) diff --git a/Content.Shared/Clothing/EntitySystems/SelfUnremovableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/SelfUnremovableClothingSystem.cs new file mode 100644 index 0000000000..ab0c41c5c7 --- /dev/null +++ b/Content.Shared/Clothing/EntitySystems/SelfUnremovableClothingSystem.cs @@ -0,0 +1,36 @@ +using Content.Shared.Clothing.Components; +using Content.Shared.Examine; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; + +namespace Content.Shared.Clothing.EntitySystems; + +/// +/// A system for the operation of a component that prohibits the player from taking off his own clothes that have this component. +/// +public sealed class SelfUnremovableClothingSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnUnequip); + SubscribeLocalEvent(OnUnequipMarkup); + } + + private void OnUnequip(Entity selfUnremovableClothing, ref BeingUnequippedAttemptEvent args) + { + if (TryComp(selfUnremovableClothing, out var clothing) && (clothing.Slots & args.SlotFlags) == SlotFlags.NONE) + return; + + if (args.UnEquipTarget == args.Unequipee) + { + args.Cancel(); + } + } + + private void OnUnequipMarkup(Entity selfUnremovableClothing, ref ExaminedEvent args) + { + args.PushMarkup(Loc.GetString("comp-self-unremovable-clothing")); + } +} diff --git a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs index c828b22481..aa3381c6bf 100644 --- a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs @@ -95,7 +95,7 @@ private void StartDoAfter(EntityUid user, EntityUid item, EntityUid wearer, Togg if (component.StripDelay == null) return; - var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, component.StripDelay.Value); + var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, item, component.StripDelay.Value); var args = new DoAfterArgs(EntityManager, user, time, new ToggleClothingDoAfterEvent(), item, wearer, item) { diff --git a/Content.Shared/Strip/Components/StrippableComponent.cs b/Content.Shared/Strip/Components/StrippableComponent.cs index 4faca4d8f2..5191b3f3f9 100644 --- a/Content.Shared/Strip/Components/StrippableComponent.cs +++ b/Content.Shared/Strip/Components/StrippableComponent.cs @@ -44,6 +44,15 @@ public abstract class BaseBeforeStripEvent(TimeSpan initialTime, bool stealth = public SlotFlags TargetSlots { get; } = SlotFlags.GLOVES; } + /// + /// Used to modify strip times. Raised directed at the item being stripped. + /// + /// + /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player. + /// + [ByRefEvent] + public sealed class BeforeItemStrippedEvent(TimeSpan initialTime, bool stealth = false) : BaseBeforeStripEvent(initialTime, stealth); + /// /// Used to modify strip times. Raised directed at the user. /// diff --git a/Content.Shared/Strip/SharedStrippableSystem.cs b/Content.Shared/Strip/SharedStrippableSystem.cs index e42f6e3aa7..935dc33540 100644 --- a/Content.Shared/Strip/SharedStrippableSystem.cs +++ b/Content.Shared/Strip/SharedStrippableSystem.cs @@ -28,13 +28,19 @@ private void OnActivateInWorld(EntityUid uid, StrippableComponent component, Act args.Handled = true; } - public (TimeSpan Time, bool Stealth) GetStripTimeModifiers(EntityUid user, EntityUid target, TimeSpan initialTime) + /// + /// Modify the strip time via events. Raised directed at the item being stripped, the player stripping someone and the player being stripped. + /// + public (TimeSpan Time, bool Stealth) GetStripTimeModifiers(EntityUid user, EntityUid targetPlayer, EntityUid? targetItem, TimeSpan initialTime) { - var userEv = new BeforeStripEvent(initialTime); + var itemEv = new BeforeItemStrippedEvent(initialTime, false); + if (targetItem != null) + RaiseLocalEvent(targetItem.Value, ref itemEv); + var userEv = new BeforeStripEvent(itemEv.Time, itemEv.Stealth); RaiseLocalEvent(user, ref userEv); - var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth); - RaiseLocalEvent(target, ref ev); - return (ev.Time, ev.Stealth); + var targetEv = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth); + RaiseLocalEvent(targetPlayer, ref targetEv); + return (targetEv.Time, targetEv.Stealth); } private void OnDragDrop(EntityUid uid, StrippableComponent component, ref DragDropDraggedEvent args) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 006ed4f5d0..ee4f9cd5a6 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,18 +1,4 @@ Entries: -- author: ElectroJr - changes: - - message: Ghosts can once again open paper & other UIs - type: Fix - id: 6614 - time: '2024-05-24T05:03:03.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/27999 -- author: nikthechampiongr - changes: - - message: Firelocks will no longer randomly pulse closing lights. - type: Fix - id: 6615 - time: '2024-05-24T14:44:42.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/28227 - author: ElectroJr changes: - message: Fixed modular grenade visuals getting stuck in an incorrect state. @@ -3811,3 +3797,36 @@ id: 7113 time: '2024-08-15T12:34:41.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/30865 +- author: to4no_fix + changes: + - message: Added a new electropack that shocks when a trigger is triggered + type: Add + - message: Added a new shock collar that shocks when a trigger is triggered + type: Add + - message: Two shock collars and two remote signallers added to the warden's locker + type: Add + - message: Shock collar added as a new target for the thief + type: Add + - message: A new Special Means technology has been added to the Arsenal research + branch at the 1st research level. Its research opens up the possibility of producing + electropacks at security techfab. The cost of technology research is 5000 + type: Add + id: 7114 + time: '2024-08-15T14:30:39.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/30529 +- author: Mervill + changes: + - message: The Gas Analyzer won't spuriously shut down for seemly no reason. + type: Tweak + - message: The Gas Analyzer will always switch to the device tab when a new object + is scanned. + type: Tweak + - message: The Gas Analyzer's interaction range is now equal to the standard interaction + range + type: Fix + - message: Clicking the Gas Analyzer when it's in your hand has proper enable/disable + behavior. + type: Fix + id: 7115 + time: '2024-08-15T14:45:13.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/30763 diff --git a/Resources/Locale/en-US/administration/ui/player-panel.ftl b/Resources/Locale/en-US/administration/ui/player-panel.ftl index ed63dd6d10..cfb014948d 100644 --- a/Resources/Locale/en-US/administration/ui/player-panel.ftl +++ b/Resources/Locale/en-US/administration/ui/player-panel.ftl @@ -20,3 +20,4 @@ player-panel-logs = Logs player-panel-delete = Delete player-panel-rejuvenate = Rejuvenate player-panel-false = False +player-panel-true = True diff --git a/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl b/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl index 652bb19cb5..a2cb5301b2 100644 --- a/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl +++ b/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl @@ -1,6 +1,6 @@ ## Entity -gas-analyzer-component-player-cannot-reach-message = You can't reach there. +gas-analyzer-object-out-of-range = The object went out of range. gas-analyzer-shutoff = The gas analyzer shuts off. ## UI diff --git a/Resources/Locale/en-US/clothing/components/self-unremovable-clothing-component.ftl b/Resources/Locale/en-US/clothing/components/self-unremovable-clothing-component.ftl new file mode 100644 index 0000000000..bb7ff0206f --- /dev/null +++ b/Resources/Locale/en-US/clothing/components/self-unremovable-clothing-component.ftl @@ -0,0 +1 @@ +comp-self-unremovable-clothing = This cannot be removed without outside help. diff --git a/Resources/Locale/en-US/research/technologies.ftl b/Resources/Locale/en-US/research/technologies.ftl index 4fbb0e1bd3..0b0970ec08 100644 --- a/Resources/Locale/en-US/research/technologies.ftl +++ b/Resources/Locale/en-US/research/technologies.ftl @@ -26,6 +26,7 @@ research-technology-salvage-weapons = Salvage Weapons research-technology-draconic-munitions = Draconic Munitions research-technology-uranium-munitions = Uranium Munitions research-technology-explosive-technology = Explosive Technology +research-technology-special-means = Special Means research-technology-weaponized-laser-manipulation = Weaponized Laser Manipulation research-technology-nonlethal-ammunition = Nonlethal Ammunition research-technology-practice-ammunition = Practice Ammunition diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/security.yml b/Resources/Prototypes/Catalog/Fills/Lockers/security.yml index 2d012128e6..b72fce1107 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/security.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/security.yml @@ -20,6 +20,10 @@ - id: ClothingOuterHardsuitWarden - id: HoloprojectorSecurity - id: BookSpaceLaw + - id: ClothingNeckShockCollar + amount: 2 + - id: RemoteSignaller + amount: 2 - type: entity id: LockerWardenFilled @@ -42,6 +46,10 @@ - id: DoorRemoteArmory - id: HoloprojectorSecurity - id: BookSpaceLaw + - id: ClothingNeckShockCollar + amount: 2 + - id: RemoteSignaller + amount: 2 - type: entity id: LockerSecurityFilled diff --git a/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml b/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml index 2d5bf42466..f94b773886 100644 --- a/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml +++ b/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml @@ -314,6 +314,29 @@ - type: Unremoveable deleteOnDrop: false +- type: entity + parent: ClothingBackpack + id: ClothingBackpackElectropack + name: electropack + suffix: SelfUnremovable + description: Shocks on the signal. It is used to keep a particularly dangerous criminal under control. + components: + - type: Sprite + sprite: Clothing/Back/Backpacks/electropack.rsi + state: icon + - type: Clothing + stripDelay: 10 + equipDelay: 5 # to avoid accidentally falling into the trap associated with SelfUnremovableClothing + - type: SelfUnremovableClothing + - type: ShockOnTrigger + damage: 5 + duration: 3 + cooldown: 4 + - type: TriggerOnSignal + - type: DeviceLinkSink + ports: + - Trigger + # Debug - type: entity parent: ClothingBackpack diff --git a/Resources/Prototypes/Entities/Objects/Devices/shock_collar.yml b/Resources/Prototypes/Entities/Objects/Devices/shock_collar.yml new file mode 100644 index 0000000000..22f2d097fc --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Devices/shock_collar.yml @@ -0,0 +1,36 @@ +- type: entity + parent: Clothing + id: ClothingNeckShockCollar + name: shock collar + suffix: SelfUnremovable + description: An electric collar that shocks on the signal. + components: + - type: Item + size: Small + - type: Sprite + sprite: Clothing/Neck/Misc/shock_collar.rsi + state: icon + - type: Clothing + sprite: Clothing/Neck/Misc/shock_collar.rsi + stripDelay: 10 + equipDelay: 5 # to avoid accidentally falling into the trap associated with SelfUnremovableClothing + quickEquip: true + slots: + - neck + - type: SelfUnremovableClothing + - type: ShockOnTrigger + damage: 5 + duration: 3 + cooldown: 4 + - type: TriggerOnSignal + - type: DeviceLinkSink + ports: + - Trigger + - type: GuideHelp + guides: + - Security + - type: StealTarget + stealGroup: ClothingNeckShockCollar + - type: Tag + tags: + - WhitelistChameleon diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 98d5440e3e..e795a5836e 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -16,7 +16,7 @@ mask: - MachineMask layer: - - MachineLayer + - MachineLayer - type: Lathe - type: MaterialStorage - type: Destructible @@ -808,6 +808,7 @@ - WeaponLaserCannon - WeaponLaserCarbine - WeaponXrayCannon + - ClothingBackpackElectropack - type: MaterialStorage whitelist: tags: diff --git a/Resources/Prototypes/Objectives/objectiveGroups.yml b/Resources/Prototypes/Objectives/objectiveGroups.yml index e62aa9fdf6..c692c85dff 100644 --- a/Resources/Prototypes/Objectives/objectiveGroups.yml +++ b/Resources/Prototypes/Objectives/objectiveGroups.yml @@ -73,6 +73,7 @@ ForensicScannerStealObjective: 1 #sec FlippoEngravedLighterStealObjective: 0.5 ClothingHeadHatWardenStealObjective: 1 + ClothingNeckShockCollarStealObjective: 1 ClothingOuterHardsuitVoidParamedStealObjective: 1 #med MedicalTechFabCircuitboardStealObjective: 1 ClothingHeadsetAltMedicalStealObjective: 1 diff --git a/Resources/Prototypes/Objectives/stealTargetGroups.yml b/Resources/Prototypes/Objectives/stealTargetGroups.yml index 1a9b4223cb..e818442b4c 100644 --- a/Resources/Prototypes/Objectives/stealTargetGroups.yml +++ b/Resources/Prototypes/Objectives/stealTargetGroups.yml @@ -272,6 +272,13 @@ sprite: Clothing/Neck/Medals/clownmedal.rsi state: icon +- type: stealTargetGroup + id: ClothingNeckShockCollar + name: shock collar + sprite: + sprite: Clothing/Neck/Misc/shock_collar.rsi + state: icon + #Thief structures - type: stealTargetGroup diff --git a/Resources/Prototypes/Objectives/thief.yml b/Resources/Prototypes/Objectives/thief.yml index 672f9b2ba7..092a724da2 100644 --- a/Resources/Prototypes/Objectives/thief.yml +++ b/Resources/Prototypes/Objectives/thief.yml @@ -316,6 +316,17 @@ - type: Objective difficulty: 1 +- type: entity + parent: BaseThiefStealObjective + id: ClothingNeckShockCollarStealObjective + components: + - type: NotJobRequirement + job: Warden + - type: StealCondition + stealGroup: ClothingNeckShockCollar + - type: Objective + difficulty: 1 + # Structures - type: entity diff --git a/Resources/Prototypes/Recipes/Lathes/security.yml b/Resources/Prototypes/Recipes/Lathes/security.yml index 1e6b70f943..a54d5b6235 100644 --- a/Resources/Prototypes/Recipes/Lathes/security.yml +++ b/Resources/Prototypes/Recipes/Lathes/security.yml @@ -38,7 +38,7 @@ materials: Steel: 250 Plastic: 100 - + - type: latheRecipe id: WeaponLaserCarbine result: WeaponLaserCarbine @@ -89,6 +89,15 @@ Plastic: 250 Gold: 100 +- type: latheRecipe + id: ClothingBackpackElectropack + result: ClothingBackpackElectropack + completetime: 4 + materials: + Steel: 500 + Plastic: 250 + Cloth: 500 + - type: latheRecipe id: ForensicPad result: ForensicPad @@ -655,7 +664,7 @@ Steel: 1000 Glass: 500 Plastic: 500 - + - type: latheRecipe id: MagazineGrenadeEmpty result: MagazineGrenadeEmpty @@ -663,7 +672,7 @@ materials: Steel: 150 Plastic: 50 - + - type: latheRecipe id: GrenadeEMP result: GrenadeEMP @@ -672,7 +681,7 @@ Steel: 150 Plastic: 100 Glass: 20 - + - type: latheRecipe id: GrenadeBlast result: GrenadeBlast @@ -681,7 +690,7 @@ Steel: 450 Plastic: 300 Gold: 150 - + - type: latheRecipe id: GrenadeFlash result: GrenadeFlash diff --git a/Resources/Prototypes/Research/arsenal.yml b/Resources/Prototypes/Research/arsenal.yml index 1cfa1fec80..553258fdb3 100644 --- a/Resources/Prototypes/Research/arsenal.yml +++ b/Resources/Prototypes/Research/arsenal.yml @@ -58,8 +58,8 @@ cost: 5000 recipeUnlocks: - MagazineShotgunBeanbag - - BoxShellTranquilizer - - BoxBeanbag + - BoxShellTranquilizer + - BoxBeanbag - WeaponDisabler - type: technology @@ -115,6 +115,18 @@ - ExplosivePayload - ChemicalPayload +- type: technology + id: SpecialMeans + name: research-technology-special-means + icon: + sprite: Clothing/Back/Backpacks/electropack.rsi + state: icon + discipline: Arsenal + tier: 1 + cost: 5000 + recipeUnlocks: + - ClothingBackpackElectropack + # Tier 2 - type: technology @@ -144,7 +156,7 @@ - type: technology id: BasicShuttleArmament name: research-technology-basic-shuttle-armament - icon: + icon: sprite: Structures/Power/cage_recharger.rsi state: full discipline: Arsenal @@ -189,11 +201,11 @@ cost: 15000 recipeUnlocks: - WeaponLaserSvalinn - + - type: technology id: AdvancedShuttleWeapon name: research-technology-advanced-shuttle-weapon - icon: + icon: sprite: Objects/Weapons/Guns/Ammunition/Magazine/Grenade/grenade_cartridge.rsi state: icon discipline: Arsenal diff --git a/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/equipped-BACKPACK.png b/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/equipped-BACKPACK.png new file mode 100644 index 0000000000..3ea6cdc4b0 Binary files /dev/null and b/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/equipped-BACKPACK.png differ diff --git a/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/icon.png b/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/icon.png new file mode 100644 index 0000000000..b4b755e008 Binary files /dev/null and b/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/icon.png differ diff --git a/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/inhand-left.png b/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/inhand-left.png new file mode 100644 index 0000000000..49f6243a2c Binary files /dev/null and b/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/inhand-left.png differ diff --git a/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/inhand-right.png b/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/inhand-right.png new file mode 100644 index 0000000000..26901ce4c9 Binary files /dev/null and b/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/inhand-right.png differ diff --git a/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/meta.json b/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/meta.json new file mode 100644 index 0000000000..4e7738117e --- /dev/null +++ b/Resources/Textures/Clothing/Back/Backpacks/electropack.rsi/meta.json @@ -0,0 +1,33 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/2d26ce62c273d025bed77a0e6c4bdc770b789bb0", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon", + "delays": [ + [ + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "equipped-BACKPACK", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/equipped-NECK.png b/Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/equipped-NECK.png new file mode 100644 index 0000000000..ffca3249f1 Binary files /dev/null and b/Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/equipped-NECK.png differ diff --git a/Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/icon.png b/Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/icon.png new file mode 100644 index 0000000000..f8e0a9cb8e Binary files /dev/null and b/Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/icon.png differ diff --git a/Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/meta.json b/Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/meta.json new file mode 100644 index 0000000000..3119a51a15 --- /dev/null +++ b/Resources/Textures/Clothing/Neck/Misc/shock_collar.rsi/meta.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Drawn by EmoGarbage404 (github) for Space Station 14", + "size": { + "x": 32, + "y": 32 + }, + + "states": [ + { + "name": "equipped-NECK", + "directions": 4 + }, + { + "name": "icon" + } + ] + }