From ad18c6e9a54b033d93eaa7bebd40e38e2a997390 Mon Sep 17 00:00:00 2001 From: beck-thompson <107373427+beck-thompson@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:51:58 -0700 Subject: [PATCH] Secret stash refractor (#29396) * First commit * Will do this in another PR! * maybe? * Moved stuff to ToolOpenableSystem because its smarter and cooler --- .../Nutrition/EntitySystems/UtensilSystem.cs | 3 +- .../Plants/PottedPlantHideComponent.cs | 17 - .../Plants/PottedPlantHideSystem.cs | 63 --- .../Components/SecretStashComponent.cs | 70 +-- .../EntitySystems/SecretStashSystem.cs | 399 +++++++++--------- .../Tools/Components/ToolOpenableComponent.cs | 84 ++++ .../Tools/Systems/ToolOpenableSystem.cs | 171 ++++++++ .../components/secret-stash-component.ftl | 29 +- .../components/tool-openable-component.ftl | 6 + .../Structures/Furniture/potted_plants.yml | 6 +- .../Entities/Structures/Furniture/toilet.yml | 16 +- 11 files changed, 513 insertions(+), 351 deletions(-) delete mode 100644 Content.Shared/Plants/PottedPlantHideComponent.cs delete mode 100644 Content.Shared/Plants/PottedPlantHideSystem.cs create mode 100644 Content.Shared/Tools/Components/ToolOpenableComponent.cs create mode 100644 Content.Shared/Tools/Systems/ToolOpenableSystem.cs create mode 100644 Resources/Locale/en-US/tools/components/tool-openable-component.ftl diff --git a/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs b/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs index d40288183f..1f3d5afb43 100644 --- a/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs @@ -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(OnAfterInteract, after: new[] { typeof(ItemSlotsSystem) }); + SubscribeLocalEvent(OnAfterInteract, after: new[] { typeof(ItemSlotsSystem), typeof(ToolOpenableSystem) }); } /// diff --git a/Content.Shared/Plants/PottedPlantHideComponent.cs b/Content.Shared/Plants/PottedPlantHideComponent.cs deleted file mode 100644 index 2e02272494..0000000000 --- a/Content.Shared/Plants/PottedPlantHideComponent.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Content.Shared.Storage.Components; -using Robust.Shared.Audio; - -namespace Content.Shared.Plants -{ - /// - /// Interaction wrapper for . - /// Gently rustle after each interaction with plant. - /// - [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 index cbe052f8d5..0000000000 --- a/Content.Shared/Plants/PottedPlantHideSystem.cs +++ /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(OnInit); - SubscribeLocalEvent(OnInteractUsing); - SubscribeLocalEvent(OnInteractHand); - } - - private void OnInit(EntityUid uid, PottedPlantHideComponent component, ComponentInit args) - { - EntityManager.EnsureComponent(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)); - } - } -} diff --git a/Content.Shared/Storage/Components/SecretStashComponent.cs b/Content.Shared/Storage/Components/SecretStashComponent.cs index 8595f79ca5..3bf8e2e871 100644 --- a/Content.Shared/Storage/Components/SecretStashComponent.cs +++ b/Content.Shared/Storage/Components/SecretStashComponent.cs @@ -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 { /// /// Logic for a secret slot stash, like plant pot or toilet cistern. - /// Unlike it doesn't have interaction logic or verbs. - /// Other classes like should implement it. + /// Unlike it has logic for opening and closing + /// the stash. /// [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] [Access(typeof(SecretStashSystem))] public sealed partial class SecretStashComponent : Component { /// - /// Max item size that can be fitted into secret stash. + /// Max item size that can be inserted into secret stash. /// [DataField("maxItemSize")] public ProtoId MaxItemSize = "Small"; /// - /// If stash has way to open then this will switch between open and closed. - /// - [DataField, AutoNetworkedField] - public bool ToggleOpen; - - /// - /// Prying the door. + /// 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. /// [DataField] - public float PryDoorTime = 1f; - - [DataField] - public ProtoId PryingQuality = "Prying"; + public SoundSpecifier? TryInsertItemSound; /// - /// Is stash openable?. - /// - [DataField, AutoNetworkedField] - public bool OpenableStash = false; - - /// - /// IC secret stash name. For example "the toilet cistern". - /// If empty string, will replace it with entity name in init. + /// 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. /// [DataField] - public string SecretPartName { get; set; } = ""; + public SoundSpecifier? TryRemoveItemSound; + /// + /// If true, verbs will appear to help interact with the stash. + /// [DataField, AutoNetworkedField] - public string ExamineStash = "comp-secret-stash-on-examine-found-hidden-item"; + public bool HasVerbs = true; + + /// + /// The name of the secret stash. For example "the toilet cistern". + /// If null, the name of the entity will be used instead. + /// + [DataField] + public string? SecretStashName; /// /// Container used to keep secret stash item. /// [ViewVariables] public ContainerSlot ItemContainer = default!; - - } - - /// - /// Simple pry event for prying open a stash door. - /// - [Serializable, NetSerializable] - public sealed partial class StashPryDoAfterEvent : SimpleDoAfterEvent - { - } - - /// - /// Visualizers for handling stash open closed state if stash has door. - /// - [Serializable, NetSerializable] - public enum StashVisuals : byte - { - DoorVisualState, - } - - [Serializable, NetSerializable] - public enum DoorVisualState : byte - { - DoorOpen, - DoorClosed } } diff --git a/Content.Shared/Storage/EntitySystems/SecretStashSystem.cs b/Content.Shared/Storage/EntitySystems/SecretStashSystem.cs index 9aee1b982e..901d744df5 100644 --- a/Content.Shared/Storage/EntitySystems/SecretStashSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SecretStashSystem.cs @@ -8,215 +8,206 @@ using Robust.Shared.Containers; using Content.Shared.Interaction; using Content.Shared.Tools.Systems; using Content.Shared.Examine; +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 +namespace Content.Shared.Storage.EntitySystems; + +/// +/// Secret Stash allows an item to be hidden within. +/// +public sealed class SecretStashSystem : EntitySystem { - /// - /// Secret Stash allows an item to be hidden within. - /// - 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 SharedAudioSystem _audio = default!; + [Dependency] private readonly ToolOpenableSystem _toolOpenableSystem = default!; + + + public override void Initialize() { - [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!; + base.Initialize(); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnDestroyed); + SubscribeLocalEvent(OnInteractUsing, after: new[] { typeof(ToolOpenableSystem) }); + SubscribeLocalEvent(OnInteractHand); + SubscribeLocalEvent>(OnGetVerb); + } - public override void Initialize() + private void OnInit(Entity entity, ref ComponentInit args) + { + entity.Comp.ItemContainer = _containerSystem.EnsureContainer(entity, "stash", out _); + } + + private void OnDestroyed(Entity entity, ref DestructionEventArgs args) + { + var storedInside = _containerSystem.EmptyContainer(entity.Comp.ItemContainer); + if (storedInside != null && storedInside.Count >= 1) { - base.Initialize(); - SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(OnDestroyed); - SubscribeLocalEvent(OnSecretStashPried); - SubscribeLocalEvent(OnInteractUsing); - SubscribeLocalEvent(OnInteractHand); - SubscribeLocalEvent(OnExamine); - } - - private void OnInit(EntityUid uid, SecretStashComponent component, ComponentInit args) - { - component.ItemContainer = _containerSystem.EnsureContainer(uid, "stash", out _); - } - - private void OnDestroyed(EntityUid uid, SecretStashComponent component, DestructionEventArgs args) - { - _containerSystem.EmptyContainer(component.ItemContainer); - } - - /// - /// Is there something inside secret stash item container? - /// - public bool HasItemInside(EntityUid uid, SecretStashComponent? component = null) - { - if (!Resolve(uid, ref component)) - return false; - return component.ItemContainer.ContainedEntity != null; - } - - private void OnInteractUsing(EntityUid uid, SecretStashComponent component, InteractUsingEvent args) - { - if (args.Handled) - return; - - if (!component.OpenableStash) - return; - - // 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(EntityUid uid, SecretStashComponent component, InteractHandEvent args) - { - if (args.Handled) - return; - - if (!component.OpenableStash) - return; - - // trying to get something from stash? - if (component.ToggleOpen) - { - var gotItem = TryGetItem(uid, args.User); - if (gotItem) - { - args.Handled = true; - return; - } - } - args.Handled = true; - } - - private void OnSecretStashPried(EntityUid uid, SecretStashComponent component, StashPryDoAfterEvent args) - { - if (args.Cancelled) - return; - - ToggleOpen(uid, component); - } - - public void ToggleOpen(EntityUid uid, SecretStashComponent? component = null, MetaDataComponent? meta = null) - { - if (!Resolve(uid, ref component)) - return; - - component.ToggleOpen = !component.ToggleOpen; - - UpdateAppearance(uid, component); - Dirty(uid, component, meta); - } - - private void UpdateAppearance(EntityUid uid, SecretStashComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - _appearance.SetData(uid, StashVisuals.DoorVisualState, component.ToggleOpen ? DoorVisualState.DoorOpen : DoorVisualState.DoorClosed); - } - - /// - /// Tries to hide item inside secret stash from hands of user. - /// - /// True if item was hidden inside stash - 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; - } - - // 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; - } - - // try to move item from hands to stash container - if (!_handsSystem.TryDropIntoContainer(userUid, itemToHideUid, container)) - { - 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; - } - - /// - /// Try get item and place it in users hand. - /// If user can't take it by hands, will drop item from container. - /// - /// True if user received item - 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; - } - - _handsSystem.PickupOrDrop(userUid, container.ContainedEntity.Value, handsComp: hands); - - // show success message - var successMsg = Loc.GetString("comp-secret-stash-action-get-item-found-something", - ("stash", GetSecretPartName(uid, component))); - _popupSystem.PopupClient(successMsg, uid, userUid); - - return true; - } - - private void OnExamine(EntityUid uid, SecretStashComponent component, ExaminedEvent args) - { - if (args.IsInDetailsRange && component.ToggleOpen) - { - if (HasItemInside(uid)) - { - var msg = Loc.GetString(component.ExamineStash); - args.PushMarkup(msg); - } - } - } - - private string GetSecretPartName(EntityUid uid, SecretStashComponent stash) - { - if (stash.SecretPartName != "") - return Loc.GetString(stash.SecretPartName); - - var entityName = Loc.GetString("comp-secret-stash-secret-part-name", ("this", uid)); - - return entityName; + var popup = Loc.GetString("comp-secret-stash-on-destroyed-popup", ("stashname", GetStashName(entity))); + _popupSystem.PopupEntity(popup, storedInside[0], PopupType.MediumCaution); } } + + private void OnInteractUsing(Entity entity, ref InteractUsingEvent args) + { + if (args.Handled || !IsStashOpen(entity)) + return; + + args.Handled = TryStashItem(entity, args.User, args.Used); + } + + private void OnInteractHand(Entity entity, ref InteractHandEvent args) + { + if (args.Handled || !IsStashOpen(entity)) + return; + + args.Handled = TryGetItem(entity, args.User); + } + + /// + /// Tries to hide the given item into the stash. + /// + /// True if item was hidden inside stash and false otherwise. + private bool TryStashItem(Entity entity, EntityUid userUid, EntityUid itemToHideUid) + { + if (!TryComp(itemToHideUid, out var itemComp)) + return false; + + _audio.PlayPredicted(entity.Comp.TryInsertItemSound, entity, userUid, AudioParams.Default.WithVariation(0.25f)); + + // check if secret stash is already occupied + var container = entity.Comp.ItemContainer; + if (HasItemInside(entity)) + { + var popup = Loc.GetString("comp-secret-stash-action-hide-container-not-empty"); + _popupSystem.PopupClient(popup, entity, userUid); + return false; + } + + // check if item is too big to fit into secret stash + if (_item.GetSizePrototype(itemComp.Size) > _item.GetSizePrototype(entity.Comp.MaxItemSize)) + { + var msg = Loc.GetString("comp-secret-stash-action-hide-item-too-big", + ("item", itemToHideUid), ("stashname", GetStashName(entity))); + _popupSystem.PopupClient(msg, entity, userUid); + return false; + } + + // try to move item from hands to stash container + if (!_handsSystem.TryDropIntoContainer(userUid, itemToHideUid, container)) + return false; + + // 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; + } + + /// + /// 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. + /// + /// True if user received item. + private bool TryGetItem(Entity entity, EntityUid userUid) + { + if (!TryComp(userUid, out var handsComp)) + return false; + + _audio.PlayPredicted(entity.Comp.TryRemoveItemSound, entity, userUid, AudioParams.Default.WithVariation(0.25f)); + + // check if secret stash has something inside + var itemInStash = entity.Comp.ItemContainer.ContainedEntity; + if (itemInStash == null) + return false; + + _handsSystem.PickupOrDrop(userUid, itemInStash.Value, handsComp: handsComp); + + // show success message + var successMsg = Loc.GetString("comp-secret-stash-action-get-item-found-something", + ("stashname", GetStashName(entity))); + _popupSystem.PopupClient(successMsg, entity, userUid); + + return true; + } + + private void OnGetVerb(Entity entity, ref GetVerbsEvent args) + { + if (!args.CanInteract || !args.CanAccess || !entity.Comp.HasVerbs) + return; + + var user = args.User; + var item = args.Using; + var stashName = GetStashName(entity); + + var itemVerb = new InteractionVerb(); + + // This will add the verb relating to inserting / grabbing items. + if (IsStashOpen(entity)) + { + if (item != null) + { + itemVerb.Text = Loc.GetString("comp-secret-stash-verb-insert-into-stash"); + if (HasItemInside(entity)) + { + 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); + } + } + + #region Helper functions + + /// + /// The stash name if it exists, or the entity name if it doesn't. + /// + private string GetStashName(Entity entity) + { + if (entity.Comp.SecretStashName == null) + return Identity.Name(entity, EntityManager); + return Loc.GetString(entity.Comp.SecretStashName); + } + + /// + /// True if the stash is open OR the there is no toolOpenableComponent attacheded to the entity + /// and false otherwise. + /// + private bool IsStashOpen(Entity stash) + { + return _toolOpenableSystem.IsOpen(stash); + } + + private bool HasItemInside(Entity 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 index 0000000000..82cdf611da --- /dev/null +++ b/Content.Shared/Tools/Components/ToolOpenableComponent.cs @@ -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 +{ + /// + /// Logic for using tools (Or verbs) to open / close something on an entity. + /// + [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] + public sealed partial class ToolOpenableComponent : Component + { + /// + /// Is the openable part open or closed? + /// + [DataField, AutoNetworkedField] + public bool IsOpen = false; + + /// + /// If a tool is needed to open the entity, this time will be used. + /// + [DataField] + public float OpenTime = 1f; + + /// + /// If a tool is needed to close the entity, this time will be used. + /// + [DataField] + public float CloseTime = 1f; + + /// + /// What type of tool quality is needed to open this? + /// If null, the it will only be openable by a verb. + /// + [DataField] + public ProtoId? OpenToolQualityNeeded; + + /// + /// What type of tool quality is needed to close this. + /// If null, this will only be closable by a verb. + /// + [DataField] + public ProtoId? CloseToolQualityNeeded; + + /// + /// If true, verbs will appear to help interact with opening / closing. + /// + [DataField, AutoNetworkedField] + public bool HasVerbs = true; + + /// + /// The name of what is being open and closed. + /// E.g toilet lid, pannel, compartment. + /// + [DataField, AutoNetworkedField] + public string? Name; + + } + + /// + /// Simple do after event for opening or closing. + /// + [Serializable, NetSerializable] + public sealed partial class ToolOpenableDoAfterEventToggleOpen : SimpleDoAfterEvent + { + } + + /// + /// Visualizers for handling stash open closed state if stash has door. + /// + [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 index 0000000000..4951040350 --- /dev/null +++ b/Content.Shared/Tools/Systems/ToolOpenableSystem.cs @@ -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(OnInit); + SubscribeLocalEvent(OnOpenableStateToggled); + SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent>(OnGetVerb); + } + + private void OnInit(Entity entity, ref ComponentInit args) + { + UpdateAppearance(entity); + Dirty(entity); + } + + private void OnInteractUsing(Entity entity, ref InteractUsingEvent args) + { + if (args.Handled) + return; + + if (TryOpenClose(entity, args.Used, args.User)) + args.Handled = true; + } + + /// + /// Try to open or close what is openable. + /// + /// Returns false if you can't interact with the openable thing with the given item. + private bool TryOpenClose(Entity 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 entity, ref ToolOpenableDoAfterEventToggleOpen args) + { + if (args.Cancelled) + return; + + ToggleState(entity); + } + + /// + /// Toggle the state and update appearance. + /// + private void ToggleState(Entity entity) + { + entity.Comp.IsOpen = !entity.Comp.IsOpen; + UpdateAppearance(entity); + Dirty(entity); + } + + #region Helper functions + + private string GetName(Entity 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 entity) + { + _appearance.SetData(entity, ToolOpenableVisuals.ToolOpenableVisualState, entity.Comp.IsOpen ? ToolOpenableVisualState.Open : ToolOpenableVisualState.Closed); + } + + #endregion + + #region User interface functions + + private void OnExamine(Entity 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 entity, ref GetVerbsEvent 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 +} diff --git a/Resources/Locale/en-US/storage/components/secret-stash-component.ftl b/Resources/Locale/en-US/storage/components/secret-stash-component.ftl index d0dfed2b5d..2a8f505e2b 100644 --- a/Resources/Locale/en-US/storage/components/secret-stash-component.ftl +++ b/Resources/Locale/en-US/storage/components/secret-stash-component.ftl @@ -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. +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}! -secret-stash-part-plant = the plant -secret-stash-part-toilet = the toilet cistern +### 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 index 0000000000..c45b6c69da --- /dev/null +++ b/Resources/Locale/en-US/tools/components/tool-openable-component.ftl @@ -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. diff --git a/Resources/Prototypes/Entities/Structures/Furniture/potted_plants.yml b/Resources/Prototypes/Entities/Structures/Furniture/potted_plants.yml index 53d268facf..2d35c94bf1 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/potted_plants.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/potted_plants.yml @@ -24,9 +24,11 @@ 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 {} diff --git a/Resources/Prototypes/Entities/Structures/Furniture/toilet.yml b/Resources/Prototypes/Entities/Structures/Furniture/toilet.yml index 1eb5176e9e..6a603b7deb 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/toilet.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/toilet.yml @@ -68,9 +68,11 @@ - 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 @@ -103,10 +105,10 @@ 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