Special digestion & kudzu-eating (#16061)

This commit is contained in:
Kara
2023-05-03 19:49:25 -07:00
committed by GitHub
parent fb61af886c
commit 133cbcbe88
16 changed files with 190 additions and 63 deletions

View File

@@ -1,9 +1,11 @@
using Content.Server.Body.Systems; using Content.Server.Body.Systems;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Content.Shared.Whitelist;
namespace Content.Server.Body.Components namespace Content.Server.Body.Components
{ {
[RegisterComponent, Access(typeof(StomachSystem))] [RegisterComponent, Access(typeof(StomachSystem), typeof(FoodSystem))]
public sealed class StomachComponent : Component public sealed class StomachComponent : Component
{ {
public float AccumulatedFrameTime; public float AccumulatedFrameTime;
@@ -20,12 +22,6 @@ namespace Content.Server.Body.Components
[DataField("bodySolutionName")] [DataField("bodySolutionName")]
public string BodySolutionName = BloodstreamComponent.DefaultChemicalsSolutionName; public string BodySolutionName = BloodstreamComponent.DefaultChemicalsSolutionName;
/// <summary>
/// Initial internal solution storage volume
/// </summary>
[DataField("initialMaxVolume", readOnly: true)]
public readonly FixedPoint2 InitialMaxVolume = FixedPoint2.New(50);
/// <summary> /// <summary>
/// Time in seconds between reagents being ingested and them being /// Time in seconds between reagents being ingested and them being
/// transferred to <see cref="BloodstreamComponent"/> /// transferred to <see cref="BloodstreamComponent"/>
@@ -33,6 +29,12 @@ namespace Content.Server.Body.Components
[DataField("digestionDelay")] [DataField("digestionDelay")]
public float DigestionDelay = 20; public float DigestionDelay = 20;
/// <summary>
/// A whitelist for what special-digestible-required foods this stomach is capable of eating.
/// </summary>
[DataField("specialDigestible")]
public EntityWhitelist? SpecialDigestible = null;
/// <summary> /// <summary>
/// Used to track how long each reagent has been in the stomach /// Used to track how long each reagent has been in the stomach
/// </summary> /// </summary>

View File

@@ -16,7 +16,6 @@ namespace Content.Server.Body.Systems
public override void Initialize() public override void Initialize()
{ {
SubscribeLocalEvent<StomachComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<StomachComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier); SubscribeLocalEvent<StomachComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
} }
@@ -87,11 +86,6 @@ namespace Content.Server.Body.Systems
component.AccumulatedFrameTime = component.UpdateInterval; 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, public bool CanTransferSolution(EntityUid uid, Solution solution,
SolutionContainerManagerComponent? solutions = null) SolutionContainerManagerComponent? solutions = null)
{ {

View File

@@ -5,6 +5,7 @@ using Content.Server.NPC.Queries.Considerations;
using Content.Server.NPC.Queries.Curves; using Content.Server.NPC.Queries.Curves;
using Content.Server.NPC.Queries.Queries; using Content.Server.NPC.Queries.Queries;
using Content.Server.Nutrition.Components; using Content.Server.Nutrition.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Server.Storage.Components; using Content.Server.Storage.Components;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
@@ -24,6 +25,7 @@ public sealed class NPCUtilitySystem : EntitySystem
[Dependency] private readonly FactionSystem _faction = default!; [Dependency] private readonly FactionSystem _faction = default!;
[Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly FoodSystem _food = default!;
/// <summary> /// <summary>
/// Runs the UtilityQueryPrototype and returns the best-matching entities. /// Runs the UtilityQueryPrototype and returns the best-matching entities.
@@ -120,6 +122,11 @@ public sealed class NPCUtilitySystem : EntitySystem
if (!TryComp<FoodComponent>(targetUid, out var food)) if (!TryComp<FoodComponent>(targetUid, out var food))
return 0f; return 0f;
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
if (!_food.IsDigestibleBy(owner, targetUid, food))
return 0f;
return 1f; return 1f;
} }
case TargetAccessibleCon: case TargetAccessibleCon:

View File

@@ -1,3 +1,4 @@
using Content.Server.Body.Components;
using Content.Server.Chemistry.EntitySystems; using Content.Server.Chemistry.EntitySystems;
using Content.Server.Nutrition.EntitySystems; using Content.Server.Nutrition.EntitySystems;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
@@ -34,6 +35,25 @@ namespace Content.Server.Nutrition.Components
[DataField("utensilRequired")] [DataField("utensilRequired")]
public bool UtensilRequired = false; public bool UtensilRequired = false;
/// <summary>
/// If this is set to true, eating this food will require you to have a stomach with a
/// <see cref="StomachComponent.SpecialDigestible"/> that includes this entity in its whitelist,
/// rather than just being digestible by anything that can eat food.
/// </summary>
/// <remarks>
/// TODO think about making this a little more complex, right now you cant disallow mobs from eating stuff
/// that everyone else can eat
/// </remarks>
[DataField("requiresSpecialDigestion")]
public bool RequiresSpecialDigestion = false;
/// <summary>
/// Stomachs required to digest this entity.
/// Used to simulate 'ruminant' digestive systems (which can digest grass)
/// </summary>
[DataField("requiredStomachs")]
public int RequiredStomachs = 1;
/// <summary> /// <summary>
/// The localization identifier for the eat message. Needs a "food" entity argument passed to it. /// The localization identifier for the eat message. Needs a "food" entity argument passed to it.
/// </summary> /// </summary>

View File

@@ -1,3 +1,4 @@
using System.Linq;
using Content.Server.Body.Components; using Content.Server.Body.Components;
using Content.Server.Body.Systems; using Content.Server.Body.Systems;
using Content.Server.Chemistry.EntitySystems; using Content.Server.Chemistry.EntitySystems;
@@ -5,6 +6,7 @@ using Content.Server.Nutrition.Components;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.Body.Organ;
using Content.Shared.Chemistry; using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database; using Content.Shared.Database;
@@ -85,16 +87,30 @@ namespace Content.Server.Nutrition.EntitySystems
public bool TryFeed(EntityUid user, EntityUid target, EntityUid food, FoodComponent foodComp) public bool TryFeed(EntityUid user, EntityUid target, EntityUid food, FoodComponent foodComp)
{ {
//Suppresses self-eating //Suppresses self-eating
if (food == user || EntityManager.TryGetComponent<MobStateComponent>(food, out var mobState) && _mobStateSystem.IsAlive(food, mobState)) // Suppresses eating alive mobs if (food == user || TryComp<MobStateComponent>(food, out var mobState) && _mobStateSystem.IsAlive(food, mobState)) // Suppresses eating alive mobs
return false; return false;
// Target can't be fed or they're already eating // Target can't be fed or they're already eating
if (!EntityManager.HasComponent<BodyComponent>(target)) if (!TryComp<BodyComponent>(target, out var body))
return false; return false;
if (!_solutionContainerSystem.TryGetSolution(food, foodComp.SolutionName, out var foodSolution) || foodSolution.Name == null) if (!_solutionContainerSystem.TryGetSolution(food, foodComp.SolutionName, out var foodSolution) || foodSolution.Name == null)
return false; return false;
if (!_bodySystem.TryGetBodyOrganComponents<StomachComponent>(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); var flavors = _flavorProfileSystem.GetLocalizedFlavorsMessage(food, user, foodSolution);
if (foodComp.UsesRemaining <= 0) if (foodComp.UsesRemaining <= 0)
@@ -113,8 +129,6 @@ namespace Content.Server.Nutrition.EntitySystems
if (!TryGetRequiredUtensils(user, foodComp, out _)) if (!TryGetRequiredUtensils(user, foodComp, out _))
return true; return true;
var forceFeed = user != target;
if (forceFeed) if (forceFeed)
{ {
var userName = Identity.Entity(user, EntityManager); 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 transferAmount = component.TransferAmount != null ? FixedPoint2.Min((FixedPoint2) component.TransferAmount, solution.Volume) : solution.Volume;
var split = _solutionContainerSystem.SplitSolution(uid, solution, transferAmount); var split = _solutionContainerSystem.SplitSolution(uid, solution, transferAmount);
//TODO: Get the stomach UID somehow without nabbing owner //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. // No stomach so just popup a message that they can't eat.
if (firstStomach == null) if (stomachToUse == null)
{ {
_solutionContainerSystem.TryAddSolution(uid, solution, split); _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); _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); _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; var flavors = args.FlavorMessage;
@@ -285,49 +318,43 @@ namespace Content.Server.Nutrition.EntitySystems
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
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(food, ref foodComp, false))
if (!Resolve(uid, ref food, false) || !Resolve(target, ref body, false)) return false;
return;
if (IsMouthBlocked(target)) if (!_bodySystem.TryGetBodyOrganComponents<StomachComponent>(uid, out var stomachs))
return; return false;
if (!_solutionContainerSystem.TryGetSolution(uid, food.SolutionName, out var foodSolution)) return IsDigestibleBy(food, foodComp, stomachs);
return; }
if (!_bodySystem.TryGetBodyOrganComponents<StomachComponent>(target, out var stomachs, body)) /// <summary>
return; /// Returns true if <paramref name="stomachs"/> has a <see cref="StomachComponent"/> that is capable of
/// digesting this <paramref name="food"/> (or if they even have enough stomachs in the first place).
/// </summary>
private bool IsDigestibleBy(EntityUid food, FoodComponent component, List<(StomachComponent, OrganComponent)> stomachs)
{
var digestible = true;
if (food.UsesRemaining <= 0) if (stomachs.Count < component.RequiredStomachs)
DeleteAndSpawnTrash(food, uid); return false;
var firstStomach = stomachs.FirstOrNull( if (!component.RequiresSpecialDigestion)
stomach => _stomachSystem.CanTransferSolution(((IComponent) stomach.Comp).Owner, foodSolution)); return true;
if (firstStomach == null) foreach (var (comp, _) in stomachs)
return; {
if (comp.SpecialDigestible == null)
continue;
// logging if (!comp.SpecialDigestible.IsValid(food, EntityManager))
if (user == null) return false;
_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}");
var filter = user == null ? Filter.Entities(target) : Filter.Entities(target, user.Value); return digestible;
_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);
} }
private bool TryGetRequiredUtensils(EntityUid user, FoodComponent component, private bool TryGetRequiredUtensils(EntityUid user, FoodComponent component,

View File

@@ -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 = You can't eat any more!
food-system-you-cannot-eat-any-more-other = They 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-try-use-food-is-empty = {CAPITALIZE(THE($entity))} is empty!
food-system-wrong-utensil = you can't eat {$food} with a {$utensil}. 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 food-system-verb-eat = Eat

View File

@@ -43,9 +43,8 @@
- type: SolutionContainerManager - type: SolutionContainerManager
solutions: solutions:
stomach: stomach:
maxVol: 100 maxVol: 40
- type: Stomach - type: Stomach
maxVolume: 10
- type: Metabolizer - type: Metabolizer
maxReagents: 3 maxReagents: 3
metabolizerTypes: [ Animal ] metabolizerTypes: [ Animal ]

View File

@@ -0,0 +1,10 @@
- type: entity
id: OrganAnimalRuminantStomach
parent: OrganAnimalStomach
name: ruminant stomach
noSpawn: true
components:
- type: SolutionContainerManager
solutions:
stomach:
maxVol: 80

View File

@@ -26,8 +26,11 @@
noSpawn: true noSpawn: true
components: components:
- type: Stomach - type: Stomach
maxVolume: 50
updateInterval: 1.5 updateInterval: 1.5
- type: SolutionContainerManager
solutions:
stomach:
maxVol: 50
- type: Metabolizer - type: Metabolizer
updateFrequency: 1.5 updateFrequency: 1.5

View File

@@ -11,7 +11,9 @@
parent: OrganAnimalStomach parent: OrganAnimalStomach
suffix: "rat" suffix: "rat"
components: components:
- type: Stomach - type: SolutionContainerManager
maxVolume: 50 # they're hungry solutions:
stomach:
maxVol: 50
- type: Sprite - type: Sprite
state: stomach state: stomach

View File

@@ -4,4 +4,7 @@
noSpawn: true noSpawn: true
components: components:
- type: Stomach - type: Stomach
maxVol: 50 - type: SolutionContainerManager
solutions:
stomach:
maxVol: 50

View File

@@ -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

View File

@@ -416,6 +416,10 @@
- type: Faction - type: Faction
factions: factions:
- Passive - Passive
- type: Body
prototype: AnimalRuminant
- type: HTN
rootTask: RuminantCompound
- type: entity - type: entity
name: crab name: crab
@@ -519,6 +523,10 @@
- type: Faction - type: Faction
factions: factions:
- Passive - 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. # 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 - type: entity

View File

@@ -81,7 +81,19 @@
kudzu: kudzu:
!type:SpreaderNode !type:SpreaderNode
nodeGroupID: Spreader 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 - type: entity
id: WeakKudzu id: WeakKudzu
@@ -154,3 +166,11 @@
ignoreWhitelist: ignoreWhitelist:
tags: tags:
- Flesh - Flesh
- type: Food # delightfully devilish !
delay: 0.5
- type: SolutionContainerManager
solutions:
food:
reagents:
- ReagentId: Protein
Quantity: 2

View File

@@ -15,6 +15,14 @@
- tasks: - tasks:
- id: IdleCompound - id: IdleCompound
- type: htnCompound
id: RuminantCompound
branches:
- tasks:
- id: FoodCompound
- tasks:
- id: IdleCompound
- type: htnCompound - type: htnCompound
id: DragonCarpCompound id: DragonCarpCompound
branches: branches: