From 24b7b6a64e195c19beba6357cb874efd9844003a Mon Sep 17 00:00:00 2001 From: Cat_L Date: Mon, 27 Jul 2026 04:41:50 +0300 Subject: [PATCH 1/4] Add Stabikor --- .../Systems/CuttableItemSystem.cs | 33 ++ .../Stabikor/Systems/StabikorSystem.cs | 336 ++++++++++++++++++ .../Components/CuttableItemComponent.cs | 47 +++ .../CuttableItem/CuttableDoAfterEvent.cs | 7 + .../Systems/SharedCuttableItemSystem.cs | 124 +++++++ .../Stabikor/Components/StabikorComponent.cs | 111 ++++++ .../Stabikor/Systems/SharedStabikorSystem.cs | 141 ++++++++ .../entities/clothing/other/stabikor.ftl | 2 + .../_Arcane/stabikor/stabikor-component.ftl | 53 +++ .../cuttable-Item-component.ftl" | 5 + .../Catalog/Fills/Lockers/security.yml | 4 + .../Recipes/Lathes/Packs/security.yml | 1 + .../Prototypes/Recipes/Lathes/security.yml | 13 + .../Entities/Clothing/Other/stabikor.yml | 28 ++ .../_Orion/Research/Nodes/security.yml | 1 + .../Neck/Misc/petcollar.rsi/equipped-HAND.png | Bin 0 -> 399 bytes .../Neck/Misc/petcollar.rsi/equipped-NECK.png | Bin 0 -> 537 bytes .../Clothing/Neck/Misc/petcollar.rsi/icon.png | Bin 0 -> 645 bytes .../Neck/Misc/petcollar.rsi/meta.json | 23 ++ 19 files changed, 929 insertions(+) create mode 100644 Content.Server/_Arcane/CuttableItem/Systems/CuttableItemSystem.cs create mode 100644 Content.Server/_Arcane/Stabikor/Systems/StabikorSystem.cs create mode 100644 Content.Shared/_Arcane/CuttableItem/Components/CuttableItemComponent.cs create mode 100644 Content.Shared/_Arcane/CuttableItem/CuttableDoAfterEvent.cs create mode 100644 Content.Shared/_Arcane/CuttableItem/Systems/SharedCuttableItemSystem.cs create mode 100644 Content.Shared/_Arcane/Stabikor/Components/StabikorComponent.cs create mode 100644 Content.Shared/_Arcane/Stabikor/Systems/SharedStabikorSystem.cs create mode 100644 Resources/Locale/ru-RU/_Arcane/entities/clothing/other/stabikor.ftl create mode 100644 Resources/Locale/ru-RU/_Arcane/stabikor/stabikor-component.ftl create mode 100644 "Resources/Locale/ru-RU/_Arcane/\321\201uttableItem/cuttable-Item-component.ftl" create mode 100644 Resources/Prototypes/_Arcane/Entities/Clothing/Other/stabikor.yml create mode 100644 Resources/Textures/_Arcane/Clothing/Neck/Misc/petcollar.rsi/equipped-HAND.png create mode 100644 Resources/Textures/_Arcane/Clothing/Neck/Misc/petcollar.rsi/equipped-NECK.png create mode 100644 Resources/Textures/_Arcane/Clothing/Neck/Misc/petcollar.rsi/icon.png create mode 100644 Resources/Textures/_Arcane/Clothing/Neck/Misc/petcollar.rsi/meta.json diff --git a/Content.Server/_Arcane/CuttableItem/Systems/CuttableItemSystem.cs b/Content.Server/_Arcane/CuttableItem/Systems/CuttableItemSystem.cs new file mode 100644 index 00000000000..1122f7e5475 --- /dev/null +++ b/Content.Server/_Arcane/CuttableItem/Systems/CuttableItemSystem.cs @@ -0,0 +1,33 @@ +using Content.Server.Radio.EntitySystems; +using Content.Shared.Radio; +using Content.Shared._Arcane.CuttableItem.Components; +using Robust.Shared.Prototypes; + + +namespace Content.Server._Arcane.CuttableItem.Systems; + +public sealed class CuttableItemSystem : EntitySystem +{ + [Dependency] private readonly RadioSystem _radio = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnItemCut); + } + + private void OnItemCut(EntityUid uid, CuttableItemComponent component, CuttableCutEvent args) + { + if (!_prototypeManager.TryIndex(component.RadioChannel, out var channel)) + return; + + var userName = Name(args.User); + var userItem = Name(args.Item); + + var message = Loc.GetString(component.AlertMessage, ("user", userName), ("item", userItem)); + + _radio.SendRadioMessage(uid, message, channel, uid); + } +} diff --git a/Content.Server/_Arcane/Stabikor/Systems/StabikorSystem.cs b/Content.Server/_Arcane/Stabikor/Systems/StabikorSystem.cs new file mode 100644 index 00000000000..ced1db7e5a5 --- /dev/null +++ b/Content.Server/_Arcane/Stabikor/Systems/StabikorSystem.cs @@ -0,0 +1,336 @@ +using Content.Shared._Arcane.Stabikor.Components; +using Robust.Shared.Containers; +using Content.Shared.CombatMode; +using Content.Server.Electrocution; +using Content.Shared.Popups; +using Content.Shared.Interaction; +using Content.Shared.Access.Components; +using Content.Server.Access.Systems; +using Robust.Shared.Audio.Systems; +using Content.Server.Administration; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Inventory; +using Robust.Server.Player; + +namespace Content.Server._Arcane.Stabikor.Systems; + +public sealed partial class StabikorSystem : EntitySystem +{ + [Dependency] private readonly InventorySystem _inventorySystem = default!; + [Dependency] private readonly SharedTransformSystem _transformSystem = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly ElectrocutionSystem _electrocution = default!; + [Dependency] private readonly IdCardSystem _idCard = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedHandsSystem _handsSystem = default!; + [Dependency] private readonly QuickDialogSystem _quickDialog = default!; + [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; + [Dependency] private SharedContainerSystem _containerSystem = default!; + [Dependency] private IPlayerManager _playerManager = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnToggleCombatAction); + SubscribeLocalEvent(OnInteractUsing); + SubscribeNetworkEvent(OnOpenDialogNetworkReceived); + SubscribeNetworkEvent(OnIsLockedNetworkReceived); + } + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (!comp.IsActive || comp.WearingEntity == null) + continue; + + comp.Timer -= frameTime; + + if (comp.Timer <= 0) + { + if (!RemoveStabikor(uid, comp)) + return; + } + } + } + + private void OnToggleCombatAction(ToggleCombatActionEvent args) + { + var user = args.Performer; + + if (TryComp(user, out var mobState)) + { + if (mobState.CurrentState == Shared.Mobs.MobState.Critical || + mobState.CurrentState == Shared.Mobs.MobState.Dead) + { + return; + } + } + + var targetSlots = new[] { "neck", "gloves" }; + EntityUid stabikorItem = default; + StabikorComponent? stabikorComp = null; + + foreach (var slot in targetSlots) + { + if (_inventorySystem.TryGetSlotEntity(user, slot, out var item) && + TryComp(item, out var comp)) + { + stabikorItem = item!.Value; + stabikorComp = comp; + break; + } + } + + if (stabikorComp is not { IsActive: true }) + return; + + if (args.Action.Comp.Enabled) + { + _electrocution.TryDoElectrocution(user, stabikorItem, stabikorComp.Damage, TimeSpan.FromSeconds(stabikorComp.TimeStan), refresh: false, ignoreInsulation: true); + + _popup.PopupEntity(Loc.GetString("stabikor-disarm-shock-popup"), user, user, PopupType.LargeCaution); + + _combatMode.SetInCombatMode(user, false); + + args.Handled = true; + } + } + + private void OnInteractUsing(EntityUid uid, StabikorComponent comp, InteractUsingEvent args) + { + var user = args.User; + + var parent = Transform(uid).ParentUid; + var isEquipped = parent.IsValid() && HasComp(parent); + + if (!isEquipped) + { + _popup.PopupEntity(Loc.GetString("stabikor-not-equipped"), uid, user); + _audio.PlayPvs(comp.DenySound, uid); + + return; + } + + if (!_idCard.TryFindIdCard(args.Used, out var idCard) || + !TryComp(idCard.Owner, out var accessComp)) + { + return; + } + + var cardAccess = accessComp.Tags; + + if (comp.IsLocked) + { + var hasUnlockAccess = comp.UnlockAccess.Exists(proto => cardAccess.Contains(proto.Id)); + + if (hasUnlockAccess) + { + if (RemoveStabikor(uid, comp)) + _audio.PlayPvs(comp.UnlockSound, uid); + + args.Handled = true; + } + else + { + _popup.PopupEntity(Loc.GetString("stabikor-unlocked-deny"), uid, user, PopupType.SmallCaution); + + _audio.PlayPvs(comp.DenySound, uid); + } + } + else + { + var hasLockAccess = comp.LockAccess.Exists(proto => cardAccess.Contains(proto.Id)); + + if (hasLockAccess) + { + if (ActivateStabikor(uid, parent, comp, user)) + _audio.PlayPvs(comp.LockSound, uid); + + args.Handled = true; + } + else + { + _popup.PopupEntity(Loc.GetString("stabikor-locked-deny"), uid, user, PopupType.SmallCaution); + + _audio.PlayPvs(comp.DenySound, uid); + } + } + } + + public void OpenDialog(EntityUid uid, StabikorComponent comp, EntityUid user) + { + + _containerSystem.TryGetContainingContainer((uid, null, null), out var container); + var parent = container?.Owner ?? EntityUid.Invalid; + var isInContainer = parent.IsValid(); + + var targetEntity = isInContainer ? parent : uid; + if (!_transformSystem.InRange(user, targetEntity, 2f)) + return; + + if (!_handsSystem.TryGetActiveItem(user, out var heldItem) || + !_idCard.TryFindIdCard(heldItem.Value, out var idCard) || + !TryComp(idCard.Owner, out var accessComp)) + return; + + if (comp.IsLocked) + return; + + if (!_playerManager.TryGetSessionByEntity(user, out var session)) + return; + + _quickDialog.OpenDialog(session, Loc.GetString("stabikor-dialog-title"), Loc.GetString("stabikor-dialog-field") + "\n", (string input) => + { + if (!EntityManager.EntityExists(uid)) + return; + + if (string.IsNullOrEmpty(input)) + { + comp.Duration = 60f; + _popup.PopupEntity(Loc.GetString("stabikor-duration-set-cancel-fallback", ("time", 1)), uid, user); + return; + } + + if (!int.TryParse(input, out var durationMinutes) || durationMinutes < 1 || durationMinutes > 900) + { + _popup.PopupEntity(Loc.GetString("stabikor-dialog-invalid-range"), user, user, PopupType.SmallCaution); + _audio.PlayPvs(comp.DenySound, uid); + return; + } + + comp.Duration = durationMinutes * 60f; + _popup.PopupEntity(Loc.GetString("stabikor-duration-set-success", ("time", durationMinutes)), uid, user); + + _audio.PlayPvs(comp.UnlockSound, uid); + Dirty(uid, comp); + }); + } + + public void IsLocked(EntityUid uid, StabikorComponent comp, EntityUid user) + { + _containerSystem.TryGetContainingContainer((uid, null, null), out var container); + var parent = container?.Owner ?? EntityUid.Invalid; + var isInContainer = parent.IsValid(); + + var targetEntity = isInContainer ? parent : uid; + if (!_transformSystem.InRange(user, targetEntity, 2f)) + return; + + if (!_handsSystem.TryGetActiveItem(user, out var heldItem) || + !_idCard.TryFindIdCard(heldItem.Value, out var idCard) || + !TryComp(idCard.Owner, out var accessComp)) + return; + + var cardAccess = accessComp.Tags; + + if (comp.IsLocked) + { + if (comp.UnlockAccess.Exists(proto => cardAccess.Contains(proto.Id))) + { + if (!RemoveStabikor(uid, comp)) + return; + } + else + { + _popup.PopupEntity(Loc.GetString("stabikor-unlocked-deny"), uid, user, PopupType.SmallCaution); + _audio.PlayPvs(comp.DenySound, uid); + } + } + else + { + if (comp.LockAccess.Exists(proto => cardAccess.Contains(proto.Id))) + { + if (!ActivateStabikor(uid, parent, comp, user)) + return; + } + else + { + _popup.PopupEntity(Loc.GetString("stabikor-locked-deny"), uid, user, PopupType.SmallCaution); + _audio.PlayPvs(comp.DenySound, uid); + } + } + } + + private bool ActivateStabikor(EntityUid uid, EntityUid wearerUid, StabikorComponent comp, EntityUid user) + { + if (comp.IsActive) + return false; + + if (_inventorySystem.TryGetSlotEntity(wearerUid, "gloves", out var glovesItem) && glovesItem == uid || + _inventorySystem.TryGetSlotEntity(wearerUid, "neck", out var neckItem) && neckItem == uid) + { + comp.Timer = comp.Duration; + comp.IsLocked = true; + comp.IsActive = true; + comp.WearingEntity = wearerUid; + Dirty(uid, comp); + + _audio.PlayPvs(comp.LockSound, uid); + + _popup.PopupEntity(Loc.GetString("stabikor-activated-success", ("item", uid), ("user", Loc.GetString(Name(wearerUid)))), uid); + + return true; + } + + _audio.PlayPvs(comp.DenySound, uid); + + _popup.PopupEntity(Loc.GetString("stabikor-not-equipped"), uid, user); + return false; + } + + private bool RemoveStabikor(EntityUid uid, StabikorComponent comp) + { + comp.Timer = 0f; + comp.IsLocked = false; + comp.IsActive = false; + + if (comp.WearingEntity is not { Valid: true } user) + { + Dirty(uid, comp); + return false; + } + comp.WearingEntity = null; + Dirty(uid, comp); + + if (!_inventorySystem.TryGetContainingSlot(uid, out var slotDef)) + return false; + + if (!_inventorySystem.TryUnequip(user, slotDef.Name, force: true)) + return false; + + _audio.PlayPvs(comp.UnlockSound, uid); + + _popup.PopupEntity(Loc.GetString("stabikor-moment-shutdown", ("item", uid), ("user", Loc.GetString(Name(user)))), uid); + + return true; + } + + private void OnOpenDialogNetworkReceived(OpenDialogEvent msg, EntitySessionEventArgs args) + { + var uid = GetEntity(msg.Verp); + var user = args.SenderSession.AttachedEntity; + + if (user == null) + return; + + if (TryComp(uid, out var comp)) + OpenDialog(uid, comp, user.Value); + } + + private void OnIsLockedNetworkReceived(ToggleLockEvent msg, EntitySessionEventArgs args) + { + var uid = GetEntity(msg.Verp); + + var user = args.SenderSession.AttachedEntity; + + if (user == null) + return; + + if (TryComp(uid, out var comp)) + IsLocked(uid, comp, user.Value); + } +} diff --git a/Content.Shared/_Arcane/CuttableItem/Components/CuttableItemComponent.cs b/Content.Shared/_Arcane/CuttableItem/Components/CuttableItemComponent.cs new file mode 100644 index 00000000000..2d74ad600cd --- /dev/null +++ b/Content.Shared/_Arcane/CuttableItem/Components/CuttableItemComponent.cs @@ -0,0 +1,47 @@ +using Robust.Shared.GameStates; +using Content.Shared.Radio; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared._Arcane.CuttableItem.Components; + +/// +/// This component allows you to get rid of an object using tools, but notifies others about it via a radio channel. +/// +[RegisterComponent] +[NetworkedComponent] +[AutoGenerateComponentState] +public sealed partial class CuttableItemComponent : Component +{ + /// + /// A list of quality tools that can be used to cut this object. + /// + [DataField, AutoNetworkedField] + public List ToolQualities = new() + { + "Sawing" + }; + + /// + /// The time in seconds required to cut the object. + /// + [DataField, AutoNetworkedField] + public float Delay = 45.0f; + + /// + /// The ID of the prototype radio channel for sending the notification. + /// + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer)), AutoNetworkedField] + public string RadioChannel = "Security"; + + /// + /// The localization key for the message in the communication channel. + /// + [DataField, AutoNetworkedField] + public string AlertMessage = "cuttable-item-alert-activated"; +} + +public sealed class CuttableCutEvent(EntityUid user, EntityUid item) : HandledEntityEventArgs +{ + public EntityUid User { get; } = user; + public EntityUid Item { get; } = item; +} diff --git a/Content.Shared/_Arcane/CuttableItem/CuttableDoAfterEvent.cs b/Content.Shared/_Arcane/CuttableItem/CuttableDoAfterEvent.cs new file mode 100644 index 00000000000..1b509a6e7b9 --- /dev/null +++ b/Content.Shared/_Arcane/CuttableItem/CuttableDoAfterEvent.cs @@ -0,0 +1,7 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.CuttableItem; + +[Serializable, NetSerializable] +public sealed partial class CuttableDoAfterEvent : SimpleDoAfterEvent { } diff --git a/Content.Shared/_Arcane/CuttableItem/Systems/SharedCuttableItemSystem.cs b/Content.Shared/_Arcane/CuttableItem/Systems/SharedCuttableItemSystem.cs new file mode 100644 index 00000000000..0dad83d9bc0 --- /dev/null +++ b/Content.Shared/_Arcane/CuttableItem/Systems/SharedCuttableItemSystem.cs @@ -0,0 +1,124 @@ +using Content.Shared._Arcane.CuttableItem.Components; +using Content.Shared.Interaction; +using Content.Shared.Tools; +using Content.Shared.DoAfter; +using Content.Shared.CuttableItem; +using Content.Shared.Popups; +using Content.Shared.Inventory; +using Content.Shared.Tools.Systems; +using Robust.Shared.Network; +using Content.Shared.Examine; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Shared._Arcane.CuttableItem.Systems; + +public sealed class SharedCuttableItemSystem : EntitySystem +{ + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedToolSystem _toolSystem = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly InventorySystem _inventorySystem = default!; + [Dependency] private readonly SharedTransformSystem _transformSystem = default!; + [Dependency] private readonly INetManager _netManager = default!; + [Dependency] private IPrototypeManager _prototypeManager = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnCutCompleted); + SubscribeLocalEvent(OnExamined); + } + + private void OnInteractUsing(EntityUid uid, CuttableItemComponent comp, InteractUsingEvent args) + { + var toolFound = false; + + foreach (var quality in comp.ToolQualities) + { + if (_toolSystem.HasQuality(args.Used, quality)) + { + toolFound = true; + break; + } + } + + if (_netManager.IsClient || !toolFound) + return; + + args.Handled = true; + + var doAfterArgs = new DoAfterArgs(EntityManager, args.User, comp.Delay, new CuttableDoAfterEvent(), uid, target: uid, used: args.Used) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = true + }; + + _popup.PopupEntity(Loc.GetString("cuttable-item-attempt-broken-popup", ("item", uid), ("user", Loc.GetString(Name(args.User)))), uid); + + _doAfter.TryStartDoAfter(doAfterArgs); + Dirty(uid, comp); + } + + private void OnCutCompleted(EntityUid uid, CuttableItemComponent comp, CuttableDoAfterEvent args) + { + if (args.Cancelled || args.Handled || _netManager.IsClient) + return; + + args.Handled = true; + + var victim = Transform(uid).ParentUid; + + if (victim.IsValid()) + { + if (_inventorySystem.TryGetSlots(victim, out var slotDefinitions)) + { + if (_netManager.IsClient) + return; + + foreach (var slotDef in slotDefinitions) + { + if (_inventorySystem.TryGetSlotEntity(victim, slotDef.Name, out var slotEntity) && slotEntity == uid) + { + var target = args.User; + + if (_inventorySystem.TryUnequip(target, victim, slotDef.Name, force: true)) + { + _transformSystem.AttachToGridOrMap(uid); + + var victimCoords = Transform(victim).Coordinates; + _transformSystem.SetCoordinates(uid, victimCoords); + _popup.PopupEntity(Loc.GetString("cuttable-item-broken-moment-popup", ("item", uid)), uid); + + var ev = new CuttableCutEvent(target, uid); + RaiseLocalEvent(uid, ev); + } + break; + } + } + } + } + } + + private void OnExamined(EntityUid uid, CuttableItemComponent comp, ref ExaminedEvent args) + { + if (comp.ToolQualities.Count == 0) + return; + + var message = new FormattedMessage(); + message.AddMarkupOrThrow(Loc.GetString("cuttable-item-examine-header") + "\n"); + + foreach (var qualityId in comp.ToolQualities) + { + if (_prototypeManager.TryIndex(qualityId, out var qualityProto)) + { + var qualityName = Loc.GetString(qualityProto.Name); + message.AddMarkupOrThrow($" - [color=yellow]{qualityName}[/color]\n"); + } + } + args.PushMessage(message); + } +} diff --git a/Content.Shared/_Arcane/Stabikor/Components/StabikorComponent.cs b/Content.Shared/_Arcane/Stabikor/Components/StabikorComponent.cs new file mode 100644 index 00000000000..f7748307b5a --- /dev/null +++ b/Content.Shared/_Arcane/Stabikor/Components/StabikorComponent.cs @@ -0,0 +1,111 @@ +using Content.Shared.Access; +using Robust.Shared.Prototypes; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; +using Content.Shared.Tag; + +namespace Content.Shared._Arcane.Stabikor.Components; + +/// +/// A component that punishes creatures with a bad tone with electric shocks +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class StabikorComponent : Component +{ + /// + /// Stores the UID of the player who is wearing the object + /// + [ViewVariables, AutoNetworkedField] + public EntityUid? WearingEntity; + + [ViewVariables] + public TimeSpan LastVerbClickTime = TimeSpan.Zero; + + /// + /// The time in seconds for which the object is blocked + /// + [DataField, AutoNetworkedField] + public float Duration = 60f; + + /// + /// Timer to count down the time until withdrawal + /// + [AutoNetworkedField] + public float Timer = 0f; + + /// + /// Is the blocking process currently active + /// + [AutoNetworkedField] + public bool IsActive = false; + + /// + /// Electric shock damage after punishment + /// + [DataField] + public int Damage = 5; + + /// + /// The time of the knockout after the punishment + /// + [DataField] + public float TimeStan = 5.0f; + + /// + /// Object status: blocked or not + /// + [AutoNetworkedField] + public bool IsLocked = false; + + /// + /// Who can CLOSE the object + /// + [DataField, AutoNetworkedField] + public List> LockAccess = new() + { + "Security" + }; + + /// + /// Who can OPEN the object + /// + [DataField, AutoNetworkedField] + public List> UnlockAccess = new() + { + "Armory", + "HeadOfSecurity", + "Captain", + "CentralCommand" + }; + + /// + /// Sound of successful blocking + /// + [DataField, AutoNetworkedField] + public SoundSpecifier LockSound = new SoundPathSpecifier("/Audio/Effects/beep1.ogg"); + + /// + /// The sound of a successful unlock + /// + [DataField, AutoNetworkedField] + public SoundSpecifier UnlockSound = new SoundPathSpecifier("/Audio/Effects/beep1.ogg"); + + /// + /// The sound of an error / denial of access + /// + [DataField, AutoNetworkedField] + public SoundSpecifier DenySound = new SoundPathSpecifier("/Audio/Effects/beep_landmine.ogg"); + +} + +[Serializable, NetSerializable] +public sealed class OpenDialogEvent(NetEntity verp) : EntityEventArgs +{ + public NetEntity Verp = verp; +} +[Serializable, NetSerializable] +public sealed class ToggleLockEvent(NetEntity verp) : EntityEventArgs +{ + public NetEntity Verp = verp; +} diff --git a/Content.Shared/_Arcane/Stabikor/Systems/SharedStabikorSystem.cs b/Content.Shared/_Arcane/Stabikor/Systems/SharedStabikorSystem.cs new file mode 100644 index 00000000000..e5105bb235d --- /dev/null +++ b/Content.Shared/_Arcane/Stabikor/Systems/SharedStabikorSystem.cs @@ -0,0 +1,141 @@ +using Content.Shared._Arcane.Stabikor.Components; +using Robust.Shared.Containers; +using Content.Shared.Popups; +using Content.Shared.Examine; +using Content.Shared.Inventory.Events; +using Content.Shared.Access.Components; +using Content.Shared.Verbs; +using Robust.Shared.Utility; +using Content.Shared.Access.Systems; +using Content.Shared.Hands.EntitySystems; +using Robust.Shared.Timing; +using Robust.Shared.Network; + +namespace Content.Shared._Arcane.Stabikor.Systems; + +public sealed class SharedStabikorSystem : EntitySystem +{ + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedIdCardSystem _idCard = default!; + [Dependency] private readonly SharedHandsSystem _handsSystem = default!; + [Dependency] private SharedContainerSystem _containerSystem = default!; + [Dependency] private IGameTiming _gameTiming = default!; + [Dependency] private readonly INetManager _netManager = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent(OnGotUnequipped); + SubscribeLocalEvent(OnBeingUnequippedAttempt); + SubscribeLocalEvent>(OnGetActivationVerbs); + } + + private void OnBeingUnequippedAttempt(EntityUid uid, StabikorComponent comp, BeingUnequippedAttemptEvent args) + { + if (comp.IsLocked || comp.IsActive) + { + if (!_netManager.IsClient) + _popup.PopupEntity(Loc.GetString("stabikor-unequip-blocked-active"), uid, args.Unequipee, PopupType.SmallCaution); + + args.Cancel(); + } + } + + private void OnGotUnequipped(EntityUid uid, StabikorComponent comp, ref GotUnequippedEvent args) + { + if (comp.IsActive) + comp.Timer = 0; + + if (!_netManager.IsClient) + Dirty(uid, comp); + + } + + private void OnExamine(EntityUid uid, StabikorComponent comp, ExaminedEvent args) + { + var state = comp.IsLocked ? "stabikor-examine-locked" : "stabikor-examine-unlocked"; + + args.PushMarkup(Loc.GetString("stabikor-examine-status-main", ("mode", Loc.GetString(state)))); + + var durationTotalMinutes = (int) (comp.Duration / 60f); + var durationHours = durationTotalMinutes / 60; + var durationMinutes = durationTotalMinutes % 60; + + args.PushMarkup(Loc.GetString("stabikor-examine-duration-info", + ("hours", durationHours), + ("minutes", durationMinutes))); + + if (comp.IsLocked && comp.Timer > 0) + { + var remainingTotalMinutes = (int) (comp.Timer / 60f); + var remainingHours = remainingTotalMinutes / 60; + var remainingMinutes = remainingTotalMinutes % 60; + var remainingSeconds = (int) (comp.Timer % 60f); + + args.PushMarkup(Loc.GetString("stabikor-examine-timer-remaining", + ("hours", remainingHours), + ("minutes", remainingMinutes), + ("seconds", remainingSeconds))); + } + Dirty(uid, comp); + } + + private void OnGetActivationVerbs(EntityUid uid, StabikorComponent comp, GetVerbsEvent args) + { + if (!_containerSystem.TryGetContainingContainer((uid, null, null), out var container)) + return; + + var parent = container.Owner; + var isInContainer = parent.IsValid(); + + if (!isInContainer && (!args.CanAccess || !args.CanInteract)) + return; + + if (!_handsSystem.TryGetActiveItem(args.User, out var heldItem) || + !_idCard.TryFindIdCard(heldItem.Value, out var idCard) || + !TryComp(idCard.Owner, out var accessComp)) + return; + + var settingsIcon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")); + + if (!comp.IsLocked) + { + args.Verbs.Add(new ActivationVerb() + { + Text = Loc.GetString("stabikor-verb-set-duration"), + Icon = settingsIcon, + Act = () => FlipOpenDialog(uid, comp) + }); + } + + var verbText = comp.IsLocked ? "stabikor-verb-unlock" : "stabikor-verb-lock"; + + args.Verbs.Add(new ActivationVerb() + { + Text = Loc.GetString(verbText), + Icon = settingsIcon, + Act = () => FlipToggleLock(uid, comp) + }); + } + + private void FlipOpenDialog(EntityUid uid, StabikorComponent comp) + { + if (_gameTiming.CurTime < comp.LastVerbClickTime + TimeSpan.FromSeconds(0.4)) + return; + + comp.LastVerbClickTime = _gameTiming.CurTime; + + RaiseNetworkEvent(new OpenDialogEvent(GetNetEntity(uid))); + } + + private void FlipToggleLock(EntityUid uid, StabikorComponent comp) + { + if (_gameTiming.CurTime < comp.LastVerbClickTime + TimeSpan.FromSeconds(0.4)) + return; + + comp.LastVerbClickTime = _gameTiming.CurTime; + + RaiseNetworkEvent(new ToggleLockEvent(GetNetEntity(uid))); + } +} diff --git a/Resources/Locale/ru-RU/_Arcane/entities/clothing/other/stabikor.ftl b/Resources/Locale/ru-RU/_Arcane/entities/clothing/other/stabikor.ftl new file mode 100644 index 00000000000..3e6bd794bc0 --- /dev/null +++ b/Resources/Locale/ru-RU/_Arcane/entities/clothing/other/stabikor.ftl @@ -0,0 +1,2 @@ +ent-ClothingStabikor = устройство "стаби-кор" + .desc = Ингибитор насилия для подавления агрессивных ассистентов. diff --git a/Resources/Locale/ru-RU/_Arcane/stabikor/stabikor-component.ftl b/Resources/Locale/ru-RU/_Arcane/stabikor/stabikor-component.ftl new file mode 100644 index 00000000000..9e014e2ee30 --- /dev/null +++ b/Resources/Locale/ru-RU/_Arcane/stabikor/stabikor-component.ftl @@ -0,0 +1,53 @@ +stabikor-activate-verb = Активировать +stabikor-activated-success = { $item } защелкивается! +stabikor-not-equipped = Объект не обнаружен. +stabikor-moment-shutdown = { $item } расстегивается и падает на землю! +stabikor-disarm-shock-popup = Вас сильно бьет током за попытку агрессии! Боевой режим отключен. + +stabikor-locked-deny = У вас недостаточно прав для блокировки этого устройства! +stabikor-unlocked-deny = У вас недостаточно прав для разблокировки этого устройства! + +stabikor-examine-status-main = На устройстве горит индикатор режима: [color=yellow]{$mode}[/color]. + +stabikor-examine-duration-info = + Время работы настроено на [color=cyan]{$minutes}[/color] { $minutes -> + [one] минуту + [few] минуты + *[other] минут + }. +stabikor-examine-timer-remaining = + До отключения осталось: { $hours -> + [0] { "" } + *[other] [color=orange]{$hours}[/color] { $hours -> + [one] час + [few] часа + *[other] часов + } + } { $minutes -> + [0] { "" } + *[other] [color=orange]{$minutes}[/color] { $minutes -> + [one] минута и + [few] минуты и + *[other] минут и + } + } [color=orange]{$seconds}[/color] { $seconds -> + [one] секунда + [few] секунды + *[other] секунд + }. + +stabikor-examine-locked = заблокирован +stabikor-examine-unlocked = ожидание + +stabikor-verb-set-duration = Настроить таймер +stabikor-verb-lock = Активировать блокировку +stabikor-verb-unlock = Снять блокировку + +stabikor-dialog-title = Конфигурация таймера +stabikor-dialog-field = + Введите время работы устройства + (в минутах, от 1 до 900): +stabikor-dialog-invalid-range = Неверный диапазон времени! Введите число от 1 до 900. +stabikor-duration-set-success = Время работы успешно настроено на {$time} мин. + +stabikor-unequip-blocked-active = Устройство невозможно снять до окончания таймера! diff --git "a/Resources/Locale/ru-RU/_Arcane/\321\201uttableItem/cuttable-Item-component.ftl" "b/Resources/Locale/ru-RU/_Arcane/\321\201uttableItem/cuttable-Item-component.ftl" new file mode 100644 index 00000000000..48cee7e695f --- /dev/null +++ "b/Resources/Locale/ru-RU/_Arcane/\321\201uttableItem/cuttable-Item-component.ftl" @@ -0,0 +1,5 @@ +cuttable-item-broken-moment-popup = { $item } снялся! +cuttable-item-attempt-broken-popup = { $user } пытается снять { $item }! +cuttable-item-examine-header = Кажется, можно освободиться, используя инструмент с качеством: + +cuttable-item-alert-activated = Зафиксировано несанкционированное снятие { $item }. Нарушитель: { $user } diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/security.yml b/Resources/Prototypes/Catalog/Fills/Lockers/security.yml index a9776b2b1a0..467c6bff9a9 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/security.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/security.yml @@ -120,6 +120,10 @@ - id: BookSpaceLaw - id: ClothingBackpackElectropack amount: 2 + # Arcane-Start + - id: ClothingStabikor + amount: 2 + # Arcane-End - id: RemoteSignaller amount: 2 - id: Binoculars diff --git a/Resources/Prototypes/Recipes/Lathes/Packs/security.yml b/Resources/Prototypes/Recipes/Lathes/Packs/security.yml index 39aef993fa1..f6af09b70ac 100644 --- a/Resources/Prototypes/Recipes/Lathes/Packs/security.yml +++ b/Resources/Prototypes/Recipes/Lathes/Packs/security.yml @@ -97,6 +97,7 @@ - SmokeGrenade # Orion-End - ClothingBackpackElectropack + - ClothingStabikor # Arcane - HoloprojectorSecurity - HoloprojectorSecurityAdvanced - AntiMindControlDevice diff --git a/Resources/Prototypes/Recipes/Lathes/security.yml b/Resources/Prototypes/Recipes/Lathes/security.yml index 825b2c28e77..b5869b287d7 100644 --- a/Resources/Prototypes/Recipes/Lathes/security.yml +++ b/Resources/Prototypes/Recipes/Lathes/security.yml @@ -121,6 +121,19 @@ Plastic: 250 Cloth: 500 +# Arcane-Start +- type: latheRecipe + id: ClothingStabikor + result: ClothingStabikor + categories: + - Clothing + completetime: 4 + materials: + Steel: 500 + Plastic: 250 + Cloth: 500 + # Arcane-End + - type: latheRecipe id: ClothingEyesHudSecurity result: ClothingEyesHudSecurity diff --git a/Resources/Prototypes/_Arcane/Entities/Clothing/Other/stabikor.yml b/Resources/Prototypes/_Arcane/Entities/Clothing/Other/stabikor.yml new file mode 100644 index 00000000000..2180873f33e --- /dev/null +++ b/Resources/Prototypes/_Arcane/Entities/Clothing/Other/stabikor.yml @@ -0,0 +1,28 @@ +- type: entity + parent: Clothing + id: ClothingStabikor + name: The Stabikor device + description: Aggression stabilizer for aggressive assistants + components: + - type: Item + size: Small + sprite: _Arcane/Clothing/Neck/Misc/petcollar.rsi + - type: Clothing + quickEquip: true + equipDelay: 1.5 + slots: + - neck + - gloves + sprite: _Arcane/Clothing/Neck/Misc/petcollar.rsi + - type: Sprite + state: icon + sprite: _Arcane/Clothing/Neck/Misc/petcollar.rsi + scale: 0.75,0.75 + - type: Tag + tags: + - Recyclable + - type: Stabikor + - type: CuttableItem + toolQualities: + - Sawing + delay: 45.0 diff --git a/Resources/Prototypes/_Orion/Research/Nodes/security.yml b/Resources/Prototypes/_Orion/Research/Nodes/security.yml index 99e6afbb367..4a849d0926c 100644 --- a/Resources/Prototypes/_Orion/Research/Nodes/security.yml +++ b/Resources/Prototypes/_Orion/Research/Nodes/security.yml @@ -79,6 +79,7 @@ - Zipties - FlashlightSeclite - ClothingBackpackElectropack + - ClothingStabikor # Arcane - ForensicPad - TargetClown - TargetHuman diff --git a/Resources/Textures/_Arcane/Clothing/Neck/Misc/petcollar.rsi/equipped-HAND.png b/Resources/Textures/_Arcane/Clothing/Neck/Misc/petcollar.rsi/equipped-HAND.png new file mode 100644 index 0000000000000000000000000000000000000000..0632814ca446dbe1b01b8dbefb5adf60310f07bb GIT binary patch literal 399 zcmV;A0dW3_P)5KS3S=9)aWv@CYOafKP#RM_@~Q7Xe-(b17wA+kh?i*N-3d*J9W5+b#m! zmW(xtORf6lUXM~%+poX%=Eomh1bCihsO+_MJK$#5w2J_{5eM6(0E0Co zJRK4cF8N>qNx)zY2~URvgiAhHKoT%mL&DP`0pXGd2eiVG081hCeG-7)VXHn8U@3&Y zPXf?8Y}H2sEQQebNdS6>t@=oSr4af)2|(*`YdsJI=p2DAok>{U%+D<%KnS1(duItC z1kgDIog>iII!gc{fD+(c6u_$-iX90+>7=&~65v%1#f}7^bkbV~3Ggb1Vn+i0y^}}t te**vj|Np+Z=+FQF00v1!K~w_()jueKPd%M<00000NkvXXu0mjf0021&n1lcT literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Arcane/Clothing/Neck/Misc/petcollar.rsi/equipped-NECK.png b/Resources/Textures/_Arcane/Clothing/Neck/Misc/petcollar.rsi/equipped-NECK.png new file mode 100644 index 0000000000000000000000000000000000000000..82472c994e062436b53194481ffc3c92813f473e GIT binary patch literal 537 zcmV+!0_OdRP)XmYUNI3_$ph!c-K~hlW1Z1CubtiVt*WXCzLtiV zV3v2)0N~^^fq-BxmkYKIw_z9pd@T)5zeIu$>n=+oUHa8D;<{iGOz(*#St2 z5kN{Lw>1@zTgB6SseqJ7Zfhzaw~D9vQUNKE+}2b;ZWT}Sr2t@PlrwLg`Nb=Xv{$Jv01|6 z)C3?p0)!8F^a<4SEC-L+lYkkG8AyJQ%UuBWI6li=cL20t2|!EC+j%O$TSc|`Pyt$E z-p*42-YTlihYHXV^LCyJ@K#Z6K2(5~n78v(z_V9$Z~T7%00960L-7pu00006NklLa;FqL?j5>1ph$5CY9h{uo8qcg0)~15Ui}Ntf#6y@Iswe5hcRT|nAUXy0LBoS<7pwR zXQ>2$Hd^{1V(CQOX?=T*3WWld%Viz0Hnp|8;cPSs;4BL#Gnot(i$z_S%jL+H-f)~! z12B8vRqJHR+_TrI0<>B!>U27^)2PwH`5rA_?&~#jwlQDJ0HX!{Vd27AMH%1+j+IJ< zN~IE2>vcM5HtFc*Mz4{R+NBD>CXHH+OmW^7Qv(=}$JFolwa0KcBwKpJahe2B@1B(K z5pmRusown)m9MX*4m%d2a#$zv+;(G9tO8Y9y&G=aB&53P6NDvs`ONe3&Jf zb>`X$U>2BbycYgen0e$*2VGiGNl9wUd58eg6l@5XA1m{bYA@+5ze(t)N+u2wV4~7U zFa*pGmHC_6lXT%RSGSQD0Myd|0w6+TzY3TK0Z<@89JJdcjU0L3*wL5eod6fv5LIO^ zjU4&V*tsua6aWet0cex0000 Date: Thu, 6 Aug 2026 12:28:23 +0300 Subject: [PATCH 2/4] fixs --- .../Systems/AggressionInhibitorSystem.cs | 273 ++++++++++++++ .../Systems/CuttableItemSystem.cs | 59 ++- .../Stabikor/Systems/StabikorSystem.cs | 336 ------------------ .../AggressionInhibitorComponent.cs} | 51 ++- .../SharedAggressionInhibitorSystem.cs | 199 +++++++++++ .../Components/CuttableItemComponent.cs | 27 +- .../Systems/SharedCuttableItemSystem.cs | 76 +--- .../Stabikor/Systems/SharedStabikorSystem.cs | 141 -------- .../aggression_inhibitor_component.ftl | 79 ++++ .../cuttable/cuttable_Item_component.ftl | 5 + .../clothing/other/aggression_inhibitor.ftl | 2 + .../aggression_inhibitor_component.ftl | 84 +++++ .../cuttableItem/cuttable_Item_component.ftl | 0 .../clothing/other/aggression_inhibitor.ftl | 2 + .../entities/clothing/other/stabikor.ftl | 2 - .../_Arcane/stabikor/stabikor-component.ftl | 53 --- .../Catalog/Cargo/cargo_vending.yml | 2 +- .../Catalog/Fills/Lockers/security.yml | 2 +- .../Entities/Structures/Machines/lathe.yml | 1 + .../Recipes/Lathes/Packs/medical.yml | 18 + .../Recipes/Lathes/Packs/security.yml | 7 +- .../Prototypes/Recipes/Lathes/security.yml | 4 +- .../Other/{stabikor.yml => inhibitor.yml} | 22 +- .../Recipes/Lathes/Packs/misc.yml | 5 + .../_Orion/Research/Nodes/cyborg.yml | 5 + .../_Orion/Research/Nodes/engineering.yml | 12 + .../_Orion/Research/Nodes/medical.yml | 5 + .../_Orion/Research/Nodes/security.yml | 2 +- .../_Orion/Research/Nodes/service.yml | 6 + .../Neck/Misc/petcollar.rsi/meta.json | 2 +- 30 files changed, 825 insertions(+), 657 deletions(-) create mode 100644 Content.Server/_Arcane/AggressionInhibitor/Systems/AggressionInhibitorSystem.cs delete mode 100644 Content.Server/_Arcane/Stabikor/Systems/StabikorSystem.cs rename Content.Shared/_Arcane/{Stabikor/Components/StabikorComponent.cs => AggressionInhibitor/Components/AggressionInhibitorComponent.cs} (70%) create mode 100644 Content.Shared/_Arcane/AggressionInhibitor/Systems/SharedAggressionInhibitorSystem.cs delete mode 100644 Content.Shared/_Arcane/Stabikor/Systems/SharedStabikorSystem.cs create mode 100644 Resources/Locale/en-US/_Arcane/aggression-Inhibitor/aggression_inhibitor_component.ftl create mode 100644 Resources/Locale/en-US/_Arcane/cuttable/cuttable_Item_component.ftl create mode 100644 Resources/Locale/en-US/_Arcane/entities/clothing/other/aggression_inhibitor.ftl create mode 100644 Resources/Locale/ru-RU/_Arcane/aggression-Inhibitor/aggression_inhibitor_component.ftl rename "Resources/Locale/ru-RU/_Arcane/\321\201uttableItem/cuttable-Item-component.ftl" => Resources/Locale/ru-RU/_Arcane/cuttableItem/cuttable_Item_component.ftl (100%) create mode 100644 Resources/Locale/ru-RU/_Arcane/entities/clothing/other/aggression_inhibitor.ftl delete mode 100644 Resources/Locale/ru-RU/_Arcane/entities/clothing/other/stabikor.ftl delete mode 100644 Resources/Locale/ru-RU/_Arcane/stabikor/stabikor-component.ftl rename Resources/Prototypes/_Arcane/Entities/Clothing/Other/{stabikor.yml => inhibitor.yml} (52%) diff --git a/Content.Server/_Arcane/AggressionInhibitor/Systems/AggressionInhibitorSystem.cs b/Content.Server/_Arcane/AggressionInhibitor/Systems/AggressionInhibitorSystem.cs new file mode 100644 index 00000000000..e17f2a59c80 --- /dev/null +++ b/Content.Server/_Arcane/AggressionInhibitor/Systems/AggressionInhibitorSystem.cs @@ -0,0 +1,273 @@ +using Content.Shared._Arcane.AggressionInhibitor.Components; +using Robust.Shared.Containers; +using Content.Shared.CombatMode; +using Content.Shared.Popups; +using Content.Shared.Interaction; +using Content.Shared.Access.Components; +using Content.Server.Access.Systems; +using Robust.Shared.Audio.Systems; +using Content.Server.Administration; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Inventory; +using Robust.Server.Player; + +namespace Content.Server._Arcane.AggressionInhibitor.Systems; + +public sealed partial class AggressionInhibitorSystem : EntitySystem +{ + [Dependency] private InventorySystem _inventorySystem = default!; + [Dependency] private SharedTransformSystem _transformSystem = default!; + [Dependency] private SharedPopupSystem _popup = default!; + [Dependency] private IdCardSystem _idCard = default!; + [Dependency] private SharedAudioSystem _audio = default!; + [Dependency] private SharedHandsSystem _handsSystem = default!; + [Dependency] private QuickDialogSystem _quickDialog = default!; + [Dependency] private SharedCombatModeSystem _combatMode = default!; + [Dependency] private SharedContainerSystem _containerSystem = default!; + [Dependency] private IPlayerManager _playerManager = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnOpenDialogReceived); + SubscribeLocalEvent(OnToggleLockReceived); + } + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (!comp.IsActive || comp.WearingEntity == null) + continue; + + comp.Timer -= frameTime; + + if (comp.Timer <= 0) + { + if (!RemoveInhibitor(uid, comp)) + continue; + } + Dirty(uid, comp); + } + } + + private void OnInteractUsing(EntityUid uid, AggressionInhibitorComponent comp, InteractUsingEvent args) + { + var user = args.User; + + var parent = Transform(uid).ParentUid; + var isEquipped = parent.IsValid() && HasComp(parent); + + if (!isEquipped) + { + _audio.PlayPvs(comp.DenySound, uid); + + args.Handled = true; + return; + } + + if (!_idCard.TryFindIdCard(args.Used, out var idCard) || + !TryComp(idCard.Owner, out var accessComp)) + return; + + var cardAccess = accessComp.Tags; + + if (comp.IsLocked) + { + var hasUnlockAccess = comp.UnlockAccess.Exists(proto => cardAccess.Contains(proto.Id)); + + if (hasUnlockAccess) + { + if (!RemoveInhibitor(uid, comp)) + return; + + args.Handled = true; + return; + } + else _audio.PlayPvs(comp.DenySound, uid); + } + else + { + var hasLockAccess = comp.LockAccess.Exists(proto => cardAccess.Contains(proto.Id)); + + if (hasLockAccess) + { + if (!ActivateInhibitor(uid, parent, comp, user)) + return; + + args.Handled = true; + return; + } + else _audio.PlayPvs(comp.DenySound, uid); + } + } + + public void OpenDialog(EntityUid uid, AggressionInhibitorComponent comp, EntityUid user) + { + EntityUid? parent = _containerSystem.TryGetContainingContainer((uid, null, null), out var container) + ? container.Owner + : null; + + var targetEntity = parent ?? uid; + if (!_transformSystem.InRange(user, targetEntity, 2f)) + return; + + if (!_handsSystem.TryGetActiveItem(user, out var heldItem) || + !_idCard.TryFindIdCard(heldItem.Value, out var idCard) || + !TryComp(idCard.Owner, out var accessComp)) + return; + + if (comp.IsLocked) + return; + + var cardAccess = accessComp.Tags; + var hasSettingsAccess = comp.LockAccess.Exists(proto => cardAccess.Contains(proto.Id)); + + if (!hasSettingsAccess) + { + _audio.PlayPvs(comp.DenySound, uid); + return; + } + + if (!_playerManager.TryGetSessionByEntity(user, out var session)) + return; + + _quickDialog.OpenDialog(session, Loc.GetString("stabikor-dialog-title"), Loc.GetString("stabikor-dialog-field") + "\n", (string input) => + { + if (!EntityManager.EntityExists(uid) || comp.IsLocked) + return; + + if (string.IsNullOrEmpty(input)) + { + comp.Duration = 60f; + Dirty(uid, comp); + _popup.PopupEntity(Loc.GetString("stabikor-duration-set-cancel-fallback", ("time", 1)), uid, user); + return; + } + + if (!int.TryParse(input, out var durationMinutes) || durationMinutes < 1 || durationMinutes > 900) + { + _popup.PopupEntity(Loc.GetString("stabikor-dialog-invalid-range"), user, user, PopupType.SmallCaution); + + _audio.PlayPvs(comp.DenySound, uid); + return; + } + + comp.Duration = durationMinutes * 60f; + _popup.PopupEntity(Loc.GetString("stabikor-duration-set-success", ("time", durationMinutes)), uid, user); + + _audio.PlayPvs(comp.UnlockSound, uid); + + Dirty(uid, comp); + }); + } + + public void ToggleLock(EntityUid uid, AggressionInhibitorComponent comp, EntityUid user) + { + EntityUid? parent = _containerSystem.TryGetContainingContainer((uid, null, null), out var container) + ? container.Owner + : null; + + var targetEntity = parent ?? uid; + if (!_transformSystem.InRange(user, targetEntity, 2f)) + return; + + if (!_handsSystem.TryGetActiveItem(user, out var heldItem) || + !_idCard.TryFindIdCard(heldItem.Value, out var idCard) || + !TryComp(idCard.Owner, out var accessComp)) + return; + + var cardAccess = accessComp.Tags; + + if (comp.IsLocked) + { + if (comp.UnlockAccess.Exists(proto => cardAccess.Contains(proto.Id))) + { + if (!RemoveInhibitor(uid, comp)) + return; + } + else _audio.PlayPvs(comp.DenySound, uid); + } + else + { + if (comp.LockAccess.Exists(proto => cardAccess.Contains(proto.Id))) + { + if (!ActivateInhibitor(uid, parent ?? uid, comp, user)) + return; + } + else _audio.PlayPvs(comp.DenySound, uid); + } + } + + private bool ActivateInhibitor(EntityUid uid, EntityUid wearerUid, AggressionInhibitorComponent comp, EntityUid user) + { + if (comp.IsActive) + return false; + + if (_inventorySystem.TryGetContainingSlot(uid, out var slotDef)) + { + if ((slotDef.SlotFlags & SlotFlags.POCKET) != 0) + return false; + + if (_inventorySystem.TryGetSlotEntity(wearerUid, slotDef.Name, out var slotItem) && slotItem == uid) + { + comp.Timer = comp.Duration; + comp.IsLocked = true; + comp.IsActive = true; + comp.WearingEntity = wearerUid; + _combatMode.SetInCombatMode(user, false); + + Dirty(uid, comp); + + _audio.PlayPvs(comp.LockSound, uid); + + _popup.PopupEntity(Loc.GetString("stabikor-activated-success", ("item", uid), ("user", Name(wearerUid))), uid); + + return true; + } + } + _audio.PlayPvs(comp.DenySound, uid); + + _popup.PopupEntity(Loc.GetString("stabikor-not-equipped"), uid, user); + return false; + } + + private bool RemoveInhibitor(EntityUid uid, AggressionInhibitorComponent comp) + { + if (comp.WearingEntity is not { Valid: true } user) + return false; + + if (_inventorySystem.TryGetContainingSlot(uid, out var slotDef)) + { + if (!_inventorySystem.TryUnequip(user, user, slotDef.Name, force: true)) + return false; + } + + comp.Timer = 0f; + comp.IsLocked = false; + comp.IsActive = false; + comp.WearingEntity = null; + + Dirty(uid, comp); + + _audio.PlayPvs(comp.UnlockSound, uid); + + _popup.PopupEntity(Loc.GetString("stabikor-moment-shutdown", ("item", uid)), uid); + + return true; + } + + private void OnOpenDialogReceived(EntityUid uid, AggressionInhibitorComponent comp, OpenDialogEvent args) + { + OpenDialog(uid, comp, args.User); + } + + private void OnToggleLockReceived(EntityUid uid, AggressionInhibitorComponent comp, ToggleLockEvent args) + { + ToggleLock(uid, comp, args.User); + } +} diff --git a/Content.Server/_Arcane/CuttableItem/Systems/CuttableItemSystem.cs b/Content.Server/_Arcane/CuttableItem/Systems/CuttableItemSystem.cs index 1122f7e5475..2e070505a3b 100644 --- a/Content.Server/_Arcane/CuttableItem/Systems/CuttableItemSystem.cs +++ b/Content.Server/_Arcane/CuttableItem/Systems/CuttableItemSystem.cs @@ -2,31 +2,74 @@ using Content.Shared.Radio; using Content.Shared._Arcane.CuttableItem.Components; using Robust.Shared.Prototypes; - +using Content.Shared.CuttableItem; +using Content.Shared.Popups; +using Content.Shared.Inventory; namespace Content.Server._Arcane.CuttableItem.Systems; -public sealed class CuttableItemSystem : EntitySystem +public sealed partial class CuttableItemSystem : EntitySystem { - [Dependency] private readonly RadioSystem _radio = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private RadioSystem _radio = default!; + [Dependency] private IPrototypeManager _prototypeManager = default!; + [Dependency] private SharedPopupSystem _popup = default!; + [Dependency] private InventorySystem _inventorySystem = default!; + [Dependency] private SharedTransformSystem _transformSystem = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnItemCut); + SubscribeLocalEvent(OnCutCompleted); + } + + private void OnCutCompleted(EntityUid uid, CuttableItemComponent comp, CuttableDoAfterEvent args) + { + if (args.Cancelled || args.Handled) + return; + + args.Handled = true; + + var victim = Transform(uid).ParentUid; + if (!victim.IsValid()) + return; + + if (!_inventorySystem.TryGetSlots(victim, out var slotDefinitions)) + return; + + foreach (var slotDef in slotDefinitions) + { + if (_inventorySystem.TryGetSlotEntity(victim, slotDef.Name, out var slotEntity) && slotEntity == uid) + { + var target = args.User; + + if (_inventorySystem.TryUnequip(target, victim, slotDef.Name, force: true)) + { + _transformSystem.AttachToGridOrMap(uid); + + var victimCoords = Transform(victim).Coordinates; + _transformSystem.SetCoordinates(uid, victimCoords); + + _popup.PopupEntity(Loc.GetString("cuttable-item-broken-moment-popup", ("item", uid)), uid); + + var ev = new CuttableCutEvent(target); + RaiseLocalEvent(uid, ev); + } + break; + } + } } - private void OnItemCut(EntityUid uid, CuttableItemComponent component, CuttableCutEvent args) + private void OnItemCut(EntityUid uid, CuttableItemComponent comp, CuttableCutEvent args) { - if (!_prototypeManager.TryIndex(component.RadioChannel, out var channel)) + if (!_prototypeManager.TryIndex(comp.RadioChannel, out var channel)) return; var userName = Name(args.User); - var userItem = Name(args.Item); + var userItem = Name(uid); - var message = Loc.GetString(component.AlertMessage, ("user", userName), ("item", userItem)); + var message = Loc.GetString(comp.AlertMessage, ("user", userName), ("item", userItem)); _radio.SendRadioMessage(uid, message, channel, uid); } diff --git a/Content.Server/_Arcane/Stabikor/Systems/StabikorSystem.cs b/Content.Server/_Arcane/Stabikor/Systems/StabikorSystem.cs deleted file mode 100644 index ced1db7e5a5..00000000000 --- a/Content.Server/_Arcane/Stabikor/Systems/StabikorSystem.cs +++ /dev/null @@ -1,336 +0,0 @@ -using Content.Shared._Arcane.Stabikor.Components; -using Robust.Shared.Containers; -using Content.Shared.CombatMode; -using Content.Server.Electrocution; -using Content.Shared.Popups; -using Content.Shared.Interaction; -using Content.Shared.Access.Components; -using Content.Server.Access.Systems; -using Robust.Shared.Audio.Systems; -using Content.Server.Administration; -using Content.Shared.Hands.EntitySystems; -using Content.Shared.Inventory; -using Robust.Server.Player; - -namespace Content.Server._Arcane.Stabikor.Systems; - -public sealed partial class StabikorSystem : EntitySystem -{ - [Dependency] private readonly InventorySystem _inventorySystem = default!; - [Dependency] private readonly SharedTransformSystem _transformSystem = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly ElectrocutionSystem _electrocution = default!; - [Dependency] private readonly IdCardSystem _idCard = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedHandsSystem _handsSystem = default!; - [Dependency] private readonly QuickDialogSystem _quickDialog = default!; - [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; - [Dependency] private SharedContainerSystem _containerSystem = default!; - [Dependency] private IPlayerManager _playerManager = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnToggleCombatAction); - SubscribeLocalEvent(OnInteractUsing); - SubscribeNetworkEvent(OnOpenDialogNetworkReceived); - SubscribeNetworkEvent(OnIsLockedNetworkReceived); - } - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var comp)) - { - if (!comp.IsActive || comp.WearingEntity == null) - continue; - - comp.Timer -= frameTime; - - if (comp.Timer <= 0) - { - if (!RemoveStabikor(uid, comp)) - return; - } - } - } - - private void OnToggleCombatAction(ToggleCombatActionEvent args) - { - var user = args.Performer; - - if (TryComp(user, out var mobState)) - { - if (mobState.CurrentState == Shared.Mobs.MobState.Critical || - mobState.CurrentState == Shared.Mobs.MobState.Dead) - { - return; - } - } - - var targetSlots = new[] { "neck", "gloves" }; - EntityUid stabikorItem = default; - StabikorComponent? stabikorComp = null; - - foreach (var slot in targetSlots) - { - if (_inventorySystem.TryGetSlotEntity(user, slot, out var item) && - TryComp(item, out var comp)) - { - stabikorItem = item!.Value; - stabikorComp = comp; - break; - } - } - - if (stabikorComp is not { IsActive: true }) - return; - - if (args.Action.Comp.Enabled) - { - _electrocution.TryDoElectrocution(user, stabikorItem, stabikorComp.Damage, TimeSpan.FromSeconds(stabikorComp.TimeStan), refresh: false, ignoreInsulation: true); - - _popup.PopupEntity(Loc.GetString("stabikor-disarm-shock-popup"), user, user, PopupType.LargeCaution); - - _combatMode.SetInCombatMode(user, false); - - args.Handled = true; - } - } - - private void OnInteractUsing(EntityUid uid, StabikorComponent comp, InteractUsingEvent args) - { - var user = args.User; - - var parent = Transform(uid).ParentUid; - var isEquipped = parent.IsValid() && HasComp(parent); - - if (!isEquipped) - { - _popup.PopupEntity(Loc.GetString("stabikor-not-equipped"), uid, user); - _audio.PlayPvs(comp.DenySound, uid); - - return; - } - - if (!_idCard.TryFindIdCard(args.Used, out var idCard) || - !TryComp(idCard.Owner, out var accessComp)) - { - return; - } - - var cardAccess = accessComp.Tags; - - if (comp.IsLocked) - { - var hasUnlockAccess = comp.UnlockAccess.Exists(proto => cardAccess.Contains(proto.Id)); - - if (hasUnlockAccess) - { - if (RemoveStabikor(uid, comp)) - _audio.PlayPvs(comp.UnlockSound, uid); - - args.Handled = true; - } - else - { - _popup.PopupEntity(Loc.GetString("stabikor-unlocked-deny"), uid, user, PopupType.SmallCaution); - - _audio.PlayPvs(comp.DenySound, uid); - } - } - else - { - var hasLockAccess = comp.LockAccess.Exists(proto => cardAccess.Contains(proto.Id)); - - if (hasLockAccess) - { - if (ActivateStabikor(uid, parent, comp, user)) - _audio.PlayPvs(comp.LockSound, uid); - - args.Handled = true; - } - else - { - _popup.PopupEntity(Loc.GetString("stabikor-locked-deny"), uid, user, PopupType.SmallCaution); - - _audio.PlayPvs(comp.DenySound, uid); - } - } - } - - public void OpenDialog(EntityUid uid, StabikorComponent comp, EntityUid user) - { - - _containerSystem.TryGetContainingContainer((uid, null, null), out var container); - var parent = container?.Owner ?? EntityUid.Invalid; - var isInContainer = parent.IsValid(); - - var targetEntity = isInContainer ? parent : uid; - if (!_transformSystem.InRange(user, targetEntity, 2f)) - return; - - if (!_handsSystem.TryGetActiveItem(user, out var heldItem) || - !_idCard.TryFindIdCard(heldItem.Value, out var idCard) || - !TryComp(idCard.Owner, out var accessComp)) - return; - - if (comp.IsLocked) - return; - - if (!_playerManager.TryGetSessionByEntity(user, out var session)) - return; - - _quickDialog.OpenDialog(session, Loc.GetString("stabikor-dialog-title"), Loc.GetString("stabikor-dialog-field") + "\n", (string input) => - { - if (!EntityManager.EntityExists(uid)) - return; - - if (string.IsNullOrEmpty(input)) - { - comp.Duration = 60f; - _popup.PopupEntity(Loc.GetString("stabikor-duration-set-cancel-fallback", ("time", 1)), uid, user); - return; - } - - if (!int.TryParse(input, out var durationMinutes) || durationMinutes < 1 || durationMinutes > 900) - { - _popup.PopupEntity(Loc.GetString("stabikor-dialog-invalid-range"), user, user, PopupType.SmallCaution); - _audio.PlayPvs(comp.DenySound, uid); - return; - } - - comp.Duration = durationMinutes * 60f; - _popup.PopupEntity(Loc.GetString("stabikor-duration-set-success", ("time", durationMinutes)), uid, user); - - _audio.PlayPvs(comp.UnlockSound, uid); - Dirty(uid, comp); - }); - } - - public void IsLocked(EntityUid uid, StabikorComponent comp, EntityUid user) - { - _containerSystem.TryGetContainingContainer((uid, null, null), out var container); - var parent = container?.Owner ?? EntityUid.Invalid; - var isInContainer = parent.IsValid(); - - var targetEntity = isInContainer ? parent : uid; - if (!_transformSystem.InRange(user, targetEntity, 2f)) - return; - - if (!_handsSystem.TryGetActiveItem(user, out var heldItem) || - !_idCard.TryFindIdCard(heldItem.Value, out var idCard) || - !TryComp(idCard.Owner, out var accessComp)) - return; - - var cardAccess = accessComp.Tags; - - if (comp.IsLocked) - { - if (comp.UnlockAccess.Exists(proto => cardAccess.Contains(proto.Id))) - { - if (!RemoveStabikor(uid, comp)) - return; - } - else - { - _popup.PopupEntity(Loc.GetString("stabikor-unlocked-deny"), uid, user, PopupType.SmallCaution); - _audio.PlayPvs(comp.DenySound, uid); - } - } - else - { - if (comp.LockAccess.Exists(proto => cardAccess.Contains(proto.Id))) - { - if (!ActivateStabikor(uid, parent, comp, user)) - return; - } - else - { - _popup.PopupEntity(Loc.GetString("stabikor-locked-deny"), uid, user, PopupType.SmallCaution); - _audio.PlayPvs(comp.DenySound, uid); - } - } - } - - private bool ActivateStabikor(EntityUid uid, EntityUid wearerUid, StabikorComponent comp, EntityUid user) - { - if (comp.IsActive) - return false; - - if (_inventorySystem.TryGetSlotEntity(wearerUid, "gloves", out var glovesItem) && glovesItem == uid || - _inventorySystem.TryGetSlotEntity(wearerUid, "neck", out var neckItem) && neckItem == uid) - { - comp.Timer = comp.Duration; - comp.IsLocked = true; - comp.IsActive = true; - comp.WearingEntity = wearerUid; - Dirty(uid, comp); - - _audio.PlayPvs(comp.LockSound, uid); - - _popup.PopupEntity(Loc.GetString("stabikor-activated-success", ("item", uid), ("user", Loc.GetString(Name(wearerUid)))), uid); - - return true; - } - - _audio.PlayPvs(comp.DenySound, uid); - - _popup.PopupEntity(Loc.GetString("stabikor-not-equipped"), uid, user); - return false; - } - - private bool RemoveStabikor(EntityUid uid, StabikorComponent comp) - { - comp.Timer = 0f; - comp.IsLocked = false; - comp.IsActive = false; - - if (comp.WearingEntity is not { Valid: true } user) - { - Dirty(uid, comp); - return false; - } - comp.WearingEntity = null; - Dirty(uid, comp); - - if (!_inventorySystem.TryGetContainingSlot(uid, out var slotDef)) - return false; - - if (!_inventorySystem.TryUnequip(user, slotDef.Name, force: true)) - return false; - - _audio.PlayPvs(comp.UnlockSound, uid); - - _popup.PopupEntity(Loc.GetString("stabikor-moment-shutdown", ("item", uid), ("user", Loc.GetString(Name(user)))), uid); - - return true; - } - - private void OnOpenDialogNetworkReceived(OpenDialogEvent msg, EntitySessionEventArgs args) - { - var uid = GetEntity(msg.Verp); - var user = args.SenderSession.AttachedEntity; - - if (user == null) - return; - - if (TryComp(uid, out var comp)) - OpenDialog(uid, comp, user.Value); - } - - private void OnIsLockedNetworkReceived(ToggleLockEvent msg, EntitySessionEventArgs args) - { - var uid = GetEntity(msg.Verp); - - var user = args.SenderSession.AttachedEntity; - - if (user == null) - return; - - if (TryComp(uid, out var comp)) - IsLocked(uid, comp, user.Value); - } -} diff --git a/Content.Shared/_Arcane/Stabikor/Components/StabikorComponent.cs b/Content.Shared/_Arcane/AggressionInhibitor/Components/AggressionInhibitorComponent.cs similarity index 70% rename from Content.Shared/_Arcane/Stabikor/Components/StabikorComponent.cs rename to Content.Shared/_Arcane/AggressionInhibitor/Components/AggressionInhibitorComponent.cs index f7748307b5a..021f9ef4e67 100644 --- a/Content.Shared/_Arcane/Stabikor/Components/StabikorComponent.cs +++ b/Content.Shared/_Arcane/AggressionInhibitor/Components/AggressionInhibitorComponent.cs @@ -2,16 +2,14 @@ using Robust.Shared.Prototypes; using Robust.Shared.Audio; using Robust.Shared.GameStates; -using Robust.Shared.Serialization; -using Content.Shared.Tag; -namespace Content.Shared._Arcane.Stabikor.Components; +namespace Content.Shared._Arcane.AggressionInhibitor.Components; /// /// A component that punishes creatures with a bad tone with electric shocks /// [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] -public sealed partial class StabikorComponent : Component +public sealed partial class AggressionInhibitorComponent : Component { /// /// Stores the UID of the player who is wearing the object @@ -25,7 +23,7 @@ public sealed partial class StabikorComponent : Component /// /// The time in seconds for which the object is blocked /// - [DataField, AutoNetworkedField] + [AutoNetworkedField] public float Duration = 60f; /// @@ -44,13 +42,14 @@ public sealed partial class StabikorComponent : Component /// Electric shock damage after punishment /// [DataField] - public int Damage = 5; + public int Damage = 10; /// - /// The time of the knockout after the punishment + /// The time of the knockout after the punishment (in seconds). /// [DataField] - public float TimeStan = 5.0f; + public float TimeStun = 10.0f; + /// /// Object status: blocked or not @@ -61,23 +60,14 @@ public sealed partial class StabikorComponent : Component /// /// Who can CLOSE the object /// - [DataField, AutoNetworkedField] - public List> LockAccess = new() - { - "Security" - }; + [DataField(required: true), AutoNetworkedField] + public List> LockAccess = new(); /// /// Who can OPEN the object /// - [DataField, AutoNetworkedField] - public List> UnlockAccess = new() - { - "Armory", - "HeadOfSecurity", - "Captain", - "CentralCommand" - }; + [DataField(required: true), AutoNetworkedField] + public List> UnlockAccess = new(); /// /// Sound of successful blocking @@ -99,13 +89,20 @@ public sealed partial class StabikorComponent : Component } -[Serializable, NetSerializable] -public sealed class OpenDialogEvent(NetEntity verp) : EntityEventArgs +/// +/// A local event for requesting the opening of the time dialog +/// +public sealed class OpenDialogEvent(EntityUid target, EntityUid user) : EntityEventArgs { - public NetEntity Verp = verp; + public EntityUid Target { get; } = target; + public EntityUid User { get; } = user; } -[Serializable, NetSerializable] -public sealed class ToggleLockEvent(NetEntity verp) : EntityEventArgs + +/// +/// A local event for requesting a lock change (ToggleLock) +/// +public sealed class ToggleLockEvent(EntityUid target, EntityUid user) : EntityEventArgs { - public NetEntity Verp = verp; + public EntityUid Target { get; } = target; + public EntityUid User { get; } = user; } diff --git a/Content.Shared/_Arcane/AggressionInhibitor/Systems/SharedAggressionInhibitorSystem.cs b/Content.Shared/_Arcane/AggressionInhibitor/Systems/SharedAggressionInhibitorSystem.cs new file mode 100644 index 00000000000..96541ec1e46 --- /dev/null +++ b/Content.Shared/_Arcane/AggressionInhibitor/Systems/SharedAggressionInhibitorSystem.cs @@ -0,0 +1,199 @@ +using Content.Shared._Arcane.AggressionInhibitor.Components; +using Robust.Shared.Containers; +using Content.Shared.Popups; +using Content.Shared.Examine; +using Content.Shared.Inventory.Events; +using Content.Shared.Access.Components; +using Content.Shared.Verbs; +using Robust.Shared.Utility; +using Content.Shared.Access.Systems; +using Content.Shared.Hands.EntitySystems; +using Robust.Shared.Timing; +using Robust.Shared.Network; +using Content.Shared.CombatMode; +using Content.Shared.Inventory; +using Content.Shared.Electrocution; + +namespace Content.Shared._Arcane.AggressionInhibitor.Systems; + +public sealed partial class SharedAggressionInhibitorSystem : EntitySystem +{ + [Dependency] private SharedPopupSystem _popup = default!; + [Dependency] private SharedIdCardSystem _idCard = default!; + [Dependency] private SharedHandsSystem _handsSystem = default!; + [Dependency] private SharedContainerSystem _containerSystem = default!; + [Dependency] private IGameTiming _gameTiming = default!; + [Dependency] private INetManager _netManager = default!; + [Dependency] private InventorySystem _inventorySystem = default!; + [Dependency] private SharedElectrocutionSystem _electrocution = default!; + [Dependency] private SharedCombatModeSystem _combatMode = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnToggleCombatAction, before: [typeof(SharedCombatModeSystem)]); + SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent(OnGotUnequipped); + SubscribeLocalEvent(OnBeingUnequippedAttempt); + SubscribeLocalEvent>(OnGetActivationVerbs); + } + + private void OnToggleCombatAction(ToggleCombatActionEvent args) + { + if (args.Handled || _netManager.IsClient) + return; + + var user = args.Performer; + + if (TryComp(user, out var mobState)) + { + if (mobState.CurrentState == Mobs.MobState.Critical || + mobState.CurrentState == Mobs.MobState.Dead) + return; + } + + EntityUid inhibitorItem = default; + AggressionInhibitorComponent? inhibitorComp = null; + + var slotEnumerator = _inventorySystem.GetSlotEnumerator(user); + while (slotEnumerator.MoveNext(out var containerSlot)) + { + var slotItem = containerSlot.ContainedEntity; + if (slotItem == null) + continue; + + var uid = slotItem.Value; + + if (_inventorySystem.TryGetContainingSlot(uid, out var slotDef)) + { + if ((slotDef.SlotFlags & SlotFlags.POCKET) != 0) + continue; + } + + if (TryComp(uid, out var comp) && comp.IsActive) + { + inhibitorItem = uid; + inhibitorComp = comp; + break; + } + } + + if (inhibitorComp == null) + return; + + if (!_combatMode.IsInCombatMode(user)) + { + _electrocution.TryDoElectrocution(user, inhibitorItem, inhibitorComp.Damage, TimeSpan.FromSeconds(inhibitorComp.TimeStun), refresh: false, ignoreInsulation: true); + + _popup.PopupEntity(Loc.GetString("stabikor-disarm-shock-popup"), user, user, PopupType.LargeCaution); + + _combatMode.SetInCombatMode(user, false); + + args.Handled = true; + } + } + + private void OnBeingUnequippedAttempt(EntityUid uid, AggressionInhibitorComponent comp, BeingUnequippedAttemptEvent args) + { + if (comp.IsLocked || comp.IsActive) + { + _popup.PopupPredicted(Loc.GetString("stabikor-unequip-blocked-active"), uid, args.Unequipee, PopupType.SmallCaution); + args.Cancel(); + } + } + + private void OnGotUnequipped(EntityUid uid, AggressionInhibitorComponent comp, ref GotUnequippedEvent args) + { + if (comp.IsActive) + { + if (_netManager.IsServer) + { + comp.Timer = 0; + Dirty(uid, comp); + } + } + } + + private void OnExamine(EntityUid uid, AggressionInhibitorComponent comp, ExaminedEvent args) + { + var state = comp.IsLocked ? "stabikor-examine-locked" : "stabikor-examine-unlocked"; + args.PushMarkup(Loc.GetString("stabikor-examine-status-main", ("mode", Loc.GetString(state)))); + + var durationTotalMinutes = (int) (comp.Duration / 60f); + var durationHours = durationTotalMinutes / 60; + var durationMinutes = durationTotalMinutes % 60; + + args.PushMarkup(Loc.GetString("stabikor-examine-duration-info", + ("hours", durationHours), + ("minutes", durationMinutes))); + + if (comp.IsLocked && comp.Timer > 0) + { + var remainingTotalMinutes = (int) (comp.Timer / 60f); + var remainingHours = remainingTotalMinutes / 60; + var remainingMinutes = remainingTotalMinutes % 60; + var remainingSeconds = (int) (comp.Timer % 60f); + + args.PushMarkup(Loc.GetString("stabikor-examine-timer-remaining", + ("hours", remainingHours), + ("minutes", remainingMinutes), + ("seconds", remainingSeconds))); + } + } + + private void OnGetActivationVerbs(EntityUid uid, AggressionInhibitorComponent comp, GetVerbsEvent args) + { + var isInContainer = _containerSystem.TryGetContainingContainer((uid, null, null), out var container) + && container.Owner.IsValid(); + + if (!isInContainer && (!args.CanAccess || !args.CanInteract)) + return; + + if (!_handsSystem.TryGetActiveItem(args.User, out var heldItem) || + !_idCard.TryFindIdCard(heldItem.Value, out var idCard) || + !TryComp(idCard.Owner, out var accessComp)) + return; + + var settingsIcon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")); + + if (!comp.IsLocked) + { + args.Verbs.Add(new ActivationVerb() + { + Text = Loc.GetString("stabikor-verb-set-duration"), + Icon = settingsIcon, + Act = () => FlipOpenDialog(uid, args.User, comp) + }); + } + + var verbText = comp.IsLocked ? "stabikor-verb-unlock" : "stabikor-verb-lock"; + + args.Verbs.Add(new ActivationVerb() + { + Text = Loc.GetString(verbText), + Icon = settingsIcon, + Act = () => FlipToggleLock(uid, args.User, comp) + }); + } + + private void FlipOpenDialog(EntityUid uid, EntityUid user, AggressionInhibitorComponent comp) + { + if (_gameTiming.CurTime < comp.LastVerbClickTime + TimeSpan.FromSeconds(0.4)) + return; + + comp.LastVerbClickTime = _gameTiming.CurTime; + + RaiseLocalEvent(uid, new OpenDialogEvent(uid, user)); + } + + private void FlipToggleLock(EntityUid uid, EntityUid user, AggressionInhibitorComponent comp) + { + if (_gameTiming.CurTime < comp.LastVerbClickTime + TimeSpan.FromSeconds(0.4)) + return; + + comp.LastVerbClickTime = _gameTiming.CurTime; + + RaiseLocalEvent(uid, new ToggleLockEvent(uid, user)); + } +} diff --git a/Content.Shared/_Arcane/CuttableItem/Components/CuttableItemComponent.cs b/Content.Shared/_Arcane/CuttableItem/Components/CuttableItemComponent.cs index 2d74ad600cd..7692f3d43b6 100644 --- a/Content.Shared/_Arcane/CuttableItem/Components/CuttableItemComponent.cs +++ b/Content.Shared/_Arcane/CuttableItem/Components/CuttableItemComponent.cs @@ -1,6 +1,7 @@ using Robust.Shared.GameStates; using Content.Shared.Radio; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Content.Shared.Tools; +using Robust.Shared.Prototypes; namespace Content.Shared._Arcane.CuttableItem.Components; @@ -13,35 +14,31 @@ namespace Content.Shared._Arcane.CuttableItem.Components; public sealed partial class CuttableItemComponent : Component { /// - /// A list of quality tools that can be used to cut this object. + /// The quality of the tools needed to cut the object. /// - [DataField, AutoNetworkedField] - public List ToolQualities = new() - { - "Sawing" - }; + [DataField(required: true), AutoNetworkedField] + public List> ToolQualities = new(); /// - /// The time in seconds required to cut the object. + /// The time in seconds required to cut through the object. /// [DataField, AutoNetworkedField] public float Delay = 45.0f; /// - /// The ID of the prototype radio channel for sending the notification. + /// The ID of the prototype radio channel for sending notifications when an attempt is made to cut through. /// - [DataField(customTypeSerializer: typeof(PrototypeIdSerializer)), AutoNetworkedField] - public string RadioChannel = "Security"; + [DataField, AutoNetworkedField] + public ProtoId RadioChannel = "Security"; /// - /// The localization key for the message in the communication channel. + /// The localization key for the message being sent to the communication channel. /// [DataField, AutoNetworkedField] - public string AlertMessage = "cuttable-item-alert-activated"; + public LocId AlertMessage = "cuttable-item-alert-activated"; } -public sealed class CuttableCutEvent(EntityUid user, EntityUid item) : HandledEntityEventArgs +public sealed class CuttableCutEvent(EntityUid user) : HandledEntityEventArgs { public EntityUid User { get; } = user; - public EntityUid Item { get; } = item; } diff --git a/Content.Shared/_Arcane/CuttableItem/Systems/SharedCuttableItemSystem.cs b/Content.Shared/_Arcane/CuttableItem/Systems/SharedCuttableItemSystem.cs index 0dad83d9bc0..8b40930c35e 100644 --- a/Content.Shared/_Arcane/CuttableItem/Systems/SharedCuttableItemSystem.cs +++ b/Content.Shared/_Arcane/CuttableItem/Systems/SharedCuttableItemSystem.cs @@ -1,10 +1,8 @@ using Content.Shared._Arcane.CuttableItem.Components; using Content.Shared.Interaction; -using Content.Shared.Tools; using Content.Shared.DoAfter; using Content.Shared.CuttableItem; using Content.Shared.Popups; -using Content.Shared.Inventory; using Content.Shared.Tools.Systems; using Robust.Shared.Network; using Content.Shared.Examine; @@ -13,14 +11,12 @@ namespace Content.Shared._Arcane.CuttableItem.Systems; -public sealed class SharedCuttableItemSystem : EntitySystem +public sealed partial class SharedCuttableItemSystem : EntitySystem { - [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; - [Dependency] private readonly SharedToolSystem _toolSystem = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly InventorySystem _inventorySystem = default!; - [Dependency] private readonly SharedTransformSystem _transformSystem = default!; - [Dependency] private readonly INetManager _netManager = default!; + [Dependency] private SharedDoAfterSystem _doAfter = default!; + [Dependency] private SharedToolSystem _toolSystem = default!; + [Dependency] private SharedPopupSystem _popup = default!; + [Dependency] private INetManager _netManager = default!; [Dependency] private IPrototypeManager _prototypeManager = default!; public override void Initialize() @@ -28,78 +24,38 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnInteractUsing); - SubscribeLocalEvent(OnCutCompleted); SubscribeLocalEvent(OnExamined); } private void OnInteractUsing(EntityUid uid, CuttableItemComponent comp, InteractUsingEvent args) { - var toolFound = false; + if (args.Handled || _netManager.IsClient) + return; - foreach (var quality in comp.ToolQualities) + var toolFound = false; + for (var i = 0; i < comp.ToolQualities.Count; i++) { - if (_toolSystem.HasQuality(args.Used, quality)) + if (_toolSystem.HasQuality(args.Used, comp.ToolQualities[i])) { toolFound = true; break; } } - if (_netManager.IsClient || !toolFound) + if (!toolFound) return; - args.Handled = true; - - var doAfterArgs = new DoAfterArgs(EntityManager, args.User, comp.Delay, new CuttableDoAfterEvent(), uid, target: uid, used: args.Used) + var doAfterArgs = new DoAfterArgs(EntityManager, args.User, TimeSpan.FromSeconds(comp.Delay), new CuttableDoAfterEvent(), uid, target: uid, used: args.Used) { BreakOnMove = true, BreakOnDamage = true, NeedHand = true }; - _popup.PopupEntity(Loc.GetString("cuttable-item-attempt-broken-popup", ("item", uid), ("user", Loc.GetString(Name(args.User)))), uid); - - _doAfter.TryStartDoAfter(doAfterArgs); - Dirty(uid, comp); - } - - private void OnCutCompleted(EntityUid uid, CuttableItemComponent comp, CuttableDoAfterEvent args) - { - if (args.Cancelled || args.Handled || _netManager.IsClient) - return; - - args.Handled = true; - - var victim = Transform(uid).ParentUid; - - if (victim.IsValid()) + if (_doAfter.TryStartDoAfter(doAfterArgs)) { - if (_inventorySystem.TryGetSlots(victim, out var slotDefinitions)) - { - if (_netManager.IsClient) - return; - - foreach (var slotDef in slotDefinitions) - { - if (_inventorySystem.TryGetSlotEntity(victim, slotDef.Name, out var slotEntity) && slotEntity == uid) - { - var target = args.User; - - if (_inventorySystem.TryUnequip(target, victim, slotDef.Name, force: true)) - { - _transformSystem.AttachToGridOrMap(uid); - - var victimCoords = Transform(victim).Coordinates; - _transformSystem.SetCoordinates(uid, victimCoords); - _popup.PopupEntity(Loc.GetString("cuttable-item-broken-moment-popup", ("item", uid)), uid); - - var ev = new CuttableCutEvent(target, uid); - RaiseLocalEvent(uid, ev); - } - break; - } - } - } + args.Handled = true; + _popup.PopupEntity(Loc.GetString("cuttable-item-attempt-broken-popup", ("item", uid), ("user", Name(args.User))), uid); } } @@ -113,7 +69,7 @@ private void OnExamined(EntityUid uid, CuttableItemComponent comp, ref ExaminedE foreach (var qualityId in comp.ToolQualities) { - if (_prototypeManager.TryIndex(qualityId, out var qualityProto)) + if (_prototypeManager.TryIndex(qualityId, out var qualityProto)) { var qualityName = Loc.GetString(qualityProto.Name); message.AddMarkupOrThrow($" - [color=yellow]{qualityName}[/color]\n"); diff --git a/Content.Shared/_Arcane/Stabikor/Systems/SharedStabikorSystem.cs b/Content.Shared/_Arcane/Stabikor/Systems/SharedStabikorSystem.cs deleted file mode 100644 index e5105bb235d..00000000000 --- a/Content.Shared/_Arcane/Stabikor/Systems/SharedStabikorSystem.cs +++ /dev/null @@ -1,141 +0,0 @@ -using Content.Shared._Arcane.Stabikor.Components; -using Robust.Shared.Containers; -using Content.Shared.Popups; -using Content.Shared.Examine; -using Content.Shared.Inventory.Events; -using Content.Shared.Access.Components; -using Content.Shared.Verbs; -using Robust.Shared.Utility; -using Content.Shared.Access.Systems; -using Content.Shared.Hands.EntitySystems; -using Robust.Shared.Timing; -using Robust.Shared.Network; - -namespace Content.Shared._Arcane.Stabikor.Systems; - -public sealed class SharedStabikorSystem : EntitySystem -{ - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly SharedIdCardSystem _idCard = default!; - [Dependency] private readonly SharedHandsSystem _handsSystem = default!; - [Dependency] private SharedContainerSystem _containerSystem = default!; - [Dependency] private IGameTiming _gameTiming = default!; - [Dependency] private readonly INetManager _netManager = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnExamine); - SubscribeLocalEvent(OnGotUnequipped); - SubscribeLocalEvent(OnBeingUnequippedAttempt); - SubscribeLocalEvent>(OnGetActivationVerbs); - } - - private void OnBeingUnequippedAttempt(EntityUid uid, StabikorComponent comp, BeingUnequippedAttemptEvent args) - { - if (comp.IsLocked || comp.IsActive) - { - if (!_netManager.IsClient) - _popup.PopupEntity(Loc.GetString("stabikor-unequip-blocked-active"), uid, args.Unequipee, PopupType.SmallCaution); - - args.Cancel(); - } - } - - private void OnGotUnequipped(EntityUid uid, StabikorComponent comp, ref GotUnequippedEvent args) - { - if (comp.IsActive) - comp.Timer = 0; - - if (!_netManager.IsClient) - Dirty(uid, comp); - - } - - private void OnExamine(EntityUid uid, StabikorComponent comp, ExaminedEvent args) - { - var state = comp.IsLocked ? "stabikor-examine-locked" : "stabikor-examine-unlocked"; - - args.PushMarkup(Loc.GetString("stabikor-examine-status-main", ("mode", Loc.GetString(state)))); - - var durationTotalMinutes = (int) (comp.Duration / 60f); - var durationHours = durationTotalMinutes / 60; - var durationMinutes = durationTotalMinutes % 60; - - args.PushMarkup(Loc.GetString("stabikor-examine-duration-info", - ("hours", durationHours), - ("minutes", durationMinutes))); - - if (comp.IsLocked && comp.Timer > 0) - { - var remainingTotalMinutes = (int) (comp.Timer / 60f); - var remainingHours = remainingTotalMinutes / 60; - var remainingMinutes = remainingTotalMinutes % 60; - var remainingSeconds = (int) (comp.Timer % 60f); - - args.PushMarkup(Loc.GetString("stabikor-examine-timer-remaining", - ("hours", remainingHours), - ("minutes", remainingMinutes), - ("seconds", remainingSeconds))); - } - Dirty(uid, comp); - } - - private void OnGetActivationVerbs(EntityUid uid, StabikorComponent comp, GetVerbsEvent args) - { - if (!_containerSystem.TryGetContainingContainer((uid, null, null), out var container)) - return; - - var parent = container.Owner; - var isInContainer = parent.IsValid(); - - if (!isInContainer && (!args.CanAccess || !args.CanInteract)) - return; - - if (!_handsSystem.TryGetActiveItem(args.User, out var heldItem) || - !_idCard.TryFindIdCard(heldItem.Value, out var idCard) || - !TryComp(idCard.Owner, out var accessComp)) - return; - - var settingsIcon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")); - - if (!comp.IsLocked) - { - args.Verbs.Add(new ActivationVerb() - { - Text = Loc.GetString("stabikor-verb-set-duration"), - Icon = settingsIcon, - Act = () => FlipOpenDialog(uid, comp) - }); - } - - var verbText = comp.IsLocked ? "stabikor-verb-unlock" : "stabikor-verb-lock"; - - args.Verbs.Add(new ActivationVerb() - { - Text = Loc.GetString(verbText), - Icon = settingsIcon, - Act = () => FlipToggleLock(uid, comp) - }); - } - - private void FlipOpenDialog(EntityUid uid, StabikorComponent comp) - { - if (_gameTiming.CurTime < comp.LastVerbClickTime + TimeSpan.FromSeconds(0.4)) - return; - - comp.LastVerbClickTime = _gameTiming.CurTime; - - RaiseNetworkEvent(new OpenDialogEvent(GetNetEntity(uid))); - } - - private void FlipToggleLock(EntityUid uid, StabikorComponent comp) - { - if (_gameTiming.CurTime < comp.LastVerbClickTime + TimeSpan.FromSeconds(0.4)) - return; - - comp.LastVerbClickTime = _gameTiming.CurTime; - - RaiseNetworkEvent(new ToggleLockEvent(GetNetEntity(uid))); - } -} diff --git a/Resources/Locale/en-US/_Arcane/aggression-Inhibitor/aggression_inhibitor_component.ftl b/Resources/Locale/en-US/_Arcane/aggression-Inhibitor/aggression_inhibitor_component.ftl new file mode 100644 index 00000000000..9aac8b4287b --- /dev/null +++ b/Resources/Locale/en-US/_Arcane/aggression-Inhibitor/aggression_inhibitor_component.ftl @@ -0,0 +1,79 @@ +stabikor-activate-verb = Activate +stabikor-activated-success = The { $item } snaps shut! +stabikor-not-equipped = Object not detected. +stabikor-moment-shutdown = The { $item } unfastens and falls to the ground! +stabikor-disarm-shock-popup = You are severely shocked for attempting aggression! Combat mode disabled. + +stabikor-examine-status-main = The device mode indicator lights up: [color=yellow]{$mode}[/color]. + +stabikor-examine-duration-info = Operation time is set to{ $hours -> + [0] { "" } + [one] {" "}[color=cyan]{$hours}[/color] hour + *[other] {" "}[color=cyan]{$hours}[/color] hours +}{ $hours -> + [0] { "" } + *[other] { $minutes -> + [0] { "" } + *[other] {" and"} + } +}{ $minutes -> + [0] { "" } + [one] {" "}[color=cyan]{$minutes}[/color] minute + *[other] {" "}[color=cyan]{$minutes}[/color] minutes +}. + +stabikor-examine-timer-remaining = + Time remaining until shutdown:{ $hours -> + [0] { "" } + *[other] {" "}[color=orange]{$hours}[/color] { $hours -> + [one] hour + *[other] hours + } + }{ $hours -> + [0] { "" } + *[other] { $minutes -> + [0] { "" } + *[other] {" and"} + } + }{ $minutes -> + [0] { "" } + *[other] {" "}[color=orange]{$minutes}[/color] { $minutes -> + [one] minute + *[other] minutes + } + }{ $hours -> + [0] { $minutes -> + [0] { "" } + *[other] { $seconds -> + [0] { "" } + *[other] {" and"} + } + } + *[other] { $seconds -> + [0] { "" } + *[other] {" and"} + } + }{ $seconds -> + [0] { "" } + *[other] {" "}[color=orange]{$seconds}[/color] { $seconds -> + [one] second + *[other] seconds + } + }. + +stabikor-examine-locked = locked +stabikor-examine-unlocked = standby + +stabikor-verb-set-duration = Set Timer +stabikor-verb-lock = Activate Lock +stabikor-verb-unlock = Release Lock + +stabikor-dialog-title = Timer Configuration +stabikor-dialog-field = + Enter the device operation time + (in minutes, from 1 to 900): +stabikor-dialog-invalid-range = Invalid time range! Enter a number from 1 to 900. +stabikor-duration-set-success = Operation time successfully set to {$time} min. +stabikor-duration-set-cancel-fallback = Input canceled, set to default time: {$time} min. + +stabikor-unequip-blocked-active = The device cannot be removed until the timer expires! diff --git a/Resources/Locale/en-US/_Arcane/cuttable/cuttable_Item_component.ftl b/Resources/Locale/en-US/_Arcane/cuttable/cuttable_Item_component.ftl new file mode 100644 index 00000000000..bf44ade3af2 --- /dev/null +++ b/Resources/Locale/en-US/_Arcane/cuttable/cuttable_Item_component.ftl @@ -0,0 +1,5 @@ +cuttable-item-broken-moment-popup = The { $item } has been cut off! +cuttable-item-attempt-broken-popup = { $user } is attempting to cut the { $item }! +cuttable-item-examine-header = It looks like you can break free using a tool with quality: + +cuttable-item-alert-activated = Unauthorized removal of { $item } detected. Perpetrator: { $user } diff --git a/Resources/Locale/en-US/_Arcane/entities/clothing/other/aggression_inhibitor.ftl b/Resources/Locale/en-US/_Arcane/entities/clothing/other/aggression_inhibitor.ftl new file mode 100644 index 00000000000..40b4962185c --- /dev/null +++ b/Resources/Locale/en-US/_Arcane/entities/clothing/other/aggression_inhibitor.ftl @@ -0,0 +1,2 @@ +ent-ClothingAggressionInhibitor = aggression inhibitor + .desc = Stabilizes the behavior of assistants using electrical current! An ID card reader can be seen on the casing. diff --git a/Resources/Locale/ru-RU/_Arcane/aggression-Inhibitor/aggression_inhibitor_component.ftl b/Resources/Locale/ru-RU/_Arcane/aggression-Inhibitor/aggression_inhibitor_component.ftl new file mode 100644 index 00000000000..fb2d35af282 --- /dev/null +++ b/Resources/Locale/ru-RU/_Arcane/aggression-Inhibitor/aggression_inhibitor_component.ftl @@ -0,0 +1,84 @@ +stabikor-activate-verb = Активировать +stabikor-activated-success = { $item } защелкивается! +stabikor-not-equipped = Объект не обнаружен. +stabikor-moment-shutdown = { $item } расстегивается и падает на землю! +stabikor-disarm-shock-popup = Вас сильно бьет током за попытку агрессии! Боевой режим отключен. + +stabikor-examine-status-main = На устройстве горит индикатор режима: [color=yellow]{$mode}[/color]. + +stabikor-examine-duration-info = Время работы настроено на{ $hours -> + [0] { "" } + [one] {" "}[color=cyan]{$hours}[/color] час + [few] {" "}[color=cyan]{$hours}[/color] часа + *[other] {" "}[color=cyan]{$hours}[/color] часов +}{ $hours -> + [0] { "" } + *[other] { $minutes -> + [0] { "" } + *[other] {" и"} + } +}{ $minutes -> + [0] { "" } + [one] {" "}[color=cyan]{$minutes}[/color] минуту + [few] {" "}[color=cyan]{$minutes}[/color] минуты + *[other] {" "}[color=cyan]{$minutes}[/color] минут +}. + +stabikor-examine-timer-remaining = + До отключения осталось:{ $hours -> + [0] { "" } + *[other] {" "}[color=orange]{$hours}[/color] { $hours -> + [one] час + [few] часа + *[other] часов + } + }{ $hours -> + [0] { "" } + *[other] { $minutes -> + [0] { "" } + *[other] {" и"} + } + }{ $minutes -> + [0] { "" } + *[other] {" "}[color=orange]{$minutes}[/color] { $minutes -> + [one] минута + [few] минуты + *[other] минут + } + }{ $hours -> + [0] { $minutes -> + [0] { "" } + *[other] { $seconds -> + [0] { "" } + *[other] {" и"} + } + } + *[other] { $seconds -> + [0] { "" } + *[other] {" и"} + } + }{ $seconds -> + [0] { "" } + *[other] {" "}[color=orange]{$seconds}[/color] { $seconds -> + [one] секунда + [few] секунды + *[other] секунд + } + }. + +stabikor-examine-locked = заблокирован +stabikor-examine-unlocked = ожидание + +stabikor-verb-set-duration = Настроить таймер +stabikor-verb-lock = Активировать блокировку +stabikor-verb-unlock = Снять блокировку + +stabikor-dialog-title = Конфигурация таймера +stabikor-dialog-field = + Введите время работы устройства + (в минутах, от 1 до 900): +stabikor-dialog-invalid-range = Неверный диапазон времени! Введите число от 1 до 900. +stabikor-duration-set-success = Время работы успешно настроено на {$time} мин. +stabikor-duration-set-cancel-fallback = Ввод отменён, установлено время по умолчанию: {$time} мин. + +stabikor-unequip-blocked-active = Устройство невозможно снять до окончания таймера! diff --git "a/Resources/Locale/ru-RU/_Arcane/\321\201uttableItem/cuttable-Item-component.ftl" b/Resources/Locale/ru-RU/_Arcane/cuttableItem/cuttable_Item_component.ftl similarity index 100% rename from "Resources/Locale/ru-RU/_Arcane/\321\201uttableItem/cuttable-Item-component.ftl" rename to Resources/Locale/ru-RU/_Arcane/cuttableItem/cuttable_Item_component.ftl diff --git a/Resources/Locale/ru-RU/_Arcane/entities/clothing/other/aggression_inhibitor.ftl b/Resources/Locale/ru-RU/_Arcane/entities/clothing/other/aggression_inhibitor.ftl new file mode 100644 index 00000000000..e174ca6b914 --- /dev/null +++ b/Resources/Locale/ru-RU/_Arcane/entities/clothing/other/aggression_inhibitor.ftl @@ -0,0 +1,2 @@ +ent-ClothingAggressionInhibitor = ингибитор агрессии + .desc = Стабилизирует поведение ассистентов с помощью силы тока! На корпусе виднеется считыватель ID-карт. diff --git a/Resources/Locale/ru-RU/_Arcane/entities/clothing/other/stabikor.ftl b/Resources/Locale/ru-RU/_Arcane/entities/clothing/other/stabikor.ftl deleted file mode 100644 index 3e6bd794bc0..00000000000 --- a/Resources/Locale/ru-RU/_Arcane/entities/clothing/other/stabikor.ftl +++ /dev/null @@ -1,2 +0,0 @@ -ent-ClothingStabikor = устройство "стаби-кор" - .desc = Ингибитор насилия для подавления агрессивных ассистентов. diff --git a/Resources/Locale/ru-RU/_Arcane/stabikor/stabikor-component.ftl b/Resources/Locale/ru-RU/_Arcane/stabikor/stabikor-component.ftl deleted file mode 100644 index 9e014e2ee30..00000000000 --- a/Resources/Locale/ru-RU/_Arcane/stabikor/stabikor-component.ftl +++ /dev/null @@ -1,53 +0,0 @@ -stabikor-activate-verb = Активировать -stabikor-activated-success = { $item } защелкивается! -stabikor-not-equipped = Объект не обнаружен. -stabikor-moment-shutdown = { $item } расстегивается и падает на землю! -stabikor-disarm-shock-popup = Вас сильно бьет током за попытку агрессии! Боевой режим отключен. - -stabikor-locked-deny = У вас недостаточно прав для блокировки этого устройства! -stabikor-unlocked-deny = У вас недостаточно прав для разблокировки этого устройства! - -stabikor-examine-status-main = На устройстве горит индикатор режима: [color=yellow]{$mode}[/color]. - -stabikor-examine-duration-info = - Время работы настроено на [color=cyan]{$minutes}[/color] { $minutes -> - [one] минуту - [few] минуты - *[other] минут - }. -stabikor-examine-timer-remaining = - До отключения осталось: { $hours -> - [0] { "" } - *[other] [color=orange]{$hours}[/color] { $hours -> - [one] час - [few] часа - *[other] часов - } - } { $minutes -> - [0] { "" } - *[other] [color=orange]{$minutes}[/color] { $minutes -> - [one] минута и - [few] минуты и - *[other] минут и - } - } [color=orange]{$seconds}[/color] { $seconds -> - [one] секунда - [few] секунды - *[other] секунд - }. - -stabikor-examine-locked = заблокирован -stabikor-examine-unlocked = ожидание - -stabikor-verb-set-duration = Настроить таймер -stabikor-verb-lock = Активировать блокировку -stabikor-verb-unlock = Снять блокировку - -stabikor-dialog-title = Конфигурация таймера -stabikor-dialog-field = - Введите время работы устройства - (в минутах, от 1 до 900): -stabikor-dialog-invalid-range = Неверный диапазон времени! Введите число от 1 до 900. -stabikor-duration-set-success = Время работы успешно настроено на {$time} мин. - -stabikor-unequip-blocked-active = Устройство невозможно снять до окончания таймера! diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_vending.yml b/Resources/Prototypes/Catalog/Cargo/cargo_vending.yml index c484db5f47a..a1d03566128 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_vending.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_vending.yml @@ -33,7 +33,7 @@ sprite: Objects/Specific/Service/vending_machine_restock.rsi state: base product: CrateVendingMachineRestockClothesFilled - cost: 5300 + cost: 6000 # Arcane-Edit 5300 > 6000 category: cargoproduct-category-name-service group: market diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/security.yml b/Resources/Prototypes/Catalog/Fills/Lockers/security.yml index 530ae5485e3..d6a1d8ba81c 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/security.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/security.yml @@ -44,7 +44,7 @@ - id: ClothingBackpackElectropack amount: 2 # Arcane-Start - - id: ClothingStabikor + - id: ClothingAggressionInhibitor amount: 2 # Arcane-End - id: RemoteSignaller diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index d66962c5c9d..97e795164d2 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -181,6 +181,7 @@ - SecurityAmmoStaticGoob - SyndicateAmmoLightStatic - ScienceExplosives + - MedicalBeamGunSyndicate - type: BlueprintReceiver whitelist: tags: diff --git a/Resources/Prototypes/Recipes/Lathes/Packs/medical.yml b/Resources/Prototypes/Recipes/Lathes/Packs/medical.yml index cd2a0d2a5cd..a013c41fb2d 100644 --- a/Resources/Prototypes/Recipes/Lathes/Packs/medical.yml +++ b/Resources/Prototypes/Recipes/Lathes/Packs/medical.yml @@ -52,6 +52,9 @@ - Drill - Saw - Hemostat + # Arcane-Start + - SawElectric + # Arcane-End - type: latheRecipePack id: MedicalStatic @@ -65,6 +68,10 @@ - OffsetCane - OffsetCaneWood - JetInjector + # Arcane-Start + - Bloodpack + - ClothingNeckStethoscope + # Arcane-End - type: latheRecipePack id: RollerBedsStatic @@ -79,6 +86,9 @@ - ClothingHandsGlovesLatex - ClothingHandsGlovesNitrile - ClothingMaskSterile + # Arcane-Start + - ClothingEyesNightVisionMedicalGoggles + # Arcane-End # These are all empty - type: latheRecipePack @@ -102,6 +112,14 @@ - ChemMasterMachineCircuitboard - CondenserMachineCircuitBoard - HotplateMachineCircuitboard + # Arcane-Start + - MedicalScannerMachineCircuitboard + - MedicalRecordsComputerCircuitboard + - CrewMonitoringComputerCircuitboard + - MedicalBiofabMachineBoard + - DiagnoserMachineCircuitboard + - VaccinatorMachineCircuitboard + # Arcane-End ## Dynamic diff --git a/Resources/Prototypes/Recipes/Lathes/Packs/security.yml b/Resources/Prototypes/Recipes/Lathes/Packs/security.yml index 07a5791daec..8932541fee6 100644 --- a/Resources/Prototypes/Recipes/Lathes/Packs/security.yml +++ b/Resources/Prototypes/Recipes/Lathes/Packs/security.yml @@ -60,6 +60,11 @@ - SpeedLoaderMagnum - SpeedLoaderMagnumEmpty # - ShrapnelPayload + # Arcane-Start + - MagazineShotgun + - MagazineShotgunEmpty + - MagazineShotgunSlug + # Arcane-End - type: latheRecipePack id: SecurityWeaponsStatic @@ -83,7 +88,7 @@ - SmokeGrenade # Orion-End - ClothingBackpackElectropack - - ClothingStabikor # Arcane + - ClothingAggressionInhibitor # Arcane - HoloprojectorSecurity - HoloprojectorSecurityAdvanced - AntiMindControlDevice diff --git a/Resources/Prototypes/Recipes/Lathes/security.yml b/Resources/Prototypes/Recipes/Lathes/security.yml index 19b74c0f39d..b240a2ef143 100644 --- a/Resources/Prototypes/Recipes/Lathes/security.yml +++ b/Resources/Prototypes/Recipes/Lathes/security.yml @@ -64,8 +64,8 @@ # Arcane-Start - type: latheRecipe - id: ClothingStabikor - result: ClothingStabikor + id: ClothingAggressionInhibitor + result: ClothingAggressionInhibitor categories: - Clothing completetime: 4 diff --git a/Resources/Prototypes/_Arcane/Entities/Clothing/Other/stabikor.yml b/Resources/Prototypes/_Arcane/Entities/Clothing/Other/inhibitor.yml similarity index 52% rename from Resources/Prototypes/_Arcane/Entities/Clothing/Other/stabikor.yml rename to Resources/Prototypes/_Arcane/Entities/Clothing/Other/inhibitor.yml index 2180873f33e..2ed239e3e82 100644 --- a/Resources/Prototypes/_Arcane/Entities/Clothing/Other/stabikor.yml +++ b/Resources/Prototypes/_Arcane/Entities/Clothing/Other/inhibitor.yml @@ -1,15 +1,15 @@ - type: entity parent: Clothing - id: ClothingStabikor - name: The Stabikor device - description: Aggression stabilizer for aggressive assistants + id: ClothingAggressionInhibitor + name: The aggression inhibitor + description: Stabilizes the behavior of assistants with the help of amperage! An ID card reader is visible on the case. components: - type: Item size: Small sprite: _Arcane/Clothing/Neck/Misc/petcollar.rsi - type: Clothing quickEquip: true - equipDelay: 1.5 + equipDelay: 2.5 slots: - neck - gloves @@ -18,10 +18,16 @@ state: icon sprite: _Arcane/Clothing/Neck/Misc/petcollar.rsi scale: 0.75,0.75 - - type: Tag - tags: - - Recyclable - - type: Stabikor + - type: AggressionInhibitor + damage: 5 + timeStun: 5.0 + lockAccess: + - Security + unlockAccess: + - Armory + - HeadOfSecurity + - Captain + - CentralCommand - type: CuttableItem toolQualities: - Sawing diff --git a/Resources/Prototypes/_Goobstation/Recipes/Lathes/Packs/misc.yml b/Resources/Prototypes/_Goobstation/Recipes/Lathes/Packs/misc.yml index a895a349cf0..4b5f5d46ae9 100644 --- a/Resources/Prototypes/_Goobstation/Recipes/Lathes/Packs/misc.yml +++ b/Resources/Prototypes/_Goobstation/Recipes/Lathes/Packs/misc.yml @@ -36,6 +36,11 @@ - CartridgePuncturase - CartridgeTirizene +- type: latheRecipePack + id: MedicalBeamGunSyndicate + recipes: + - MedicalBeamGunSyndicate + - type: latheRecipePack id: ProtoLatheMiscStatic recipes: diff --git a/Resources/Prototypes/_Orion/Research/Nodes/cyborg.yml b/Resources/Prototypes/_Orion/Research/Nodes/cyborg.yml index 34336a78be1..1aaddc810de 100644 --- a/Resources/Prototypes/_Orion/Research/Nodes/cyborg.yml +++ b/Resources/Prototypes/_Orion/Research/Nodes/cyborg.yml @@ -133,6 +133,7 @@ - AdvancedSurgery recipeUnlocks: - BorgModuleAdvancedSurgery + - BorgModuleAdvancedTopical # Arcane position: -5,0 - type: technology @@ -157,6 +158,10 @@ - BorgModuleTool - BorgModuleCable - BorgModuleFireExtinguisher + # Arcane-Start + - BorgModuleInflatable + - BorgModuleArtistry + # Arcane-End position: -5,-1 - type: technology diff --git a/Resources/Prototypes/_Orion/Research/Nodes/engineering.yml b/Resources/Prototypes/_Orion/Research/Nodes/engineering.yml index 73f431081eb..a8493403501 100644 --- a/Resources/Prototypes/_Orion/Research/Nodes/engineering.yml +++ b/Resources/Prototypes/_Orion/Research/Nodes/engineering.yml @@ -28,6 +28,7 @@ - TimerTrigger - VoiceSignaller - VoiceTrigger + - VoiceSensor # Arcane - ProximitySignaller position: 12,4 @@ -194,6 +195,7 @@ - CableStack - CableMVStack - CableHVStack + - CableDetStack1 # Arcane - ClothingHeadHatWelding - Welder - WelderMini @@ -212,6 +214,16 @@ - LightTube - ClothingMaskWeldingGas - SignallerAdvanced + # Arcane-Start + - EmptyDetonator + - TowelColorWhite + - MaterialDurathread + - FauxTileDarkAstroGrass + - FauxTileLightAstroGrass + - FauxTileDesertAstroSand + - FauxTileAstroIronsand + - FauxTileAstroIronsandBorderless + # Arcane-End - FauxTileAstroGrass - FauxTileMowedAstroGrass - FauxTileJungleAstroGrass diff --git a/Resources/Prototypes/_Orion/Research/Nodes/medical.yml b/Resources/Prototypes/_Orion/Research/Nodes/medical.yml index 710aed9760d..41960ef5293 100644 --- a/Resources/Prototypes/_Orion/Research/Nodes/medical.yml +++ b/Resources/Prototypes/_Orion/Research/Nodes/medical.yml @@ -18,6 +18,10 @@ - ScanningReagentHaloperidol - ScanningReagentCryostylane recipeUnlocks: + # Arcane-Start + - JetInjector + - ChemistryEmptyVialSmall + # Arcane-End - LargeBeaker - Beaker - Bloodpack @@ -137,6 +141,7 @@ requiredExperiments: - ScanningReagentHaloperidol recipeUnlocks: + - AdvancedJetInjector # Arcane - HandheldCrewMonitor - SyringeGun - DefibrillatorCompact diff --git a/Resources/Prototypes/_Orion/Research/Nodes/security.yml b/Resources/Prototypes/_Orion/Research/Nodes/security.yml index 4a849d0926c..3249adae54f 100644 --- a/Resources/Prototypes/_Orion/Research/Nodes/security.yml +++ b/Resources/Prototypes/_Orion/Research/Nodes/security.yml @@ -79,7 +79,7 @@ - Zipties - FlashlightSeclite - ClothingBackpackElectropack - - ClothingStabikor # Arcane + - ClothingAggressionInhibitor # Arcane - ForensicPad - TargetClown - TargetHuman diff --git a/Resources/Prototypes/_Orion/Research/Nodes/service.yml b/Resources/Prototypes/_Orion/Research/Nodes/service.yml index bfdd949a511..463a6bea5fc 100644 --- a/Resources/Prototypes/_Orion/Research/Nodes/service.yml +++ b/Resources/Prototypes/_Orion/Research/Nodes/service.yml @@ -12,6 +12,11 @@ - type: General amount: 500 recipeUnlocks: + # Arcane-Start + - UtilityKnife + - BoxFolderClipboardEmpty + - BoxFolderPlasticClipboardEmpty + # Arcane-End - HandLabeler - ParcelWrap - CutterMachineCircuitboard @@ -148,6 +153,7 @@ - type: General amount: 500 recipeUnlocks: + - SmartFridgeCircuitboard # Arcane - ElectricGrillMachineCircuitboard - MicrowaveMachineCircuitboard - FoodBowlBig diff --git a/Resources/Textures/_Arcane/Clothing/Neck/Misc/petcollar.rsi/meta.json b/Resources/Textures/_Arcane/Clothing/Neck/Misc/petcollar.rsi/meta.json index a057e4dc822..b4294846589 100644 --- a/Resources/Textures/_Arcane/Clothing/Neck/Misc/petcollar.rsi/meta.json +++ b/Resources/Textures/_Arcane/Clothing/Neck/Misc/petcollar.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Created by nikitok007 (Discord) for Space Station 14 server MK: Erida 18+", + "copyright": "Created by Kat_L (Discord) for Space Station 14 server Arcane", "size": { "x": 32, "y": 32 From 0d8645d386634a77d6b91615af54f8b7a58c4b56 Mon Sep 17 00:00:00 2001 From: Cat_L Date: Thu, 6 Aug 2026 13:13:22 +0300 Subject: [PATCH 3/4] fixs-tests --- Resources/Prototypes/Entities/Structures/Machines/lathe.yml | 1 - Resources/Prototypes/Recipes/Lathes/Packs/medical.yml | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 916e0c39aba..981e81928da 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -181,7 +181,6 @@ - SecurityAmmoStaticGoob - SyndicateAmmoLightStatic - ScienceExplosives - - MedicalBeamGunSyndicate - type: BlueprintReceiver whitelist: tags: diff --git a/Resources/Prototypes/Recipes/Lathes/Packs/medical.yml b/Resources/Prototypes/Recipes/Lathes/Packs/medical.yml index a013c41fb2d..0fee2d388ea 100644 --- a/Resources/Prototypes/Recipes/Lathes/Packs/medical.yml +++ b/Resources/Prototypes/Recipes/Lathes/Packs/medical.yml @@ -71,6 +71,7 @@ # Arcane-Start - Bloodpack - ClothingNeckStethoscope + - MedicalBeamGunSyndicate # Arcane-End - type: latheRecipePack From 3c112479d17721746c93ecf4eac6c8b598f024f0 Mon Sep 17 00:00:00 2001 From: Cat_L Date: Mon, 10 Aug 2026 14:19:20 +0300 Subject: [PATCH 4/4] fixs-3 --- .../Systems/AggressionInhibitorSystem.cs | 112 +++++++++++------- .../Systems/CuttableItemSystem.cs | 27 ++--- .../AggressionInhibitorComponent.cs | 13 +- .../SharedAggressionInhibitorSystem.cs | 75 +++++------- .../Recipes/Lathes/Packs/medical.yml | 8 +- .../Prototypes/Recipes/Lathes/security.yml | 2 +- .../Recipes/Lathes/Packs/misc.yml | 5 - 7 files changed, 121 insertions(+), 121 deletions(-) diff --git a/Content.Server/_Arcane/AggressionInhibitor/Systems/AggressionInhibitorSystem.cs b/Content.Server/_Arcane/AggressionInhibitor/Systems/AggressionInhibitorSystem.cs index e17f2a59c80..d03477a4b32 100644 --- a/Content.Server/_Arcane/AggressionInhibitor/Systems/AggressionInhibitorSystem.cs +++ b/Content.Server/_Arcane/AggressionInhibitor/Systems/AggressionInhibitorSystem.cs @@ -10,11 +10,13 @@ using Content.Shared.Hands.EntitySystems; using Content.Shared.Inventory; using Robust.Server.Player; +using Robust.Shared.Timing; namespace Content.Server._Arcane.AggressionInhibitor.Systems; public sealed partial class AggressionInhibitorSystem : EntitySystem { + [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private InventorySystem _inventorySystem = default!; [Dependency] private SharedTransformSystem _transformSystem = default!; [Dependency] private SharedPopupSystem _popup = default!; @@ -34,23 +36,21 @@ public override void Initialize() SubscribeLocalEvent(OnOpenDialogReceived); SubscribeLocalEvent(OnToggleLockReceived); } + public override void Update(float frameTime) { base.Update(frameTime); + var now = _timing.CurTime; var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var comp)) { - if (!comp.IsActive || comp.WearingEntity == null) + if (now < comp.NextUpdate || !comp.IsActive || comp.WearingEntity == null) continue; - comp.Timer -= frameTime; + if (!RemoveInhibitor(uid, comp)) + continue; - if (comp.Timer <= 0) - { - if (!RemoveInhibitor(uid, comp)) - continue; - } Dirty(uid, comp); } } @@ -59,12 +59,14 @@ private void OnInteractUsing(EntityUid uid, AggressionInhibitorComponent comp, I { var user = args.User; - var parent = Transform(uid).ParentUid; - var isEquipped = parent.IsValid() && HasComp(parent); + if (!TryComp(uid, out var xform)) + return; + + var parent = xform.ParentUid; - if (!isEquipped) + if (!parent.IsValid() && HasComp(parent)) { - _audio.PlayPvs(comp.DenySound, uid); + PlaybackDenySound(uid, comp); args.Handled = true; return; @@ -74,13 +76,9 @@ private void OnInteractUsing(EntityUid uid, AggressionInhibitorComponent comp, I !TryComp(idCard.Owner, out var accessComp)) return; - var cardAccess = accessComp.Tags; - if (comp.IsLocked) { - var hasUnlockAccess = comp.UnlockAccess.Exists(proto => cardAccess.Contains(proto.Id)); - - if (hasUnlockAccess) + if (GetHasUnlockAccess(comp, accessComp.Tags)) { if (!RemoveInhibitor(uid, comp)) return; @@ -88,13 +86,12 @@ private void OnInteractUsing(EntityUid uid, AggressionInhibitorComponent comp, I args.Handled = true; return; } - else _audio.PlayPvs(comp.DenySound, uid); + else + PlaybackDenySound(uid, comp); } else { - var hasLockAccess = comp.LockAccess.Exists(proto => cardAccess.Contains(proto.Id)); - - if (hasLockAccess) + if (GetHasLockAccess(comp, accessComp.Tags)) { if (!ActivateInhibitor(uid, parent, comp, user)) return; @@ -102,7 +99,8 @@ private void OnInteractUsing(EntityUid uid, AggressionInhibitorComponent comp, I args.Handled = true; return; } - else _audio.PlayPvs(comp.DenySound, uid); + else + PlaybackDenySound(uid, comp); } } @@ -124,12 +122,9 @@ public void OpenDialog(EntityUid uid, AggressionInhibitorComponent comp, EntityU if (comp.IsLocked) return; - var cardAccess = accessComp.Tags; - var hasSettingsAccess = comp.LockAccess.Exists(proto => cardAccess.Contains(proto.Id)); - - if (!hasSettingsAccess) + if (!GetHasLockAccess(comp, accessComp.Tags)) { - _audio.PlayPvs(comp.DenySound, uid); + PlaybackDenySound(uid, comp); return; } @@ -144,6 +139,8 @@ public void OpenDialog(EntityUid uid, AggressionInhibitorComponent comp, EntityU if (string.IsNullOrEmpty(input)) { comp.Duration = 60f; + comp.NextUpdate = _timing.CurTime + TimeSpan.FromSeconds(comp.Duration); + Dirty(uid, comp); _popup.PopupEntity(Loc.GetString("stabikor-duration-set-cancel-fallback", ("time", 1)), uid, user); return; @@ -152,15 +149,15 @@ public void OpenDialog(EntityUid uid, AggressionInhibitorComponent comp, EntityU if (!int.TryParse(input, out var durationMinutes) || durationMinutes < 1 || durationMinutes > 900) { _popup.PopupEntity(Loc.GetString("stabikor-dialog-invalid-range"), user, user, PopupType.SmallCaution); - - _audio.PlayPvs(comp.DenySound, uid); + PlaybackDenySound(uid, comp); return; } comp.Duration = durationMinutes * 60f; - _popup.PopupEntity(Loc.GetString("stabikor-duration-set-success", ("time", durationMinutes)), uid, user); + comp.NextUpdate = _timing.CurTime + TimeSpan.FromMinutes(durationMinutes); - _audio.PlayPvs(comp.UnlockSound, uid); + _popup.PopupEntity(Loc.GetString("stabikor-duration-set-success", ("time", durationMinutes)), uid, user); + PlaybackUnlockSound(uid, comp); Dirty(uid, comp); }); @@ -181,25 +178,25 @@ public void ToggleLock(EntityUid uid, AggressionInhibitorComponent comp, EntityU !TryComp(idCard.Owner, out var accessComp)) return; - var cardAccess = accessComp.Tags; - if (comp.IsLocked) { - if (comp.UnlockAccess.Exists(proto => cardAccess.Contains(proto.Id))) + if (GetHasUnlockAccess(comp, accessComp.Tags)) { if (!RemoveInhibitor(uid, comp)) return; } - else _audio.PlayPvs(comp.DenySound, uid); + else + PlaybackDenySound(uid, comp); } else { - if (comp.LockAccess.Exists(proto => cardAccess.Contains(proto.Id))) + if (GetHasLockAccess(comp, accessComp.Tags)) { if (!ActivateInhibitor(uid, parent ?? uid, comp, user)) return; } - else _audio.PlayPvs(comp.DenySound, uid); + else + PlaybackDenySound(uid, comp); } } @@ -215,22 +212,22 @@ private bool ActivateInhibitor(EntityUid uid, EntityUid wearerUid, AggressionInh if (_inventorySystem.TryGetSlotEntity(wearerUid, slotDef.Name, out var slotItem) && slotItem == uid) { - comp.Timer = comp.Duration; + comp.NextUpdate = _timing.CurTime + TimeSpan.FromSeconds(comp.Duration); comp.IsLocked = true; comp.IsActive = true; comp.WearingEntity = wearerUid; - _combatMode.SetInCombatMode(user, false); + _combatMode.SetInCombatMode(wearerUid, false); Dirty(uid, comp); - _audio.PlayPvs(comp.LockSound, uid); + PlaybackLockSound(uid, comp); _popup.PopupEntity(Loc.GetString("stabikor-activated-success", ("item", uid), ("user", Name(wearerUid))), uid); return true; } } - _audio.PlayPvs(comp.DenySound, uid); + PlaybackDenySound(uid, comp); _popup.PopupEntity(Loc.GetString("stabikor-not-equipped"), uid, user); return false; @@ -241,26 +238,53 @@ private bool RemoveInhibitor(EntityUid uid, AggressionInhibitorComponent comp) if (comp.WearingEntity is not { Valid: true } user) return false; - if (_inventorySystem.TryGetContainingSlot(uid, out var slotDef)) + if (_containerSystem.TryGetContainingContainer(uid, out var container)) { - if (!_inventorySystem.TryUnequip(user, user, slotDef.Name, force: true)) + if (!_containerSystem.TryRemoveFromContainer(uid, force: true)) return false; + + _transformSystem.SetCoordinates(uid, _transformSystem.GetMoverCoordinates(user)); } - comp.Timer = 0f; + comp.NextUpdate = TimeSpan.MaxValue; comp.IsLocked = false; comp.IsActive = false; comp.WearingEntity = null; Dirty(uid, comp); - _audio.PlayPvs(comp.UnlockSound, uid); + PlaybackUnlockSound(uid, comp); _popup.PopupEntity(Loc.GetString("stabikor-moment-shutdown", ("item", uid)), uid); return true; } + private static bool GetHasLockAccess(AggressionInhibitorComponent comp, HashSet> cardAccess) + { + return comp.LockAccess.Exists(proto => cardAccess.Contains(proto.Id)); + } + + private static bool GetHasUnlockAccess(AggressionInhibitorComponent comp, HashSet> cardAccess) + { + return comp.UnlockAccess.Exists(proto => cardAccess.Contains(proto.Id)); + } + + private void PlaybackDenySound(EntityUid uid, AggressionInhibitorComponent comp) + { + _audio.PlayPvs(comp.DenySound, uid); + } + + private void PlaybackUnlockSound(EntityUid uid, AggressionInhibitorComponent comp) + { + _audio.PlayPvs(comp.UnlockSound, uid); + } + + private void PlaybackLockSound(EntityUid uid, AggressionInhibitorComponent comp) + { + _audio.PlayPvs(comp.LockSound, uid); + } + private void OnOpenDialogReceived(EntityUid uid, AggressionInhibitorComponent comp, OpenDialogEvent args) { OpenDialog(uid, comp, args.User); diff --git a/Content.Server/_Arcane/CuttableItem/Systems/CuttableItemSystem.cs b/Content.Server/_Arcane/CuttableItem/Systems/CuttableItemSystem.cs index 2e070505a3b..28401f06eff 100644 --- a/Content.Server/_Arcane/CuttableItem/Systems/CuttableItemSystem.cs +++ b/Content.Server/_Arcane/CuttableItem/Systems/CuttableItemSystem.cs @@ -40,24 +40,23 @@ private void OnCutCompleted(EntityUid uid, CuttableItemComponent comp, CuttableD foreach (var slotDef in slotDefinitions) { - if (_inventorySystem.TryGetSlotEntity(victim, slotDef.Name, out var slotEntity) && slotEntity == uid) - { - var target = args.User; + if (!_inventorySystem.TryGetSlotEntity(victim, slotDef.Name, out var slotEntity) || slotEntity != uid) + continue; - if (_inventorySystem.TryUnequip(target, victim, slotDef.Name, force: true)) - { - _transformSystem.AttachToGridOrMap(uid); + var target = args.User; - var victimCoords = Transform(victim).Coordinates; - _transformSystem.SetCoordinates(uid, victimCoords); + if (!_inventorySystem.TryUnequip(target, victim, slotDef.Name, force: true)) + continue; - _popup.PopupEntity(Loc.GetString("cuttable-item-broken-moment-popup", ("item", uid)), uid); + _transformSystem.AttachToGridOrMap(uid); - var ev = new CuttableCutEvent(target); - RaiseLocalEvent(uid, ev); - } - break; - } + var victimCoords = Transform(victim).Coordinates; + _transformSystem.SetCoordinates(uid, victimCoords); + + _popup.PopupEntity(Loc.GetString("cuttable-item-broken-moment-popup", ("item", uid)), uid); + + var ev = new CuttableCutEvent(target); + RaiseLocalEvent(uid, ev); } } diff --git a/Content.Shared/_Arcane/AggressionInhibitor/Components/AggressionInhibitorComponent.cs b/Content.Shared/_Arcane/AggressionInhibitor/Components/AggressionInhibitorComponent.cs index 021f9ef4e67..4414d5127b2 100644 --- a/Content.Shared/_Arcane/AggressionInhibitor/Components/AggressionInhibitorComponent.cs +++ b/Content.Shared/_Arcane/AggressionInhibitor/Components/AggressionInhibitorComponent.cs @@ -2,13 +2,14 @@ using Robust.Shared.Prototypes; using Robust.Shared.Audio; using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared._Arcane.AggressionInhibitor.Components; /// /// A component that punishes creatures with a bad tone with electric shocks /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] public sealed partial class AggressionInhibitorComponent : Component { /// @@ -21,16 +22,16 @@ public sealed partial class AggressionInhibitorComponent : Component public TimeSpan LastVerbClickTime = TimeSpan.Zero; /// - /// The time in seconds for which the object is blocked + /// When to go to the next step of the schedule. /// - [AutoNetworkedField] - public float Duration = 60f; + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField] + public TimeSpan NextUpdate; /// - /// Timer to count down the time until withdrawal + /// The time in seconds for which the object is blocked /// [AutoNetworkedField] - public float Timer = 0f; + public float Duration = 60f; /// /// Is the blocking process currently active diff --git a/Content.Shared/_Arcane/AggressionInhibitor/Systems/SharedAggressionInhibitorSystem.cs b/Content.Shared/_Arcane/AggressionInhibitor/Systems/SharedAggressionInhibitorSystem.cs index 96541ec1e46..556492e73e1 100644 --- a/Content.Shared/_Arcane/AggressionInhibitor/Systems/SharedAggressionInhibitorSystem.cs +++ b/Content.Shared/_Arcane/AggressionInhibitor/Systems/SharedAggressionInhibitorSystem.cs @@ -3,11 +3,8 @@ using Content.Shared.Popups; using Content.Shared.Examine; using Content.Shared.Inventory.Events; -using Content.Shared.Access.Components; using Content.Shared.Verbs; using Robust.Shared.Utility; -using Content.Shared.Access.Systems; -using Content.Shared.Hands.EntitySystems; using Robust.Shared.Timing; using Robust.Shared.Network; using Content.Shared.CombatMode; @@ -18,16 +15,14 @@ namespace Content.Shared._Arcane.AggressionInhibitor.Systems; public sealed partial class SharedAggressionInhibitorSystem : EntitySystem { + private static SpriteSpecifier.Texture _settingsIcon = new(new ResPath("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")); [Dependency] private SharedPopupSystem _popup = default!; - [Dependency] private SharedIdCardSystem _idCard = default!; - [Dependency] private SharedHandsSystem _handsSystem = default!; [Dependency] private SharedContainerSystem _containerSystem = default!; - [Dependency] private IGameTiming _gameTiming = default!; + [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private INetManager _netManager = default!; [Dependency] private InventorySystem _inventorySystem = default!; [Dependency] private SharedElectrocutionSystem _electrocution = default!; [Dependency] private SharedCombatModeSystem _combatMode = default!; - public override void Initialize() { base.Initialize(); @@ -41,7 +36,7 @@ public override void Initialize() private void OnToggleCombatAction(ToggleCombatActionEvent args) { - if (args.Handled || _netManager.IsClient) + if (args.Handled) return; var user = args.Performer; @@ -59,21 +54,18 @@ private void OnToggleCombatAction(ToggleCombatActionEvent args) var slotEnumerator = _inventorySystem.GetSlotEnumerator(user); while (slotEnumerator.MoveNext(out var containerSlot)) { - var slotItem = containerSlot.ContainedEntity; - if (slotItem == null) + if (containerSlot.ContainedEntity is not { } slotItem) continue; - var uid = slotItem.Value; - - if (_inventorySystem.TryGetContainingSlot(uid, out var slotDef)) + if (_inventorySystem.TryGetContainingSlot(slotItem, out var slotDef)) { - if ((slotDef.SlotFlags & SlotFlags.POCKET) != 0) + if (slotDef.SlotFlags.HasFlag(SlotFlags.POCKET)) continue; } - if (TryComp(uid, out var comp) && comp.IsActive) + if (TryComp(slotItem, out var comp) && comp.IsActive) { - inhibitorItem = uid; + inhibitorItem = slotItem; inhibitorComp = comp; break; } @@ -84,13 +76,15 @@ private void OnToggleCombatAction(ToggleCombatActionEvent args) if (!_combatMode.IsInCombatMode(user)) { - _electrocution.TryDoElectrocution(user, inhibitorItem, inhibitorComp.Damage, TimeSpan.FromSeconds(inhibitorComp.TimeStun), refresh: false, ignoreInsulation: true); - - _popup.PopupEntity(Loc.GetString("stabikor-disarm-shock-popup"), user, user, PopupType.LargeCaution); - _combatMode.SetInCombatMode(user, false); - args.Handled = true; + + if (_netManager.IsServer) + { + _electrocution.TryDoElectrocution(user, inhibitorItem, inhibitorComp.Damage, TimeSpan.FromSeconds(inhibitorComp.TimeStun), refresh: false, ignoreInsulation: true); + + _popup.PopupEntity(Loc.GetString("stabikor-disarm-shock-popup"), user, user, PopupType.LargeCaution); + } } } @@ -105,13 +99,10 @@ private void OnBeingUnequippedAttempt(EntityUid uid, AggressionInhibitorComponen private void OnGotUnequipped(EntityUid uid, AggressionInhibitorComponent comp, ref GotUnequippedEvent args) { - if (comp.IsActive) + if (comp.IsActive && _netManager.IsServer) { - if (_netManager.IsServer) - { - comp.Timer = 0; - Dirty(uid, comp); - } + comp.NextUpdate = default; + Dirty(uid, comp); } } @@ -128,12 +119,13 @@ private void OnExamine(EntityUid uid, AggressionInhibitorComponent comp, Examine ("hours", durationHours), ("minutes", durationMinutes))); - if (comp.IsLocked && comp.Timer > 0) + var remaining = comp.NextUpdate - _timing.CurTime; + + if (comp.IsLocked && remaining.Ticks > 0) { - var remainingTotalMinutes = (int) (comp.Timer / 60f); - var remainingHours = remainingTotalMinutes / 60; - var remainingMinutes = remainingTotalMinutes % 60; - var remainingSeconds = (int) (comp.Timer % 60f); + var remainingHours = (int) remaining.TotalHours; + var remainingMinutes = remaining.Minutes; + var remainingSeconds = remaining.Seconds; args.PushMarkup(Loc.GetString("stabikor-examine-timer-remaining", ("hours", remainingHours), @@ -150,19 +142,12 @@ private void OnGetActivationVerbs(EntityUid uid, AggressionInhibitorComponent co if (!isInContainer && (!args.CanAccess || !args.CanInteract)) return; - if (!_handsSystem.TryGetActiveItem(args.User, out var heldItem) || - !_idCard.TryFindIdCard(heldItem.Value, out var idCard) || - !TryComp(idCard.Owner, out var accessComp)) - return; - - var settingsIcon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")); - if (!comp.IsLocked) { args.Verbs.Add(new ActivationVerb() { Text = Loc.GetString("stabikor-verb-set-duration"), - Icon = settingsIcon, + Icon = _settingsIcon, Act = () => FlipOpenDialog(uid, args.User, comp) }); } @@ -172,27 +157,27 @@ private void OnGetActivationVerbs(EntityUid uid, AggressionInhibitorComponent co args.Verbs.Add(new ActivationVerb() { Text = Loc.GetString(verbText), - Icon = settingsIcon, + Icon = _settingsIcon, Act = () => FlipToggleLock(uid, args.User, comp) }); } private void FlipOpenDialog(EntityUid uid, EntityUid user, AggressionInhibitorComponent comp) { - if (_gameTiming.CurTime < comp.LastVerbClickTime + TimeSpan.FromSeconds(0.4)) + if (_timing.CurTime < comp.LastVerbClickTime + TimeSpan.FromSeconds(0.4)) return; - comp.LastVerbClickTime = _gameTiming.CurTime; + comp.LastVerbClickTime = _timing.CurTime; RaiseLocalEvent(uid, new OpenDialogEvent(uid, user)); } private void FlipToggleLock(EntityUid uid, EntityUid user, AggressionInhibitorComponent comp) { - if (_gameTiming.CurTime < comp.LastVerbClickTime + TimeSpan.FromSeconds(0.4)) + if (_timing.CurTime < comp.LastVerbClickTime + TimeSpan.FromSeconds(0.4)) return; - comp.LastVerbClickTime = _gameTiming.CurTime; + comp.LastVerbClickTime = _timing.CurTime; RaiseLocalEvent(uid, new ToggleLockEvent(uid, user)); } diff --git a/Resources/Prototypes/Recipes/Lathes/Packs/medical.yml b/Resources/Prototypes/Recipes/Lathes/Packs/medical.yml index 0fee2d388ea..24c35f017c3 100644 --- a/Resources/Prototypes/Recipes/Lathes/Packs/medical.yml +++ b/Resources/Prototypes/Recipes/Lathes/Packs/medical.yml @@ -52,9 +52,7 @@ - Drill - Saw - Hemostat - # Arcane-Start - - SawElectric - # Arcane-End + - SawElectric # Arcane - type: latheRecipePack id: MedicalStatic @@ -87,9 +85,7 @@ - ClothingHandsGlovesLatex - ClothingHandsGlovesNitrile - ClothingMaskSterile - # Arcane-Start - - ClothingEyesNightVisionMedicalGoggles - # Arcane-End + - ClothingEyesNightVisionMedicalGoggles # Arcane # These are all empty - type: latheRecipePack diff --git a/Resources/Prototypes/Recipes/Lathes/security.yml b/Resources/Prototypes/Recipes/Lathes/security.yml index b240a2ef143..782bdc31ff3 100644 --- a/Resources/Prototypes/Recipes/Lathes/security.yml +++ b/Resources/Prototypes/Recipes/Lathes/security.yml @@ -73,7 +73,7 @@ Steel: 500 Plastic: 250 Cloth: 500 - # Arcane-End +# Arcane-End - type: latheRecipe id: ClothingEyesHudSecurity diff --git a/Resources/Prototypes/_Goobstation/Recipes/Lathes/Packs/misc.yml b/Resources/Prototypes/_Goobstation/Recipes/Lathes/Packs/misc.yml index 4b5f5d46ae9..a895a349cf0 100644 --- a/Resources/Prototypes/_Goobstation/Recipes/Lathes/Packs/misc.yml +++ b/Resources/Prototypes/_Goobstation/Recipes/Lathes/Packs/misc.yml @@ -36,11 +36,6 @@ - CartridgePuncturase - CartridgeTirizene -- type: latheRecipePack - id: MedicalBeamGunSyndicate - recipes: - - MedicalBeamGunSyndicate - - type: latheRecipePack id: ProtoLatheMiscStatic recipes: