diff --git a/Content.Client/GameObjects/Components/Watercloset/ToiletVisualizer.cs b/Content.Client/GameObjects/Components/Watercloset/ToiletVisualizer.cs new file mode 100644 index 0000000000..f776b68e75 --- /dev/null +++ b/Content.Client/GameObjects/Components/Watercloset/ToiletVisualizer.cs @@ -0,0 +1,26 @@ +#nullable enable +using Content.Shared.GameObjects.Components.Watercloset; +using Robust.Client.GameObjects; +using Robust.Client.Interfaces.GameObjects.Components; + +namespace Content.Client.GameObjects.Components.Watercloset +{ + public class ToiletVisualizer : AppearanceVisualizer + { + public override void OnChangeData(AppearanceComponent component) + { + base.OnChangeData(component); + + if (!component.Owner.TryGetComponent(out ISpriteComponent? sprite)) return; + + if (!component.TryGetData(ToiletVisuals.LidOpen, out bool lidOpen)) lidOpen = false; + if (!component.TryGetData(ToiletVisuals.SeatUp, out bool seatUp)) seatUp = false; + + var state = string.Format("{0}_toilet_{1}", + lidOpen ? "open" : "closed", + seatUp ? "seat_up" : "seat_down"); + + sprite.LayerSetState(0, state); + } + } +} diff --git a/Content.Client/IgnoredComponents.cs b/Content.Client/IgnoredComponents.cs index 77d93add0d..6823e56a43 100644 --- a/Content.Client/IgnoredComponents.cs +++ b/Content.Client/IgnoredComponents.cs @@ -237,6 +237,8 @@ namespace Content.Client "DamageOnLand", "GasFilter", "Recyclable", + "SecretStash", + "Toilet", "ClusterFlash" }; } diff --git a/Content.Server/Construction/Conditions/ToiletLidClosed.cs b/Content.Server/Construction/Conditions/ToiletLidClosed.cs new file mode 100644 index 0000000000..a1ad695bf6 --- /dev/null +++ b/Content.Server/Construction/Conditions/ToiletLidClosed.cs @@ -0,0 +1,35 @@ +#nullable enable +using System.Threading.Tasks; +using Content.Server.GameObjects.Components.Watercloset; +using Content.Shared.Construction; +using JetBrains.Annotations; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Content.Server.Construction.Conditions +{ + [UsedImplicitly] + public class ToiletLidClosed : IEdgeCondition + { + public async Task Condition(IEntity entity) + { + if (!entity.TryGetComponent(out ToiletComponent? toilet)) return false; + return !toilet.LidOpen; + } + + public bool DoExamine(IEntity entity, FormattedMessage message, bool inExamineRange) + { + if (!entity.TryGetComponent(out ToiletComponent? toilet)) return false; + if (!toilet.LidOpen) return false; + + message.AddMarkup(Loc.GetString("Use a [color=yellow]crowbar[/color] to close the lid.\n")); + return true; + } + + public void ExposeData(ObjectSerializer serializer) + { + } + } +} diff --git a/Content.Server/GameObjects/Components/Items/Storage/SecretStashComponent.cs b/Content.Server/GameObjects/Components/Items/Storage/SecretStashComponent.cs new file mode 100644 index 0000000000..b2255dee11 --- /dev/null +++ b/Content.Server/GameObjects/Components/Items/Storage/SecretStashComponent.cs @@ -0,0 +1,119 @@ +#nullable enable +using Content.Server.GameObjects.Components.GUI; +using Content.Server.Interfaces.GameObjects.Components.Items; +using Content.Shared.GameObjects.EntitySystems; +using Content.Shared.Interfaces; +using Robust.Server.GameObjects.Components.Container; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Items.Storage +{ + /// + /// Logic for secret single slot stash, like plant pot or toilet cistern + /// + [RegisterComponent] + public class SecretStashComponent : Component, IDestroyAct + { + public override string Name => "SecretStash"; + + [ViewVariables] private int _maxItemSize; + [ViewVariables] private string _secretPartName = ""; + + [ViewVariables] private ContainerSlot _itemContainer = default!; + + public override void Initialize() + { + base.Initialize(); + _itemContainer = ContainerManagerComponent.Ensure("stash", Owner, out _); + } + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + serializer.DataField(ref _maxItemSize, "maxItemSize", (int) ReferenceSizes.Pocket); + serializer.DataField(ref _secretPartName, "secretPartName", Loc.GetString("{0:theName}")); + } + + /// + /// Tries to hide item inside secret stash from hands of user + /// + /// + /// + /// True if item was hidden inside stash + public bool TryHideItem(IEntity user, IEntity itemToHide) + { + if (_itemContainer.ContainedEntity != null) + { + Owner.PopupMessage(user, Loc.GetString("There's already something in here?!")); + return false; + } + + if (!itemToHide.TryGetComponent(out ItemComponent? item)) + return false; + + if (item.Size > _maxItemSize) + { + Owner.PopupMessage(user, + Loc.GetString("{0:TheName} is too big to fit in {1}!", itemToHide, _secretPartName)); + return false; + } + + if (!user.TryGetComponent(out IHandsComponent? hands)) + return false; + + if (!hands.Drop(itemToHide, _itemContainer)) + return false; + + Owner.PopupMessage(user, Loc.GetString("You hide {0:theName} in {1}.", itemToHide, _secretPartName)); + return true; + } + + /// + /// Try get item and place it in users hand + /// If user can't take it by hands, will drop item from container + /// + /// + /// True if user recieved item + public bool TryGetItem(IEntity user) + { + if (_itemContainer.ContainedEntity == null) + return false; + + Owner.PopupMessage(user, Loc.GetString("There was something inside {0}!", _secretPartName)); + + if (user.TryGetComponent(out HandsComponent? hands)) + { + if (!_itemContainer.ContainedEntity.TryGetComponent(out ItemComponent? item)) + return false; + hands.PutInHandOrDrop(item); + } + else if (_itemContainer.Remove(_itemContainer.ContainedEntity)) + { + _itemContainer.ContainedEntity.Transform.Coordinates = Owner.Transform.Coordinates; + } + + return true; + } + + /// + /// Is there something inside secret stash item container? + /// + /// + public bool HasItemInside() + { + return _itemContainer.ContainedEntity != null; + } + + public void OnDestroy(DestructionEventArgs eventArgs) + { + // drop item inside + if (_itemContainer.ContainedEntity != null) + { + _itemContainer.ContainedEntity.Transform.Coordinates = Owner.Transform.Coordinates; + } + } + } +} diff --git a/Content.Server/GameObjects/Components/PottedPlantHideComponent.cs b/Content.Server/GameObjects/Components/PottedPlantHideComponent.cs index cc924cffd4..288fa0242d 100644 --- a/Content.Server/GameObjects/Components/PottedPlantHideComponent.cs +++ b/Content.Server/GameObjects/Components/PottedPlantHideComponent.cs @@ -17,74 +17,33 @@ namespace Content.Server.GameObjects.Components [RegisterComponent] public class PottedPlantHideComponent : Component, IInteractUsing, IInteractHand { - private const int MaxItemSize = (int) ReferenceSizes.Pocket; - public override string Name => "PottedPlantHide"; - [ViewVariables] private ContainerSlot _itemContainer; + [ViewVariables] private SecretStashComponent _secretStash = default!; public override void Initialize() { base.Initialize(); - - _itemContainer = - ContainerManagerComponent.Ensure("potted_plant_hide", Owner, out _); + _secretStash = Owner.EnsureComponent(); } async Task IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs) { - if (_itemContainer.ContainedEntity != null) - { - Rustle(); - - Owner.PopupMessage(eventArgs.User, Loc.GetString("There's already something in here?!")); - return false; - } - - var size = eventArgs.Using.GetComponent().Size; - - // TODO: use proper text macro system for this. - - if (size > MaxItemSize) - { - Owner.PopupMessage(eventArgs.User, - Loc.GetString("{0:TheName} is too big to fit in the plant!", eventArgs.Using)); - return false; - } - - var handsComponent = eventArgs.User.GetComponent(); - - if (!handsComponent.Drop(eventArgs.Using, _itemContainer)) - { - return false; - } - - Owner.PopupMessage(eventArgs.User, Loc.GetString("You hide {0:theName} in the plant.", eventArgs.Using)); Rustle(); - return true; + return _secretStash.TryHideItem(eventArgs.User, eventArgs.Using); } bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs) { Rustle(); - if (_itemContainer.ContainedEntity == null) + var gotItem = _secretStash.TryGetItem(eventArgs.User); + if (!gotItem) { Owner.PopupMessage(eventArgs.User, Loc.GetString("You root around in the roots.")); - return true; } - Owner.PopupMessage(eventArgs.User, Loc.GetString("There was something in there!")); - if (eventArgs.User.TryGetComponent(out HandsComponent hands)) - { - hands.PutInHandOrDrop(_itemContainer.ContainedEntity.GetComponent()); - } - else if (_itemContainer.Remove(_itemContainer.ContainedEntity)) - { - _itemContainer.ContainedEntity.Transform.Coordinates = Owner.Transform.Coordinates; - } - - return true; + return gotItem; } private void Rustle() diff --git a/Content.Server/GameObjects/Components/Watercloset/ToiletComponent.cs b/Content.Server/GameObjects/Components/Watercloset/ToiletComponent.cs new file mode 100644 index 0000000000..e2f0ff1376 --- /dev/null +++ b/Content.Server/GameObjects/Components/Watercloset/ToiletComponent.cs @@ -0,0 +1,174 @@ +#nullable enable +using Content.Server.GameObjects.Components.Interactable; +using Content.Server.GameObjects.Components.Items.Storage; +using Content.Server.GameObjects.Components.Strap; +using Content.Server.Interfaces.Chat; +using Content.Server.Interfaces.GameObjects; +using Content.Server.Utility; +using Content.Shared.Audio; +using Content.Shared.GameObjects.Components.Body; +using Content.Shared.GameObjects.Components.Body.Part; +using Content.Shared.GameObjects.Components.Interactable; +using Content.Shared.GameObjects.Components.Watercloset; +using Content.Shared.GameObjects.EntitySystems; +using Content.Shared.Interfaces; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Server.GameObjects; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Server.Interfaces.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Random; +using Robust.Shared.Utility; +using Robust.Shared.ViewVariables; +using System.Threading.Tasks; + +namespace Content.Server.GameObjects.Components.Watercloset +{ + [RegisterComponent] + public class ToiletComponent : Component, IInteractUsing, + IInteractHand, IMapInit, IExamine, ISuicideAct + { + public sealed override string Name => "Toilet"; + + private const float PryLidTime = 1f; + + private bool _isPrying = false; + + [ViewVariables] public bool LidOpen { get; private set; } + [ViewVariables] public bool IsSeatUp { get; private set; } + + [ViewVariables] private SecretStashComponent _secretStash = default!; + + public override void Initialize() + { + base.Initialize(); + _secretStash = Owner.EnsureComponent(); + } + + public void MapInit() + { + // roll is toilet seat will be up or down + var random = IoCManager.Resolve(); + IsSeatUp = random.Prob(0.5f); + UpdateSprite(); + } + + public async Task InteractUsing(InteractUsingEventArgs eventArgs) + { + // are player trying place or lift of cistern lid? + if (eventArgs.Using.TryGetComponent(out ToolComponent? tool) + && tool!.HasQuality(ToolQuality.Prying)) + { + // check if someone is already prying this toilet + if (_isPrying) + return false; + _isPrying = true; + + if (!await tool.UseTool(eventArgs.User, Owner, PryLidTime, ToolQuality.Prying)) + { + _isPrying = false; + return false; + } + + _isPrying = false; + + // all cool - toggle lid + LidOpen = !LidOpen; + UpdateSprite(); + + return true; + } + // maybe player trying to hide something inside cistern? + else if (LidOpen) + { + return _secretStash.TryHideItem(eventArgs.User, eventArgs.Using); + } + + return false; + } + + public bool InteractHand(InteractHandEventArgs eventArgs) + { + // trying get something from stash? + if (LidOpen) + { + var gotItem = _secretStash.TryGetItem(eventArgs.User); + + if (gotItem) + return true; + } + + // just want to up/down seat? + // check that nobody seats on seat right now + if (Owner.TryGetComponent(out StrapComponent? strap)) + { + if (strap.BuckledEntities.Count != 0) + return false; + } + + ToggleToiletSeat(); + return true; + } + + public void Examine(FormattedMessage message, bool inDetailsRange) + { + if (inDetailsRange && LidOpen) + { + if (_secretStash.HasItemInside()) + { + message.AddMarkup(Loc.GetString("There is [color=darkgreen]something[/color] inside cistern!")); + } + } + } + + public void ToggleToiletSeat() + { + IsSeatUp = !IsSeatUp; + EntitySystem.Get() + .PlayFromEntity("/Audio/Effects/toilet_seat_down.ogg", Owner, AudioHelpers.WithVariation(0.05f)); + + UpdateSprite(); + } + + private void UpdateSprite() + { + if (Owner.TryGetComponent(out AppearanceComponent? appearance)) + { + appearance.SetData(ToiletVisuals.LidOpen, LidOpen); + appearance.SetData(ToiletVisuals.SeatUp, IsSeatUp); + } + } + + public SuicideKind Suicide(IEntity victim, IChatManager chat) + { + // check that victim even have head + if (victim.TryGetComponent(out var body) && + body.GetPartsOfType(BodyPartType.Head).Count != 0) + { + var othersMessage = Loc.GetString("{0:theName} sticks their head into {1:theName} and flushes it!", victim, Owner); + victim.PopupMessageOtherClients(othersMessage); + + var selfMessage = Loc.GetString("You stick your head into {0:theName} and flush it!", Owner); + victim.PopupMessage(selfMessage); + + return SuicideKind.Asphyxiation; + } + else + { + var othersMessage = Loc.GetString("{0:theName} bashes themselves with {1:theName}!", victim, Owner); + victim.PopupMessageOtherClients(othersMessage); + + var selfMessage = Loc.GetString("You bash yourself with {0:theName}!", Owner); + victim.PopupMessage(selfMessage); + + return SuicideKind.Blunt; + } + } + + } +} diff --git a/Content.Shared/GameObjects/Components/Watercloset/SharedWaterclosetVisuals.cs b/Content.Shared/GameObjects/Components/Watercloset/SharedWaterclosetVisuals.cs new file mode 100644 index 0000000000..0ee66dd2c9 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Watercloset/SharedWaterclosetVisuals.cs @@ -0,0 +1,13 @@ +#nullable enable +using Robust.Shared.Serialization; +using System; + +namespace Content.Shared.GameObjects.Components.Watercloset +{ + [Serializable, NetSerializable] + public enum ToiletVisuals + { + LidOpen, + SeatUp + } +} diff --git a/Resources/Audio/Effects/toilet_seat_down.ogg b/Resources/Audio/Effects/toilet_seat_down.ogg new file mode 100644 index 0000000000..dc568adf23 Binary files /dev/null and b/Resources/Audio/Effects/toilet_seat_down.ogg differ diff --git a/Resources/Prototypes/Entities/Constructible/Furniture/potted_plants.yml b/Resources/Prototypes/Entities/Constructible/Furniture/potted_plants.yml index a5bcf67edd..8272e0e572 100644 --- a/Resources/Prototypes/Entities/Constructible/Furniture/potted_plants.yml +++ b/Resources/Prototypes/Entities/Constructible/Furniture/potted_plants.yml @@ -1,4 +1,4 @@ -- type: entity +- type: entity id: PottedPlantBase abstract: true components: @@ -23,6 +23,8 @@ - type: Sprite sprite: Constructible/Misc/potted_plants.rsi - type: PottedPlantHide + - type: SecretStash + secretPartName: the plant - type: Anchorable - type: Pullable diff --git a/Resources/Prototypes/Entities/Constructible/Watercloset/toilet.yml b/Resources/Prototypes/Entities/Constructible/Watercloset/toilet.yml new file mode 100644 index 0000000000..cb73fef1f9 --- /dev/null +++ b/Resources/Prototypes/Entities/Constructible/Watercloset/toilet.yml @@ -0,0 +1,39 @@ +- type: entity + name: toilet + id: ToiletEmpty + suffix: Empty + parent: SeatBase + description: The HT-451, a torque rotation-based, waste disposal unit for small matter. This one seems remarkably clean. + components: + - type: Sprite + sprite: Constructible/Watercloset/toilet.rsi + state: closed_toilet_seat_up + netsync: false + - type: Toilet + - type: SecretStash + secretPartName: the toilet cistern + - type: SolutionContainer + maxVol: 250 + - type: Physics + shapes: + - !type:PhysShapeAabb + layer: [ Passable ] + - type: Construction + graph: toilet + node: toilet + - type: Appearance + visuals: + - type: ToiletVisualizer + +- type: entity + id: ToiletDirtyWater + parent: ToiletEmpty + suffix: Dirty Water + components: + - type: SolutionContainer + contents: + reagents: + - ReagentId: chem.Water + Quantity: 180 + - ReagentId: chem.Toxin + Quantity: 20 diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/toilet.yml b/Resources/Prototypes/Recipes/Construction/Graphs/toilet.yml new file mode 100644 index 0000000000..faf5cc9f6b --- /dev/null +++ b/Resources/Prototypes/Recipes/Construction/Graphs/toilet.yml @@ -0,0 +1,29 @@ +- type: constructionGraph + id: toilet + start: start + graph: + - node: start + edges: + - to: toilet + completed: + - !type:SnapToGrid { } + steps: + - material: Metal + amount: 5 + doAfter: 1 + - node: toilet + edges: + - to: start + completed: + - !type:SpawnPrototype + prototype: SteelSheet1 + amount: 5 + - !type:EmptyAllContainers {} + - !type:DeleteEntity {} + conditions: + - !type:EntityAnchored + anchored: false + - !type:ToiletLidClosed {} + steps: + - tool: Welding + doAfter: 2 \ No newline at end of file diff --git a/Resources/Textures/Constructible/Watercloset/toilet.rsi/closed_toilet_seat_down.png b/Resources/Textures/Constructible/Watercloset/toilet.rsi/closed_toilet_seat_down.png new file mode 100644 index 0000000000..af9b04af2b Binary files /dev/null and b/Resources/Textures/Constructible/Watercloset/toilet.rsi/closed_toilet_seat_down.png differ diff --git a/Resources/Textures/Constructible/Watercloset/toilet.rsi/closed_toilet_seat_up.png b/Resources/Textures/Constructible/Watercloset/toilet.rsi/closed_toilet_seat_up.png new file mode 100644 index 0000000000..0a3a4db1b1 Binary files /dev/null and b/Resources/Textures/Constructible/Watercloset/toilet.rsi/closed_toilet_seat_up.png differ diff --git a/Resources/Textures/Constructible/Watercloset/toilet.rsi/meta.json b/Resources/Textures/Constructible/Watercloset/toilet.rsi/meta.json new file mode 100644 index 0000000000..0761994277 --- /dev/null +++ b/Resources/Textures/Constructible/Watercloset/toilet.rsi/meta.json @@ -0,0 +1,27 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from cev-eris at commit https://github.com/discordia-space/CEV-Eris/commit/2cb66bae0e253e13d37f8939e0983bb94fee243e", + "states": [ + { + "name": "closed_toilet_seat_down", + "directions": 4 + }, + { + "name": "closed_toilet_seat_up", + "directions": 4 + }, + { + "name": "open_toilet_seat_down", + "directions": 4 + }, + { + "name": "open_toilet_seat_up", + "directions": 4 + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Constructible/Watercloset/toilet.rsi/open_toilet_seat_down.png b/Resources/Textures/Constructible/Watercloset/toilet.rsi/open_toilet_seat_down.png new file mode 100644 index 0000000000..a0844c8793 Binary files /dev/null and b/Resources/Textures/Constructible/Watercloset/toilet.rsi/open_toilet_seat_down.png differ diff --git a/Resources/Textures/Constructible/Watercloset/toilet.rsi/open_toilet_seat_up.png b/Resources/Textures/Constructible/Watercloset/toilet.rsi/open_toilet_seat_up.png new file mode 100644 index 0000000000..bcbe594c2b Binary files /dev/null and b/Resources/Textures/Constructible/Watercloset/toilet.rsi/open_toilet_seat_up.png differ