From 75e47fff9e5150a4de37e4d3c8a8b278f0a1a2cd Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Tue, 13 Feb 2024 17:08:07 -0500 Subject: [PATCH] Add verbs to Open/Close Openable containers, and add optional seals (#24780) * Implement closing; add open/close verbs * Add breakable seals * Allow custom verb names; make condiment bottles closeable * Remove pointless VV annotations and false defaults * Split Sealable off into a new component * Should have a Closed event too * Oh hey, there are icons I could use * Ternary operator * Add support for seal visualizers * Moved Sealable to Shared, added networking * Replaced bottle_close1.ogg --- .../Nutrition/Components/OpenableComponent.cs | 34 ++++++++-- .../Nutrition/EntitySystems/OpenableSystem.cs | 64 +++++++++++++++++- .../Nutrition/Components/SealableComponent.cs | 32 +++++++++ .../Components/SharedFoodComponent.cs | 7 ++ .../Nutrition/EntitySystems/SealableSystem.cs | 59 ++++++++++++++++ .../EntitySystems/SharedOpenableSystem.cs | 17 +++++ Resources/Audio/Items/attributions.yml | 7 +- Resources/Audio/Items/bottle_close1.ogg | Bin 0 -> 6427 bytes .../nutrition/components/drink-component.ftl | 2 + .../components/openable-component.ftl | 2 + .../Consumable/Drinks/drinks-cartons.yml | 4 ++ .../Consumable/Drinks/drinks_bottles.yml | 31 ++++++++- .../Consumable/Food/Containers/condiments.yml | 1 + .../SoundCollections/drink_close_sounds.yml | 4 ++ 14 files changed, 255 insertions(+), 9 deletions(-) create mode 100644 Content.Shared/Nutrition/Components/SealableComponent.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/SealableSystem.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/SharedOpenableSystem.cs create mode 100644 Resources/Audio/Items/bottle_close1.ogg create mode 100644 Resources/Locale/en-US/nutrition/components/openable-component.ftl create mode 100644 Resources/Prototypes/SoundCollections/drink_close_sounds.yml diff --git a/Content.Server/Nutrition/Components/OpenableComponent.cs b/Content.Server/Nutrition/Components/OpenableComponent.cs index 63efd52096..cc24bf44dc 100644 --- a/Content.Server/Nutrition/Components/OpenableComponent.cs +++ b/Content.Server/Nutrition/Components/OpenableComponent.cs @@ -14,20 +14,20 @@ public sealed partial class OpenableComponent : Component /// Whether this drink or food is opened or not. /// Drinks can only be drunk or poured from/into when open, and food can only be eaten when open. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public bool Opened; /// /// If this is false you cant press Z to open it. /// Requires an OpenBehavior damage threshold or other logic to open. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public bool OpenableByHand = true; /// /// Text shown when examining and its open. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public LocId ExamineText = "drink-component-on-examine-is-opened"; /// @@ -35,12 +35,38 @@ public sealed partial class OpenableComponent : Component /// Defaults to the popup drink uses since its "correct". /// It's still generic enough that you should change it if you make openable non-drinks, i.e. unwrap it first, peel it first. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public LocId ClosedPopup = "drink-component-try-use-drink-not-open"; + /// + /// Text to show in the verb menu for the "Open" action. + /// You may want to change this for non-drinks, i.e. "Peel", "Unwrap" + /// + [DataField] + public LocId OpenVerbText = "openable-component-verb-open"; + + /// + /// Text to show in the verb menu for the "Close" action. + /// You may want to change this for non-drinks, i.e. "Wrap" + /// + [DataField] + public LocId CloseVerbText = "openable-component-verb-close"; + /// /// Sound played when opening. /// [DataField] public SoundSpecifier Sound = new SoundCollectionSpecifier("canOpenSounds"); + + /// + /// Can this item be closed again after opening? + /// + [DataField] + public bool Closeable; + + /// + /// Sound played when closing. + /// + [DataField] + public SoundSpecifier? CloseSound; } diff --git a/Content.Server/Nutrition/EntitySystems/OpenableSystem.cs b/Content.Server/Nutrition/EntitySystems/OpenableSystem.cs index d7b7da25b8..373b97700f 100644 --- a/Content.Server/Nutrition/EntitySystems/OpenableSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/OpenableSystem.cs @@ -1,22 +1,23 @@ using Content.Server.Chemistry.EntitySystems; using Content.Server.Fluids.EntitySystems; +using Content.Shared.Nutrition.EntitySystems; using Content.Server.Nutrition.Components; using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Nutrition.Components; using Content.Shared.Popups; +using Content.Shared.Verbs; using Content.Shared.Weapons.Melee.Events; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; -using Robust.Shared.GameObjects; +using Robust.Shared.Utility; namespace Content.Server.Nutrition.EntitySystems; /// /// Provides API for openable food and drinks, handles opening on use and preventing transfer when closed. /// -public sealed class OpenableSystem : EntitySystem +public sealed class OpenableSystem : SharedOpenableSystem { [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; @@ -32,6 +33,7 @@ public sealed class OpenableSystem : EntitySystem SubscribeLocalEvent(OnTransferAttempt); SubscribeLocalEvent(HandleIfClosed); SubscribeLocalEvent(HandleIfClosed); + SubscribeLocalEvent>(AddOpenCloseVerbs); } private void OnInit(EntityUid uid, OpenableComponent comp, ComponentInit args) @@ -71,6 +73,36 @@ public sealed class OpenableSystem : EntitySystem args.Handled = !comp.Opened; } + private void AddOpenCloseVerbs(EntityUid uid, OpenableComponent comp, GetVerbsEvent args) + { + if (args.Hands == null || !args.CanAccess || !args.CanInteract) + return; + + Verb verb; + if (comp.Opened) + { + if (!comp.Closeable) + return; + + verb = new() + { + Text = Loc.GetString(comp.CloseVerbText), + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/close.svg.192dpi.png")), + Act = () => TryClose(args.Target, comp) + }; + } + else + { + verb = new() + { + Text = Loc.GetString(comp.OpenVerbText), + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/open.svg.192dpi.png")), + Act = () => TryOpen(args.Target, comp) + }; + } + args.Verbs.Add(verb); + } + /// /// Returns true if the entity either does not have OpenableComponent or it is opened. /// Drinks that don't have OpenableComponent are automatically open, so it returns true. @@ -123,6 +155,17 @@ public sealed class OpenableSystem : EntitySystem comp.Opened = opened; + if (opened) + { + var ev = new OpenableOpenedEvent(); + RaiseLocalEvent(uid, ref ev); + } + else + { + var ev = new OpenableClosedEvent(); + RaiseLocalEvent(uid, ref ev); + } + UpdateAppearance(uid, comp); } @@ -139,4 +182,19 @@ public sealed class OpenableSystem : EntitySystem _audio.PlayPvs(comp.Sound, uid); return true; } + + /// + /// If opened, closes it and plays the close sound, if one is defined. + /// + /// Whether it got closed + public bool TryClose(EntityUid uid, OpenableComponent? comp = null) + { + if (!Resolve(uid, ref comp, false) || !comp.Opened || !comp.Closeable) + return false; + + SetOpen(uid, false, comp); + if (comp.CloseSound != null) + _audio.PlayPvs(comp.CloseSound, uid); + return true; + } } diff --git a/Content.Shared/Nutrition/Components/SealableComponent.cs b/Content.Shared/Nutrition/Components/SealableComponent.cs new file mode 100644 index 0000000000..1c2f732e7a --- /dev/null +++ b/Content.Shared/Nutrition/Components/SealableComponent.cs @@ -0,0 +1,32 @@ +using Content.Shared.Nutrition.EntitySystems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Nutrition.Components; + +/// +/// Represents a tamper-evident seal on an Openable. +/// Only affects the Examine text. +/// Once the seal has been broken, it cannot be resealed. +/// +[NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, Access(typeof(SealableSystem))] +public sealed partial class SealableComponent : Component +{ + /// + /// Whether the item's seal is intact (i.e. it has never been opened) + /// + [DataField, AutoNetworkedField] + public bool Sealed = true; + + /// + /// Text shown when examining and the item's seal has not been broken. + /// + [DataField] + public LocId ExamineTextSealed = "drink-component-on-examine-is-sealed"; + + /// + /// Text shown when examining and the item's seal has been broken. + /// + [DataField] + public LocId ExamineTextUnsealed = "drink-component-on-examine-is-unsealed"; +} diff --git a/Content.Shared/Nutrition/Components/SharedFoodComponent.cs b/Content.Shared/Nutrition/Components/SharedFoodComponent.cs index 99ddabd3ce..07c02fb22b 100644 --- a/Content.Shared/Nutrition/Components/SharedFoodComponent.cs +++ b/Content.Shared/Nutrition/Components/SharedFoodComponent.cs @@ -16,4 +16,11 @@ namespace Content.Shared.Nutrition.Components Opened, Layer } + + [Serializable, NetSerializable] + public enum SealableVisuals : byte + { + Sealed, + Layer, + } } diff --git a/Content.Shared/Nutrition/EntitySystems/SealableSystem.cs b/Content.Shared/Nutrition/EntitySystems/SealableSystem.cs new file mode 100644 index 0000000000..b0873f23a1 --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/SealableSystem.cs @@ -0,0 +1,59 @@ +using Content.Shared.Examine; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Components; + +namespace Content.Shared.Nutrition.EntitySystems; + +public sealed partial class SealableSystem : EntitySystem +{ + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnExamined, after: new[] { typeof(SharedOpenableSystem) }); + SubscribeLocalEvent(OnOpened); + } + + private void OnExamined(EntityUid uid, SealableComponent comp, ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + var sealedText = comp.Sealed ? Loc.GetString(comp.ExamineTextSealed) : Loc.GetString(comp.ExamineTextUnsealed); + + args.PushMarkup(sealedText); + } + + private void OnOpened(EntityUid uid, SealableComponent comp, OpenableOpenedEvent args) + { + comp.Sealed = false; + + Dirty(uid, comp); + + UpdateAppearance(uid, comp); + } + + /// + /// Update seal visuals to the current value. + /// + public void UpdateAppearance(EntityUid uid, SealableComponent? comp = null, AppearanceComponent? appearance = null) + { + if (!Resolve(uid, ref comp)) + return; + + _appearance.SetData(uid, SealableVisuals.Sealed, comp.Sealed, appearance); + } + + /// + /// Returns true if the entity's seal is intact. + /// Items without SealableComponent are considered unsealed. + /// + public bool IsSealed(EntityUid uid, SealableComponent? comp = null) + { + if (!Resolve(uid, ref comp, false)) + return false; + + return comp.Sealed; + } +} diff --git a/Content.Shared/Nutrition/EntitySystems/SharedOpenableSystem.cs b/Content.Shared/Nutrition/EntitySystems/SharedOpenableSystem.cs new file mode 100644 index 0000000000..274de89003 --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/SharedOpenableSystem.cs @@ -0,0 +1,17 @@ +namespace Content.Shared.Nutrition.EntitySystems; + +public abstract partial class SharedOpenableSystem : EntitySystem +{ +} + +/// +/// Raised after an Openable is opened. +/// +[ByRefEvent] +public record struct OpenableOpenedEvent; + +/// +/// Raised after an Openable is closed. +/// +[ByRefEvent] +public record struct OpenableClosedEvent; diff --git a/Resources/Audio/Items/attributions.yml b/Resources/Audio/Items/attributions.yml index 51d8d9cf95..8942e41db2 100644 --- a/Resources/Audio/Items/attributions.yml +++ b/Resources/Audio/Items/attributions.yml @@ -63,6 +63,11 @@ copyright: "User volivieri on freesound.org. Modified by Velcroboy on github." source: "https://freesound.org/people/volivieri/sounds/37190/" +- files: ["bottle_close1.ogg"] + license: "CC0-1.0" + copyright: "User MellowAudio on freesound.org. Modified by Tayrtahn on github." + source: "https://freesound.org/people/MellowAudio/sounds/591485/" + - files: ["bow_pull.ogg"] license: "CC-BY-3.0" copyright: "User jzdnvdoosj on freesound.org. Converted to ogg by mirrorcult" @@ -92,7 +97,7 @@ license: "CC-BY-4.0" copyright: "User LoafDV on freesound.org. Converted to ogg end edited by lzk228" source: "https://freesound.org/people/LoafDV/sounds/131596/" - + - files: ["shovel_dig.ogg"] license: "CC-BY-SA-3.0" copyright: "Taken from tgstation, modified by themias (github) for ss14" diff --git a/Resources/Audio/Items/bottle_close1.ogg b/Resources/Audio/Items/bottle_close1.ogg new file mode 100644 index 0000000000000000000000000000000000000000..b6db8fae79e3f6bc0e849581fd2e2d9f2e5b0f7f GIT binary patch literal 6427 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 x`~s>EDaMQD*x0`83xQ6GE!xC>qwl1IzJ%I0w(D