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