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 (!Resolve(food, ref foodComp, false))
+ return false;
- if (IsMouthBlocked(target))
- return;
+ if (!_bodySystem.TryGetBodyOrganComponents(uid, out var stomachs))
+ return false;
- if (!_solutionContainerSystem.TryGetSolution(uid, food.SolutionName, out var foodSolution))
- return;
+ return IsDigestibleBy(food, foodComp, stomachs);
+ }
- if (!_bodySystem.TryGetBodyOrganComponents(target, out var stomachs, body))
- return;
+ ///
+ /// 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 (food.UsesRemaining <= 0)
- DeleteAndSpawnTrash(food, uid);
+ if (stomachs.Count < component.RequiredStomachs)
+ return false;
- var firstStomach = stomachs.FirstOrNull(
- stomach => _stomachSystem.CanTransferSolution(((IComponent) stomach.Comp).Owner, foodSolution));
+ if (!component.RequiresSpecialDigestion)
+ return true;
- if (firstStomach == null)
- return;
+ foreach (var (comp, _) in stomachs)
+ {
+ if (comp.SpecialDigestible == null)
+ continue;
- // 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 (!comp.SpecialDigestible.IsValid(food, EntityManager))
+ return false;
+ }
- var filter = user == null ? Filter.Entities(target) : Filter.Entities(target, user.Value);
- _popupSystem.PopupEntity(Loc.GetString(food.EatMessage, ("food", food.Owner)), target, filter, true);
-
- 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 (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: