From: Tayrtahn Date: Tue, 13 Feb 2024 22:08:07 +0000 (-0500) Subject: Add verbs to Open/Close Openable containers, and add optional seals (#24780) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=75e47fff9e5150a4de37e4d3c8a8b278f0a1a2cd;p=space-station-14.git 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 --- 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 0000000000..b6db8fae79 Binary files /dev/null and b/Resources/Audio/Items/bottle_close1.ogg differ diff --git a/Resources/Locale/en-US/nutrition/components/drink-component.ftl b/Resources/Locale/en-US/nutrition/components/drink-component.ftl index 2bbe23dd43..9a388744b0 100644 --- a/Resources/Locale/en-US/nutrition/components/drink-component.ftl +++ b/Resources/Locale/en-US/nutrition/components/drink-component.ftl @@ -1,6 +1,8 @@ drink-component-on-use-is-empty = {$owner} is empty! drink-component-on-examine-is-empty = [color=gray]Empty[/color] drink-component-on-examine-is-opened = [color=yellow]Opened[/color] +drink-component-on-examine-is-sealed = The seal is intact. +drink-component-on-examine-is-unsealed = The seal is broken. drink-component-on-examine-is-full = Full drink-component-on-examine-is-mostly-full = Mostly Full drink-component-on-examine-is-half-full = Halfway Full diff --git a/Resources/Locale/en-US/nutrition/components/openable-component.ftl b/Resources/Locale/en-US/nutrition/components/openable-component.ftl new file mode 100644 index 0000000000..3acc24cf53 --- /dev/null +++ b/Resources/Locale/en-US/nutrition/components/openable-component.ftl @@ -0,0 +1,2 @@ +openable-component-verb-open = Open +openable-component-verb-close = Close diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks-cartons.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks-cartons.yml index cab95b0803..f232bf1d34 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks-cartons.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks-cartons.yml @@ -6,6 +6,10 @@ - type: Openable sound: collection: bottleOpenSounds #Could use a new sound someday ¯\_(ツ)_/¯ + closeable: true + closeSound: + collection: bottleCloseSounds + - type: Sealable - type: SolutionContainerManager solutions: drink: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml index b0b2c2729f..0119fab531 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml @@ -10,6 +10,9 @@ - type: Openable sound: collection: bottleOpenSounds + closeable: true + closeSound: + collection: bottleCloseSounds - type: SolutionContainerManager solutions: drink: @@ -131,6 +134,7 @@ Quantity: 100 - type: Sprite sprite: Objects/Consumable/Drinks/absinthebottle.rsi + - type: Sealable - type: entity parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull] @@ -170,6 +174,7 @@ Quantity: 100 - type: Sprite sprite: Objects/Consumable/Drinks/bottleofnothing.rsi + - type: Sealable - type: entity parent: [DrinkBottleVisualsOpenable, DrinkBottleGlassBaseFull] @@ -185,6 +190,8 @@ Quantity: 100 - type: Sprite sprite: Objects/Consumable/Drinks/champagnebottle.rsi + - type: Openable + closeable: false # Champagne corks are fat. Not worth the effort. - type: entity parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull] @@ -202,6 +209,7 @@ currentLabel: cognac - type: Sprite sprite: Objects/Consumable/Drinks/cognacbottle.rsi + - type: Sealable - type: entity parent: [DrinkBottleVisualsAll, DrinkBottlePlasticBaseFull] @@ -219,6 +227,7 @@ currentLabel: cola - type: Sprite sprite: Objects/Consumable/Drinks/colabottle.rsi + - type: Sealable - type: entity parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull] @@ -256,6 +265,7 @@ currentLabel: gin - type: Sprite sprite: Objects/Consumable/Drinks/ginbottle.rsi + - type: Sealable - type: entity parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull] @@ -271,6 +281,7 @@ Quantity: 100 - type: Sprite sprite: Objects/Consumable/Drinks/gildlagerbottle.rsi + - type: Sealable - type: entity parent: [DrinkBottleVisualsOpenable, DrinkBottleGlassBaseFull] @@ -288,6 +299,7 @@ currentLabel: coffee liqueur - type: Sprite sprite: Objects/Consumable/Drinks/coffeeliqueurbottle.rsi + - type: Sealable - type: entity parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull] @@ -342,6 +354,7 @@ Quantity: 100 - type: Sprite sprite: Objects/Consumable/Drinks/pwinebottle.rsi + - type: Sealable - type: entity parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull] @@ -359,6 +372,7 @@ currentLabel: rum - type: Sprite sprite: Objects/Consumable/Drinks/rumbottle.rsi + - type: Sealable - type: entity parent: [DrinkBottleVisualsAll, DrinkBottlePlasticBaseFull] @@ -377,6 +391,7 @@ currentLabel: space mountain wind - type: Sprite sprite: Objects/Consumable/Drinks/space_mountain_wind_bottle.rsi + - type: Sealable - type: entity parent: [DrinkBottleVisualsAll, DrinkBottlePlasticBaseFull] @@ -395,6 +410,7 @@ currentLabel: space-up - type: Sprite sprite: Objects/Consumable/Drinks/space-up_bottle.rsi + - type: Sealable - type: entity parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull] @@ -412,6 +428,7 @@ currentLabel: tequila - type: Sprite sprite: Objects/Consumable/Drinks/tequillabottle.rsi + - type: Sealable - type: entity parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull] @@ -429,6 +446,7 @@ currentLabel: vermouth - type: Sprite sprite: Objects/Consumable/Drinks/vermouthbottle.rsi + - type: Sealable - type: entity parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull] @@ -446,6 +464,7 @@ currentLabel: vodka - type: Sprite sprite: Objects/Consumable/Drinks/vodkabottle.rsi + - type: Sealable - type: entity parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull] @@ -463,6 +482,7 @@ currentLabel: whiskey - type: Sprite sprite: Objects/Consumable/Drinks/whiskeybottle.rsi + - type: Sealable - type: entity parent: [DrinkBottleVisualsOpenable, DrinkBottleGlassBaseFull] @@ -480,6 +500,7 @@ currentLabel: wine - type: Sprite sprite: Objects/Consumable/Drinks/winebottle.rsi + - type: Sealable # Small Bottles @@ -500,6 +521,8 @@ Quantity: 50 - type: Sprite sprite: Objects/Consumable/Drinks/beer.rsi + - type: Openable + closeable: false - type: entity parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull] @@ -518,6 +541,8 @@ currentLabel: beer - type: Sprite sprite: Objects/Consumable/Drinks/beer.rsi + - type: Openable + closeable: false - type: entity @@ -535,9 +560,10 @@ reagents: - ReagentId: Ale Quantity: 50 - - type: Sprite sprite: Objects/Consumable/Drinks/alebottle.rsi + - type: Openable + closeable: false - type: entity parent: [DrinkBottleVisualsAll, DrinkBottlePlasticBaseFull] @@ -556,6 +582,8 @@ currentLabel: ale - type: Sprite sprite: Objects/Consumable/Drinks/alebottle.rsi + - type: Openable + closeable: false - type: entity parent: [DrinkBottleVisualsOpenable, DrinkBottlePlasticBaseFull] @@ -587,6 +615,7 @@ fillBaseName: icon- inHandsMaxFillLevels: 2 inHandsFillBaseName: -fill- + - type: Sealable - type: entity parent: DrinkWaterBottleFull diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/condiments.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/condiments.yml index 197e7ea982..58fe2a3415 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/condiments.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/condiments.yml @@ -337,6 +337,7 @@ - type: Openable sound: collection: pop + closeable: true - type: SolutionContainerManager solutions: food: diff --git a/Resources/Prototypes/SoundCollections/drink_close_sounds.yml b/Resources/Prototypes/SoundCollections/drink_close_sounds.yml new file mode 100644 index 0000000000..9da4d28168 --- /dev/null +++ b/Resources/Prototypes/SoundCollections/drink_close_sounds.yml @@ -0,0 +1,4 @@ +- type: soundCollection + id: bottleCloseSounds + files: + - /Audio/Items/bottle_close1.ogg