From 99a5e06b98a7ac6cd8611b05451ba5c3e2286bf3 Mon Sep 17 00:00:00 2001 From: DrSmugleaf Date: Fri, 19 Jun 2020 15:20:59 +0200 Subject: [PATCH] Adds eating with utensils (#1136) * Add Utensil component For eating. With utensils. Added to fork, plastic fork, spoon, plastic spoon and plastic knife. Ignored component on the client. * Add break chance to utensils Set to 20% for plastic ones * Add break sound to utensils * Add utensil kinds None, fork, spoon and knife. For sporks, forknifes and sporknifes, of course. * Add restricting foods by utensils needed * Fix utensils breaking when food isn't eaten * Moved getting held utensils to FoodComponent * Add breaking a clicking utensil even if its not necessary to eat the food * Move use utensil code to a separate method * Add telling a handless entity when they need an utensil to eat The immersion is off the charts * Change food trash to only be held when the food was also being held * Fix Wi-Fi utensils * Remove unnecessary utensil ItemGroup * Made TryUseFood public, removed redundant trash position update * Renamed UtensilKind to UtensilType * Remove eating food when clicking with it on nothing * Disable eating food when clicked directly if it requires an untensil to eat --- Content.Client/EntryPoint.cs | 1 + .../Components/Nutrition/FoodComponent.cs | 119 ++++++++++++++--- .../Components/Utensil/UtensilComponent.cs | 124 ++++++++++++++++++ .../Utensil/SharedUtensilComponent.cs | 21 +++ Resources/Audio/items/snap.ogg | Bin 0 -> 5923 bytes .../Prototypes/Entities/Items/utensils.yml | 24 +++- 6 files changed, 263 insertions(+), 26 deletions(-) create mode 100644 Content.Server/GameObjects/Components/Utensil/UtensilComponent.cs create mode 100644 Content.Shared/GameObjects/Components/Utensil/SharedUtensilComponent.cs create mode 100644 Resources/Audio/items/snap.ogg diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index 6ec1fcc427..16e41ce2fb 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -167,6 +167,7 @@ namespace Content.Client "SecureEntityStorage", "PresetIdCard", "SolarControlConsole", + "Utensil", }; foreach (var ignoreName in registerIgnore) diff --git a/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs b/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs index 242efc0d0b..7cdaa14916 100644 --- a/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs +++ b/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; using Content.Server.GameObjects.Components.Chemistry; -using Content.Server.GameObjects.Components.Sound; +using Content.Server.GameObjects.Components.Utensil; using Content.Server.GameObjects.EntitySystems; +using Content.Server.Utility; using Content.Shared.Chemistry; +using Content.Shared.GameObjects.Components.Utensil; using Content.Shared.Interfaces; using Robust.Server.GameObjects.EntitySystems; using Robust.Shared.Audio; @@ -10,6 +13,7 @@ using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; +using Robust.Shared.Log; using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; @@ -32,18 +36,28 @@ namespace Content.Server.GameObjects.Components.Nutrition private SolutionComponent _contents; [ViewVariables] private ReagentUnit _transferAmount; + private UtensilType _utensilsNeeded; public int UsesRemaining => _contents.CurrentVolume == 0 ? 0 : Math.Max(1, (int)Math.Ceiling((_contents.CurrentVolume / _transferAmount).Float())); - public override void ExposeData(ObjectSerializer serializer) { base.ExposeData(serializer); serializer.DataField(ref _useSound, "useSound", "/Audio/items/eatfood.ogg"); serializer.DataField(ref _transferAmount, "transferAmount", ReagentUnit.New(5)); serializer.DataField(ref _trashPrototype, "trash", "TrashPlate"); + + if (serializer.Reading) + { + var utensils = serializer.ReadDataField("utensils", new List()); + foreach (var utensil in utensils) + { + _utensilsNeeded |= utensil; + Dirty(); + } + } } public override void Initialize() @@ -55,15 +69,27 @@ namespace Content.Server.GameObjects.Components.Nutrition bool IUse.UseEntity(UseEntityEventArgs eventArgs) { + if (_utensilsNeeded != UtensilType.None) + { + eventArgs.User.PopupMessage(eventArgs.User, Loc.GetString("You need to use a {0} to eat that!", _utensilsNeeded)); + return false; + } + return TryUseFood(eventArgs.User, null); } + // Feeding someone else void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { + if (eventArgs.Target == null) + { + return; + } + TryUseFood(eventArgs.User, eventArgs.Target); } - private bool TryUseFood(IEntity user, IEntity target) + public bool TryUseFood(IEntity user, IEntity target, UtensilComponent utensilUsed = null) { if (user == null) { @@ -78,20 +104,65 @@ namespace Content.Server.GameObjects.Components.Nutrition var trueTarget = target ?? user; - if (trueTarget.TryGetComponent(out StomachComponent stomachComponent)) + if (!trueTarget.TryGetComponent(out StomachComponent stomach)) { - var transferAmount = ReagentUnit.Min(_transferAmount, _contents.CurrentVolume); - var split = _contents.SplitSolution(transferAmount); - if (stomachComponent.TryTransferSolution(split)) + return false; + } + + var utensils = utensilUsed != null + ? new List {utensilUsed} + : null; + + if (_utensilsNeeded != UtensilType.None) + { + utensils = new List(); + var types = UtensilType.None; + + if (user.TryGetComponent(out HandsComponent hands)) { - _entitySystem.GetEntitySystem() - .PlayFromEntity(_useSound, trueTarget, AudioParams.Default.WithVolume(-1f)); - trueTarget.PopupMessage(user, Loc.GetString("Nom")); + foreach (var item in hands.GetAllHeldItems()) + { + if (!item.Owner.TryGetComponent(out UtensilComponent utensil)) + { + continue; + } + + utensils.Add(utensil); + types |= utensil.Types; + } } - else + + if (!types.HasFlag(_utensilsNeeded)) { - _contents.TryAddSolution(split); - trueTarget.PopupMessage(user, Loc.GetString("You can't eat any more!")); + trueTarget.PopupMessage(user, Loc.GetString("You need to be holding a {0} to eat that!", _utensilsNeeded)); + return false; + } + } + + if (!InteractionChecks.InRangeUnobstructed(user, trueTarget.Transform.MapPosition)) + { + return false; + } + + var transferAmount = ReagentUnit.Min(_transferAmount, _contents.CurrentVolume); + var split = _contents.SplitSolution(transferAmount); + if (!stomach.TryTransferSolution(split)) + { + _contents.TryAddSolution(split); + trueTarget.PopupMessage(user, Loc.GetString("You can't eat any more!")); + return false; + } + + _entitySystem.GetEntitySystem() + .PlayFromEntity(_useSound, trueTarget, AudioParams.Default.WithVolume(-1f)); + trueTarget.PopupMessage(user, Loc.GetString("Nom")); + + // If utensils were used + if (utensils != null) + { + foreach (var utensil in utensils) + { + utensil.TryBreak(user); } } @@ -102,19 +173,27 @@ namespace Content.Server.GameObjects.Components.Nutrition //We're empty. Become trash. var position = Owner.Transform.GridPosition; - Owner.Delete(); var finisher = Owner.EntityManager.SpawnEntity(_trashPrototype, position); - if (user.TryGetComponent(out HandsComponent handsComponent) && finisher.TryGetComponent(out ItemComponent itemComponent)) + + // If the user is holding the item + if (user.TryGetComponent(out HandsComponent handsComponent) && + handsComponent.IsHolding(Owner)) { - if (handsComponent.CanPutInHand(itemComponent)) + Owner.Delete(); + + // Put the trash in the user's hand + if (finisher.TryGetComponent(out ItemComponent item) && + handsComponent.CanPutInHand(item)) { - handsComponent.PutInHand(itemComponent); - return true; + handsComponent.PutInHand(item); } } - finisher.Transform.GridPosition = user.Transform.GridPosition; - return true; + else + { + Owner.Delete(); + } + return true; } } } diff --git a/Content.Server/GameObjects/Components/Utensil/UtensilComponent.cs b/Content.Server/GameObjects/Components/Utensil/UtensilComponent.cs new file mode 100644 index 0000000000..3069bd72c3 --- /dev/null +++ b/Content.Server/GameObjects/Components/Utensil/UtensilComponent.cs @@ -0,0 +1,124 @@ +using System.Collections.Generic; +using Content.Server.GameObjects.Components.Nutrition; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Utility; +using Content.Shared.GameObjects.Components.Utensil; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.IoC; +using Robust.Shared.Random; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Utensil +{ + [RegisterComponent] + public class UtensilComponent : SharedUtensilComponent, IAfterInteract + { +#pragma warning disable 649 + [Dependency] private readonly IEntitySystemManager _entitySystem; + [Dependency] private readonly IRobustRandom _random; +#pragma warning restore 649 + + protected UtensilType _types = UtensilType.None; + + [ViewVariables] + public override UtensilType Types + { + get => _types; + set + { + _types = value; + Dirty(); + } + } + + /// + /// The chance that the utensil has to break with each use. + /// A value of 0 means that it is unbreakable. + /// + [ViewVariables] + private float _breakChance; + + /// + /// The sound to be played if the utensil breaks. + /// + [ViewVariables] + private string _breakSound; + + public void AddType(UtensilType type) + { + Types |= type; + } + + public bool HasAnyType(UtensilType type) + { + return (_types & type) != UtensilType.None; + } + + public bool HasType(UtensilType type) + { + return _types.HasFlag(type); + } + + public void RemoveType(UtensilType type) + { + Types &= ~type; + } + + internal void TryBreak(IEntity user) + { + if (_random.Prob(_breakChance)) + { + _entitySystem.GetEntitySystem() + .PlayFromEntity(_breakSound, user, AudioParams.Default.WithVolume(-2f)); + Owner.Delete(); + } + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + if (serializer.Reading) + { + var types = serializer.ReadDataField("types", new List()); + foreach (var type in types) + { + AddType(type); + } + } + + serializer.DataField(ref _breakChance, "breakChance", 0); + serializer.DataField(ref _breakSound, "breakSound", "/Audio/items/snap.ogg"); + } + + void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + { + TryUseUtensil(eventArgs.User, eventArgs.Target); + } + + private void TryUseUtensil(IEntity user, IEntity target) + { + if (user == null || target == null) + { + return; + } + + if (!target.TryGetComponent(out FoodComponent food)) + { + return; + } + + if (!InteractionChecks.InRangeUnobstructed(user, target.Transform.MapPosition)) + { + return; + } + + food.TryUseFood(user, null, this); + } + } +} diff --git a/Content.Shared/GameObjects/Components/Utensil/SharedUtensilComponent.cs b/Content.Shared/GameObjects/Components/Utensil/SharedUtensilComponent.cs new file mode 100644 index 0000000000..faccf1287d --- /dev/null +++ b/Content.Shared/GameObjects/Components/Utensil/SharedUtensilComponent.cs @@ -0,0 +1,21 @@ +using System; +using Robust.Shared.GameObjects; + +namespace Content.Shared.GameObjects.Components.Utensil +{ + [Flags] + public enum UtensilType : byte + { + None = 0, + Fork = 1, + Spoon = 1 << 1, + Knife = 1 << 2 + } + + public class SharedUtensilComponent : Component + { + public override string Name => "Utensil"; + + public virtual UtensilType Types { get; set; } + } +} diff --git a/Resources/Audio/items/snap.ogg b/Resources/Audio/items/snap.ogg new file mode 100644 index 0000000000000000000000000000000000000000..9763ea1ed2164d58eaaf8b750e7415ffe518811c GIT binary patch literal 5923 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