From 133cbcbe88b53a81b0100c8336f9238ec58b29e0 Mon Sep 17 00:00:00 2001 From: Kara Date: Wed, 3 May 2023 19:49:25 -0700 Subject: [PATCH] Special digestion & kudzu-eating (#16061) --- .../Body/Components/StomachComponent.cs | 16 +-- Content.Server/Body/Systems/StomachSystem.cs | 6 - .../NPC/Systems/NPCUtilitySystem.cs | 7 ++ .../Nutrition/Components/FoodComponent.cs | 20 ++++ .../Nutrition/EntitySystems/FoodSystem.cs | 107 +++++++++++------- .../nutrition/components/food-component.ftl | 6 +- .../Body/Organs/{ => Animal}/animal.yml | 3 +- .../Body/Organs/Animal/ruminant.yml | 10 ++ Resources/Prototypes/Body/Organs/arachnid.yml | 5 +- Resources/Prototypes/Body/Organs/rat.yml | 8 +- .../Prototypes/Body/Organs/reptilian.yml | 5 +- .../Body/Prototypes/{ => Animal}/animal.yml | 0 .../Body/Prototypes/Animal/ruminant.yml | 22 ++++ .../Prototypes/Entities/Mobs/NPCs/animals.yml | 8 ++ .../Entities/Objects/Misc/kudzu.yml | 22 +++- Resources/Prototypes/NPCs/mob.yml | 8 ++ 16 files changed, 190 insertions(+), 63 deletions(-) rename Resources/Prototypes/Body/Organs/{ => Animal}/animal.yml (97%) create mode 100644 Resources/Prototypes/Body/Organs/Animal/ruminant.yml rename Resources/Prototypes/Body/Prototypes/{ => Animal}/animal.yml (100%) create mode 100644 Resources/Prototypes/Body/Prototypes/Animal/ruminant.yml diff --git a/Content.Server/Body/Components/StomachComponent.cs b/Content.Server/Body/Components/StomachComponent.cs index 32b0b4adb7..a22b11ffa9 100644 --- a/Content.Server/Body/Components/StomachComponent.cs +++ b/Content.Server/Body/Components/StomachComponent.cs @@ -1,9 +1,11 @@ using Content.Server.Body.Systems; +using Content.Server.Nutrition.EntitySystems; using Content.Shared.FixedPoint; +using Content.Shared.Whitelist; namespace Content.Server.Body.Components { - [RegisterComponent, Access(typeof(StomachSystem))] + [RegisterComponent, Access(typeof(StomachSystem), typeof(FoodSystem))] public sealed class StomachComponent : Component { public float AccumulatedFrameTime; @@ -20,12 +22,6 @@ namespace Content.Server.Body.Components [DataField("bodySolutionName")] public string BodySolutionName = BloodstreamComponent.DefaultChemicalsSolutionName; - /// - /// Initial internal solution storage volume - /// - [DataField("initialMaxVolume", readOnly: true)] - public readonly FixedPoint2 InitialMaxVolume = FixedPoint2.New(50); - /// /// Time in seconds between reagents being ingested and them being /// transferred to @@ -33,6 +29,12 @@ namespace Content.Server.Body.Components [DataField("digestionDelay")] public float DigestionDelay = 20; + /// + /// A whitelist for what special-digestible-required foods this stomach is capable of eating. + /// + [DataField("specialDigestible")] + public EntityWhitelist? SpecialDigestible = null; + /// /// Used to track how long each reagent has been in the stomach /// diff --git a/Content.Server/Body/Systems/StomachSystem.cs b/Content.Server/Body/Systems/StomachSystem.cs index 36f02c0b82..3a4bd8e9c8 100644 --- a/Content.Server/Body/Systems/StomachSystem.cs +++ b/Content.Server/Body/Systems/StomachSystem.cs @@ -16,7 +16,6 @@ namespace Content.Server.Body.Systems public override void Initialize() { - SubscribeLocalEvent(OnComponentInit); SubscribeLocalEvent(OnApplyMetabolicMultiplier); } @@ -87,11 +86,6 @@ namespace Content.Server.Body.Systems component.AccumulatedFrameTime = component.UpdateInterval; } - private void OnComponentInit(EntityUid uid, StomachComponent component, ComponentInit args) - { - _solutionContainerSystem.EnsureSolution(uid, DefaultSolutionName, component.InitialMaxVolume, out _); - } - public bool CanTransferSolution(EntityUid uid, Solution solution, SolutionContainerManagerComponent? solutions = null) { diff --git a/Content.Server/NPC/Systems/NPCUtilitySystem.cs b/Content.Server/NPC/Systems/NPCUtilitySystem.cs index 65477982de..5fc6818893 100644 --- a/Content.Server/NPC/Systems/NPCUtilitySystem.cs +++ b/Content.Server/NPC/Systems/NPCUtilitySystem.cs @@ -5,6 +5,7 @@ using Content.Server.NPC.Queries.Considerations; using Content.Server.NPC.Queries.Curves; using Content.Server.NPC.Queries.Queries; using Content.Server.Nutrition.Components; +using Content.Server.Nutrition.EntitySystems; using Content.Server.Storage.Components; using Content.Shared.Examine; using Content.Shared.Mobs.Systems; @@ -24,6 +25,7 @@ public sealed class NPCUtilitySystem : EntitySystem [Dependency] private readonly FactionSystem _faction = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly FoodSystem _food = default!; /// /// Runs the UtilityQueryPrototype and returns the best-matching entities. @@ -120,6 +122,11 @@ public sealed class NPCUtilitySystem : EntitySystem if (!TryComp(targetUid, out var food)) return 0f; + var owner = blackboard.GetValue(NPCBlackboard.Owner); + + if (!_food.IsDigestibleBy(owner, targetUid, food)) + return 0f; + return 1f; } case TargetAccessibleCon: diff --git a/Content.Server/Nutrition/Components/FoodComponent.cs b/Content.Server/Nutrition/Components/FoodComponent.cs index 5d7d325b02..f6a74f109b 100644 --- a/Content.Server/Nutrition/Components/FoodComponent.cs +++ b/Content.Server/Nutrition/Components/FoodComponent.cs @@ -1,3 +1,4 @@ +using Content.Server.Body.Components; using Content.Server.Chemistry.EntitySystems; using Content.Server.Nutrition.EntitySystems; using Content.Shared.FixedPoint; @@ -34,6 +35,25 @@ namespace Content.Server.Nutrition.Components [DataField("utensilRequired")] public bool UtensilRequired = false; + /// + /// If this is set to true, eating this food will require you to have a stomach with a + /// that includes this entity in its whitelist, + /// rather than just being digestible by anything that can eat food. + /// + /// + /// TODO think about making this a little more complex, right now you cant disallow mobs from eating stuff + /// that everyone else can eat + /// + [DataField("requiresSpecialDigestion")] + public bool RequiresSpecialDigestion = false; + + /// + /// Stomachs required to digest this entity. + /// Used to simulate 'ruminant' digestive systems (which can digest grass) + /// + [DataField("requiredStomachs")] + public int RequiredStomachs = 1; + /// /// The localization identifier for the eat message. Needs a "food" entity argument passed to it. /// diff --git a/Content.Server/Nutrition/EntitySystems/FoodSystem.cs b/Content.Server/Nutrition/EntitySystems/FoodSystem.cs index 65132cd633..41e2e61f28 100644 --- a/Content.Server/Nutrition/EntitySystems/FoodSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/FoodSystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Server.Body.Components; using Content.Server.Body.Systems; using Content.Server.Chemistry.EntitySystems; @@ -5,6 +6,7 @@ using Content.Server.Nutrition.Components; using Content.Server.Popups; using Content.Shared.Administration.Logs; using Content.Shared.Body.Components; +using Content.Shared.Body.Organ; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Reagent; using Content.Shared.Database; @@ -85,16 +87,30 @@ namespace Content.Server.Nutrition.EntitySystems public bool TryFeed(EntityUid user, EntityUid target, EntityUid food, FoodComponent foodComp) { //Suppresses self-eating - if (food == user || EntityManager.TryGetComponent(food, out var mobState) && _mobStateSystem.IsAlive(food, mobState)) // Suppresses eating alive mobs + if (food == user || TryComp(food, out var mobState) && _mobStateSystem.IsAlive(food, mobState)) // Suppresses eating alive mobs return false; // Target can't be fed or they're already eating - if (!EntityManager.HasComponent(target)) + if (!TryComp(target, out var body)) return false; if (!_solutionContainerSystem.TryGetSolution(food, foodComp.SolutionName, out var foodSolution) || foodSolution.Name == null) return false; + if (!_bodySystem.TryGetBodyOrganComponents(target, out var stomachs, body)) + return false; + + var forceFeed = user != target; + + if (!IsDigestibleBy(food, foodComp, stomachs)) + { + _popupSystem.PopupEntity( + forceFeed + ? Loc.GetString("food-system-cant-digest-other", ("entity", food)) + : Loc.GetString("food-system-cant-digest", ("entity", food)), user, user); + return false; + } + var flavors = _flavorProfileSystem.GetLocalizedFlavorsMessage(food, user, foodSolution); if (foodComp.UsesRemaining <= 0) @@ -113,8 +129,6 @@ namespace Content.Server.Nutrition.EntitySystems if (!TryGetRequiredUtensils(user, foodComp, out _)) return true; - var forceFeed = user != target; - if (forceFeed) { var userName = Identity.Entity(user, EntityManager); @@ -183,11 +197,30 @@ namespace Content.Server.Nutrition.EntitySystems var transferAmount = component.TransferAmount != null ? FixedPoint2.Min((FixedPoint2) component.TransferAmount, solution.Volume) : solution.Volume; var split = _solutionContainerSystem.SplitSolution(uid, solution, transferAmount); + //TODO: Get the stomach UID somehow without nabbing owner - var firstStomach = stomachs.FirstOrNull(stomach => _stomachSystem.CanTransferSolution(stomach.Comp.Owner, split)); + // Get the stomach with the highest available solution volume + var highestAvailable = FixedPoint2.Zero; + StomachComponent? stomachToUse = null; + foreach (var (stomach, _) in stomachs) + { + var owner = stomach.Owner; + if (!_stomachSystem.CanTransferSolution(owner, split)) + continue; + + if (!_solutionContainerSystem.TryGetSolution(owner, StomachSystem.DefaultSolutionName, + out var stomachSol)) + continue; + + if (stomachSol.AvailableVolume <= highestAvailable) + continue; + + stomachToUse = stomach; + highestAvailable = stomachSol.AvailableVolume; + } // No stomach so just popup a message that they can't eat. - if (firstStomach == null) + if (stomachToUse == null) { _solutionContainerSystem.TryAddSolution(uid, solution, split); _popupSystem.PopupEntity(forceFeed ? Loc.GetString("food-system-you-cannot-eat-any-more-other") : Loc.GetString("food-system-you-cannot-eat-any-more"), args.Target.Value, args.User); @@ -195,7 +228,7 @@ namespace Content.Server.Nutrition.EntitySystems } _reaction.DoEntityReaction(args.Target.Value, solution, ReactionMethod.Ingestion); - _stomachSystem.TryTransferSolution(firstStomach.Value.Comp.Owner, split, firstStomach.Value.Comp); + _stomachSystem.TryTransferSolution(stomachToUse.Owner, split, stomachToUse); var flavors = args.FlavorMessage; @@ -285,49 +318,43 @@ namespace Content.Server.Nutrition.EntitySystems } /// - /// Force feeds someone remotely. Does not require utensils (well, not the normal type anyways). + /// Returns true if the food item can be digested by the user. /// - public void ProjectileForceFeed(EntityUid uid, EntityUid target, EntityUid? user, FoodComponent? food = null, BodyComponent? body = null) + public bool IsDigestibleBy(EntityUid uid, EntityUid food, FoodComponent? foodComp = null) { - // TODO: Combine with regular feeding because holy code duplication batman. - if (!Resolve(uid, ref food, false) || !Resolve(target, ref body, false)) - return; - - if (IsMouthBlocked(target)) - return; - - if (!_solutionContainerSystem.TryGetSolution(uid, food.SolutionName, out var foodSolution)) - return; + if (!Resolve(food, ref foodComp, false)) + return false; - if (!_bodySystem.TryGetBodyOrganComponents(target, out var stomachs, body)) - return; + if (!_bodySystem.TryGetBodyOrganComponents(uid, out var stomachs)) + return false; - if (food.UsesRemaining <= 0) - DeleteAndSpawnTrash(food, uid); + return IsDigestibleBy(food, foodComp, stomachs); + } - var firstStomach = stomachs.FirstOrNull( - stomach => _stomachSystem.CanTransferSolution(((IComponent) stomach.Comp).Owner, foodSolution)); + /// + /// Returns true if has a that is capable of + /// digesting this (or if they even have enough stomachs in the first place). + /// + private bool IsDigestibleBy(EntityUid food, FoodComponent component, List<(StomachComponent, OrganComponent)> stomachs) + { + var digestible = true; - if (firstStomach == null) - return; + if (stomachs.Count < component.RequiredStomachs) + return false; - // logging - if (user == null) - _adminLogger.Add(LogType.ForceFeed, $"{ToPrettyString(uid):food} {SolutionContainerSystem.ToPrettyString(foodSolution):solution} was thrown into the mouth of {ToPrettyString(target):target}"); - else - _adminLogger.Add(LogType.ForceFeed, $"{ToPrettyString(user.Value):user} threw {ToPrettyString(uid):food} {SolutionContainerSystem.ToPrettyString(foodSolution):solution} into the mouth of {ToPrettyString(target):target}"); + if (!component.RequiresSpecialDigestion) + return true; - var filter = user == null ? Filter.Entities(target) : Filter.Entities(target, user.Value); - _popupSystem.PopupEntity(Loc.GetString(food.EatMessage, ("food", food.Owner)), target, filter, true); + foreach (var (comp, _) in stomachs) + { + if (comp.SpecialDigestible == null) + continue; - foodSolution.DoEntityReaction(uid, ReactionMethod.Ingestion); - _stomachSystem.TryTransferSolution(((IComponent) firstStomach.Value.Comp).Owner, foodSolution, firstStomach.Value.Comp); - SoundSystem.Play(food.UseSound.GetSound(), Filter.Pvs(target), target, AudioParams.Default.WithVolume(-1f)); + if (!comp.SpecialDigestible.IsValid(food, EntityManager)) + return false; + } - if (string.IsNullOrEmpty(food.TrashPrototype)) - EntityManager.QueueDeleteEntity(food.Owner); - else - DeleteAndSpawnTrash(food, uid); + return digestible; } private bool TryGetRequiredUtensils(EntityUid user, FoodComponent component, diff --git a/Resources/Locale/en-US/nutrition/components/food-component.ftl b/Resources/Locale/en-US/nutrition/components/food-component.ftl index e42fed40e4..bd5766cc0a 100644 --- a/Resources/Locale/en-US/nutrition/components/food-component.ftl +++ b/Resources/Locale/en-US/nutrition/components/food-component.ftl @@ -13,8 +13,10 @@ food-system-remove-mask = You need to take off the {$entity} first. food-system-you-cannot-eat-any-more = You can't eat any more! food-system-you-cannot-eat-any-more-other = They 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-try-use-food-is-empty = {CAPITALIZE(THE($entity))} is empty! +food-system-wrong-utensil = You can't eat {THE($food)} with {INDEFINITE($utensil)}. +food-system-cant-digest = You can't digest {THE($entity)}! +food-system-cant-digest-other = They can't digest {THE($entity)}! food-system-verb-eat = Eat diff --git a/Resources/Prototypes/Body/Organs/animal.yml b/Resources/Prototypes/Body/Organs/Animal/animal.yml similarity index 97% rename from Resources/Prototypes/Body/Organs/animal.yml rename to Resources/Prototypes/Body/Organs/Animal/animal.yml index d8f036e604..a7a5769490 100644 --- a/Resources/Prototypes/Body/Organs/animal.yml +++ b/Resources/Prototypes/Body/Organs/Animal/animal.yml @@ -43,9 +43,8 @@ - type: SolutionContainerManager solutions: stomach: - maxVol: 100 + maxVol: 40 - type: Stomach - maxVolume: 10 - type: Metabolizer maxReagents: 3 metabolizerTypes: [ Animal ] diff --git a/Resources/Prototypes/Body/Organs/Animal/ruminant.yml b/Resources/Prototypes/Body/Organs/Animal/ruminant.yml new file mode 100644 index 0000000000..3c3062ddec --- /dev/null +++ b/Resources/Prototypes/Body/Organs/Animal/ruminant.yml @@ -0,0 +1,10 @@ +- type: entity + id: OrganAnimalRuminantStomach + parent: OrganAnimalStomach + name: ruminant stomach + noSpawn: true + components: + - type: SolutionContainerManager + solutions: + stomach: + maxVol: 80 diff --git a/Resources/Prototypes/Body/Organs/arachnid.yml b/Resources/Prototypes/Body/Organs/arachnid.yml index 4af60d876d..1541859664 100644 --- a/Resources/Prototypes/Body/Organs/arachnid.yml +++ b/Resources/Prototypes/Body/Organs/arachnid.yml @@ -26,8 +26,11 @@ noSpawn: true components: - type: Stomach - maxVolume: 50 updateInterval: 1.5 + - type: SolutionContainerManager + solutions: + stomach: + maxVol: 50 - type: Metabolizer updateFrequency: 1.5 diff --git a/Resources/Prototypes/Body/Organs/rat.yml b/Resources/Prototypes/Body/Organs/rat.yml index ba2776b073..868505cb43 100644 --- a/Resources/Prototypes/Body/Organs/rat.yml +++ b/Resources/Prototypes/Body/Organs/rat.yml @@ -11,7 +11,9 @@ parent: OrganAnimalStomach suffix: "rat" components: - - type: Stomach - maxVolume: 50 # they're hungry + - type: SolutionContainerManager + solutions: + stomach: + maxVol: 50 - type: Sprite - state: stomach \ No newline at end of file + state: stomach diff --git a/Resources/Prototypes/Body/Organs/reptilian.yml b/Resources/Prototypes/Body/Organs/reptilian.yml index 3233431a02..1761313807 100644 --- a/Resources/Prototypes/Body/Organs/reptilian.yml +++ b/Resources/Prototypes/Body/Organs/reptilian.yml @@ -4,4 +4,7 @@ noSpawn: true components: - type: Stomach - maxVol: 50 + - type: SolutionContainerManager + solutions: + stomach: + maxVol: 50 diff --git a/Resources/Prototypes/Body/Prototypes/animal.yml b/Resources/Prototypes/Body/Prototypes/Animal/animal.yml similarity index 100% rename from Resources/Prototypes/Body/Prototypes/animal.yml rename to Resources/Prototypes/Body/Prototypes/Animal/animal.yml diff --git a/Resources/Prototypes/Body/Prototypes/Animal/ruminant.yml b/Resources/Prototypes/Body/Prototypes/Animal/ruminant.yml new file mode 100644 index 0000000000..cd3ab1fdd7 --- /dev/null +++ b/Resources/Prototypes/Body/Prototypes/Animal/ruminant.yml @@ -0,0 +1,22 @@ +- type: body + id: AnimalRuminant + name: "ruminant" + root: torso + slots: + torso: + part: TorsoAnimal + connections: + - legs + organs: + lungs: OrganAnimalLungs + stomach: OrganAnimalRuminantStomach + stomach2: OrganAnimalRuminantStomach + liver: OrganAnimalLiver + heart: OrganAnimalHeart + kidneys: OrganAnimalKidneys + legs: + part: LegsAnimal + connections: + - feet + feet: + part: FeetAnimal diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index fa5b72ebbd..d6ab19ef2f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -416,6 +416,10 @@ - type: Faction factions: - Passive + - type: Body + prototype: AnimalRuminant + - type: HTN + rootTask: RuminantCompound - type: entity name: crab @@ -519,6 +523,10 @@ - type: Faction factions: - Passive + - type: Body + prototype: AnimalRuminant + - type: HTN + rootTask: RuminantCompound # Note that we gotta make this bitch vomit someday when you feed it anthrax or sumthin. Needs to be a small item thief too and aggressive if attacked. - type: entity diff --git a/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml b/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml index af62439c87..79d4fc15f5 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml @@ -81,7 +81,19 @@ kudzu: !type:SpreaderNode nodeGroupID: Spreader - + - type: Food + requiredStomachs: 2 # ruminants have 4 stomachs but i dont care to give them literally 4 stomachs. 2 is good + # TODO make botany plants edible to ruminants as well ... + delay: 0.5 + - type: FlavorProfile + flavors: + - fiber + - type: SolutionContainerManager + solutions: + food: + reagents: + - ReagentId: Nutriment + Quantity: 2 - type: entity id: WeakKudzu @@ -154,3 +166,11 @@ ignoreWhitelist: tags: - Flesh + - type: Food # delightfully devilish ! + delay: 0.5 + - type: SolutionContainerManager + solutions: + food: + reagents: + - ReagentId: Protein + Quantity: 2 diff --git a/Resources/Prototypes/NPCs/mob.yml b/Resources/Prototypes/NPCs/mob.yml index e0a8f6a924..dc1b970af8 100644 --- a/Resources/Prototypes/NPCs/mob.yml +++ b/Resources/Prototypes/NPCs/mob.yml @@ -15,6 +15,14 @@ - tasks: - id: IdleCompound +- type: htnCompound + id: RuminantCompound + branches: + - tasks: + - id: FoodCompound + - tasks: + - id: IdleCompound + - type: htnCompound id: DragonCarpCompound branches: -- 2.51.2