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 0000000000..9763ea1ed2 Binary files /dev/null and b/Resources/Audio/items/snap.ogg differ diff --git a/Resources/Prototypes/Entities/Items/utensils.yml b/Resources/Prototypes/Entities/Items/utensils.yml index 577440d813..245b1b6642 100644 --- a/Resources/Prototypes/Entities/Items/utensils.yml +++ b/Resources/Prototypes/Entities/Items/utensils.yml @@ -5,7 +5,6 @@ components: - type: Sprite sprite: Objects/utensils.rsi - - type: Icon sprite: Objects/utensils.rsi @@ -17,9 +16,11 @@ components: - type: Sprite state: fork - - type: Icon state: fork + - type: Utensil + types: + - Fork - type: entity parent: UtensilBase @@ -28,9 +29,12 @@ components: - type: Sprite state: plastic_fork - - type: Icon state: plastic_fork + - type: Utensil + types: + - Fork + breakChance: 0.20 - type: entity parent: UtensilBase @@ -40,9 +44,11 @@ components: - type: Sprite state: spoon - - type: Icon state: spoon + - type: Utensil + types: + - Spoon - type: entity parent: UtensilBase @@ -52,9 +58,12 @@ components: - type: Sprite state: plastic_spoon - - type: Icon state: plastic_spoon + - type: Utensil + types: + - Spoon + breakChance: 0.20 - type: entity parent: UtensilBase @@ -63,6 +72,9 @@ components: - type: Sprite state: plastic_knife - - type: Icon state: plastic_knife + - type: Utensil + types: + - Knife + breakChance: 0.20