/// 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>
/// 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;
}
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!;
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)
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.
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);
}
_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;
+ }
}
--- /dev/null
+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";
+}
Opened,
Layer
}
+
+ [Serializable, NetSerializable]
+ public enum SealableVisuals : byte
+ {
+ Sealed,
+ Layer,
+ }
}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
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"
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"
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
--- /dev/null
+openable-component-verb-open = Open
+openable-component-verb-close = Close
- type: Openable
sound:
collection: bottleOpenSounds #Could use a new sound someday ¯\_(ツ)_/¯
+ closeable: true
+ closeSound:
+ collection: bottleCloseSounds
+ - type: Sealable
- type: SolutionContainerManager
solutions:
drink:
- 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
- type: Openable
sound:
collection: pop
+ closeable: true
- type: SolutionContainerManager
solutions:
food:
--- /dev/null
+- type: soundCollection
+ id: bottleCloseSounds
+ files:
+ - /Audio/Items/bottle_close1.ogg