diff --git a/Content.Server/Nutrition/Components/FoodComponent.cs b/Content.Server/Nutrition/Components/FoodComponent.cs index 8327a55edd..073fd74c3d 100644 --- a/Content.Server/Nutrition/Components/FoodComponent.cs +++ b/Content.Server/Nutrition/Components/FoodComponent.cs @@ -1,24 +1,10 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Content.Server.Body.Behavior; -using Content.Server.Body.Components; -using Content.Server.Body.Systems; using Content.Server.Chemistry.EntitySystems; -using Content.Server.Hands.Components; -using Content.Server.Items; -using Content.Shared.Body.Components; -using Content.Shared.Chemistry.Reagent; +using Content.Server.Nutrition.EntitySystems; using Content.Shared.FixedPoint; -using Content.Shared.Interaction; -using Content.Shared.Interaction.Helpers; -using Content.Shared.Popups; using Content.Shared.Sound; -using Robust.Shared.Audio; +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; -using Robust.Shared.Localization; -using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; @@ -26,9 +12,8 @@ using Robust.Shared.ViewVariables; namespace Content.Server.Nutrition.Components { - [RegisterComponent] - [ComponentReference(typeof(IAfterInteract))] - public class FoodComponent : Component, IUse, IAfterInteract + [RegisterComponent, Friend(typeof(FoodSystem))] + public class FoodComponent : Component { public override string Name => "Food"; @@ -37,21 +22,31 @@ namespace Content.Server.Nutrition.Components [ViewVariables] [DataField("useSound")] - private SoundSpecifier UseSound { get; set; } = new SoundPathSpecifier("/Audio/Items/eatfood.ogg"); + public SoundSpecifier UseSound { get; set; } = new SoundPathSpecifier("/Audio/Items/eatfood.ogg"); [ViewVariables] [DataField("trash", customTypeSerializer: typeof(PrototypeIdSerializer))] - private string? TrashPrototype { get; set; } + public string? TrashPrototype { get; set; } [ViewVariables] [DataField("transferAmount")] - private FixedPoint2? TransferAmount { get; set; } = FixedPoint2.New(5); + public FixedPoint2? TransferAmount { get; set; } = FixedPoint2.New(5); + + /// + /// Acceptable utensil to use + /// + [DataField("utensil")] + public UtensilType Utensil = UtensilType.Fork; //There are more "solid" than "liquid" food + + /// + /// Is utensil required to eat this food + /// + [DataField("utensilRequired")] + public bool UtensilRequired = false; - [DataField("utensilsNeeded")] - private UtensilType _utensilsNeeded = UtensilType.None; [DataField("eatMessage")] - private string _eatMessage = "food-nom"; + public string EatMessage = "food-nom"; [ViewVariables] public int UsesRemaining @@ -71,168 +66,5 @@ namespace Content.Server.Nutrition.Components : Math.Max(1, (int) Math.Ceiling((solution.CurrentVolume / (FixedPoint2)TransferAmount).Float())); } } - - protected override void Initialize() - { - base.Initialize(); - // Owner.EnsureComponentWarn(); - } - - bool IUse.UseEntity(UseEntityEventArgs eventArgs) - { - if (_utensilsNeeded != UtensilType.None) - { - eventArgs.User.PopupMessage(Loc.GetString("food-you-need-utensil", ("utensil", _utensilsNeeded))); - return false; - } - - return TryUseFood(eventArgs.User, null); - } - - // Feeding someone else - async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) - { - if (eventArgs.Target == null) - { - return false; - } - - TryUseFood(eventArgs.User, eventArgs.Target); - return true; - } - - public bool TryUseFood(IEntity? user, IEntity? target, UtensilComponent? utensilUsed = null) - { - var solutionContainerSys = EntitySystem.Get(); - var bodySys = EntitySystem.Get(); - var stomachSys = EntitySystem.Get(); - - if (!solutionContainerSys.TryGetSolution(Owner.Uid, SolutionName, out var solution)) - { - return false; - } - - if (user == null) - { - return false; - } - - if (UsesRemaining <= 0) - { - user.PopupMessage(Loc.GetString("food-component-try-use-food-is-empty", ("entity", Owner))); - DeleteAndSpawnTrash(user); - return false; - } - - var trueTarget = target ?? user; - - if (!trueTarget.TryGetComponent(out SharedBodyComponent? body) || - !bodySys.TryGetComponentsOnMechanisms(body.OwnerUid, out var stomachs, body)) - { - 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)) - { - foreach (var item in hands.GetAllHeldItems()) - { - if (!item.Owner.TryGetComponent(out UtensilComponent? utensil)) - { - continue; - } - - utensils.Add(utensil); - types |= utensil.Types; - } - } - - if (!types.HasFlag(_utensilsNeeded)) - { - trueTarget.PopupMessage(user, - Loc.GetString("food-you-need-to-hold-utensil", ("utensil", _utensilsNeeded))); - return false; - } - } - - if (!user.InRangeUnobstructed(trueTarget, popup: true)) - { - return false; - } - - var transferAmount = TransferAmount != null ? FixedPoint2.Min((FixedPoint2)TransferAmount, solution.CurrentVolume) : solution.CurrentVolume; - var split = solutionContainerSys.SplitSolution(Owner.Uid, solution, transferAmount); - var firstStomach = stomachs.FirstOrDefault(stomach => stomachSys.CanTransferSolution(stomach.OwnerUid, split)); - - if (firstStomach == null) - { - solutionContainerSys.TryAddSolution(Owner.Uid, solution, split); - trueTarget.PopupMessage(user, Loc.GetString("food-you-cannot-eat-any-more")); - return false; - } - - // TODO: Account for partial transfer. - split.DoEntityReaction(trueTarget.Uid, ReactionMethod.Ingestion); - stomachSys.TryTransferSolution(firstStomach.OwnerUid, split, firstStomach); - SoundSystem.Play(Filter.Pvs(trueTarget), UseSound.GetSound(), trueTarget, AudioParams.Default.WithVolume(-1f)); - trueTarget.PopupMessage(user, Loc.GetString(_eatMessage, ("food", Owner))); - - // If utensils were used - if (utensils != null) - { - foreach (var utensil in utensils) - { - utensil.TryBreak(user); - } - } - - if (UsesRemaining > 0) - { - return true; - } - - if (string.IsNullOrEmpty(TrashPrototype)) - { - Owner.QueueDelete(); - return true; - } - - DeleteAndSpawnTrash(user); - - return true; - } - - private void DeleteAndSpawnTrash(IEntity user) - { - //We're empty. Become trash. - var position = Owner.Transform.Coordinates; - var finisher = Owner.EntityManager.SpawnEntity(TrashPrototype, position); - - // If the user is holding the item - if (user.TryGetComponent(out HandsComponent? handsComponent) && - handsComponent.IsHolding(Owner)) - { - Owner.Delete(); - - // Put the trash in the user's hand - if (finisher.TryGetComponent(out ItemComponent? item) && - handsComponent.CanPutInHand(item)) - { - handsComponent.PutInHand(item); - } - } - else - { - Owner.Delete(); - } - } } } diff --git a/Content.Server/Nutrition/Components/SliceableFoodComponent.cs b/Content.Server/Nutrition/Components/SliceableFoodComponent.cs index 458957de3f..f1b5901fb8 100644 --- a/Content.Server/Nutrition/Components/SliceableFoodComponent.cs +++ b/Content.Server/Nutrition/Components/SliceableFoodComponent.cs @@ -65,8 +65,8 @@ namespace Content.Server.Nutrition.Components { return false; } - - if (!eventArgs.Using.TryGetComponent(out UtensilComponent? utensil) || !utensil.HasType(UtensilType.Knife)) + + if (!eventArgs.Using.TryGetComponent(out UtensilComponent? utensil) || (utensil.Types & UtensilType.Knife) == 0) { return false; } diff --git a/Content.Server/Nutrition/Components/UtensilComponent.cs b/Content.Server/Nutrition/Components/UtensilComponent.cs index 5275a313ed..11c1fdcb9d 100644 --- a/Content.Server/Nutrition/Components/UtensilComponent.cs +++ b/Content.Server/Nutrition/Components/UtensilComponent.cs @@ -1,20 +1,15 @@ using System; -using System.Threading.Tasks; -using Content.Shared.Interaction; -using Content.Shared.Interaction.Helpers; +using Content.Server.Nutrition.EntitySystems; using Content.Shared.Sound; -using Robust.Shared.Audio; +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Player; -using Robust.Shared.Random; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Content.Server.Nutrition.Components { - [RegisterComponent] - public class UtensilComponent : Component, IAfterInteract + [RegisterComponent, Friend(typeof(UtensilSystem))] + public class UtensilComponent : Component { public override string Name => "Utensil"; @@ -40,66 +35,18 @@ namespace Content.Server.Nutrition.Components /// [ViewVariables] [DataField("breakChance")] - private float _breakChance; + public float BreakChance; /// /// The sound to be played if the utensil breaks. /// [ViewVariables] [DataField("breakSound")] - private SoundSpecifier _breakSound = new SoundPathSpecifier("/Audio/Items/snap.ogg"); - - public void AddType(UtensilType type) - { - Types |= type; - } - - public bool HasAnyType(UtensilType type) - { - return (_types & type) != UtensilType.None; - } - - public bool HasType(UtensilType type) - { - return (_types & type) != 0; - } - - public void RemoveType(UtensilType type) - { - Types &= ~type; - } - - internal void TryBreak(IEntity user) - { - if (IoCManager.Resolve().Prob(_breakChance)) - { - SoundSystem.Play(Filter.Pvs(user), _breakSound.GetSound(), user, AudioParams.Default.WithVolume(-2f)); - Owner.Delete(); - } - } - - async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) - { - return TryUseUtensil(eventArgs.User, eventArgs.Target); - } - - private bool TryUseUtensil(IEntity user, IEntity? target) - { - if (target == null || !target.TryGetComponent(out FoodComponent? food)) - { - return false; - } - - if (!user.InRangeUnobstructed(target, popup: true)) - { - return false; - } - - food.TryUseFood(user, null, this); - return true; - } + public SoundSpecifier BreakSound = new SoundPathSpecifier("/Audio/Items/snap.ogg"); } + // If you want to make a fancy output on "wrong" composite utensil use (like: you need fork and knife) + // There should be Dictionary I guess (Dictionary) [Flags] public enum UtensilType : byte { diff --git a/Content.Server/Nutrition/EntitySystems/FoodSystem.cs b/Content.Server/Nutrition/EntitySystems/FoodSystem.cs new file mode 100644 index 0000000000..64cc4c44ef --- /dev/null +++ b/Content.Server/Nutrition/EntitySystems/FoodSystem.cs @@ -0,0 +1,218 @@ +using Content.Server.Body.Components; +using Content.Server.Body.Systems; +using Content.Server.Chemistry.EntitySystems; +using Content.Server.Hands.Components; +using Content.Server.Items; +using Content.Server.Nutrition.Components; +using Content.Server.Popups; +using Content.Shared.Body.Components; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Helpers; +using Content.Shared.Verbs; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Log; +using Robust.Shared.Player; +using System.Collections.Generic; +using System.Linq; + +namespace Content.Server.Nutrition.EntitySystems +{ + /// + /// Handles feeding attempts both on yourself and on the target. + /// + internal class FoodSystem : EntitySystem + { + [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; + [Dependency] private readonly BodySystem _bodySystem = default!; + [Dependency] private readonly StomachSystem _stomachSystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly UtensilSystem _utensilSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnUseFoodInHand); + SubscribeLocalEvent(OnFeedFood); + SubscribeLocalEvent(AddEatVerb); + } + + /// + /// Eat item + /// + private void OnUseFoodInHand(EntityUid uid, FoodComponent foodComponent, UseInHandEvent ev) + { + if (ev.Handled) + return; + + if (TryUseFood(uid, ev.UserUid, ev.UserUid)) + ev.Handled = true; + } + + /// + /// Feed someone else + /// + private void OnFeedFood(EntityUid uid, FoodComponent foodComponent, AfterInteractEvent ev) + { + if (ev.Handled || ev.Target == null) + return; + + if (TryUseFood(uid, ev.UserUid, ev.Target.Uid)) + ev.Handled = true; + } + + /// + /// Tries to feed specified target + /// + /// Food entity. + /// Feeding initiator. + /// Feeding target. + /// True if the portion of food was consumed + public bool TryUseFood(EntityUid uid, EntityUid userUid, EntityUid targetUid, FoodComponent? component = null) + { + if (!Resolve(uid, ref component)) + return false; + + if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution)) + return false; + + if (component.UsesRemaining <= 0) + { + _popupSystem.PopupEntity(Loc.GetString("food-system-try-use-food-is-empty", ("entity", EntityManager.GetEntity(uid))), userUid, Filter.Entities(userUid)); + DeleteAndSpawnTrash(userUid, component); + return false; + } + + if (!EntityManager.TryGetComponent(targetUid, out SharedBodyComponent ? body) || + !_bodySystem.TryGetComponentsOnMechanisms(targetUid, out var stomachs, body)) + return false; + + var usedUtensils = new List(); + + //Not blocking eating itself if "required" filed is not set (allows usage of multiple types of utensils) + //TODO: maybe a chance to spill soup on eating without spoon?! + if (component.Utensil != UtensilType.None) + { + if (EntityManager.TryGetComponent(userUid, out HandsComponent? hands)) + { + var usedTypes = UtensilType.None; + + foreach (var item in hands.GetAllHeldItems()) + { + // Is utensil? + if (!item.Owner.TryGetComponent(out UtensilComponent? utensil)) + continue; + + if ((utensil.Types & component.Utensil) != 0 && // Acceptable type? + (usedTypes & utensil.Types) != utensil.Types) // Type is not used already? (removes usage of identical utensils) + { + // Add to used list + usedTypes |= utensil.Types; + usedUtensils.Add(utensil); + } + } + + // If "required" field is set, try to block eating without proper utensils used + if (component.UtensilRequired && (usedTypes & component.Utensil) != component.Utensil) + { + _popupSystem.PopupEntity(Loc.GetString("food-you-need-to-hold-utensil", ("utensil", component.Utensil ^ usedTypes)), userUid, Filter.Entities(userUid)); + return false; + } + } + } + + if (userUid != targetUid && !userUid.InRangeUnobstructed(targetUid, popup: true)) + return false; + + var transferAmount = component.TransferAmount != null ? FixedPoint2.Min((FixedPoint2) component.TransferAmount, solution.CurrentVolume) : solution.CurrentVolume; + var split = _solutionContainerSystem.SplitSolution(uid, solution, transferAmount); + var firstStomach = stomachs.FirstOrDefault(stomach => _stomachSystem.CanTransferSolution(stomach.OwnerUid, split)); + + if (firstStomach == null) + { + _solutionContainerSystem.TryAddSolution(uid, solution, split); + _popupSystem.PopupEntity(Loc.GetString("food-system-you-cannot-eat-any-more"), targetUid, Filter.Entities(targetUid)); + return false; + } + + // TODO: Account for partial transfer. + split.DoEntityReaction(targetUid, ReactionMethod.Ingestion); + _stomachSystem.TryTransferSolution(firstStomach.OwnerUid, split, firstStomach); + + SoundSystem.Play(Filter.Pvs(targetUid), component.UseSound.GetSound(), targetUid, AudioParams.Default.WithVolume(-1f)); + _popupSystem.PopupEntity(Loc.GetString(component.EatMessage, ("food", component.Owner)), targetUid, Filter.Entities(targetUid)); + + // Try to break all used utensils + foreach (var utensil in usedUtensils) + { + _utensilSystem.TryBreak(utensil.OwnerUid, userUid); + } + + if (component.UsesRemaining > 0) + { + return true; + } + + if (string.IsNullOrEmpty(component.TrashPrototype)) + { + component.Owner.QueueDelete(); + return true; + } + + DeleteAndSpawnTrash(userUid, component); + + return true; + } + + private void DeleteAndSpawnTrash(EntityUid userUid, FoodComponent component) + { + //We're empty. Become trash. + var position = component.Owner.Transform.Coordinates; + var finisher = component.Owner.EntityManager.SpawnEntity(component.TrashPrototype, position); + + // If the user is holding the item + if (EntityManager.TryGetComponent(userUid, out HandsComponent? handsComponent) && + handsComponent.IsHolding(component.Owner)) + { + component.Owner.Delete(); + + // Put the trash in the user's hand + if (finisher.TryGetComponent(out ItemComponent? item) && + handsComponent.CanPutInHand(item)) + { + handsComponent.PutInHand(item); + } + } + else + { + component.Owner.Delete(); + } + } + + //No hands + //TODO: DoAfter based on delay after food & drinks delay PR merged... + private void AddEatVerb(EntityUid uid, FoodComponent component, GetInteractionVerbsEvent ev) + { + Logger.DebugS("action", "triggered"); + if (!ev.CanInteract || + !EntityManager.TryGetComponent(ev.User.Uid, out SharedBodyComponent? body) || + !_bodySystem.TryGetComponentsOnMechanisms(ev.User.Uid, out var stomachs, body)) + return; + + Verb verb = new(); + verb.Act = () => + { + TryUseFood(uid, ev.User.Uid, ev.User.Uid, component); + }; + + verb.Text = Loc.GetString("food-system-verb-eat"); + verb.Priority = -1; + ev.Verbs.Add(verb); + } + } +} diff --git a/Content.Server/Nutrition/EntitySystems/ForcefeedOnCollideSystem.cs b/Content.Server/Nutrition/EntitySystems/ForcefeedOnCollideSystem.cs index 02c7e30133..afa1a370d6 100644 --- a/Content.Server/Nutrition/EntitySystems/ForcefeedOnCollideSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/ForcefeedOnCollideSystem.cs @@ -1,11 +1,14 @@ -using Content.Server.Nutrition.Components; +using Content.Server.Nutrition.Components; using Content.Shared.Throwing; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; namespace Content.Server.Nutrition.EntitySystems { public class ForcefeedOnCollideSystem : EntitySystem { + [Dependency] private readonly FoodSystem _foodSystem = default!; + public override void Initialize() { base.Initialize(); @@ -22,7 +25,7 @@ namespace Content.Server.Nutrition.EntitySystems return; // the 'target' isnt really the 'user' per se.. but.. - food.TryUseFood(args.Target, args.Target); + _foodSystem.TryUseFood(food.OwnerUid, args.Target.Uid, args.Target.Uid); } private void OnLand(EntityUid uid, ForcefeedOnCollideComponent component, LandEvent args) diff --git a/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs b/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs new file mode 100644 index 0000000000..8d103dde61 --- /dev/null +++ b/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs @@ -0,0 +1,77 @@ +using Content.Server.Nutrition.Components; +using Content.Server.Popups; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Helpers; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Player; +using Robust.Shared.Random; + +namespace Content.Server.Nutrition.EntitySystems +{ + /// + /// Handles usage of the utensils on the food items + /// + internal class UtensilSystem : EntitySystem + { + [Dependency] private readonly IRobustRandom _robustRandom = default!; + [Dependency] private readonly FoodSystem _foodSystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAfterInteract); + } + + /// + /// Clicked with utensil + /// + private void OnAfterInteract(EntityUid uid, UtensilComponent component, AfterInteractEvent ev) + { + if (ev.Target == null) + return; + + if (TryUseUtensil(ev.UserUid, ev.Target.Uid, component)) + ev.Handled = true; + } + + private bool TryUseUtensil(EntityUid userUid, EntityUid targetUid, UtensilComponent component) + { + if (!EntityManager.TryGetComponent(targetUid, out FoodComponent food)) + return false; + + //Prevents food usage with a wrong utensil + if ((food.Utensil & component.Types) == 0) + { + _popupSystem.PopupEntity(Loc.GetString("food-system-wrong-utensil", ("food", food.Owner), ("utensil", component.Owner)), userUid, Filter.Entities(userUid)); + return false; + } + + if (!userUid.InRangeUnobstructed(targetUid, popup: true)) + return false; + + return _foodSystem.TryUseFood(targetUid, userUid, userUid); + } + + /// + /// Attempt to break the utensil after interaction. + /// + /// Utensil. + /// User of the utensil. + public void TryBreak(EntityUid uid, EntityUid userUid, UtensilComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + if (_robustRandom.Prob(component.BreakChance)) + { + SoundSystem.Play(Filter.Pvs(userUid), component.BreakSound.GetSound(), userUid, AudioParams.Default.WithVolume(-2f)); + component.Owner.Delete(); + } + } + } +} diff --git a/Resources/Locale/en-US/nutrition/components/food-component.ftl b/Resources/Locale/en-US/nutrition/components/food-component.ftl index 1ea007e735..658654b7dc 100644 --- a/Resources/Locale/en-US/nutrition/components/food-component.ftl +++ b/Resources/Locale/en-US/nutrition/components/food-component.ftl @@ -7,10 +7,13 @@ food-you-need-utensil = You need to use a {$utensil} to eat that! # When trying to eat food without the required utensil... but you gotta hold it food-you-need-to-hold-utensil = You need to be holding a {$utensil} to eat that! -food-you-cannot-eat-any-more = You can't eat any more! food-nom = Nom food-swallow = You swallow the {$food}. -## Entity +## System -food-component-try-use-food-is-empty = {$entity} is empty! +food-system-you-cannot-eat-any-more = You can't eat any more! +food-system-try-use-food-is-empty = {$entity} is empty! +food-system-wrong-utensil = you can't eat {$food} with a {$utensil}. + +food-system-verb-eat = Eat diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/soup.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/soup.yml index e36bd347e0..a708c7243c 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/soup.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/soup.yml @@ -5,6 +5,7 @@ components: - type: Food trash: FoodBowlBig + utensil: Spoon - type: SolutionContainerManager solutions: food: