]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Secret stash refractor (#29396)
authorbeck-thompson <107373427+beck-thompson@users.noreply.github.com>
Thu, 8 Aug 2024 23:51:58 +0000 (16:51 -0700)
committerGitHub <noreply@github.com>
Thu, 8 Aug 2024 23:51:58 +0000 (09:51 +1000)
* First commit

* Will do this in another PR!

* maybe?

* Moved stuff to ToolOpenableSystem because its smarter and cooler

Content.Server/Nutrition/EntitySystems/UtensilSystem.cs
Content.Shared/Plants/PottedPlantHideComponent.cs [deleted file]
Content.Shared/Plants/PottedPlantHideSystem.cs [deleted file]
Content.Shared/Storage/Components/SecretStashComponent.cs
Content.Shared/Storage/EntitySystems/SecretStashSystem.cs
Content.Shared/Tools/Components/ToolOpenableComponent.cs [new file with mode: 0644]
Content.Shared/Tools/Systems/ToolOpenableSystem.cs [new file with mode: 0644]
Resources/Locale/en-US/storage/components/secret-stash-component.ftl
Resources/Locale/en-US/tools/components/tool-openable-component.ftl [new file with mode: 0644]
Resources/Prototypes/Entities/Structures/Furniture/potted_plants.yml
Resources/Prototypes/Entities/Structures/Furniture/toilet.yml

index d40288183fcb023a988b288a2ac3abd873b9befc..1f3d5afb433eee2e531bdf46a81b99f8566a6c78 100644 (file)
@@ -4,6 +4,7 @@ using Content.Server.Popups;
 using Content.Shared.Interaction;
 using Content.Shared.Nutrition.Components;
 using Content.Shared.Nutrition.EntitySystems;
+using Content.Shared.Tools.EntitySystems;
 using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.Random;
@@ -25,7 +26,7 @@ namespace Content.Server.Nutrition.EntitySystems
         {
             base.Initialize();
 
-            SubscribeLocalEvent<UtensilComponent, AfterInteractEvent>(OnAfterInteract, after: new[] { typeof(ItemSlotsSystem) });
+            SubscribeLocalEvent<UtensilComponent, AfterInteractEvent>(OnAfterInteract, after: new[] { typeof(ItemSlotsSystem), typeof(ToolOpenableSystem) });
         }
 
         /// <summary>
diff --git a/Content.Shared/Plants/PottedPlantHideComponent.cs b/Content.Shared/Plants/PottedPlantHideComponent.cs
deleted file mode 100644 (file)
index 2e02272..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-using Content.Shared.Storage.Components;
-using Robust.Shared.Audio;
-
-namespace Content.Shared.Plants
-{
-    /// <summary>
-    ///     Interaction wrapper for <see cref="SecretStashComponent"/>.
-    ///     Gently rustle after each interaction with plant.
-    /// </summary>
-    [RegisterComponent]
-    [Access(typeof(PottedPlantHideSystem))]
-    public sealed partial class PottedPlantHideComponent : Component
-    {
-        [DataField("rustleSound")]
-        public SoundSpecifier RustleSound = new SoundPathSpecifier("/Audio/Effects/plant_rustle.ogg");
-    }
-}
diff --git a/Content.Shared/Plants/PottedPlantHideSystem.cs b/Content.Shared/Plants/PottedPlantHideSystem.cs
deleted file mode 100644 (file)
index cbe052f..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-using Content.Shared.Popups;
-using Content.Shared.Storage.Components;
-using Content.Shared.Storage.EntitySystems;
-using Content.Shared.Interaction;
-using Robust.Shared.Audio;
-using Robust.Shared.Audio.Systems;
-
-namespace Content.Shared.Plants
-{
-    public sealed class PottedPlantHideSystem : EntitySystem
-    {
-        [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
-        [Dependency] private readonly SecretStashSystem _stashSystem = default!;
-        [Dependency] private readonly SharedAudioSystem _audio = default!;
-
-        public override void Initialize()
-        {
-            base.Initialize();
-            SubscribeLocalEvent<PottedPlantHideComponent, ComponentInit>(OnInit);
-            SubscribeLocalEvent<PottedPlantHideComponent, InteractUsingEvent>(OnInteractUsing);
-            SubscribeLocalEvent<PottedPlantHideComponent, InteractHandEvent>(OnInteractHand);
-        }
-
-        private void OnInit(EntityUid uid, PottedPlantHideComponent component, ComponentInit args)
-        {
-            EntityManager.EnsureComponent<SecretStashComponent>(uid);
-        }
-
-        private void OnInteractUsing(EntityUid uid, PottedPlantHideComponent component, InteractUsingEvent args)
-        {
-            if (args.Handled)
-                return;
-
-            Rustle(uid, component, args.User);
-            args.Handled = _stashSystem.TryHideItem(uid, args.User, args.Used);
-        }
-
-        private void OnInteractHand(EntityUid uid, PottedPlantHideComponent component, InteractHandEvent args)
-        {
-            if (args.Handled)
-                return;
-
-            Rustle(uid, component, args.User);
-
-            var gotItem = _stashSystem.TryGetItem(uid, args.User);
-            if (!gotItem)
-            {
-                var msg = Loc.GetString("potted-plant-hide-component-interact-hand-got-no-item-message");
-                _popupSystem.PopupClient(msg, uid, args.User);
-            }
-
-            args.Handled = gotItem;
-        }
-
-        private void Rustle(EntityUid uid, PottedPlantHideComponent? component = null, EntityUid? user = null)
-        {
-            if (!Resolve(uid, ref component))
-                return;
-
-            _audio.PlayPredicted(component.RustleSound, uid, user, AudioParams.Default.WithVariation(0.25f));
-        }
-    }
-}
index 8595f79ca57b39886fb243f4b912a5cf4d6e9462..3bf8e2e871ba5c41dcd30a2c96e2cf93b182fbee 100644 (file)
@@ -7,84 +7,56 @@ using Content.Shared.Tools;
 using Robust.Shared.GameStates;
 using Content.Shared.DoAfter;
 using Robust.Shared.Serialization;
+using Robust.Shared.Audio;
 
 namespace Content.Shared.Storage.Components
 {
     /// <summary>
     ///     Logic for a secret slot stash, like plant pot or toilet cistern.
-    ///     Unlike <see cref="ItemSlotsComponent"/> it doesn't have interaction logic or verbs.
-    ///     Other classes like <see cref="ToiletComponent"/> should implement it.
+    ///     Unlike <see cref="ItemSlotsComponent"/> it has logic for opening and closing
+    ///     the stash.
     /// </summary>
     [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
     [Access(typeof(SecretStashSystem))]
     public sealed partial class SecretStashComponent : Component
     {
         /// <summary>
-        ///     Max item size that can be fitted into secret stash.
+        ///     Max item size that can be inserted into secret stash.
         /// </summary>
         [DataField("maxItemSize")]
         public ProtoId<ItemSizePrototype> MaxItemSize = "Small";
 
         /// <summary>
-        /// If stash has way to open then this will switch between open and closed.
+        ///     This sound will be played when you try to insert an item in the stash.
+        ///     The sound will be played whether or not the item is actually inserted.
         /// </summary>
-        [DataField, AutoNetworkedField]
-        public bool ToggleOpen;
+        [DataField]
+        public SoundSpecifier? TryInsertItemSound;
 
         /// <summary>
-        /// Prying the door.
+        ///     This sound will be played when you try to remove an item in the stash.
+        ///     The sound will be played whether or not the item is actually removed.
         /// </summary>
         [DataField]
-        public float PryDoorTime = 1f;
-
-        [DataField]
-        public ProtoId<ToolQualityPrototype> PryingQuality = "Prying";
+        public SoundSpecifier? TryRemoveItemSound;
 
         /// <summary>
-        /// Is stash openable?.
+        ///     If true, verbs will appear to help interact with the stash.
         /// </summary>
         [DataField, AutoNetworkedField]
-        public bool OpenableStash = false;
+        public bool HasVerbs = true;
 
         /// <summary>
-        ///     IC secret stash name. For example "the toilet cistern".
-        ///     If empty string, will replace it with entity name in init.
+        ///     The name of the secret stash. For example "the toilet cistern".
+        ///     If null, the name of the entity will be used instead.
         /// </summary>
         [DataField]
-        public string SecretPartName { get; set; } = "";
-
-        [DataField, AutoNetworkedField]
-        public string ExamineStash = "comp-secret-stash-on-examine-found-hidden-item";
+        public string? SecretStashName;
 
         /// <summary>
         ///     Container used to keep secret stash item.
         /// </summary>
         [ViewVariables]
         public ContainerSlot ItemContainer = default!;
-
-    }
-
-    /// <summary>
-    /// Simple pry event for prying open a stash door.
-    /// </summary>
-    [Serializable, NetSerializable]
-    public sealed partial class StashPryDoAfterEvent : SimpleDoAfterEvent
-    {
-    }
-
-    /// <summary>
-    /// Visualizers for handling stash open closed state if stash has door.
-    /// </summary>
-    [Serializable, NetSerializable]
-    public enum StashVisuals : byte
-    {
-        DoorVisualState,
-    }
-
-    [Serializable, NetSerializable]
-    public enum DoorVisualState : byte
-    {
-        DoorOpen,
-        DoorClosed
     }
 }
index 9aee1b982e0a56fab657cd4c8760779f2985151b..901d744df5f3863486ad776e9ae342a13c9bc5eb 100644 (file)
@@ -8,215 +8,206 @@ using Robust.Shared.Containers;
 using Content.Shared.Interaction;
 using Content.Shared.Tools.Systems;
 using Content.Shared.Examine;
-
-namespace Content.Shared.Storage.EntitySystems
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Systems;
+using Content.Shared.Verbs;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Tools.EntitySystems;
+
+namespace Content.Shared.Storage.EntitySystems;
+
+/// <summary>
+///     Secret Stash allows an item to be hidden within.
+/// </summary>
+public sealed class SecretStashSystem : EntitySystem
 {
-    /// <summary>
-    /// Secret Stash allows an item to be hidden within.
-    /// </summary>
-    public sealed class SecretStashSystem : EntitySystem
-    {
-        [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
-        [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
-        [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
-        [Dependency] private readonly SharedItemSystem _item = default!;
-        [Dependency] private readonly SharedToolSystem _tool = default!;
-        [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
-
-        public override void Initialize()
-        {
-            base.Initialize();
-            SubscribeLocalEvent<SecretStashComponent, ComponentInit>(OnInit);
-            SubscribeLocalEvent<SecretStashComponent, DestructionEventArgs>(OnDestroyed);
-            SubscribeLocalEvent<SecretStashComponent, StashPryDoAfterEvent>(OnSecretStashPried);
-            SubscribeLocalEvent<SecretStashComponent, InteractUsingEvent>(OnInteractUsing);
-            SubscribeLocalEvent<SecretStashComponent, InteractHandEvent>(OnInteractHand);
-            SubscribeLocalEvent<SecretStashComponent, ExaminedEvent>(OnExamine);
-        }
+    [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+    [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
+    [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
+    [Dependency] private readonly SharedItemSystem _item = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly ToolOpenableSystem _toolOpenableSystem = default!;
 
-        private void OnInit(EntityUid uid, SecretStashComponent component, ComponentInit args)
-        {
-            component.ItemContainer = _containerSystem.EnsureContainer<ContainerSlot>(uid, "stash", out _);
-        }
 
-        private void OnDestroyed(EntityUid uid, SecretStashComponent component, DestructionEventArgs args)
-        {
-            _containerSystem.EmptyContainer(component.ItemContainer);
-        }
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<SecretStashComponent, ComponentInit>(OnInit);
+        SubscribeLocalEvent<SecretStashComponent, DestructionEventArgs>(OnDestroyed);
+        SubscribeLocalEvent<SecretStashComponent, InteractUsingEvent>(OnInteractUsing, after: new[] { typeof(ToolOpenableSystem) });
+        SubscribeLocalEvent<SecretStashComponent, InteractHandEvent>(OnInteractHand);
+        SubscribeLocalEvent<SecretStashComponent, GetVerbsEvent<InteractionVerb>>(OnGetVerb);
+    }
+
+    private void OnInit(Entity<SecretStashComponent> entity, ref ComponentInit args)
+    {
+        entity.Comp.ItemContainer = _containerSystem.EnsureContainer<ContainerSlot>(entity, "stash", out _);
+    }
 
-        /// <summary>
-        ///     Is there something inside secret stash item container?
-        /// </summary>
-        public bool HasItemInside(EntityUid uid, SecretStashComponent? component = null)
+    private void OnDestroyed(Entity<SecretStashComponent> entity, ref DestructionEventArgs args)
+    {
+        var storedInside = _containerSystem.EmptyContainer(entity.Comp.ItemContainer);
+        if (storedInside != null && storedInside.Count >= 1)
         {
-            if (!Resolve(uid, ref component))
-                return false;
-            return component.ItemContainer.ContainedEntity != null;
+            var popup = Loc.GetString("comp-secret-stash-on-destroyed-popup", ("stashname", GetStashName(entity)));
+            _popupSystem.PopupEntity(popup, storedInside[0], PopupType.MediumCaution);
         }
+    }
 
-        private void OnInteractUsing(EntityUid uid, SecretStashComponent component, InteractUsingEvent args)
-        {
-            if (args.Handled)
-                return;
+    private void OnInteractUsing(Entity<SecretStashComponent> entity, ref InteractUsingEvent args)
+    {
+        if (args.Handled || !IsStashOpen(entity))
+            return;
 
-            if (!component.OpenableStash)
-                return;
+        args.Handled = TryStashItem(entity, args.User, args.Used);
+    }
 
-            // is player trying place or lift off cistern lid?
-            if (_tool.UseTool(args.Used, args.User, uid, component.PryDoorTime, component.PryingQuality, new StashPryDoAfterEvent()))
-                args.Handled = true;
-            // maybe player is trying to hide something inside cistern?
-            else if (component.ToggleOpen)
-            {
-                TryHideItem(uid, args.User, args.Used);
-                args.Handled = true;
-            }
-        }
+    private void OnInteractHand(Entity<SecretStashComponent> entity, ref InteractHandEvent args)
+    {
+        if (args.Handled || !IsStashOpen(entity))
+            return;
 
-        private void OnInteractHand(EntityUid uid, SecretStashComponent component, InteractHandEvent args)
-        {
-            if (args.Handled)
-                return;
+        args.Handled = TryGetItem(entity, args.User);
+    }
 
-            if (!component.OpenableStash)
-                return;
+    /// <summary>
+    ///     Tries to hide the given item into the stash.
+    /// </summary>
+    /// <returns>True if item was hidden inside stash and false otherwise.</returns>
+    private bool TryStashItem(Entity<SecretStashComponent> entity, EntityUid userUid, EntityUid itemToHideUid)
+    {
+        if (!TryComp<ItemComponent>(itemToHideUid, out var itemComp))
+            return false;
 
-            // trying to get something from stash?
-            if (component.ToggleOpen)
-            {
-                var gotItem = TryGetItem(uid, args.User);
-                if (gotItem)
-                {
-                    args.Handled = true;
-                    return;
-                }
-            }
-            args.Handled = true;
-        }
+        _audio.PlayPredicted(entity.Comp.TryInsertItemSound, entity, userUid, AudioParams.Default.WithVariation(0.25f));
 
-        private void OnSecretStashPried(EntityUid uid, SecretStashComponent component, StashPryDoAfterEvent args)
+        // check if secret stash is already occupied
+        var container = entity.Comp.ItemContainer;
+        if (HasItemInside(entity))
         {
-            if (args.Cancelled)
-                return;
-
-            ToggleOpen(uid, component);
+            var popup = Loc.GetString("comp-secret-stash-action-hide-container-not-empty");
+            _popupSystem.PopupClient(popup, entity, userUid);
+            return false;
         }
 
-        public void ToggleOpen(EntityUid uid, SecretStashComponent? component = null, MetaDataComponent? meta = null)
+        // check if item is too big to fit into secret stash
+        if (_item.GetSizePrototype(itemComp.Size) > _item.GetSizePrototype(entity.Comp.MaxItemSize))
         {
-            if (!Resolve(uid, ref component))
-                return;
-
-            component.ToggleOpen = !component.ToggleOpen;
-
-            UpdateAppearance(uid, component);
-            Dirty(uid, component, meta);
+            var msg = Loc.GetString("comp-secret-stash-action-hide-item-too-big",
+                ("item", itemToHideUid), ("stashname", GetStashName(entity)));
+            _popupSystem.PopupClient(msg, entity, userUid);
+            return false;
         }
 
-        private void UpdateAppearance(EntityUid uid, SecretStashComponent? component = null)
-        {
-            if (!Resolve(uid, ref component))
-                return;
+        // try to move item from hands to stash container
+        if (!_handsSystem.TryDropIntoContainer(userUid, itemToHideUid, container))
+            return false;
 
-            _appearance.SetData(uid, StashVisuals.DoorVisualState, component.ToggleOpen ? DoorVisualState.DoorOpen : DoorVisualState.DoorClosed);
-        }
+        // all done, show success message
+        var successMsg = Loc.GetString("comp-secret-stash-action-hide-success",
+            ("item", itemToHideUid), ("stashname", GetStashName(entity)));
+        _popupSystem.PopupClient(successMsg, entity, userUid);
+        return true;
+    }
 
-        /// <summary>
-        ///     Tries to hide item inside secret stash from hands of user.
-        /// </summary>
-        /// <returns>True if item was hidden inside stash</returns>
-        public bool TryHideItem(EntityUid uid, EntityUid userUid, EntityUid itemToHideUid,
-            SecretStashComponent? component = null, ItemComponent? item = null,
-            HandsComponent? hands = null)
-        {
-            if (!Resolve(uid, ref component))
-                return false;
-            if (!Resolve(itemToHideUid, ref item))
-                return false;
-            if (!Resolve(userUid, ref hands))
-                return false;
-
-            // check if secret stash is already occupied
-            var container = component.ItemContainer;
-            if (container.ContainedEntity != null)
-            {
-                var msg = Loc.GetString("comp-secret-stash-action-hide-container-not-empty");
-                _popupSystem.PopupClient(msg, uid, userUid);
-                return false;
-            }
+    /// <summary>
+    ///     Try the given item in the stash and place it in users hand.
+    ///     If user can't take hold the item in their hands, the item will be dropped onto the ground.
+    /// </summary>
+    /// <returns>True if user received item.</returns>
+    private bool TryGetItem(Entity<SecretStashComponent> entity, EntityUid userUid)
+    {
+        if (!TryComp<HandsComponent>(userUid, out var handsComp))
+            return false;
 
-            // check if item is too big to fit into secret stash
-            if (_item.GetSizePrototype(item.Size) > _item.GetSizePrototype(component.MaxItemSize))
-            {
-                var msg = Loc.GetString("comp-secret-stash-action-hide-item-too-big",
-                    ("item", itemToHideUid), ("stash", GetSecretPartName(uid, component)));
-                _popupSystem.PopupClient(msg, uid, userUid);
-                return false;
-            }
+        _audio.PlayPredicted(entity.Comp.TryRemoveItemSound, entity, userUid, AudioParams.Default.WithVariation(0.25f));
 
-            // try to move item from hands to stash container
-            if (!_handsSystem.TryDropIntoContainer(userUid, itemToHideUid, container))
-            {
-                return false;
-            }
+        // check if secret stash has something inside
+        var itemInStash = entity.Comp.ItemContainer.ContainedEntity;
+        if (itemInStash == null)
+            return false;
 
-            // all done, show success message
-            var successMsg = Loc.GetString("comp-secret-stash-action-hide-success",
-                ("item", itemToHideUid), ("this", GetSecretPartName(uid, component)));
-            _popupSystem.PopupClient(successMsg, uid, userUid);
-            return true;
-        }
+        _handsSystem.PickupOrDrop(userUid, itemInStash.Value, handsComp: handsComp);
 
-        /// <summary>
-        ///     Try get item and place it in users hand.
-        ///     If user can't take it by hands, will drop item from container.
-        /// </summary>
-        /// <returns>True if user received item</returns>
-        public bool TryGetItem(EntityUid uid, EntityUid userUid, SecretStashComponent? component = null,
-            HandsComponent? hands = null)
-        {
-            if (!Resolve(uid, ref component))
-                return false;
-            if (!Resolve(userUid, ref hands))
-                return false;
-
-            // check if secret stash has something inside
-            var container = component.ItemContainer;
-            if (container.ContainedEntity == null)
-            {
-                return false;
-            }
+        // show success message
+        var successMsg = Loc.GetString("comp-secret-stash-action-get-item-found-something",
+            ("stashname", GetStashName(entity)));
+        _popupSystem.PopupClient(successMsg, entity, userUid);
 
-            _handsSystem.PickupOrDrop(userUid, container.ContainedEntity.Value, handsComp: hands);
+        return true;
+    }
+
+    private void OnGetVerb(Entity<SecretStashComponent> entity, ref GetVerbsEvent<InteractionVerb> args)
+    {
+        if (!args.CanInteract || !args.CanAccess || !entity.Comp.HasVerbs)
+            return;
 
-            // show success message
-            var successMsg = Loc.GetString("comp-secret-stash-action-get-item-found-something",
-                ("stash", GetSecretPartName(uid, component)));
-            _popupSystem.PopupClient(successMsg, uid, userUid);
+        var user = args.User;
+        var item = args.Using;
+        var stashName = GetStashName(entity);
 
-            return true;
-        }
+        var itemVerb = new InteractionVerb();
 
-        private void OnExamine(EntityUid uid, SecretStashComponent component, ExaminedEvent args)
+        // This will add the verb relating to inserting / grabbing items.
+        if (IsStashOpen(entity))
         {
-            if (args.IsInDetailsRange && component.ToggleOpen)
+            if (item != null)
             {
-                if (HasItemInside(uid))
+                itemVerb.Text = Loc.GetString("comp-secret-stash-verb-insert-into-stash");
+                if (HasItemInside(entity))
                 {
-                    var msg = Loc.GetString(component.ExamineStash);
-                    args.PushMarkup(msg);
+                    itemVerb.Disabled = true;
+                    itemVerb.Message = Loc.GetString("comp-secret-stash-verb-insert-message-item-already-inside", ("stashname", stashName));
                 }
+                else
+                {
+                    itemVerb.Message = Loc.GetString("comp-secret-stash-verb-insert-message-no-item", ("item", item), ("stashname", stashName));
+                }
+
+                itemVerb.Act = () => TryStashItem(entity, user, item.Value);
+            }
+            else
+            {
+                itemVerb.Text = Loc.GetString("comp-secret-stash-verb-take-out-item");
+                itemVerb.Message = Loc.GetString("comp-secret-stash-verb-take-out-message-something", ("stashname", stashName));
+                if (!HasItemInside(entity))
+                {
+                    itemVerb.Disabled = true;
+                    itemVerb.Message = Loc.GetString("comp-secret-stash-verb-take-out-message-nothing", ("stashname", stashName));
+                }
+
+                itemVerb.Act = () => TryGetItem(entity, user);
             }
+
+            args.Verbs.Add(itemVerb);
         }
+    }
 
-        private string GetSecretPartName(EntityUid uid, SecretStashComponent stash)
-        {
-            if (stash.SecretPartName != "")
-                return Loc.GetString(stash.SecretPartName);
+    #region Helper functions
 
-            var entityName = Loc.GetString("comp-secret-stash-secret-part-name", ("this", uid));
+    /// <returns>
+    ///     The stash name if it exists, or the entity name if it doesn't.
+    ///  </returns>
+    private string GetStashName(Entity<SecretStashComponent> entity)
+    {
+        if (entity.Comp.SecretStashName == null)
+            return Identity.Name(entity, EntityManager);
+        return Loc.GetString(entity.Comp.SecretStashName);
+    }
 
-            return entityName;
-        }
+    /// <returns>
+    ///     True if the stash is open OR the there is no toolOpenableComponent attacheded to the entity
+    ///     and false otherwise.
+    ///  </returns>
+    private bool IsStashOpen(Entity<SecretStashComponent> stash)
+    {
+        return _toolOpenableSystem.IsOpen(stash);
     }
+
+    private bool HasItemInside(Entity<SecretStashComponent> entity)
+    {
+        return entity.Comp.ItemContainer.ContainedEntity != null;
+    }
+
+    #endregion
 }
diff --git a/Content.Shared/Tools/Components/ToolOpenableComponent.cs b/Content.Shared/Tools/Components/ToolOpenableComponent.cs
new file mode 100644 (file)
index 0000000..82cdf61
--- /dev/null
@@ -0,0 +1,84 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.GameStates;
+using Content.Shared.DoAfter;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Tools.Components
+{
+    /// <summary>
+    ///     Logic for using tools (Or verbs) to open / close something on an entity.
+    /// </summary>
+    [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+    public sealed partial class ToolOpenableComponent : Component
+    {
+        /// <summary>
+        ///     Is the openable part open or closed?
+        /// </summary>
+        [DataField, AutoNetworkedField]
+        public bool IsOpen = false;
+
+        /// <summary>
+        ///     If a tool is needed to open the entity, this time will be used.
+        /// </summary>
+        [DataField]
+        public float OpenTime = 1f;
+
+        /// <summary>
+        ///     If a tool is needed to close the entity, this time will be used.
+        /// </summary>
+        [DataField]
+        public float CloseTime = 1f;
+
+        /// <summary>
+        ///     What type of tool quality is needed to open this?
+        ///     If null, the it will only be openable by a verb.
+        /// </summary>
+        [DataField]
+        public ProtoId<ToolQualityPrototype>? OpenToolQualityNeeded;
+
+        /// <summary>
+        ///     What type of tool quality is needed to close this.
+        ///     If null, this will only be closable by a verb.
+        /// </summary>
+        [DataField]
+        public ProtoId<ToolQualityPrototype>? CloseToolQualityNeeded;
+
+        /// <summary>
+        ///     If true, verbs will appear to help interact with opening / closing.
+        /// </summary>
+        [DataField, AutoNetworkedField]
+        public bool HasVerbs = true;
+
+        /// <summary>
+        ///     The name of what is being open and closed.
+        ///     E.g toilet lid, pannel, compartment.
+        /// </summary>
+        [DataField, AutoNetworkedField]
+        public string? Name;
+
+    }
+
+    /// <summary>
+    ///     Simple do after event for opening or closing.
+    /// </summary>
+    [Serializable, NetSerializable]
+    public sealed partial class ToolOpenableDoAfterEventToggleOpen : SimpleDoAfterEvent
+    {
+    }
+
+    /// <summary>
+    ///     Visualizers for handling stash open closed state if stash has door.
+    /// </summary>
+    [Serializable, NetSerializable]
+    public enum ToolOpenableVisuals : byte
+    {
+        ToolOpenableVisualState,
+    }
+
+    [Serializable, NetSerializable]
+    public enum ToolOpenableVisualState : byte
+    {
+        Open,
+        Closed
+    }
+}
diff --git a/Content.Shared/Tools/Systems/ToolOpenableSystem.cs b/Content.Shared/Tools/Systems/ToolOpenableSystem.cs
new file mode 100644 (file)
index 0000000..4951040
--- /dev/null
@@ -0,0 +1,171 @@
+using Content.Shared.IdentityManagement;
+using Content.Shared.Tools.Components;
+using Content.Shared.Tools.Systems;
+using Content.Shared.Interaction;
+using Content.Shared.Examine;
+using Content.Shared.Verbs;
+
+namespace Content.Shared.Tools.EntitySystems;
+
+public sealed class ToolOpenableSystem : EntitySystem
+{
+    [Dependency] private readonly SharedToolSystem _tool = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<ToolOpenableComponent, ComponentInit>(OnInit);
+        SubscribeLocalEvent<ToolOpenableComponent, ToolOpenableDoAfterEventToggleOpen>(OnOpenableStateToggled);
+        SubscribeLocalEvent<ToolOpenableComponent, InteractUsingEvent>(OnInteractUsing);
+        SubscribeLocalEvent<ToolOpenableComponent, ExaminedEvent>(OnExamine);
+        SubscribeLocalEvent<ToolOpenableComponent, GetVerbsEvent<InteractionVerb>>(OnGetVerb);
+    }
+
+    private void OnInit(Entity<ToolOpenableComponent> entity, ref ComponentInit args)
+    {
+        UpdateAppearance(entity);
+        Dirty(entity);
+    }
+
+    private void OnInteractUsing(Entity<ToolOpenableComponent> entity, ref InteractUsingEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        if (TryOpenClose(entity, args.Used, args.User))
+            args.Handled = true;
+    }
+
+    /// <summary>
+    ///     Try to open or close what is openable.
+    /// </summary>
+    /// <returns> Returns false if you can't interact with the openable thing with the given item. </returns>
+    private bool TryOpenClose(Entity<ToolOpenableComponent> entity, EntityUid? toolToToggle, EntityUid user)
+    {
+        var neededToolQuantity = entity.Comp.IsOpen ? entity.Comp.CloseToolQualityNeeded : entity.Comp.OpenToolQualityNeeded;
+        var time = entity.Comp.IsOpen ? entity.Comp.CloseTime : entity.Comp.OpenTime;
+        var evt = new ToolOpenableDoAfterEventToggleOpen();
+
+        // If neededToolQuantity is null it can only be open be opened with the verbs.
+        if (toolToToggle == null || neededToolQuantity == null)
+            return false;
+
+        return _tool.UseTool(toolToToggle.Value, user, entity, time, neededToolQuantity, evt);
+    }
+
+    private void OnOpenableStateToggled(Entity<ToolOpenableComponent> entity, ref ToolOpenableDoAfterEventToggleOpen args)
+    {
+        if (args.Cancelled)
+            return;
+
+        ToggleState(entity);
+    }
+
+    /// <summary>
+    ///     Toggle the state and update appearance.
+    /// </summary>
+    private void ToggleState(Entity<ToolOpenableComponent> entity)
+    {
+        entity.Comp.IsOpen = !entity.Comp.IsOpen;
+        UpdateAppearance(entity);
+        Dirty(entity);
+    }
+
+    #region Helper functions
+
+    private string GetName(Entity<ToolOpenableComponent> entity)
+    {
+        if (entity.Comp.Name == null)
+            return Identity.Name(entity, EntityManager);
+        return Loc.GetString(entity.Comp.Name);
+    }
+
+    public bool IsOpen(EntityUid uid, ToolOpenableComponent? component = null)
+    {
+        if (!Resolve(uid, ref component, false))
+            return true;
+
+        return component.IsOpen;
+    }
+
+    private void UpdateAppearance(Entity<ToolOpenableComponent> entity)
+    {
+        _appearance.SetData(entity, ToolOpenableVisuals.ToolOpenableVisualState, entity.Comp.IsOpen ? ToolOpenableVisualState.Open : ToolOpenableVisualState.Closed);
+    }
+
+    #endregion
+
+    #region User interface functions
+
+    private void OnExamine(Entity<ToolOpenableComponent> entity, ref ExaminedEvent args)
+    {
+        if (!args.IsInDetailsRange)
+            return;
+
+        string msg;
+        var name = GetName(entity);
+        if (entity.Comp.IsOpen)
+            msg = Loc.GetString("tool-openable-component-examine-opened", ("name", name));
+        else
+            msg = Loc.GetString("tool-openable-component-examine-closed", ("name", name));
+
+        args.PushMarkup(msg);
+    }
+
+    private void OnGetVerb(Entity<ToolOpenableComponent> entity, ref GetVerbsEvent<InteractionVerb> args)
+    {
+        if (!args.CanInteract || !args.CanAccess || !entity.Comp.HasVerbs)
+            return;
+
+        var user = args.User;
+        var item = args.Using;
+        var name = GetName(entity);
+
+        var toggleVerb = new InteractionVerb
+        {
+            IconEntity = GetNetEntity(item)
+        };
+
+        if (entity.Comp.IsOpen)
+        {
+            toggleVerb.Text = toggleVerb.Message = Loc.GetString("tool-openable-component-verb-close");
+            var neededQual = entity.Comp.CloseToolQualityNeeded;
+
+            // If neededQual is null you don't need a tool to open / close.
+            if (neededQual != null &&
+                (item == null || !_tool.HasQuality(item.Value, neededQual)))
+            {
+                toggleVerb.Disabled = true;
+                toggleVerb.Message = Loc.GetString("tool-openable-component-verb-cant-close", ("name", name));
+            }
+
+            if (neededQual == null)
+                toggleVerb.Act = () => ToggleState(entity);
+            else
+                toggleVerb.Act = () => TryOpenClose(entity, item, user);
+
+            args.Verbs.Add(toggleVerb);
+        }
+        else
+        {
+            // The open verb should only appear when holding the correct tool or if no tool is needed.
+
+            toggleVerb.Text = toggleVerb.Message = Loc.GetString("tool-openable-component-verb-open");
+            var neededQual = entity.Comp.OpenToolQualityNeeded;
+
+            if (neededQual == null)
+            {
+                toggleVerb.Act = () => ToggleState(entity);
+                args.Verbs.Add(toggleVerb);
+            }
+            else if (item != null && _tool.HasQuality(item.Value, neededQual))
+            {
+                toggleVerb.Act = () => TryOpenClose(entity, item, user);
+                args.Verbs.Add(toggleVerb);
+            }
+        }
+    }
+
+    #endregion
+}
index d0dfed2b5dda7cd3d217f1c8c8d379f2bc9de625..2a8f505e2b1c4282e351a767edc6c5c04ee81e85 100644 (file)
@@ -1,11 +1,24 @@
 ### Secret stash component. Stuff like potted plants, comfy chair cushions, etc...
 
-comp-secret-stash-secret-part-name = { THE($item) }
-comp-secret-stash-action-hide-success = You hide { THE($item) } in { $this }
-comp-secret-stash-action-hide-container-not-empty = There's already something in here?!
-comp-secret-stash-action-hide-item-too-big = { THE($item) } is too big to fit in {$stash}!
-comp-secret-stash-action-get-item-found-something = There was something inside {$stash}!
-comp-secret-stash-on-examine-found-hidden-item = There is something hidden inside.
-
-secret-stash-part-plant = the plant
-secret-stash-part-toilet = the toilet cistern
+comp-secret-stash-action-hide-success = You hide { THE($item) } in the {$stashname}.
+comp-secret-stash-action-hide-container-not-empty = There's already something in here!?
+comp-secret-stash-action-hide-item-too-big = { THE($item) } is too big to fit in the {$stashname}.
+comp-secret-stash-action-get-item-found-something = There was something inside the {$stashname}!
+comp-secret-stash-on-examine-found-hidden-item = There is something hidden inside the {$stashname}!
+comp-secret-stash-on-destroyed-popup = Something falls out of the the {$stashname}!
+
+### Verbs
+comp-secret-stash-verb-insert-into-stash = Stash item
+comp-secret-stash-verb-insert-message-item-already-inside = There is already an item inside the {$stashname}.
+comp-secret-stash-verb-insert-message-no-item = Hide { THE($item) } in the {$stashname}.
+comp-secret-stash-verb-take-out-item = Grab item
+comp-secret-stash-verb-take-out-message-something = Take the contents of the {$stashname} out.
+comp-secret-stash-verb-take-out-message-nothing = There is nothing inside the {$stashname}.
+
+comp-secret-stash-verb-close = Close
+comp-secret-stash-verb-cant-close = You can't close the {$stashname} with that.
+comp-secret-stash-verb-open = Open
+
+### Stash names
+secret-stash-plant = plant
+secret-stash-toilet = toilet cistern
diff --git a/Resources/Locale/en-US/tools/components/tool-openable-component.ftl b/Resources/Locale/en-US/tools/components/tool-openable-component.ftl
new file mode 100644 (file)
index 0000000..c45b6c6
--- /dev/null
@@ -0,0 +1,6 @@
+tool-openable-component-examine-closed = The {$name} is closed.
+tool-openable-component-examine-opened = The {$name} is open.
+
+tool-openable-component-verb-close = Close
+tool-openable-component-verb-open = Open
+tool-openable-component-verb-cant-close = You can't close the {$name} with that.
index 53d268facf4d787d01b1310f5b8eab705b1e3a04..2d35c94bf10ce84898457d282c6ecb6c3c3e1ae2 100644 (file)
     offset: "0.0,0.3"
     sprite: Structures/Furniture/potted_plants.rsi
     noRot: true
-  - type: PottedPlantHide
   - type: SecretStash
-    secretPartName: secret-stash-part-plant
+    tryInsertItemSound: /Audio/Effects/plant_rustle.ogg
+    tryRemoveItemSound: /Audio/Effects/plant_rustle.ogg
+    hasVerbs: false
+    secretStashName: secret-stash-plant
   - type: ContainerContainer
     containers:
       stash: !type:ContainerSlot {}
index 1eb5176e9e03419d4e368d3bb02d9cc42ce74b1a..6a603b7deb25290095592fb65038f64260e67ab8 100644 (file)
   - type: PlungerUse
   - type: Appearance
   - type: SecretStash
-    secretPartName: secret-stash-part-toilet
-    examineStash: toilet-component-on-examine-found-hidden-item
-    openableStash: true
+    secretStashName: secret-stash-toilet
+  - type: ToolOpenable
+    openToolQualityNeeded: Prying
+    closeToolQualityNeeded: Prying
+    name: secret-stash-toilet
   - type: Drain
     autoDrain: false
   - type: StaticPrice
         SeatVisualState.SeatUp:
           SeatUp: { state: disposal-up }
           SeatDown: { state: disposal-down }
-      enum.StashVisuals.DoorVisualState:
-        DoorVisualState.DoorOpen:
-          DoorOpen: { state: disposal-open }
-          DoorClosed: { state: disposal-closed }
+      enum.ToolOpenableVisuals.ToolOpenableVisualState:
+        ToolOpenableVisualState.StashOpen:
+          Open: { state: disposal-open }
+          Closed: { state: disposal-closed }
 
 - type: entity
   id: ToiletDirtyWater