]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add verbs to Open/Close Openable containers, and add optional seals (#24780)
authorTayrtahn <tayrtahn@gmail.com>
Tue, 13 Feb 2024 22:08:07 +0000 (17:08 -0500)
committerGitHub <noreply@github.com>
Tue, 13 Feb 2024 22:08:07 +0000 (17:08 -0500)
* 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

14 files changed:
Content.Server/Nutrition/Components/OpenableComponent.cs
Content.Server/Nutrition/EntitySystems/OpenableSystem.cs
Content.Shared/Nutrition/Components/SealableComponent.cs [new file with mode: 0644]
Content.Shared/Nutrition/Components/SharedFoodComponent.cs
Content.Shared/Nutrition/EntitySystems/SealableSystem.cs [new file with mode: 0644]
Content.Shared/Nutrition/EntitySystems/SharedOpenableSystem.cs [new file with mode: 0644]
Resources/Audio/Items/attributions.yml
Resources/Audio/Items/bottle_close1.ogg [new file with mode: 0644]
Resources/Locale/en-US/nutrition/components/drink-component.ftl
Resources/Locale/en-US/nutrition/components/openable-component.ftl [new file with mode: 0644]
Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks-cartons.yml
Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml
Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/condiments.yml
Resources/Prototypes/SoundCollections/drink_close_sounds.yml [new file with mode: 0644]

index 63efd5209625a8a55c5c2a841c8e9267e475a37f..cc24bf44dca46705497bd8272e105564797a7b39 100644 (file)
@@ -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.
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public bool Opened;
 
     /// <summary>
     /// If this is false you cant press Z to open it.
     /// Requires an OpenBehavior damage threshold or other logic to open.
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public bool OpenableByHand = true;
 
     /// <summary>
     /// Text shown when examining and its open.
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public LocId ExamineText = "drink-component-on-examine-is-opened";
 
     /// <summary>
@@ -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.
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public LocId ClosedPopup = "drink-component-try-use-drink-not-open";
 
+    /// <summary>
+    /// Text to show in the verb menu for the "Open" action.
+    /// You may want to change this for non-drinks, i.e. "Peel", "Unwrap"
+    /// </summary>
+    [DataField]
+    public LocId OpenVerbText = "openable-component-verb-open";
+
+    /// <summary>
+    /// Text to show in the verb menu for the "Close" action.
+    /// You may want to change this for non-drinks, i.e. "Wrap"
+    /// </summary>
+    [DataField]
+    public LocId CloseVerbText = "openable-component-verb-close";
+
     /// <summary>
     /// Sound played when opening.
     /// </summary>
     [DataField]
     public SoundSpecifier Sound = new SoundCollectionSpecifier("canOpenSounds");
+
+    /// <summary>
+    /// Can this item be closed again after opening?
+    /// </summary>
+    [DataField]
+    public bool Closeable;
+
+    /// <summary>
+    /// Sound played when closing.
+    /// </summary>
+    [DataField]
+    public SoundSpecifier? CloseSound;
 }
index d7b7da25b8893730d416d10a7ba76d40f6cb9426..373b97700f40937a8640553a0f0189edc613fdff 100644 (file)
@@ -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;
 
 /// <summary>
 /// Provides API for openable food and drinks, handles opening on use and preventing transfer when closed.
 /// </summary>
-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<OpenableComponent, SolutionTransferAttemptEvent>(OnTransferAttempt);
         SubscribeLocalEvent<OpenableComponent, MeleeHitEvent>(HandleIfClosed);
         SubscribeLocalEvent<OpenableComponent, AfterInteractEvent>(HandleIfClosed);
+        SubscribeLocalEvent<OpenableComponent, GetVerbsEvent<Verb>>(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<Verb> 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);
+    }
+
     /// <summary>
     /// 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;
     }
+
+    /// <summary>
+    /// If opened, closes it and plays the close sound, if one is defined.
+    /// </summary>
+    /// <returns>Whether it got closed</returns>
+    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 (file)
index 0000000..1c2f732
--- /dev/null
@@ -0,0 +1,32 @@
+using Content.Shared.Nutrition.EntitySystems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Nutrition.Components;
+
+/// <summary>
+/// Represents a tamper-evident seal on an Openable.
+/// Only affects the Examine text.
+/// Once the seal has been broken, it cannot be resealed.
+/// </summary>
+[NetworkedComponent, AutoGenerateComponentState]
+[RegisterComponent, Access(typeof(SealableSystem))]
+public sealed partial class SealableComponent : Component
+{
+    /// <summary>
+    /// Whether the item's seal is intact (i.e. it has never been opened)
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool Sealed = true;
+
+    /// <summary>
+    /// Text shown when examining and the item's seal has not been broken.
+    /// </summary>
+    [DataField]
+    public LocId ExamineTextSealed = "drink-component-on-examine-is-sealed";
+
+    /// <summary>
+    /// Text shown when examining and the item's seal has been broken.
+    /// </summary>
+    [DataField]
+    public LocId ExamineTextUnsealed = "drink-component-on-examine-is-unsealed";
+}
index 99ddabd3ce4562db2a5e8c4f489ae9f85aa3edff..07c02fb22b23f8af1d0371c7de35ca01af5ecf41 100644 (file)
@@ -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 (file)
index 0000000..b0873f2
--- /dev/null
@@ -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<SealableComponent, ExaminedEvent>(OnExamined, after: new[] { typeof(SharedOpenableSystem) });
+        SubscribeLocalEvent<SealableComponent, OpenableOpenedEvent>(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);
+    }
+
+    /// <summary>
+    /// Update seal visuals to the current value.
+    /// </summary>
+    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);
+    }
+
+    /// <summary>
+    /// Returns true if the entity's seal is intact.
+    /// Items without SealableComponent are considered unsealed.
+    /// </summary>
+    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 (file)
index 0000000..274de89
--- /dev/null
@@ -0,0 +1,17 @@
+namespace Content.Shared.Nutrition.EntitySystems;
+
+public abstract partial class SharedOpenableSystem : EntitySystem
+{
+}
+
+/// <summary>
+/// Raised after an Openable is opened.
+/// </summary>
+[ByRefEvent]
+public record struct OpenableOpenedEvent;
+
+/// <summary>
+/// Raised after an Openable is closed.
+/// </summary>
+[ByRefEvent]
+public record struct OpenableClosedEvent;
index 51d8d9cf95a8dd5e5d9c60fd5c701e52b8b500f8..8942e41db23cb99cf81ea179d074f1217fe09a17 100644 (file)
   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 (file)
index 0000000..b6db8fa
Binary files /dev/null and b/Resources/Audio/Items/bottle_close1.ogg differ
index 2bbe23dd43753cdd25cd2a8c16e2b96d8649fd09..9a388744b0cf6ed508a2b19870159126a992f806 100644 (file)
@@ -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 (file)
index 0000000..3acc24c
--- /dev/null
@@ -0,0 +1,2 @@
+openable-component-verb-open = Open
+openable-component-verb-close = Close
index cab95b0803b6d2cfefe2062c3db010a6a812c967..f232bf1d34d4b5aeedf4bef057f6406decdf1422 100644 (file)
@@ -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:
index b0b2c2729f997e76f5f37c6d214cf5e281334b35..0119fab5316a9ac25051794221ffc780cecb6566 100644 (file)
@@ -10,6 +10,9 @@
   - type: Openable
     sound:
       collection: bottleOpenSounds
+    closeable: true
+    closeSound:
+      collection: bottleCloseSounds
   - type: SolutionContainerManager
     solutions:
       drink:
           Quantity: 100
   - type: Sprite
     sprite: Objects/Consumable/Drinks/absinthebottle.rsi
+  - type: Sealable
 
 - type: entity
   parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull]
           Quantity: 100
   - type: Sprite
     sprite: Objects/Consumable/Drinks/bottleofnothing.rsi
+  - type: Sealable
 
 - type: entity
   parent: [DrinkBottleVisualsOpenable, DrinkBottleGlassBaseFull]
           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]
     currentLabel: cognac
   - type: Sprite
     sprite: Objects/Consumable/Drinks/cognacbottle.rsi
+  - type: Sealable
 
 - type: entity
   parent: [DrinkBottleVisualsAll, DrinkBottlePlasticBaseFull]
     currentLabel: cola
   - type: Sprite
     sprite: Objects/Consumable/Drinks/colabottle.rsi
+  - type: Sealable
 
 - type: entity
   parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull]
     currentLabel: gin
   - type: Sprite
     sprite: Objects/Consumable/Drinks/ginbottle.rsi
+  - type: Sealable
 
 - type: entity
   parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull]
           Quantity: 100
   - type: Sprite
     sprite: Objects/Consumable/Drinks/gildlagerbottle.rsi
+  - type: Sealable
 
 - type: entity
   parent: [DrinkBottleVisualsOpenable, DrinkBottleGlassBaseFull]
     currentLabel: coffee liqueur
   - type: Sprite
     sprite: Objects/Consumable/Drinks/coffeeliqueurbottle.rsi
+  - type: Sealable
 
 - type: entity
   parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull]
           Quantity: 100
   - type: Sprite
     sprite: Objects/Consumable/Drinks/pwinebottle.rsi
+  - type: Sealable
 
 - type: entity
   parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull]
     currentLabel: rum
   - type: Sprite
     sprite: Objects/Consumable/Drinks/rumbottle.rsi
+  - type: Sealable
 
 - type: entity
   parent: [DrinkBottleVisualsAll, DrinkBottlePlasticBaseFull]
     currentLabel: space mountain wind
   - type: Sprite
     sprite: Objects/Consumable/Drinks/space_mountain_wind_bottle.rsi
+  - type: Sealable
 
 - type: entity
   parent: [DrinkBottleVisualsAll, DrinkBottlePlasticBaseFull]
     currentLabel: space-up
   - type: Sprite
     sprite: Objects/Consumable/Drinks/space-up_bottle.rsi
+  - type: Sealable
 
 - type: entity
   parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull]
     currentLabel: tequila
   - type: Sprite
     sprite: Objects/Consumable/Drinks/tequillabottle.rsi
+  - type: Sealable
 
 - type: entity
   parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull]
     currentLabel: vermouth
   - type: Sprite
     sprite: Objects/Consumable/Drinks/vermouthbottle.rsi
+  - type: Sealable
 
 - type: entity
   parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull]
     currentLabel: vodka
   - type: Sprite
     sprite: Objects/Consumable/Drinks/vodkabottle.rsi
+  - type: Sealable
 
 - type: entity
   parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull]
     currentLabel: whiskey
   - type: Sprite
     sprite: Objects/Consumable/Drinks/whiskeybottle.rsi
+  - type: Sealable
 
 - type: entity
   parent: [DrinkBottleVisualsOpenable, DrinkBottleGlassBaseFull]
     currentLabel: wine
   - type: Sprite
     sprite: Objects/Consumable/Drinks/winebottle.rsi
+  - type: Sealable
 
 # Small Bottles
 
           Quantity: 50
   - type: Sprite
     sprite: Objects/Consumable/Drinks/beer.rsi
+  - type: Openable
+    closeable: false
 
 - type: entity
   parent: [DrinkBottleVisualsAll, DrinkBottleGlassBaseFull]
     currentLabel: beer
   - type: Sprite
     sprite: Objects/Consumable/Drinks/beer.rsi
+  - type: Openable
+    closeable: false
 
 
 - type: entity
         reagents:
         - ReagentId: Ale
           Quantity: 50
-
   - type: Sprite
     sprite: Objects/Consumable/Drinks/alebottle.rsi
+  - type: Openable
+    closeable: false
 
 - type: entity
   parent: [DrinkBottleVisualsAll, DrinkBottlePlasticBaseFull]
     currentLabel: ale
   - type: Sprite
     sprite: Objects/Consumable/Drinks/alebottle.rsi
+  - type: Openable
+    closeable: false
 
 - type: entity
   parent: [DrinkBottleVisualsOpenable, DrinkBottlePlasticBaseFull]
     fillBaseName: icon-
     inHandsMaxFillLevels: 2
     inHandsFillBaseName: -fill-
+  - type: Sealable
 
 - type: entity
   parent: DrinkWaterBottleFull
index 197e7ea98298d63c5fe5d0b6feee8a460196a2d8..58fe2a34154378e1026c17164f4700470232bcf6 100644 (file)
   - 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 (file)
index 0000000..9da4d28
--- /dev/null
@@ -0,0 +1,4 @@
+- type: soundCollection
+  id: bottleCloseSounds
+  files:
+  - /Audio/Items/bottle_close1.ogg