From 8ad82dc665ec499b83c600d010091b866fccc28c Mon Sep 17 00:00:00 2001 From: Centronias Date: Sat, 8 Aug 2026 15:44:24 -0700 Subject: [PATCH] Refactor hunger and thirst systems (#34166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ready * oop forgot the HUD stuff * woooo examinable generalized tooooo * oop I forgot to rename the files * bungled the merge, I guess * this is called hubris * Verin's comments * PR comments how did I use `ProtoId<> Hunger` in more than a few spots but then just say "thirst can stay a literal :^)"? also changed a few spots to my beloved ``` functionInvocation( one param per line ) ``` ``` * Satiation effect condition now with ADDED FUNCTIONALITY?? :o * tfw you trust the markov chain recommendation because long generic type names are tough q-q * TFW YOU TRUST THE MARKOV CHAIN BUT IT DOESN'T UNDERSTAND THE DIFFERING SEMANTICS OF MAX AND MIN AHHHHH * tfw you think to yourself "i need to fix the effects", but then forget to and the code doesn't even compile. * centralize satiation constants * other small comments * The enum is dead. But at what cost? * oop * like 12 more ooperses * green checks are mine by right; I have performed the rites; I have done my part; you will not deny me this succor * oops I changed the semantics from `<=` to just `<` and didn't update the tests :) * serialization has bested me :agony: also PR comments * never trust the auto merge :agony: * literally just this * force engine back to master because idk why it's different * fix randomness and update master * submodule fuckery * fall before my might * okay I ran the linter, ARE YOU HAPPY?? * cursed ovis, trouble me no more * protoman stuff * inb4 linter bonks me * break non-critical stuff from the main satiation proto * ![live crundle reaction](https://preview.redd.it/xh68xqcz518g1.png?width=677&format=png&auto=webp&s=79ba10fe49ec3f8306217732321a7c5429bdfb14) inb4 this breaks something somewhere lmao * my two greatest PRs COLLIDE I wonder why robust says it's changed :hmmclueless * sanity is at an all-time low * serialization, I guess * I don't believe you * field deltas on class \w*satiation\w*components * PR comments * 我做泥團子 :pensive: * doc + kill one property that's trivially accessible after the threshold caching changes * fixes --------- Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> Co-authored-by: AJCM-git --- .../Overlays/ShowHungerIconsSystem.cs | 27 -- .../Overlays/ShowSatiationIconsSystem.cs | 57 ++++ .../Overlays/ShowThirstIconsSystem.cs | 27 -- .../Tests/Nutrition/HungerThirstTest.cs | 37 ++- .../Tests/Nutrition/SatiationTest.cs | 303 ++++++++++++++++++ .../Animals/Systems/EggLayerSystem.cs | 10 +- .../HTN/Preconditions/HungryPrecondition.cs | 26 -- .../Preconditions/SatiationPrecondition.cs | 53 +++ .../HTN/Preconditions/ThirstyPrecondition.cs | 26 -- .../Queries/Considerations/DrinkValueCon.cs | 8 +- .../Queries/Considerations/FoodValueCon.cs | 8 +- .../NPC/Systems/NPCUtilitySystem.cs | 30 +- .../Components/FatExtractorComponent.cs | 8 +- .../EntitySystems/AnimalHusbandrySystem.cs | 46 ++- .../EntitySystems/FatExtractorSystem.cs | 29 +- Content.Server/Nutrition/Hungry.cs | 38 --- Content.Server/Nutrition/SetNutrit.cs | 115 ------- Content.Server/Nutrition/SetSatiation.cs | 135 ++++++++ Content.Server/Nutrition/Thirsty.cs | 38 --- Content.Server/Nutrition/Unsatiate.cs | 103 ++++++ Content.Server/RatKing/RatKingSystem.cs | 18 +- .../Zombies/ZombieSystem.Transform.cs | 3 +- Content.Shared/Animals/UdderComponent.cs | 7 + Content.Shared/Animals/UdderSystem.cs | 12 +- Content.Shared/Animals/WoolyComponent.cs | 7 + Content.Shared/Animals/WoolySystem.cs | 12 +- .../Body/HungerEntityConditionSystem.cs | 39 --- .../Body/SatiationEntityConditionSystem.cs | 73 +++++ .../Effects/Body/SatiateEntityEffectSystem.cs | 60 ++-- .../Inventory/InventorySystem.Relay.cs | 3 +- Content.Shared/Medical/VomitSystem.cs | 15 +- .../AnimalHusbandry/ReproductiveComponent.cs | 15 +- .../Components/ExaminableHungerComponent.cs | 31 -- .../ExaminableSatiationComponent.cs | 21 ++ .../Nutrition/Components/HungerComponent.cs | 144 --------- .../Components/SatiationComponent.cs | 37 +++ .../Components/SatiationDamageComponent.cs | 34 ++ .../SatiationSpeedModifierComponent.cs | 21 ++ .../Nutrition/Components/ThirstComponent.cs | 80 ----- .../BaseSatiationEffectSystem.cs | 126 ++++++++ .../EntitySystems/ExaminableHungerSystem.cs | 40 --- .../ExaminableSatiationSystem.cs | 34 ++ .../Nutrition/EntitySystems/HungerSystem.cs | 275 ---------------- .../EntitySystems/IngestionSystem.API.cs | 8 +- .../Nutrition/EntitySystems/Satiation.cs | 147 +++++++++ .../EntitySystems/SatiationDamageSystem.cs | 49 +++ .../SatiationSpeedModifierSystem.cs | 34 ++ .../EntitySystems/SatiationSystem.Proxy.cs | 260 +++++++++++++++ .../EntitySystems/SatiationSystem.cs | 253 +++++++++++++++ .../Nutrition/EntitySystems/ThirstSystem.cs | 221 ------------- .../Prototypes/SatiationPrototype.cs | 148 +++++++++ .../Prototypes/SatiationTypePrototype.cs | 23 ++ .../Prototypes/SatiationValueSerializer.cs | 48 +++ .../Overlays/ShowHungerIconsComponent.cs | 9 - .../Overlays/ShowSatiationIconsComponent.cs | 18 ++ .../Overlays/ShowThirstIconsComponent.cs | 9 - .../Sericulture/SericultureComponent.cs | 12 +- .../Sericulture/SericultureSystem.cs | 31 +- .../Whitelist/EntityWhitelistSystem.cs | 3 +- .../guidebook/entity-effects/conditions.ftl | 8 +- .../guidebook/entity-effects/effects.ftl | 15 +- .../examinable-hunger-component.ftl | 5 - .../examinable-satiation-component.ftl | 11 + .../en-US/nutrition/nutrition-commands.ftl | 20 +- .../en-US/nutrition/satiation-types.ftl | 2 + .../Body/Satiation/Hunger/examination.yml | 12 + .../Body/Satiation/Hunger/hunger.yml | 104 ++++++ .../Body/Satiation/Thirst/thirst.yml | 69 ++++ Resources/Prototypes/Body/Satiation/types.yml | 7 + .../Prototypes/Body/Species/arachnid.yml | 1 + Resources/Prototypes/Body/Species/diona.yml | 10 +- Resources/Prototypes/Body/species_base.yml | 10 +- .../Prototypes/Entities/Clothing/Eyes/hud.yml | 30 +- .../Prototypes/Entities/Mobs/NPCs/animals.yml | 60 ++-- .../Entities/Mobs/NPCs/regalrat.yml | 48 +-- .../Prototypes/Entities/Mobs/NPCs/scurret.yml | 6 +- .../Entities/Mobs/NPCs/simplemob.yml | 26 +- .../Prototypes/Entities/Mobs/NPCs/space.yml | 23 +- .../Structures/Machines/fatextractor.yml | 1 + Resources/Prototypes/NPCs/nutrition.yml | 10 +- Resources/Prototypes/NPCs/utility_queries.yml | 2 + .../Reagents/Consumable/Drink/alcohol.yml | 48 ++- .../Reagents/Consumable/Drink/base_drink.yml | 9 +- .../Reagents/Consumable/Drink/drinks.yml | 63 ++-- .../Reagents/Consumable/Drink/soda.yml | 15 +- .../Reagents/Consumable/Food/condiments.yml | 12 +- .../Reagents/Consumable/Food/food.yml | 12 +- .../Reagents/Consumable/Food/ingredients.yml | 12 +- Resources/Prototypes/Reagents/biological.yml | 24 +- Resources/Prototypes/Reagents/botany.yml | 6 +- Resources/Prototypes/Reagents/chemicals.yml | 3 +- Resources/Prototypes/Reagents/fun.yml | 3 +- Resources/Prototypes/Reagents/medicine.yml | 15 +- Resources/Prototypes/Reagents/narcotics.yml | 6 +- Resources/Prototypes/Reagents/pyrotechnic.yml | 6 +- Resources/Prototypes/Reagents/toxins.yml | 12 +- 96 files changed, 2701 insertions(+), 1529 deletions(-) delete mode 100644 Content.Client/Overlays/ShowHungerIconsSystem.cs create mode 100644 Content.Client/Overlays/ShowSatiationIconsSystem.cs delete mode 100644 Content.Client/Overlays/ShowThirstIconsSystem.cs create mode 100644 Content.IntegrationTests/Tests/Nutrition/SatiationTest.cs delete mode 100644 Content.Server/NPC/HTN/Preconditions/HungryPrecondition.cs create mode 100644 Content.Server/NPC/HTN/Preconditions/SatiationPrecondition.cs delete mode 100644 Content.Server/NPC/HTN/Preconditions/ThirstyPrecondition.cs delete mode 100644 Content.Server/Nutrition/Hungry.cs delete mode 100644 Content.Server/Nutrition/SetNutrit.cs create mode 100644 Content.Server/Nutrition/SetSatiation.cs delete mode 100644 Content.Server/Nutrition/Thirsty.cs create mode 100644 Content.Server/Nutrition/Unsatiate.cs delete mode 100644 Content.Shared/EntityConditions/Conditions/Body/HungerEntityConditionSystem.cs create mode 100644 Content.Shared/EntityConditions/Conditions/Body/SatiationEntityConditionSystem.cs delete mode 100644 Content.Shared/Nutrition/Components/ExaminableHungerComponent.cs create mode 100644 Content.Shared/Nutrition/Components/ExaminableSatiationComponent.cs delete mode 100644 Content.Shared/Nutrition/Components/HungerComponent.cs create mode 100644 Content.Shared/Nutrition/Components/SatiationComponent.cs create mode 100644 Content.Shared/Nutrition/Components/SatiationDamageComponent.cs create mode 100644 Content.Shared/Nutrition/Components/SatiationSpeedModifierComponent.cs delete mode 100644 Content.Shared/Nutrition/Components/ThirstComponent.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/BaseSatiationEffectSystem.cs delete mode 100644 Content.Shared/Nutrition/EntitySystems/ExaminableHungerSystem.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/ExaminableSatiationSystem.cs delete mode 100644 Content.Shared/Nutrition/EntitySystems/HungerSystem.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/Satiation.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/SatiationDamageSystem.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/SatiationSpeedModifierSystem.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/SatiationSystem.Proxy.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/SatiationSystem.cs delete mode 100644 Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs create mode 100644 Content.Shared/Nutrition/Prototypes/SatiationPrototype.cs create mode 100644 Content.Shared/Nutrition/Prototypes/SatiationTypePrototype.cs create mode 100644 Content.Shared/Nutrition/Prototypes/SatiationValueSerializer.cs delete mode 100644 Content.Shared/Overlays/ShowHungerIconsComponent.cs create mode 100644 Content.Shared/Overlays/ShowSatiationIconsComponent.cs delete mode 100644 Content.Shared/Overlays/ShowThirstIconsComponent.cs delete mode 100644 Resources/Locale/en-US/nutrition/components/examinable-hunger-component.ftl create mode 100644 Resources/Locale/en-US/nutrition/components/examinable-satiation-component.ftl create mode 100644 Resources/Locale/en-US/nutrition/satiation-types.ftl create mode 100644 Resources/Prototypes/Body/Satiation/Hunger/examination.yml create mode 100644 Resources/Prototypes/Body/Satiation/Hunger/hunger.yml create mode 100644 Resources/Prototypes/Body/Satiation/Thirst/thirst.yml create mode 100644 Resources/Prototypes/Body/Satiation/types.yml diff --git a/Content.Client/Overlays/ShowHungerIconsSystem.cs b/Content.Client/Overlays/ShowHungerIconsSystem.cs deleted file mode 100644 index 3491167d87..0000000000 --- a/Content.Client/Overlays/ShowHungerIconsSystem.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Content.Shared.Nutrition.EntitySystems; -using Content.Shared.Nutrition.Components; -using Content.Shared.Overlays; -using Content.Shared.StatusIcon.Components; - -namespace Content.Client.Overlays; - -public sealed partial class ShowHungerIconsSystem : EquipmentHudSystem -{ - [Dependency] private HungerSystem _hunger = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnGetStatusIconsEvent); - } - - private void OnGetStatusIconsEvent(EntityUid uid, HungerComponent component, ref GetStatusIconsEvent ev) - { - if (!IsActive) - return; - - if (_hunger.TryGetStatusIconPrototype(component, out var iconPrototype)) - ev.StatusIcons.Add(iconPrototype); - } -} diff --git a/Content.Client/Overlays/ShowSatiationIconsSystem.cs b/Content.Client/Overlays/ShowSatiationIconsSystem.cs new file mode 100644 index 0000000000..7fabe31e9d --- /dev/null +++ b/Content.Client/Overlays/ShowSatiationIconsSystem.cs @@ -0,0 +1,57 @@ +using System.Linq; +using Content.Shared.Inventory.Events; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; +using Content.Shared.Overlays; +using Content.Shared.StatusIcon.Components; +using Robust.Shared.Prototypes; + +namespace Content.Client.Overlays; + +/// +/// This system handles the inclusion of satiation status icons for entities with the +/// +public sealed partial class ShowSatiationIconsSystem : EquipmentHudSystem +{ + [Dependency] private SatiationSystem _satiation = default!; + + private HashSet> _types = []; + + /// + protected override void UpdateInternal(RefreshEquipmentHudEvent args) + { + base.UpdateInternal(args); + + // Any time we update `ShowIcons` component, we need to reconstruct the set of satiation types to show. + _types = []; + foreach (var type in args.Components.SelectMany(comp => comp.ShownTypes)) + { + _types.Add(type); + } + } + + /// + protected override void DeactivateInternal() + { + base.DeactivateInternal(); + _types.Clear(); + } + + [SubscribeLocalEvent] + private void OnGetStatusIconsEvent(Entity entity, ref GetStatusIconsEvent args) + { + if (!IsActive) + { + return; + } + + foreach (var shownTypeId in _types) + { + if (_satiation.GetStatusIconOrNull(entity, shownTypeId) is { } iconId) + { + args.StatusIcons.Add(iconId); + } + } + } +} diff --git a/Content.Client/Overlays/ShowThirstIconsSystem.cs b/Content.Client/Overlays/ShowThirstIconsSystem.cs deleted file mode 100644 index 815d368287..0000000000 --- a/Content.Client/Overlays/ShowThirstIconsSystem.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Content.Shared.Nutrition.EntitySystems; -using Content.Shared.Nutrition.Components; -using Content.Shared.Overlays; -using Content.Shared.StatusIcon.Components; - -namespace Content.Client.Overlays; - -public sealed partial class ShowThirstIconsSystem : EquipmentHudSystem -{ - [Dependency] private ThirstSystem _thirst = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnGetStatusIconsEvent); - } - - private void OnGetStatusIconsEvent(EntityUid uid, ThirstComponent component, ref GetStatusIconsEvent ev) - { - if (!IsActive) - return; - - if (_thirst.TryGetStatusIconPrototype(component, out var iconPrototype)) - ev.StatusIcons.Add(iconPrototype); - } -} diff --git a/Content.IntegrationTests/Tests/Nutrition/HungerThirstTest.cs b/Content.IntegrationTests/Tests/Nutrition/HungerThirstTest.cs index 7a5195800e..c038d39f58 100644 --- a/Content.IntegrationTests/Tests/Nutrition/HungerThirstTest.cs +++ b/Content.IntegrationTests/Tests/Nutrition/HungerThirstTest.cs @@ -1,5 +1,4 @@ using Content.IntegrationTests.Tests.Interaction; -using Content.Shared.Chemistry.Components; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; using Robust.Shared.GameObjects; @@ -26,30 +25,34 @@ public sealed class HungerThirstTest : InteractionTest // Ensure that the player can breathe and not suffocate await AddAtmosphere(); - var hungerComponent = Comp(Player); - var thirstComponent = Comp(Player); - var hungerSystem = SEntMan.System(); - var thirstSystem = SEntMan.System(); + var satiationComponent = Comp(Player); + var entity = new Entity(SPlayer, satiationComponent); + var satiationSystem = SEntMan.System(); var ingestionSystem = SEntMan.System(); + const string okayThresholdKey = "Okay"; + // Set initial value - hungerSystem.SetHunger(SPlayer, hungerComponent.Thresholds[HungerThreshold.Okay], hungerComponent); - thirstSystem.SetThirst(SPlayer, thirstComponent, thirstComponent.ThirstThresholds[ThirstThreshold.Okay]); + Assert.That(satiationSystem.GetKeysForType(entity, SatiationSystem.Hunger), Contains.Item(okayThresholdKey)); + satiationSystem.SetValue(entity, SatiationSystem.Hunger, okayThresholdKey); + Assert.That(satiationSystem.GetKeysForType(entity, SatiationSystem.Thirst), Contains.Item(okayThresholdKey)); + satiationSystem.SetValue(entity, SatiationSystem.Thirst, okayThresholdKey); // Ensure hunger and thirst value decrease over time (the Urist gets hungrier/thirstier) - var previousHungerValue = hungerSystem.GetHunger(hungerComponent); - var previousThirstValue = thirstComponent.CurrentThirst; // TODO: combined sation system with a sane API + var previousHungerValue = satiationSystem.GetValueOrNull(entity, SatiationSystem.Hunger); + Assert.That(previousHungerValue, Is.Not.Null); + var previousThirstValue = satiationSystem.GetValueOrNull(entity, SatiationSystem.Thirst); + Assert.That(previousThirstValue, Is.Not.Null); // Simulate long enough for both update loops to run - var runTime = Math.Max((float)hungerComponent.ThresholdUpdateRate.TotalSeconds, (float)thirstComponent.UpdateRate.TotalSeconds) + 1f; - await RunSeconds(runTime); + await RunSeconds(2); - var currentHungerValue = hungerSystem.GetHunger(hungerComponent); - Assert.That(currentHungerValue, Is.LessThan(previousHungerValue), "Hunger value did not decrease over time"); + var currentHungerValue = satiationSystem.GetValueOrNull(entity, SatiationSystem.Hunger); + Assert.That(currentHungerValue, Is.Not.Null.And.LessThan(previousHungerValue), "Hunger value did not decrease over time"); previousHungerValue = currentHungerValue; - var currentThirstValue = thirstComponent.CurrentThirst; - Assert.That(currentThirstValue, Is.LessThan(previousThirstValue), "Thirst value did not decrease over time"); + var currentThirstValue = satiationSystem.GetValueOrNull(entity, SatiationSystem.Thirst); + Assert.That(currentThirstValue, Is.Not.Null.And.LessThan(previousThirstValue), "Thirst value did not decrease over time"); previousThirstValue = currentThirstValue; // Now we spawn food in the Urist's hand @@ -66,7 +69,7 @@ public sealed class HungerThirstTest : InteractionTest Assert.That(foodEaten, Is.Null, "Food item did not disappear after eating it"); // Ensure that the hunger value has increased (The Urist is less hungry) - Assert.That(hungerSystem.GetHunger(hungerComponent), Is.GreaterThan(previousHungerValue), "Hunger value did not increase after eating food"); + Assert.That(satiationSystem.GetValueOrNull(entity, SatiationSystem.Hunger), Is.GreaterThan(previousHungerValue!), "Hunger value did not increase after eating food"); // Now we spawn a drink in the Urist's hand var drink = await PlaceInHands(_drink); @@ -88,7 +91,7 @@ public sealed class HungerThirstTest : InteractionTest Assert.That(solution.Value.Comp.Solution.Volume, Is.LessThan(initialSolutionVolume), "Solution volume did not decrease after drinking"); // Ensure that the thirst value has increased (The Urist is less thirsty) - Assert.That(thirstComponent.CurrentThirst, Is.GreaterThan(previousThirstValue), "Thirst value did not increase after drinking"); + Assert.That(satiationSystem.GetValueOrNull(entity, SatiationSystem.Thirst), Is.GreaterThan(previousThirstValue!), "Thirst value did not increase after drinking"); // Make sure that the glass did not get deleted after drinking from it var glass = HandSys.GetActiveItem((SPlayer, Hands)); diff --git a/Content.IntegrationTests/Tests/Nutrition/SatiationTest.cs b/Content.IntegrationTests/Tests/Nutrition/SatiationTest.cs new file mode 100644 index 0000000000..74cbd8e037 --- /dev/null +++ b/Content.IntegrationTests/Tests/Nutrition/SatiationTest.cs @@ -0,0 +1,303 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Content.IntegrationTests.Fixtures; +using Content.IntegrationTests.Fixtures.Attributes; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; +using NUnit.Framework.Constraints; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.IntegrationTests.Tests.Nutrition; + +[TestFixture] +[TestOf(typeof(SatiationSystem))] +[TestOf(typeof(SatiationPrototype))] +public sealed class SatiationTest : GameTest +{ + private const string TestSatiationId = "TestSatiation"; + private const string DeadKey = "Dead"; + private const string MiddleKey = "Okay"; + private const string MaxxedKey = "Maxxed"; + private const string NotRealKey = "ashfdjkashfljkahdjskfjadshfgkjlhadsekljfhjalds"; + private static readonly ProtoId TestSatiationType = "Hunger"; + private const string TestProto = "TestSatiationDummy"; + private const int StartingMin = 30; + private const int StartingMax = 35; + private const int MiddleValue = 50; + private const int MaxValue = 100; + + [TestPrototypes] + private static readonly string SatiationPrototypes = $@" +- type: satiation + id: {TestSatiationId} + baseDecayRate: 1 + maximumValue: {MaxValue} + thresholds: # Intentionally out of ordinal order. + {DeadKey}: 0 + {MaxxedKey}: 100 + {MiddleKey}: {MiddleValue} + startingValueMinimum: {StartingMin} + startingValueMaximum: {StartingMax} + decayModifiers: + 25: 0.5 + alertCategory: Hunger + +- type: entity + id: {TestProto} + name: dummy + components: + - type: Satiation + satiations: + Hunger: + prototype: {TestSatiationId} +"; + + [SidedDependency(Side.Server)] private readonly SatiationSystem _satiation = default!; + + [Test, RunOnSide(Side.Server)] + [Description( + "Verifies the basic operations of 'SatiationSystem.SetValue', 'SatiationSystem.ModifyValue', and 'SatiationSystem.GetValueOrNull'")] + public void SatiationBasicTest() + { + var entity = SEntity(SSpawn(TestProto)); + + // Verify the starting value is in the starting range. + using (Assert.EnterMultipleScope()) + { + Assert.That(_satiation.GetValueOrNull(entity, TestSatiationType), + Is.LessThanOrEqualTo(StartingMax).And.GreaterThanOrEqualTo(StartingMin)); + Assert.That(_satiation.IsValueInRange(entity, TestSatiationType, above: StartingMin, below: StartingMax), + Is.True); + } + + // The rest of this modifies the value and verifies the numeric value is what's expected + + _satiation.SetValue(entity, TestSatiationType, MiddleKey); + Assert.That(_satiation.GetValueOrNull(entity, TestSatiationType), Is.EqualTo(MiddleValue)); + + _satiation.ModifyValue(entity, TestSatiationType, -20); + Assert.That(_satiation.GetValueOrNull(entity, TestSatiationType), Is.EqualTo(MiddleValue - 20)); + + _satiation.ModifyValue(entity, TestSatiationType, -int.MaxValue); + Assert.That(_satiation.GetValueOrNull(entity, TestSatiationType), Is.Zero); + + _satiation.ModifyValue(entity, TestSatiationType, int.MaxValue); + using (Assert.EnterMultipleScope()) + { + Assert.That(_satiation.GetValueOrNull(entity, TestSatiationType), Is.EqualTo(MaxValue)); + Assert.That(_satiation.GetValueOrNull(entity, TestSatiationType + NotRealKey), Is.Null); + } + } + + [Test, RunOnSide(Side.Server)] + [Description("Verifies 'SatiationSystem.TryGetValueByThreshold' when threshold keys are integers")] + public void SatiationGetValueByThresholdTest() + { + var entity = SEntity(SSpawn(TestProto)); + var dict = new Dictionary + { + // Arbitrary order to test that the implementation doesn't care about order. + [20] = 20, + [0] = 0, + [40] = 40, + [80] = 80, + [100] = 100, + [60] = 60, + }; + + // All of these work by setting a value, calling `TryGetValueByThreshold`, and verifying the exact results are + // what's expected. + + _satiation.SetValue(entity, TestSatiationType, value: 100); + using (Assert.EnterMultipleScope()) + { + var res = _satiation.TryGetValueByThreshold(entity, + TestSatiationType, + dict, + out var result, + out var nextLower); + Assert.That(res, Is.True); + Assert.That(result, Is.EqualTo(100)); + Assert.That(nextLower, Is.EqualTo(80)); + } + + _satiation.SetValue(entity, TestSatiationType, value: 55); + using (Assert.EnterMultipleScope()) + { + var res = _satiation.TryGetValueByThreshold(entity, + TestSatiationType, + dict, + out var result, + out var nextLower); + Assert.That(res, Is.True); + Assert.That(result, Is.EqualTo(60)); + Assert.That(nextLower, Is.EqualTo(40)); + } + + _satiation.SetValue(entity, TestSatiationType, value: 0); + using (Assert.EnterMultipleScope()) + { + var res = _satiation.TryGetValueByThreshold(entity, + TestSatiationType, + dict, + out var result, + out var nextLower); + Assert.That(res, Is.True); + Assert.That(result, Is.Zero); + Assert.That(nextLower, Is.Null); + } + } + + [Test, RunOnSide(Side.Server)] + [Description("Verifies 'SatiationSystem.TryGetValueByThreshold' when threshold keys are strings")] + public void SatiationGetValueByThresholdKeysTest() + { + var entity = SEntity(SSpawn(TestProto)); + var dict = new Dictionary + { + // Arbitrary order to test that the implementation doesn't care about order. + [DeadKey] = 20, + [MaxxedKey] = 0, + [MiddleKey] = 40, + }; + + // All of these work by setting a value, calling `TryGetValueByThreshold`, and verifying the exact results are + // what's expected. + + _satiation.SetValue(entity, TestSatiationType, MaxxedKey); + using (Assert.EnterMultipleScope()) + { + var res = _satiation.TryGetValueByThreshold(entity, + TestSatiationType, + dict, + out var result, + out var nextLower); + Assert.That(res, Is.True); + Assert.That(result, Is.Zero); + Assert.That(nextLower, Is.EqualTo(MiddleValue)); + } + + _satiation.ModifyValue(entity, TestSatiationType, -10); + using (Assert.EnterMultipleScope()) + { + var res = _satiation.TryGetValueByThreshold(entity, + TestSatiationType, + dict, + out var result, + out var nextLower); + Assert.That(res, Is.True); + Assert.That(result, Is.Zero); + Assert.That(nextLower, Is.EqualTo(MiddleValue)); + } + + _satiation.SetValue(entity, TestSatiationType, MiddleKey); + using (Assert.EnterMultipleScope()) + { + var res = _satiation.TryGetValueByThreshold(entity, + TestSatiationType, + dict, + out var result, + out var nextLower); + Assert.That(res, Is.True); + Assert.That(result, Is.EqualTo(40)); + Assert.That(nextLower, Is.Zero); + } + + _satiation.SetValue(entity, TestSatiationType, DeadKey); + using (Assert.EnterMultipleScope()) + { + var res = _satiation.TryGetValueByThreshold(entity, + TestSatiationType, + dict, + out var result, + out var nextLower); + Assert.That(res, Is.True); + Assert.That(result, Is.EqualTo(20)); + Assert.That(nextLower, Is.Null); + } + } + + [Test, RunOnSide(Side.Server)] + [Description("Verifies 'SatiationSystem.IsValueInRange'")] + [SuppressMessage("Assertion", + "NUnit2057:Remove unnecessary lambda expression", + Justification = "Necessity of lambda depends on build configuration")] + public void SatiationRangeTests() + { + var entity = SEntity(SSpawn(TestProto)); + + // All of these work by setting a value, then asserting that `IsValueInRange` for various ranges returns what's + // expected. + + _satiation.SetValue(entity, TestSatiationType, value: 100); + using (Assert.EnterMultipleScope()) + { + Assert.That(_satiation.IsValueInRange(entity, TestSatiationType, above: DeadKey), Is.True); + Assert.That(_satiation.IsValueInRange(entity, TestSatiationType, above: MaxxedKey), Is.False); + Assert.That(_satiation.IsValueInRange(entity, + TestSatiationType, + below: MaxxedKey, + hypotheticalValueDelta: -1), + Is.True); + } + + _satiation.SetValue(entity, TestSatiationType, value: MiddleValue + 5); + using (Assert.EnterMultipleScope()) + { + Assert.That(_satiation.IsValueInRange(entity, TestSatiationType, above: MiddleKey), Is.True); + Assert.That(_satiation.IsValueInRange(entity, TestSatiationType, above: MiddleKey, below: MaxxedKey), + Is.True); + Assert.That( + _satiation.IsValueInRange(entity, TestSatiationType, above: MaxxedKey, hypotheticalValueDelta: -10), + Is.False); + } + + using (Assert.EnterMultipleScope()) + { + // Disable "ForbidLiteral" errors. Making these all into constants to be used once is unnecessary. +#pragma warning disable RA0033 + Assert.That(_satiation.IsValueInRange(entity, TestSatiationType, above: 0), Is.True); + Assert.That(_satiation.IsValueInRange(entity, TestSatiationType, above: -1000), Is.True); + Assert.That(_satiation.IsValueInRange(entity, TestSatiationType, above: 100), Is.False); + Assert.That(() => _satiation.IsValueInRange(entity, TestSatiationType, below: 60, above: 70), + Is.False.OrFailsDebugAssertInDebug()); + Assert.That(_satiation.IsValueInRange(entity, TestSatiationType, below: 50, hypotheticalValueDelta: -10), + Is.True); +#pragma warning restore RA0033 + } + + _satiation.SetValue(entity, TestSatiationType, value: 0); + using (Assert.EnterMultipleScope()) + { + Assert.That(_satiation.IsValueInRange(entity, TestSatiationType, above: NotRealKey), Is.False); + Assert.That(() => _satiation.IsValueInRange(entity, TestSatiationType), + Is.True.OrFailsDebugAssertInDebug()); + } + } +} + +static file class ConstraintExt +{ +#if DEBUG + private const bool IsDebug = true; +#else + private const bool IsDebug = false; +#endif + + extension(Constraint constraint) + { + /// + /// Returns the receiver when #define DEBUG is false, otherwise returns a + /// with exception type . This is useful when + /// something throws in Debug, but fails gracefully otherwise. + /// + // TODO Put this somewhere else reusable https://github.com/space-wizards/space-station-14/pull/34166#discussion_r3692882923 + public Constraint OrFailsDebugAssertInDebug() => +#pragma warning disable CS0162 // Unreachable code detected + // ReSharper disable once HeuristicUnreachableCode + IsDebug ? Throws.InstanceOf() : constraint; +#pragma warning restore CS0162 // Unreachable code detected + } +} diff --git a/Content.Server/Animals/Systems/EggLayerSystem.cs b/Content.Server/Animals/Systems/EggLayerSystem.cs index ee97541559..8a14dbfc15 100644 --- a/Content.Server/Animals/Systems/EggLayerSystem.cs +++ b/Content.Server/Animals/Systems/EggLayerSystem.cs @@ -6,9 +6,11 @@ using Content.Shared.IdentityManagement; using Content.Shared.Mobs.Systems; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; using Content.Shared.Storage; using Robust.Server.Audio; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; @@ -23,7 +25,7 @@ public sealed partial class EggLayerSystem : EntitySystem [Dependency] private IRobustRandom _random = default!; [Dependency] private ActionsSystem _actions = default!; [Dependency] private AudioSystem _audio = default!; - [Dependency] private HungerSystem _hunger = default!; + [Dependency] private SatiationSystem _satiation = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private PopupSystem _popup = default!; [Dependency] private MobStateSystem _mobState = default!; @@ -83,15 +85,15 @@ public sealed partial class EggLayerSystem : EntitySystem return false; // Allow infinitely laying eggs if they can't get hungry. - if (TryComp(uid, out var hunger)) + if (TryComp(uid, out var satiation)) { - if (_hunger.GetHunger(hunger) < egglayer.HungerUsage) + if (_satiation.GetValueOrNull((uid, satiation), SatiationSystem.Hunger) < egglayer.HungerUsage) { _popup.PopupEntity(Loc.GetString("action-popup-lay-egg-too-hungry"), uid, uid); return false; } - _hunger.ModifyHunger(uid, -egglayer.HungerUsage, hunger); + _satiation.ModifyValue((uid, satiation), SatiationSystem.Hunger, -egglayer.HungerUsage); } foreach (var ent in EntitySpawnCollection.GetSpawns(egglayer.EggSpawn, _random)) diff --git a/Content.Server/NPC/HTN/Preconditions/HungryPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/HungryPrecondition.cs deleted file mode 100644 index 47d71cbc24..0000000000 --- a/Content.Server/NPC/HTN/Preconditions/HungryPrecondition.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Content.Shared.Hands.Components; -using Content.Shared.Nutrition.Components; -using Robust.Shared.Prototypes; - -namespace Content.Server.NPC.HTN.Preconditions; - -/// -/// Returns true if the active hand entity has the specified components. -/// -public sealed partial class HungryPrecondition : HTNPrecondition -{ - [Dependency] private IEntityManager _entManager = default!; - - [DataField(required: true)] - public HungerThreshold MinHungerState = HungerThreshold.Starving; - - public override bool IsMet(NPCBlackboard blackboard) - { - if (!blackboard.TryGetValue(NPCBlackboard.Owner, out var owner, _entManager)) - { - return false; - } - - return _entManager.TryGetComponent(owner, out var hunger) ? hunger.CurrentThreshold <= MinHungerState : false; - } -} diff --git a/Content.Server/NPC/HTN/Preconditions/SatiationPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/SatiationPrecondition.cs new file mode 100644 index 0000000000..2b2c0ff56a --- /dev/null +++ b/Content.Server/NPC/HTN/Preconditions/SatiationPrecondition.cs @@ -0,0 +1,53 @@ +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; +using Robust.Shared.Prototypes; + +namespace Content.Server.NPC.HTN.Preconditions; + +/// +/// IsMet if has a satiation component whose satiation for meets +/// the criteria specified by and . +/// +/// +public sealed partial class BaseSatiationPrecondition : HTNPrecondition +{ + [Dependency] private IEntityManager _entManager = default!; + + /// + /// The bottom of the range the agent's satiation must be in for this condition to be met. If null, the range has + /// no bottom. + /// + [DataField] + public SatiationValue? Above; + + /// + /// The top of the range the agent's satiation must be in for this condition to be met. If null, the range has no + /// bottom. + /// + [DataField] + public SatiationValue? Below; + + /// + /// The type of the satiation considered by this condition. If the agent does not have this satiation type, this + /// condition can never be met. + /// + [DataField] + public ProtoId SatiationType; + + /// + public override bool IsMet(NPCBlackboard blackboard) + { + if (!blackboard.TryGetValue(NPCBlackboard.Owner, out var owner, _entManager) || + !_entManager.TryGetComponent(owner, out var satiation)) + return false; + + return _entManager.System() + .IsValueInRange( + (owner, satiation), + SatiationType, + Above, + Below + ); + } +} diff --git a/Content.Server/NPC/HTN/Preconditions/ThirstyPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/ThirstyPrecondition.cs deleted file mode 100644 index 070cdba65a..0000000000 --- a/Content.Server/NPC/HTN/Preconditions/ThirstyPrecondition.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Content.Shared.Hands.Components; -using Content.Shared.Nutrition.Components; -using Robust.Shared.Prototypes; - -namespace Content.Server.NPC.HTN.Preconditions; - -/// -/// Returns true if the active hand entity has the specified components. -/// -public sealed partial class ThirstyPrecondition : HTNPrecondition -{ - [Dependency] private IEntityManager _entManager = default!; - - [DataField(required: true)] - public ThirstThreshold MinThirstState = ThirstThreshold.Parched; - - public override bool IsMet(NPCBlackboard blackboard) - { - if (!blackboard.TryGetValue(NPCBlackboard.Owner, out var owner, _entManager)) - { - return false; - } - - return _entManager.TryGetComponent(owner, out var thirst) ? thirst.CurrentThirstThreshold <= MinThirstState : false; - } -} diff --git a/Content.Server/NPC/Queries/Considerations/DrinkValueCon.cs b/Content.Server/NPC/Queries/Considerations/DrinkValueCon.cs index d45ab16fc2..25e1bf1fb4 100644 --- a/Content.Server/NPC/Queries/Considerations/DrinkValueCon.cs +++ b/Content.Server/NPC/Queries/Considerations/DrinkValueCon.cs @@ -1,6 +1,12 @@ +using Content.Shared.Nutrition.Prototypes; + namespace Content.Server.NPC.Queries.Considerations; public sealed partial class DrinkValueCon : UtilityConsideration { - + /// + /// This consideration will return 0 if the entity's thirst is above this value. + /// + [DataField(required: true)] + public SatiationValue ThirstThreshold; } diff --git a/Content.Server/NPC/Queries/Considerations/FoodValueCon.cs b/Content.Server/NPC/Queries/Considerations/FoodValueCon.cs index 9aabe5c56b..0c7919f745 100644 --- a/Content.Server/NPC/Queries/Considerations/FoodValueCon.cs +++ b/Content.Server/NPC/Queries/Considerations/FoodValueCon.cs @@ -1,6 +1,12 @@ +using Content.Shared.Nutrition.Prototypes; + namespace Content.Server.NPC.Queries.Considerations; public sealed partial class FoodValueCon : UtilityConsideration { - + /// + /// This consideration will return 0 if the entity's hunger is above this value. + /// + [DataField(required: true)] + public SatiationValue HungerThreshold; } diff --git a/Content.Server/NPC/Systems/NPCUtilitySystem.cs b/Content.Server/NPC/Systems/NPCUtilitySystem.cs index 859aac60a0..17554beabf 100644 --- a/Content.Server/NPC/Systems/NPCUtilitySystem.cs +++ b/Content.Server/NPC/Systems/NPCUtilitySystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Server.Fluids.EntitySystems; using Content.Server.Hands.Systems; using Content.Server.NPC.Queries; @@ -5,17 +6,24 @@ 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.Shared.Atmos.Components; using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; using Content.Shared.Examine; using Content.Shared.Fluids.Components; using Content.Shared.Inventory; using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.NPC.Systems; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Stealth; +using Content.Shared.Stealth.Components; using Content.Shared.Storage.Components; using Content.Shared.Stunnable; +using Content.Shared.Temperature.Components; using Content.Shared.Tools.Systems; using Content.Shared.Turrets; using Content.Shared.Weapons.Melee; @@ -26,14 +34,6 @@ using Microsoft.Extensions.ObjectPool; using Robust.Server.Containers; using Robust.Shared.Prototypes; using Robust.Shared.Utility; -using Content.Shared.Atmos.Components; -using System.Linq; -using Content.Shared.Damage.Components; -using Content.Shared.Damage.Systems; -using Content.Shared.Mobs.Components; -using Content.Shared.Temperature.Components; -using Content.Shared.Stealth; -using Content.Shared.Stealth.Components; namespace Content.Server.NPC.Systems; @@ -58,6 +58,7 @@ public sealed partial class NPCUtilitySystem : EntitySystem [Dependency] private MobThresholdSystem _thresholdSystem = default!; [Dependency] private TurretTargetSettingsSystem _turretTargetSettings = default!; [Dependency] private DamageableSystem _damageable = default!; + [Dependency] private SatiationSystem _satiation = default!; [Dependency] private SharedStealthSystem _stealth = default!; [Dependency] private EntityQuery _puddleQuery = default!; @@ -164,7 +165,7 @@ public sealed partial class NPCUtilitySystem : EntitySystem var owner = blackboard.GetValue(NPCBlackboard.Owner); switch (consideration) { - case FoodValueCon: + case FoodValueCon foodValueConsideration: { // do we have a mouth available? Is the food item opened? if (!_ingestion.CanConsume(owner, targetUid)) @@ -173,7 +174,9 @@ public sealed partial class NPCUtilitySystem : EntitySystem var avoidBadFood = !HasComp(owner); // only eat when hungry or if it will eat anything - if (TryComp(owner, out var hunger) && hunger.CurrentThreshold > HungerThreshold.Okay && avoidBadFood) + if (TryComp(owner, out var satiation) && + _satiation.IsValueInRange((owner, satiation), SatiationSystem.Hunger, below: foodValueConsideration.HungerThreshold) && + avoidBadFood) return 0f; // no mouse don't eat the uranium-235 @@ -186,14 +189,15 @@ public sealed partial class NPCUtilitySystem : EntitySystem return 1f; } - case DrinkValueCon: + case DrinkValueCon drinkValueConsideration: { // can't drink closed drinks and can't drink with a mask on... if (!_ingestion.CanConsume(owner, targetUid)) return 0f; // only drink when thirsty - if (TryComp(owner, out var thirst) && thirst.CurrentThirstThreshold > ThirstThreshold.Okay) + if (TryComp(owner, out var satiation) && + _satiation.IsValueInRange((owner, satiation), SatiationSystem.Thirst, below: drinkValueConsideration.ThirstThreshold)) return 0f; // no janicow don't drink the blood puddle @@ -511,7 +515,7 @@ public sealed partial class NPCUtilitySystem : EntitySystem { switch (filter) { - case Content.Server.NPC.Queries.Queries.ComponentFilter compFilter: + case ComponentFilter compFilter: { _entityList.Clear(); diff --git a/Content.Server/Nutrition/Components/FatExtractorComponent.cs b/Content.Server/Nutrition/Components/FatExtractorComponent.cs index e23c557236..1438df0d4e 100644 --- a/Content.Server/Nutrition/Components/FatExtractorComponent.cs +++ b/Content.Server/Nutrition/Components/FatExtractorComponent.cs @@ -1,5 +1,5 @@ using Content.Server.Nutrition.EntitySystems; -using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.Prototypes; using Robust.Shared.Audio; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; @@ -66,9 +66,9 @@ public sealed partial class FatExtractorComponent : Component public EntityUid? Stream; /// - /// A minium hunger threshold for extracting nutrition. + /// A minium hunger threshold for extracting nutrition, as specified by a . /// Ignored when emagged. /// - [DataField("minHungerThreshold")] - public HungerThreshold MinHungerThreshold = HungerThreshold.Okay; + [DataField(required: true)] + public SatiationValue MinHungerThreshold; } diff --git a/Content.Server/Nutrition/EntitySystems/AnimalHusbandrySystem.cs b/Content.Server/Nutrition/EntitySystems/AnimalHusbandrySystem.cs index 4ac4c8154f..ddd64d9b3f 100644 --- a/Content.Server/Nutrition/EntitySystems/AnimalHusbandrySystem.cs +++ b/Content.Server/Nutrition/EntitySystems/AnimalHusbandrySystem.cs @@ -9,10 +9,12 @@ using Content.Shared.NameModifier.EntitySystems; using Content.Shared.Nutrition.AnimalHusbandry; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; using Content.Shared.Storage; using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; @@ -24,7 +26,7 @@ namespace Content.Server.Nutrition.EntitySystems; public sealed partial class AnimalHusbandrySystem : EntitySystem { [Dependency] private EntityLookupSystem _entityLookup = default!; - [Dependency] private HungerSystem _hunger = default!; + [Dependency] private SatiationSystem _satiation = default!; [Dependency] private IAdminLogManager _adminLog = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private IRobustRandom _random = default!; @@ -121,12 +123,20 @@ public sealed partial class AnimalHusbandrySystem : EntitySystem if (TryComp(uid, out var interactionPopup)) _audio.PlayPvs(interactionPopup.InteractSuccessSound, uid); - _hunger.ModifyHunger(uid, -component.HungerPerBirth); - _hunger.ModifyHunger(partner, -component.HungerPerBirth); + if (TryComp(uid, out var satiation)) + { + _satiation.ModifyValue((uid, satiation), SatiationSystem.Hunger, -component.HungerPerBirth); + } + + if (TryComp(partner, out var partnerSatiation)) + { + _satiation.ModifyValue((partner, partnerSatiation), SatiationSystem.Hunger, -component.HungerPerBirth); + } component.GestationEndTime = _timing.CurTime + component.GestationDuration; component.Gestating = true; - _adminLog.Add(LogType.Action, $"{ToPrettyString(uid)} (carrier) and {ToPrettyString(partner)} (partner) successfully bred."); + _adminLog.Add(LogType.Action, + $"{ToPrettyString(uid)} (carrier) and {ToPrettyString(partner)} (partner) successfully bred."); return true; } @@ -148,11 +158,27 @@ public sealed partial class AnimalHusbandrySystem : EntitySystem if (_mobState.IsIncapacitated(uid)) return false; - if (TryComp(uid, out var hunger) && _hunger.GetHungerThreshold(hunger) < HungerThreshold.Okay) - return false; + // If no satiations, no limit to reproduction + if (!TryComp(uid, out var satiation)) + { + return true; + } - if (TryComp(uid, out var thirst) && thirst.CurrentThirstThreshold < ThirstThreshold.Okay) - return false; + // Check minimum hunger requirement + if (component!.MinHungerThreshold is { } minHunger) + { + if (!satiation.Has(SatiationSystem.Hunger) || + _satiation.IsValueInRange((uid, satiation), SatiationSystem.Hunger, below: minHunger)) + return false; + } + + // Check minimum thirst requirement + if (component.MinThirstThreshold is { } minThirst) + { + if (!satiation.Has(SatiationSystem.Thirst) || + _satiation.IsValueInRange((uid, satiation), SatiationSystem.Thirst, below: minThirst)) + return false; + } return true; } @@ -199,6 +225,7 @@ public sealed partial class AnimalHusbandrySystem : EntitySystem // Make sure the name prefix is applied _nameMod.RefreshNameModifiers(offspring); } + _adminLog.Add(LogType.Action, $"{ToPrettyString(uid)} gave birth to {ToPrettyString(offspring)}."); } @@ -225,7 +252,8 @@ public sealed partial class AnimalHusbandrySystem : EntitySystem if (_timing.CurTime < reproductive.NextBreedAttempt) continue; - reproductive.NextBreedAttempt += _random.Next(reproductive.MinBreedAttemptInterval, reproductive.MaxBreedAttemptInterval); + reproductive.NextBreedAttempt += + _random.Next(reproductive.MinBreedAttemptInterval, reproductive.MaxBreedAttemptInterval); // no. if (HasComp(uid) || TryComp(uid, out var mind) && mind.HasMind) diff --git a/Content.Server/Nutrition/EntitySystems/FatExtractorSystem.cs b/Content.Server/Nutrition/EntitySystems/FatExtractorSystem.cs index 36944d3e45..b3988cfa8f 100644 --- a/Content.Server/Nutrition/EntitySystems/FatExtractorSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/FatExtractorSystem.cs @@ -1,16 +1,17 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Nutrition.Components; -using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Storage.Components; using Content.Shared.Emag.Components; using Content.Shared.Emag.Systems; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; using Content.Shared.Power; using Content.Shared.Storage.Components; using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Server.Nutrition.EntitySystems; @@ -22,7 +23,7 @@ public sealed partial class FatExtractorSystem : EntitySystem { [Dependency] private IGameTiming _timing = default!; [Dependency] private EmagSystem _emag = default!; - [Dependency] private HungerSystem _hunger = default!; + [Dependency] private SatiationSystem _satiation = default!; [Dependency] private SharedAppearanceSystem _appearance = default!; [Dependency] private SharedAudioSystem _audio = default!; @@ -95,23 +96,35 @@ public sealed partial class FatExtractorSystem : EntitySystem component.Stream = _audio.Stop(component.Stream); } - public bool TryGetValidOccupant(EntityUid uid, [NotNullWhen(true)] out EntityUid? occupant, FatExtractorComponent? component = null, EntityStorageComponent? storage = null) + public bool TryGetValidOccupant(EntityUid uid, + [NotNullWhen(true)] out Entity? occupant, + FatExtractorComponent? component = null, + EntityStorageComponent? storage = null) { occupant = null; if (!Resolve(uid, ref component, ref storage)) return false; - occupant = storage.Contents.ContainedEntities.FirstOrDefault(); + var firstEntity = storage.Contents.ContainedEntities.FirstOrDefault(); + if (firstEntity is not { Valid: true }) + { + return false; + } - if (!TryComp(occupant, out var hunger)) + if (!TryComp(firstEntity, out var satiation)) return false; - if (_hunger.GetHunger(hunger) < component.NutritionPerSecond) + Entity entity = (firstEntity, satiation); + + if (_satiation.GetValueOrNull(entity, SatiationSystem.Hunger) < component.NutritionPerSecond && + !_emag.CheckFlag(uid, EmagType.Interaction)) return false; - if (hunger.CurrentThreshold < component.MinHungerThreshold && !_emag.CheckFlag(uid, EmagType.Interaction)) + if (_satiation.IsValueInRange(entity, SatiationSystem.Hunger, below: component.MinHungerThreshold) && + !HasComp(uid)) return false; + occupant = entity; return true; } @@ -140,7 +153,7 @@ public sealed partial class FatExtractorSystem : EntitySystem continue; fat.NextUpdate += fat.UpdateTime; - _hunger.ModifyHunger(occupant.Value, -fat.NutritionPerSecond); + _satiation.ModifyValue(occupant.Value, SatiationSystem.Hunger, -fat.NutritionPerSecond); fat.NutrientAccumulator += fat.NutritionPerSecond; if (fat.NutrientAccumulator >= fat.NutrientPerMeat) { diff --git a/Content.Server/Nutrition/Hungry.cs b/Content.Server/Nutrition/Hungry.cs deleted file mode 100644 index ce5e14ce09..0000000000 --- a/Content.Server/Nutrition/Hungry.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Content.Server.Administration; -using Content.Shared.Administration; -using Content.Shared.Nutrition.Components; -using Content.Shared.Nutrition.EntitySystems; -using Robust.Shared.Console; - -namespace Content.Server.Nutrition; - -[AdminCommand(AdminFlags.Debug)] -public sealed class Hungry : LocalizedEntityCommands -{ - public override string Command => "hungry"; - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - var player = shell.Player; - if (player == null) - { - shell.WriteError(Loc.GetString("cmd-nutrition-error-player")); - return; - } - - if (player.AttachedEntity is not {Valid: true} playerEntity) - { - shell.WriteError(Loc.GetString("cmd-nutrition-error-entity")); - return; - } - - if (!EntityManager.TryGetComponent(playerEntity, out HungerComponent? hunger)) - { - shell.WriteError(Loc.GetString("cmd-nutrition-error-component", ("comp", nameof(HungerComponent)))); - return; - } - - var hungryThreshold = hunger.Thresholds[HungerThreshold.Starving]; - EntityManager.System().SetHunger(playerEntity, hungryThreshold, hunger); - } -} diff --git a/Content.Server/Nutrition/SetNutrit.cs b/Content.Server/Nutrition/SetNutrit.cs deleted file mode 100644 index c7c716b78c..0000000000 --- a/Content.Server/Nutrition/SetNutrit.cs +++ /dev/null @@ -1,115 +0,0 @@ -using Content.Server.Administration; -using Content.Server.Database.Migrations.Postgres; -using Content.Shared.Administration; -using Content.Shared.Nutrition.Components; -using Content.Shared.Nutrition.EntitySystems; -using Robust.Shared.Console; -using System.Linq; - -namespace Content.Server.Nutrition; - -[AdminCommand(AdminFlags.Debug)] -public sealed class SetNutrit : LocalizedEntityCommands -{ - public override string Command => "setnutrit"; - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - var player = shell.Player; - if (player == null) - { - shell.WriteError(Loc.GetString("cmd-nutrition-error-player")); - return; - } - - if (player.AttachedEntity is not { Valid: true } playerEntity) - { - shell.WriteError(Loc.GetString("cmd-nutrition-error-entity")); - return; - } - - if (args.Length != 2) - { - shell.WriteError(Loc.GetString("shell-wrong-arguments-number-need-specific", - ("properAmount", 2), - ("currentAmount", args.Length) - )); - return; - } - - var systemString = args[0]; - switch (systemString) - { - case "hunger": - { - if (!EntityManager.TryGetComponent(playerEntity, out HungerComponent? hunger)) - { - shell.WriteError(Loc.GetString("cmd-nutrition-error-component", ("comp", nameof(HungerComponent)))); - return; - } - - if (!Enum.TryParse(args[1], out HungerThreshold hungerThreshold)) - { - shell.WriteError(Loc.GetString("cmd-setnutrit-error-invalid-threshold", - ("thresholdType", nameof(HungerThreshold)), - ("thresholdString", args[1]) - )); - return; - } - - var hungerValue = hunger.Thresholds[hungerThreshold]; - EntityManager.System().SetHunger(playerEntity, hungerValue, hunger); - return; - } - case "thirst": - { - if (!EntityManager.TryGetComponent(playerEntity, out ThirstComponent? thirst)) - { - shell.WriteError(Loc.GetString("cmd-nutrition-error-component", ("comp", nameof(ThirstComponent)))); - return; - } - - if (!Enum.TryParse(args[1], out ThirstThreshold thirstThreshold)) - { - shell.WriteError(Loc.GetString("cmd-setnutrit-error-invalid-threshold", - ("thresholdType", nameof(ThirstThreshold)), - ("thresholdString", args[1]) - )); - return; - } - - var thirstValue = thirst.ThirstThresholds[thirstThreshold]; - EntityManager.System().SetThirst(playerEntity, thirst, thirstValue); - return; - } - default: - { - shell.WriteError($"invalid nutrition system ${systemString}"); - return; - } - } - } - - public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) - { - switch (args.Length) - { - case 1: - { - string[] kinds = { "hunger", "thirst" }; - return CompletionResult.FromHintOptions(kinds, "nutrition system"); - } - case 2: - { - return args[0] switch - { - "hunger" => CompletionResult.FromHintOptions(Enum.GetNames(), nameof(HungerThreshold)), - "thirst" => CompletionResult.FromHintOptions(Enum.GetNames(), nameof(ThirstThreshold)), - _ => CompletionResult.Empty, - }; - } - default: - return CompletionResult.Empty; - } - } -} diff --git a/Content.Server/Nutrition/SetSatiation.cs b/Content.Server/Nutrition/SetSatiation.cs new file mode 100644 index 0000000000..e4d2ddcfa9 --- /dev/null +++ b/Content.Server/Nutrition/SetSatiation.cs @@ -0,0 +1,135 @@ +using System.Globalization; +using System.Linq; +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; +using Robust.Shared.Console; +using Robust.Shared.Prototypes; + +namespace Content.Server.Nutrition; + +/// +/// Sets the running player's entity's satiation of a given type to a given value or threshold. +/// +[AdminCommand(AdminFlags.Debug)] +public sealed partial class SetSatiation : LocalizedEntityCommands +{ + [Dependency] private IPrototypeManager _proto = default!; + [Dependency] private SatiationSystem _satiation = default!; + + /// + public override string Command => "setsatiation"; + + /// + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + var player = shell.Player; + if (player == null) + { + shell.WriteError(Loc.GetString("shell-only-players-can-run-this-command")); + return; + } + + if (player.AttachedEntity is not { Valid: true } playerEntity) + { + shell.WriteError(Loc.GetString("shell-must-be-attached-to-entity")); + return; + } + + if (args.Length != 2) + { + shell.WriteError(Loc.GetString("shell-wrong-arguments-number-need-specific", + ("properAmount", 2), + ("currentAmount", args.Length) + )); + return; + } + + if (!EntityManager.TryGetComponent(playerEntity, out var comp)) + { + shell.WriteError(Loc.GetString("shell-entity-with-uid-lacks-component", + ("uid", playerEntity.Id), + ("componentName", nameof(SatiationComponent)))); + return; + } + + ProtoId type = args[0]; + if (comp.GetOrNull(type) is not { } satiation) + { + var typeName = _proto.TryIndex(type, out var t) ? t.Name : $"\"{type}\""; + shell.WriteLine(Loc.GetString( + "shell-target-entity-does-not-have-message", + ("missing", Loc.GetString("cmd-nutrition-setsatiation-need", ("satiation", typeName))) + )); + return; + } + + if (!_proto.Resolve(satiation.Prototype, out var proto)) + { + shell.WriteError(Loc.GetString( + "cmd-nutrition-setsatiation-prototype-error", + ("satiation-proto-id", satiation.Prototype) + )); + return; + } + + if (float.TryParse(args[1], out var value)) + { + _satiation.SetValue((playerEntity, comp), type, value); + return; + } + + if (!proto.Thresholds.TryGetValue(args[1], out var valueFromKey)) + { + shell.WriteLine(Loc.GetString( + "cmd-nutrition-setsatiation-no-matching-key-error", + ("key", args[1]), + ("satiation-proto-id", proto.ID) + )); + return; + } + + _satiation.SetValue((playerEntity, comp), type, value: valueFromKey); + } + + /// + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (shell.Player?.AttachedEntity is not { } playerEntity || + !EntityManager.TryGetComponent(playerEntity, out var comp)) + return CompletionResult.Empty; + + var entity = new Entity(playerEntity, comp); + + switch (args.Length) + { + case 1: + return CompletionResult.FromHintOptions( + entity.Comp.Satiations.Keys.Select(it => it.Id), + nameof(SatiationTypePrototype) + ); + case 2: + if (!_proto.TryIndex(args[0], out var satiationProto) || + _satiation.GetMaximumValue(entity, satiationProto.ID) is not { } maxValue) + return CompletionResult.Empty; + + var keys = _satiation.GetKeysForType(entity, satiationProto.ID) + .Select(key => + new CompletionOption(key, Loc.GetString("cmd-nutrition-setsatiation-hint-key"))); + + return CompletionResult.FromHintOptions( + keys.Concat( + [ + new CompletionOption(maxValue.ToString(CultureInfo.InvariantCulture), + Loc.GetString("cmd-nutrition-setsatiation-hint-max-value")) + ] + ), + hint: null + ); + default: + return CompletionResult.Empty; + } + } +} diff --git a/Content.Server/Nutrition/Thirsty.cs b/Content.Server/Nutrition/Thirsty.cs deleted file mode 100644 index 8725a62be2..0000000000 --- a/Content.Server/Nutrition/Thirsty.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Content.Server.Administration; -using Content.Shared.Administration; -using Content.Shared.Nutrition.Components; -using Content.Shared.Nutrition.EntitySystems; -using Robust.Shared.Console; - -namespace Content.Server.Nutrition; - -[AdminCommand(AdminFlags.Debug)] -public sealed class Thirsty : LocalizedEntityCommands -{ - public override string Command => "thirsty"; - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - var player = shell.Player; - if (player == null) - { - shell.WriteError(Loc.GetString("cmd-nutrition-error-player")); - return; - } - - if (player.AttachedEntity is not {Valid: true} playerEntity) - { - shell.WriteError(Loc.GetString("cmd-nutrition-error-entity")); - return; - } - - if (!EntityManager.TryGetComponent(playerEntity, out ThirstComponent? thirst)) - { - shell.WriteError(Loc.GetString("cmd-nutrition-error-component", ("comp", nameof(ThirstComponent)))); - return; - } - - var thirstyThreshold = thirst.ThirstThresholds[ThirstThreshold.Parched]; - EntityManager.System().SetThirst(playerEntity, thirst, thirstyThreshold); - } -} diff --git a/Content.Server/Nutrition/Unsatiate.cs b/Content.Server/Nutrition/Unsatiate.cs new file mode 100644 index 0000000000..a79a338c4b --- /dev/null +++ b/Content.Server/Nutrition/Unsatiate.cs @@ -0,0 +1,103 @@ +using System.Linq; +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; +using Robust.Shared.Console; +using Robust.Shared.Prototypes; + +namespace Content.Server.Nutrition; + +/// +/// This command sets the specified Satiation(s) to 10% of its maximum. +/// +[AdminCommand(AdminFlags.Debug)] +public sealed partial class Unsatiate : LocalizedEntityCommands +{ + [Dependency] private IPrototypeManager _protoMan = default!; + [Dependency] private SatiationSystem _satiation = default!; + + /// + public override string Command => "unsatiate"; + + /// + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + var player = shell.Player; + if (player == null) + { + shell.WriteError(Loc.GetString("shell-only-players-can-run-this-command")); + return; + } + + if (player.AttachedEntity is not { Valid: true } playerEntity) + { + shell.WriteError(Loc.GetString("shell-must-be-attached-to-entity")); + return; + } + + if (!EntityManager.TryGetComponent(playerEntity, out SatiationComponent? satiation)) + { + shell.WriteError(Loc.GetString("shell-entity-with-uid-lacks-component", + ("uid", playerEntity.Id), + ("componentName", nameof(SatiationComponent)))); + return; + } + + if (args.Length == 0) + { + ExecuteWithoutArgs((playerEntity, satiation)); + } + else + { + ExecuteWithArgs(shell, (playerEntity, satiation), args); + } + } + + private void ExecuteWithoutArgs(Entity entity) + { + foreach (var satiation in entity.Comp.Satiations.Keys) + { + if (_satiation.GetMaximumValue(entity, satiation) is not { } maxValue) + continue; + + _satiation.SetValue(entity, satiation, maxValue / 10.0f); + } + } + + private void ExecuteWithArgs(IConsoleShell shell, Entity entity, string[] args) + { + // Check all of the types given before modifying any of them. + foreach (var arg in args) + { + if (_protoMan.TryIndex(arg, out var satiationType) && entity.Comp.Has(satiationType.ID)) + continue; + + shell.WriteLine(Loc.GetString("shell-target-entity-does-not-have-message", + ("missing", Loc.GetString("cmd-nutrition-satiation-need", ("satiation", arg))))); + return; + } + + foreach (var arg in args) + { + if (_satiation.GetMaximumValue(entity, arg) is not { } maxValue) + continue; + + _satiation.SetValue(entity, arg, maxValue / 10.0f); + } + } + + /// + // Always suggest any satiation types which aren't already in the arg list. + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (shell.Player?.AttachedEntity is not { } playerEntity || + !EntityManager.TryGetComponent(playerEntity, out var comp)) + return CompletionResult.Empty; + + var absentSatiationTypeIds = + comp.Satiations.Keys.Select(it => it.Id).Where(type => !args.Contains(type)); + return CompletionResult.FromHintOptions(absentSatiationTypeIds, nameof(SatiationTypePrototype)); + } +} diff --git a/Content.Server/RatKing/RatKingSystem.cs b/Content.Server/RatKing/RatKingSystem.cs index 6163034fa5..df762ad729 100644 --- a/Content.Server/RatKing/RatKingSystem.cs +++ b/Content.Server/RatKing/RatKingSystem.cs @@ -6,14 +6,16 @@ using Content.Server.NPC.HTN; using Content.Server.NPC.Systems; using Content.Server.Popups; using Content.Shared.Atmos; -using Content.Shared.Chat; using Content.Shared.Dataset; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; using Content.Shared.Pointing; using Content.Shared.Random.Helpers; using Content.Shared.RatKing; using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Content.Shared.Chat; namespace Content.Server.RatKing { @@ -23,7 +25,7 @@ namespace Content.Server.RatKing [Dependency] private AtmosphereSystem _atmos = default!; [Dependency] private ChatSystem _chat = default!; [Dependency] private HTNSystem _htn = default!; - [Dependency] private HungerSystem _hunger = default!; + [Dependency] private SatiationSystem _satiation = default!; [Dependency] private NPCSystem _npc = default!; [Dependency] private PopupSystem _popup = default!; @@ -44,17 +46,17 @@ namespace Content.Server.RatKing if (args.Handled) return; - if (!TryComp(uid, out var hunger)) + if (!TryComp(uid, out var satiation)) return; //make sure the hunger doesn't go into the negatives - if (_hunger.GetHunger(hunger) < component.HungerPerArmyUse) + if (_satiation.GetValueOrNull((uid, satiation), SatiationSystem.Hunger) < component.HungerPerArmyUse) { _popup.PopupEntity(Loc.GetString("rat-king-too-hungry"), uid, uid); return; } args.Handled = true; - _hunger.ModifyHunger(uid, -component.HungerPerArmyUse, hunger); + _satiation.ModifyValue((uid, satiation), SatiationSystem.Hunger, -component.HungerPerArmyUse); var servant = Spawn(component.ArmyMobSpawnId, Transform(uid).Coordinates); var comp = EnsureComp(servant); comp.King = uid; @@ -74,17 +76,17 @@ namespace Content.Server.RatKing if (args.Handled) return; - if (!TryComp(uid, out var hunger)) + if (!TryComp(uid, out var satiation)) return; //make sure the hunger doesn't go into the negatives - if (_hunger.GetHunger(hunger) < component.HungerPerDomainUse) + if (_satiation.GetValueOrNull((uid, satiation), SatiationSystem.Hunger) < component.HungerPerDomainUse) { _popup.PopupEntity(Loc.GetString("rat-king-too-hungry"), uid, uid); return; } args.Handled = true; - _hunger.ModifyHunger(uid, -component.HungerPerDomainUse, hunger); + _satiation.ModifyValue((uid, satiation), SatiationSystem.Hunger, -component.HungerPerDomainUse); _popup.PopupEntity(Loc.GetString("rat-king-domain-popup"), uid); var tileMix = _atmos.GetTileMixture(uid, excite: true); diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs index b668927f86..a99a07e335 100644 --- a/Content.Server/Zombies/ZombieSystem.Transform.cs +++ b/Content.Server/Zombies/ZombieSystem.Transform.cs @@ -137,8 +137,7 @@ public sealed partial class ZombieSystem //get diseases, breath, be thirst, be hungry, die in space, get double sentience, have offspring or be paraplegic. RemComp(target); RemComp(target); - RemComp(target); - RemComp(target); + RemComp(target); RemComp(target); RemComp(target); RemComp(target); diff --git a/Content.Shared/Animals/UdderComponent.cs b/Content.Shared/Animals/UdderComponent.cs index 12db8efefd..9fbadca8e0 100644 --- a/Content.Shared/Animals/UdderComponent.cs +++ b/Content.Shared/Animals/UdderComponent.cs @@ -1,6 +1,7 @@ using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.FixedPoint; +using Content.Shared.Nutrition.Prototypes; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -43,6 +44,12 @@ public sealed partial class UdderComponent : Component [DataField, AutoNetworkedField] public float HungerUsage = 10f; + /// + /// If the entity's hunger satiation is below this value, it cannot be milked. + /// + [DataField(required: true), AutoNetworkedField] + public SatiationValue MinHungerThreshold; + /// /// How long to wait before producing. /// diff --git a/Content.Shared/Animals/UdderSystem.cs b/Content.Shared/Animals/UdderSystem.cs index 1271915e5a..6746922def 100644 --- a/Content.Shared/Animals/UdderSystem.cs +++ b/Content.Shared/Animals/UdderSystem.cs @@ -5,10 +5,12 @@ using Content.Shared.IdentityManagement; using Content.Shared.Mobs.Systems; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; using Content.Shared.Popups; using Content.Shared.Udder; using Content.Shared.Verbs; using Robust.Shared.Containers; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Shared.Animals; @@ -18,7 +20,7 @@ namespace Content.Shared.Animals; /// public sealed partial class UdderSystem : EntitySystem { - [Dependency] private HungerSystem _hunger = default!; + [Dependency] private SatiationSystem _satiation = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private MobStateSystem _mobState = default!; [Dependency] private SharedPopupSystem _popupSystem = default!; @@ -71,13 +73,15 @@ public sealed partial class UdderSystem : EntitySystem continue; // Actually there is food digestion so no problem with instant reagent generation "OnFeed" - if (TryComp(uid, out HungerComponent? hunger)) + if (TryComp(uid, out var satiation)) { // Is there enough nutrition to produce reagent? - if (_hunger.GetHungerThreshold(hunger) < HungerThreshold.Okay) + if (_satiation.IsValueInRange((uid, satiation), SatiationSystem.Hunger, above: udder.MinHungerThreshold, hypotheticalValueDelta: -udder.HungerUsage)) + { continue; + } - _hunger.ModifyHunger(uid, -udder.HungerUsage, hunger); + _satiation.ModifyValue((uid, satiation), SatiationSystem.Hunger, -udder.HungerUsage); } //TODO: toxins from bloodstream !? diff --git a/Content.Shared/Animals/WoolyComponent.cs b/Content.Shared/Animals/WoolyComponent.cs index 6e6e6745e1..f192b294d3 100644 --- a/Content.Shared/Animals/WoolyComponent.cs +++ b/Content.Shared/Animals/WoolyComponent.cs @@ -1,6 +1,7 @@ using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.FixedPoint; +using Content.Shared.Nutrition.Prototypes; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -43,6 +44,12 @@ public sealed partial class WoolyComponent : Component [DataField, AutoNetworkedField] public float HungerUsage = 10f; + /// + /// If the entity's hunger satiation is below this value, it cannot be milked. + /// + [DataField(required: true), AutoNetworkedField] + public SatiationValue MinHungerThreshold; + /// /// How long to wait before growing wool. /// diff --git a/Content.Shared/Animals/WoolySystem.cs b/Content.Shared/Animals/WoolySystem.cs index 6ab0e5bfa4..bdee7dd0ec 100644 --- a/Content.Shared/Animals/WoolySystem.cs +++ b/Content.Shared/Animals/WoolySystem.cs @@ -3,7 +3,9 @@ using Content.Shared.Mobs.Systems; using Content.Shared.Nutrition; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; using Robust.Shared.Containers; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Shared.Animals; @@ -14,7 +16,7 @@ namespace Content.Shared.Animals; /// public sealed partial class WoolySystem : EntitySystem { - [Dependency] private HungerSystem _hunger = default!; + [Dependency] private SatiationSystem _satiation = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private MobStateSystem _mobState = default!; [Dependency] private SharedSolutionContainerSystem _solutionContainer = default!; @@ -64,13 +66,15 @@ public sealed partial class WoolySystem : EntitySystem continue; // Actually there is food digestion so no problem with instant reagent generation "OnFeed" - if (TryComp(uid, out HungerComponent? hunger)) + if (TryComp(uid, out var satiation)) { // Is there enough nutrition to produce reagent? - if (_hunger.GetHungerThreshold(hunger) < HungerThreshold.Okay) + if (_satiation.IsValueInRange((uid, satiation), SatiationSystem.Hunger, above: wooly.MinHungerThreshold, hypotheticalValueDelta: -wooly.HungerUsage)) + { continue; + } - _hunger.ModifyHunger(uid, -wooly.HungerUsage, hunger); + _satiation.ModifyValue((uid, satiation), SatiationSystem.Hunger, -wooly.HungerUsage); } _solutionContainer.TryAddReagent(wooly.Solution.Value, wooly.ReagentId, wooly.Quantity, out _); diff --git a/Content.Shared/EntityConditions/Conditions/Body/HungerEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/Body/HungerEntityConditionSystem.cs deleted file mode 100644 index 713adabcec..0000000000 --- a/Content.Shared/EntityConditions/Conditions/Body/HungerEntityConditionSystem.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Content.Shared.Nutrition.Components; -using Content.Shared.Nutrition.EntitySystems; -using Robust.Shared.Prototypes; - -namespace Content.Shared.EntityConditions.Conditions.Body; - -/// -/// Returns true if this entity's hunger is within a specified minimum and maximum. -/// -/// -public sealed partial class TotalHungerEntityConditionSystem : EntityConditionSystem -{ - [Dependency] private HungerSystem _hunger = default!; - - protected override void Condition(Entity entity, ref EntityConditionEvent args) - { - var total = _hunger.GetHunger(entity.Comp); - args.Result = total >= args.Condition.Min && total <= args.Condition.Max; - } -} - -/// -public sealed partial class HungerCondition : EntityConditionBase -{ - /// - /// Minimum hunger required to fulfill this condition. - /// - [DataField] - public float Min; - - /// - /// Maximum hunger required to fulfill this condition. - /// - [DataField] - public float Max = float.PositiveInfinity; - - public override string EntityConditionGuidebookText(IPrototypeManager prototype) => - Loc.GetString("entity-condition-guidebook-total-hunger", ("max", float.IsPositiveInfinity(Max) ? int.MaxValue : Max), ("min", Min)); -} diff --git a/Content.Shared/EntityConditions/Conditions/Body/SatiationEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/Body/SatiationEntityConditionSystem.cs new file mode 100644 index 0000000000..8744fa38a3 --- /dev/null +++ b/Content.Shared/EntityConditions/Conditions/Body/SatiationEntityConditionSystem.cs @@ -0,0 +1,73 @@ +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityConditions.Conditions.Body; + +/// +/// A condition which passes if the specified is between the specified +/// and . If the entity does not have the +/// specified satiation, the condition evaluates to false. +/// +public sealed partial class SatiationEntityConditionSystem : EntityConditionSystem +{ + [Dependency] private SatiationSystem _satiation = default!; + + /// + protected override void Condition(Entity entity, + ref EntityConditionEvent args) + { + if (_satiation.GetValueOrNull(entity, args.Condition.SatiationType) is not { } satiation) + return; + + args.Result = + (args.Condition.MinInclusive && satiation >= args.Condition.Min || satiation > args.Condition.Min) && + (args.Condition.MaxInclusive && satiation <= args.Condition.Max || satiation < args.Condition.Max); + } +} + +/// +public sealed partial class SatiationCondition : EntityConditionBase +{ + /// + /// The value above which this condition will fail. If is false, the condition will fail + /// if at that value as well. + /// + [DataField] + public float Max = float.PositiveInfinity; + + /// + /// The value below which this condition will fail. If is false, the condition will fail + /// if at that value as well. + /// + [DataField] + public float Min = 0; + + /// + /// If true, values exactly equal to will NOT fail. + /// + [DataField] + public bool MaxInclusive = false; + + /// + /// If true, values exactly equal to will NOT fail. + /// + [DataField] + public bool MinInclusive = false; + + /// + /// The type of satiation whose value will be considered. + /// + [DataField(required: true)] + public ProtoId SatiationType; + + /// + public override string EntityConditionGuidebookText(IPrototypeManager prototype) + { + return Loc.GetString("entity-condition-guidebook-total-satiation", + ("max", float.IsPositiveInfinity(Max) ? int.MaxValue : Max), + ("min", Min), + ("type", prototype.Index(SatiationType).Name)); + } +} diff --git a/Content.Shared/EntityEffects/Effects/Body/SatiateEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/SatiateEntityEffectSystem.cs index 5abb777349..c0d57abb68 100644 --- a/Content.Shared/EntityEffects/Effects/Body/SatiateEntityEffectSystem.cs +++ b/Content.Shared/EntityEffects/Effects/Body/SatiateEntityEffectSystem.cs @@ -1,63 +1,49 @@ using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; using Robust.Shared.Prototypes; namespace Content.Shared.EntityEffects.Effects.Body; -// TODO: These systems are in the same file since satiation should be one system instead of two. Combine these when that happens. // TODO: Arguably oxygen saturation should also be added here... /// -/// Modifies the thirst level of a given entity, multiplied by scale. +/// Attempts to find a on the target, and update its value of the specified +/// . /// -/// -public sealed partial class SatiateThirstEntityEffectsSystem : EntityEffectSystem +public sealed partial class SatiateEntityEffectsSystem : EntityEffectSystem { - [Dependency] private ThirstSystem _thirst = default!; - protected override void Effect(Entity entity, ref EntityEffectEvent args) + [Dependency] private SatiationSystem _satiation = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) { - _thirst.ModifyThirst(entity, entity.Comp, args.Effect.Factor * args.Scale); + _satiation.ModifyValue(entity, args.Effect.SatiationType, args.Effect.Factor * args.Scale); } } -/// -/// Modifies the hunger level of a given entity, multiplied by scale. -/// -/// -public sealed partial class SatiateHungerEntityEffectsSystem : EntityEffectSystem -{ - [Dependency] private HungerSystem _hunger = default!; - protected override void Effect(Entity entity, ref EntityEffectEvent args) - { - _hunger.ModifyHunger(entity, args.Effect.Factor * args.Scale, entity.Comp); - } -} - -/// -/// A type of made for satiation effects. -/// -/// The effect inheriting this BaseEffect /// -public abstract partial class Satiate : EntityEffectBase where T : EntityEffectBase +public sealed partial class Satiate : EntityEffectBase { public const float AverageSatiation = 3f; // Magic number. Not sure how it was calculated since I didn't make it. /// - /// Change in satiation. + /// How much the specified is modified by this effect. /// [DataField] public float Factor = 1.5f; -} -/// -public sealed partial class SatiateThirst : Satiate -{ - public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("entity-effect-guidebook-satiate-thirst", ("chance", Probability), ("relative", Factor / AverageSatiation)); -} + /// + /// The type of satiation to modify. + /// + [DataField(required: true)] + public ProtoId SatiationType; -/// -public sealed partial class SatiateHunger : Satiate -{ public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString("entity-effect-guidebook-satiate-hunger", ("chance", Probability), ("relative", Factor / AverageSatiation)); + { + return Loc.GetString( + "entity-effect-guidebook-satiate", + ("chance", Probability), + ("relative", Factor / AverageSatiation), + ("type", prototype.Index(SatiationType).Name) + ); + } } diff --git a/Content.Shared/Inventory/InventorySystem.Relay.cs b/Content.Shared/Inventory/InventorySystem.Relay.cs index 39724ffd75..4c0035b78c 100644 --- a/Content.Shared/Inventory/InventorySystem.Relay.cs +++ b/Content.Shared/Inventory/InventorySystem.Relay.cs @@ -104,8 +104,7 @@ public partial class InventorySystem SubscribeLocalEvent>(RefRelayInventoryEvent); SubscribeLocalEvent>(RefRelayInventoryEvent); SubscribeLocalEvent>(RefRelayInventoryEvent); - SubscribeLocalEvent>(RefRelayInventoryEvent); - SubscribeLocalEvent>(RefRelayInventoryEvent); + SubscribeLocalEvent>(RefRelayInventoryEvent); SubscribeLocalEvent>(RefRelayInventoryEvent); SubscribeLocalEvent>(RefRelayInventoryEvent); SubscribeLocalEvent>(RefRelayInventoryEvent); diff --git a/Content.Shared/Medical/VomitSystem.cs b/Content.Shared/Medical/VomitSystem.cs index 72bd7c9929..dc221906ff 100644 --- a/Content.Shared/Medical/VomitSystem.cs +++ b/Content.Shared/Medical/VomitSystem.cs @@ -11,6 +11,7 @@ using Content.Shared.Mobs.Systems; using Content.Shared.Movement.Systems; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; using Content.Shared.Popups; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; @@ -22,15 +23,14 @@ namespace Content.Shared.Medical; public sealed partial class VomitSystem : EntitySystem { [Dependency] private INetManager _netManager = default!; - [Dependency] private HungerSystem _hunger = default!; [Dependency] private MobStateSystem _mobState = default!; [Dependency] private MovementModStatusSystem _movementMod = default!; - [Dependency] private ThirstSystem _thirst = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private SharedBloodstreamSystem _bloodstream = default!; [Dependency] private SharedForensicsSystem _forensics = default!; [Dependency] private SharedPopupSystem _popup = default!; [Dependency] private SharedPuddleSystem _puddle = default!; + [Dependency] private SatiationSystem _satiation = default!; [Dependency] private SharedSolutionContainerSystem _solutionContainer = default!; public override void Initialize() @@ -86,11 +86,12 @@ public sealed partial class VomitSystem : EntitySystem return; // Vomiting makes you hungrier and thirstier - if (TryComp(uid, out var hunger)) - _hunger.ModifyHunger(uid, hungerAdded, hunger); - - if (TryComp(uid, out var thirst)) - _thirst.ModifyThirst(uid, thirst, thirstAdded); + if (TryComp(uid, out var satiation)) + { + Entity entity = (uid, satiation); + _satiation.ModifyValue(entity, SatiationSystem.Hunger, hungerAdded); + _satiation.ModifyValue(entity, SatiationSystem.Thirst, thirstAdded); + } // It fully empties the stomach, this amount from the chem stream is relatively small var solutionSize = (MathF.Abs(thirstAdded) + MathF.Abs(hungerAdded)) / 6; diff --git a/Content.Shared/Nutrition/AnimalHusbandry/ReproductiveComponent.cs b/Content.Shared/Nutrition/AnimalHusbandry/ReproductiveComponent.cs index 8d128f547e..f54cf05bd8 100644 --- a/Content.Shared/Nutrition/AnimalHusbandry/ReproductiveComponent.cs +++ b/Content.Shared/Nutrition/AnimalHusbandry/ReproductiveComponent.cs @@ -1,4 +1,5 @@ -using Content.Shared.Storage; +using Content.Shared.Nutrition.Prototypes; +using Content.Shared.Storage; using Content.Shared.Whitelist; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; @@ -86,6 +87,18 @@ public sealed partial class ReproductiveComponent : Component [DataField, ViewVariables(VVAccess.ReadWrite)] public float HungerPerBirth = 75f; + /// + /// If the entity's hunger satiation is below this value, it cannot reproduce. + /// + [DataField] + public SatiationValue? MinHungerThreshold; + + /// + /// If the entity's thirst satiation is below this value, it cannot reproduce. + /// + [DataField] + public SatiationValue? MinThirstThreshold; + /// /// Popup shown when an entity gives birth. /// Configurable for things like laying eggs. diff --git a/Content.Shared/Nutrition/Components/ExaminableHungerComponent.cs b/Content.Shared/Nutrition/Components/ExaminableHungerComponent.cs deleted file mode 100644 index 4431a5ef4a..0000000000 --- a/Content.Shared/Nutrition/Components/ExaminableHungerComponent.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Content.Shared.Nutrition.EntitySystems; -using Robust.Shared.GameStates; - -namespace Content.Shared.Nutrition.Components; - -/// -/// Adds text to the entity's description box based on its current hunger threshold. -/// -[RegisterComponent, NetworkedComponent] -[Access(typeof(ExaminableHungerSystem))] -public sealed partial class ExaminableHungerComponent : Component -{ - /// - /// Dictionary of hunger thresholds to LocIds of the messages to display. - /// - [DataField] - public Dictionary Descriptions = new() - { - { HungerThreshold.Overfed, "examinable-hunger-component-examine-overfed"}, - { HungerThreshold.Okay, "examinable-hunger-component-examine-okay"}, - { HungerThreshold.Peckish, "examinable-hunger-component-examine-peckish"}, - { HungerThreshold.Starving, "examinable-hunger-component-examine-starving"}, - { HungerThreshold.Dead, "examinable-hunger-component-examine-starving"} - }; - - /// - /// LocId of a fallback message to display if the entity has no - /// or does not have a value in for the current threshold. - /// - public LocId NoHungerDescription = "examinable-hunger-component-examine-none"; -} diff --git a/Content.Shared/Nutrition/Components/ExaminableSatiationComponent.cs b/Content.Shared/Nutrition/Components/ExaminableSatiationComponent.cs new file mode 100644 index 0000000000..80faf3946a --- /dev/null +++ b/Content.Shared/Nutrition/Components/ExaminableSatiationComponent.cs @@ -0,0 +1,21 @@ +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Nutrition.Components; + +/// +/// Component which specifies what shows in examine descriptions. +/// +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] +[Access(typeof(ExaminableSatiationSystem))] +public sealed partial class ExaminableSatiationComponent : Component +{ + /// + /// Examination localization string IDs per threshold per satiation type. + /// + [DataField(required: true), AutoNetworkedField] + public Dictionary, SatiationThresholds> Satiations; +} diff --git a/Content.Shared/Nutrition/Components/HungerComponent.cs b/Content.Shared/Nutrition/Components/HungerComponent.cs deleted file mode 100644 index d5db991b1a..0000000000 --- a/Content.Shared/Nutrition/Components/HungerComponent.cs +++ /dev/null @@ -1,144 +0,0 @@ -using Content.Shared.Alert; -using Content.Shared.Damage; -using Content.Shared.Nutrition.EntitySystems; -using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; - -namespace Content.Shared.Nutrition.Components; - -[RegisterComponent, NetworkedComponent, Access(typeof(HungerSystem))] -[AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause] -public sealed partial class HungerComponent : Component -{ - /// - /// The hunger value as authoritatively set by the server as of . - /// This value should be updated relatively infrequently. To get the current hunger, which changes with each update, - /// use . - /// - [DataField, ViewVariables(VVAccess.ReadOnly)] - [AutoNetworkedField] - public float LastAuthoritativeHungerValue; - - /// - /// The time at which was last updated. - /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - public TimeSpan LastAuthoritativeHungerChangeTime; - - /// - /// The base amount at which decays. - /// - /// Any time this is modified, should be called. - [DataField("baseDecayRate"), ViewVariables(VVAccess.ReadWrite)] - public float BaseDecayRate = 0.01666666666f; - - /// - /// The actual amount at which decays. - /// Affected by - /// - /// Any time this is modified, should be called. - [DataField("actualDecayRate"), ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - public float ActualDecayRate; - - /// - /// The last threshold this entity was at. - /// Stored in order to prevent recalculating - /// - [DataField("lastThreshold"), ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - public HungerThreshold LastThreshold; - - /// - /// The current hunger threshold the entity is at - /// - /// Any time this is modified, should be called. - [DataField("currentThreshold"), ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - public HungerThreshold CurrentThreshold; - - /// - /// A dictionary relating HungerThreshold to the amount of current hunger needed for each one - /// - [DataField("thresholds", customTypeSerializer: typeof(DictionarySerializer))] - [AutoNetworkedField] - public Dictionary Thresholds = new() - { - { HungerThreshold.Overfed, 200.0f }, - { HungerThreshold.Okay, 150.0f }, - { HungerThreshold.Peckish, 100.0f }, - { HungerThreshold.Starving, 50.0f }, - { HungerThreshold.Dead, 0.0f } - }; - - /// - /// A dictionary relating hunger thresholds to corresponding alerts. - /// - [DataField("hungerThresholdAlerts")] - [AutoNetworkedField] - public Dictionary> HungerThresholdAlerts = new() - { - { HungerThreshold.Peckish, "Peckish" }, - { HungerThreshold.Starving, "Starving" }, - { HungerThreshold.Dead, "Starving" } - }; - - [DataField] - public ProtoId HungerAlertCategory = "Hunger"; - - /// - /// A dictionary relating HungerThreshold to how much they modify . - /// - [DataField("hungerThresholdDecayModifiers", customTypeSerializer: typeof(DictionarySerializer))] - [AutoNetworkedField] - public Dictionary HungerThresholdDecayModifiers = new() - { - { HungerThreshold.Overfed, 1.2f }, - { HungerThreshold.Okay, 1f }, - { HungerThreshold.Peckish, 0.8f }, - { HungerThreshold.Starving, 0.6f }, - { HungerThreshold.Dead, 0.6f } - }; - - /// - /// The amount of slowdown applied when an entity is starving - /// - [DataField("starvingSlowdownModifier"), ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - public float StarvingSlowdownModifier = 0.75f; - - /// - /// Damage dealt when your current threshold is at HungerThreshold.Dead - /// - [DataField("starvationDamage")] - public DamageSpecifier? StarvationDamage; - - /// - /// The time when the hunger threshold will update next. - /// - [DataField("nextUpdateTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - [AutoPausedField] - public TimeSpan NextThresholdUpdateTime; - - /// - /// The time between each hunger threshold update. - /// - [ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - public TimeSpan ThresholdUpdateRate = TimeSpan.FromSeconds(1); -} - -[Serializable, NetSerializable] -public enum HungerThreshold : byte -{ - Overfed = 1 << 3, - Okay = 1 << 2, - Peckish = 1 << 1, - Starving = 1 << 0, - Dead = 0, -} diff --git a/Content.Shared/Nutrition/Components/SatiationComponent.cs b/Content.Shared/Nutrition/Components/SatiationComponent.cs new file mode 100644 index 0000000000..77c51b3144 --- /dev/null +++ b/Content.Shared/Nutrition/Components/SatiationComponent.cs @@ -0,0 +1,37 @@ +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Nutrition.Components; + +/// +/// A component which is basically just a collection of s keyed by their +/// s. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] +// Nothing should modify the dictionary once it's deserialized. Perhaps satiations can be dynamically +// added and removed in the future, but not today. +[Access(typeof(SatiationSystem))] +public sealed partial class SatiationComponent : Component +{ + /// + /// The actual s this entity has, keyed by their type. + /// + [DataField(required: true)] + [AutoNetworkedField] + public Dictionary, Satiation> Satiations = []; + + /// + /// Checks if this has a of the specified . + /// + [Access(Other = AccessPermissions.Execute)] + public bool Has(ProtoId type) => GetOrNull(type) != null; + + /// + /// Gets the of the given on this component, or + /// null if no such satiation exists. + /// + [Access(Other = AccessPermissions.Execute)] + public Satiation? GetOrNull(ProtoId type) => Satiations.GetValueOrDefault(type); +} diff --git a/Content.Shared/Nutrition/Components/SatiationDamageComponent.cs b/Content.Shared/Nutrition/Components/SatiationDamageComponent.cs new file mode 100644 index 0000000000..d176876579 --- /dev/null +++ b/Content.Shared/Nutrition/Components/SatiationDamageComponent.cs @@ -0,0 +1,34 @@ +using Content.Shared.Damage; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Nutrition.Components; + +/// +/// This component causes its entity to continuously take damage based on the entity's current satiation. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause] +[Access(typeof(SatiationDamageSystem))] +public sealed partial class SatiationDamageComponent : Component +{ + /// + /// Damage values by satiation threshold, for a satiation type. + /// + [DataField(required: true), AutoNetworkedField] + public Dictionary, SatiationThresholds> Satiations; + + /// + /// How often the damage is applied. + /// + [DataField, AutoNetworkedField] + public TimeSpan Frequency = TimeSpan.FromSeconds(1); + + /// + /// Runtime data, indicating when the next damage application will occur. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField] + public TimeSpan NextDamageTime; +} diff --git a/Content.Shared/Nutrition/Components/SatiationSpeedModifierComponent.cs b/Content.Shared/Nutrition/Components/SatiationSpeedModifierComponent.cs new file mode 100644 index 0000000000..d5f552086c --- /dev/null +++ b/Content.Shared/Nutrition/Components/SatiationSpeedModifierComponent.cs @@ -0,0 +1,21 @@ +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Nutrition.Components; + +/// +/// This component causes its entity to have movement speed modifiers applied based on the entity's current satiations. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), + Access(typeof(SatiationSpeedModifierSystem))] +public sealed partial class SatiationSpeedModifierComponent : Component +{ + /// + /// Speed modifiers, expressed as s, contained within , + /// keyed by the satiation types they are thresholds for. + /// + [DataField(required: true), AutoNetworkedField] + public Dictionary, SatiationThresholds> Satiations; +} diff --git a/Content.Shared/Nutrition/Components/ThirstComponent.cs b/Content.Shared/Nutrition/Components/ThirstComponent.cs deleted file mode 100644 index 5c32b4af28..0000000000 --- a/Content.Shared/Nutrition/Components/ThirstComponent.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Content.Shared.Alert; -using Content.Shared.Nutrition.EntitySystems; -using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; - -namespace Content.Shared.Nutrition.Components; - -[RegisterComponent, NetworkedComponent, Access(typeof(ThirstSystem))] -[AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause] -public sealed partial class ThirstComponent : Component -{ - // Base stuff - [ViewVariables(VVAccess.ReadWrite)] - [DataField("baseDecayRate")] - [AutoNetworkedField] - public float BaseDecayRate = 0.1f; - - [ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - public float ActualDecayRate; - - [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] - public ThirstThreshold CurrentThirstThreshold; - - [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] - public ThirstThreshold LastThirstThreshold; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("startingThirst")] - [AutoNetworkedField] - public float CurrentThirst = -1f; - - /// - /// The time when the hunger will update next. - /// - [DataField("nextUpdateTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - [AutoPausedField] - public TimeSpan NextUpdateTime; - - /// - /// The time between each update. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField, AutoNetworkedField] - public TimeSpan UpdateRate = TimeSpan.FromSeconds(1); - - [DataField("thresholds")] - [AutoNetworkedField] - public Dictionary ThirstThresholds = new() - { - {ThirstThreshold.OverHydrated, 600.0f}, - {ThirstThreshold.Okay, 450.0f}, - {ThirstThreshold.Thirsty, 300.0f}, - {ThirstThreshold.Parched, 150.0f}, - {ThirstThreshold.Dead, 0.0f}, - }; - - [DataField] - public ProtoId ThirstyCategory = "Thirst"; - - public static readonly Dictionary> ThirstThresholdAlertTypes = new() - { - {ThirstThreshold.Thirsty, "Thirsty"}, - {ThirstThreshold.Parched, "Parched"}, - {ThirstThreshold.Dead, "Parched"}, - }; -} - -[Flags] -public enum ThirstThreshold : byte -{ - // Hydrohomies - Dead = 0, - Parched = 1 << 0, - Thirsty = 1 << 1, - Okay = 1 << 2, - OverHydrated = 1 << 3, -} diff --git a/Content.Shared/Nutrition/EntitySystems/BaseSatiationEffectSystem.cs b/Content.Shared/Nutrition/EntitySystems/BaseSatiationEffectSystem.cs new file mode 100644 index 0000000000..486cc9a20c --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/BaseSatiationEffectSystem.cs @@ -0,0 +1,126 @@ +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.Prototypes; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.Nutrition.EntitySystems; + +/// +/// This abstract system provides a convenient interface for implementing effects which react to changes in +/// thresholds. +/// +/// +/// uses this to maintain a damage descriptor which changes depending on the entity's +/// satiation values. When the satiation values change, the damage descriptor is automatically updated by this system. +/// +/// The type of component this system interacts with +/// +/// The type of value this system maintains. It must be contained within a +/// accessible via TComp. +/// +/// Note that this is not related to +public abstract partial class BaseSatiationEffectSystem : EntitySystem where TComp : Component +{ + [Dependency] private SatiationSystem _satiation = default!; + [Dependency] private EntityQuery _satiationQuery; + [Dependency] private IGameTiming _timing = default!; + + /// + /// How to access via a . + /// + protected abstract Dictionary, SatiationThresholds> GetThresholds(TComp comp); + + /// + /// The default value to set our maintained value to in the case that our current satiation + /// is outside of . + /// + protected abstract T DefaultValue(); + + /// + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnSatiationUpdate); + } + + /// + // Updates maintained values when reaching the projected threshold time. + public override void Update(float frameTime) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var ent, out var comp, out var satiation)) + { + foreach (var (type, thresholds) in GetThresholds(comp)) + { + if (_timing.CurTime < thresholds.ProjectedThresholdChangeTime) + continue; + + UpdateSatiation((ent, comp), satiation, type); + } + } + } + + [MustCallBase] + protected virtual void OnMapInit(Entity entity, ref MapInitEvent args) + { + // Make sure we have a satiation component. Realistically, this just exists to cause test failures if an entity + // with `TComp` doesn't have a `SatiationComponent`. + var comp = EnsureComp(entity); + foreach (var type in GetThresholds(entity.Comp).Keys) + { + UpdateSatiation(entity, comp, type); + } + } + + // If a satiation value is changed directly, we react and update our maintained value. + [MustCallBase] + protected void OnSatiationUpdate(Entity entity, ref SatiationUpdateEvent args) + { + if (!_satiationQuery.TryComp(entity, out var comp)) + return; + + UpdateSatiation(entity, comp, args.Type); + } + + /// + /// Updates the maintained based on the entity's satiation. + /// + private void UpdateSatiation(Entity entity, SatiationComponent comp, ProtoId type) + { + // Get the current satiation value... + if (!GetThresholds(entity.Comp).TryGetValue(type, out var thresholds)) + return; + + // ... and then use it to get the appropriate threshold T value. + if (_satiation.TryGetValueByThreshold( + (entity, comp), + type, + thresholds.Thresholds, + out var result, + out var nextLowerThreshold)) + { + thresholds.Current = result ?? DefaultValue(); + + // Predict when our satiation will decay to the next threshold down. + thresholds.ProjectedThresholdChangeTime = nextLowerThreshold is { } lower + ? _satiation.GetTimeToDecay((entity, comp), type, lower) + : null; + } + else + { + thresholds.Current = DefaultValue(); + thresholds.ProjectedThresholdChangeTime = null; + } + + Dirty(entity); + + AfterSatiationUpdate(entity); + } + + /// + /// This function is called after completes its work maintaining . + /// + protected virtual void AfterSatiationUpdate(Entity entity) { } +} diff --git a/Content.Shared/Nutrition/EntitySystems/ExaminableHungerSystem.cs b/Content.Shared/Nutrition/EntitySystems/ExaminableHungerSystem.cs deleted file mode 100644 index aee545564a..0000000000 --- a/Content.Shared/Nutrition/EntitySystems/ExaminableHungerSystem.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Content.Shared.Examine; -using Content.Shared.IdentityManagement; -using Content.Shared.Nutrition.Components; - -namespace Content.Shared.Nutrition.EntitySystems; - -/// -public sealed partial class ExaminableHungerSystem : EntitySystem -{ - [Dependency] private HungerSystem _hunger = default!; - - [Dependency] private EntityQuery _hungerQuery = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnExamine); - } - - /// - /// Defines the text provided on examine. - /// Changes depending on the amount of hunger the target has. - /// - private void OnExamine(Entity entity, ref ExaminedEvent args) - { - var identity = Identity.Entity(entity, EntityManager); - - if (!_hungerQuery.TryComp(entity, out var hungerComp) - || !entity.Comp.Descriptions.TryGetValue(_hunger.GetHungerThreshold(hungerComp), out var locId)) - { - // Use a fallback message if the entity has no HungerComponent - // or is missing a description for the current threshold - locId = entity.Comp.NoHungerDescription; - } - - var msg = Loc.GetString(locId, ("entity", identity)); - args.PushMarkup(msg); - } -} diff --git a/Content.Shared/Nutrition/EntitySystems/ExaminableSatiationSystem.cs b/Content.Shared/Nutrition/EntitySystems/ExaminableSatiationSystem.cs new file mode 100644 index 0000000000..8936b7d63e --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/ExaminableSatiationSystem.cs @@ -0,0 +1,34 @@ +using Content.Shared.Examine; +using Content.Shared.IdentityManagement; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.Prototypes; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Nutrition.EntitySystems; + +/// +/// Makes descriptions specified in show up in response to +/// s. +/// +/// +public sealed partial class ExaminableSatiationSystem : BaseSatiationEffectSystem +{ + protected override Dictionary, SatiationThresholds> GetThresholds( + ExaminableSatiationComponent comp + ) => comp.Satiations; + + protected override LocId? DefaultValue() => null; + + [SubscribeLocalEvent] + private void OnExamine(Entity entity, ref ExaminedEvent args) + { + var identity = Identity.Entity(entity, EntityManager); + foreach (var (_, thresholds) in entity.Comp.Satiations) + { + if (thresholds.Current is not { } loc) + continue; + + args.PushMarkup(Loc.GetString(loc, ("entity", identity))); + } + } +} diff --git a/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs b/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs deleted file mode 100644 index 071fa626e4..0000000000 --- a/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Content.Shared.Alert; -using Content.Shared.Damage.Systems; -using Content.Shared.Mobs.Systems; -using Content.Shared.Movement.Systems; -using Content.Shared.Nutrition.Components; -using Content.Shared.Rejuvenate; -using Content.Shared.StatusIcon; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using Robust.Shared.Timing; - -namespace Content.Shared.Nutrition.EntitySystems; - -public sealed partial class HungerSystem : EntitySystem -{ - [Dependency] private IGameTiming _timing = default!; - [Dependency] private IRobustRandom _random = default!; - [Dependency] private AlertsSystem _alerts = default!; - [Dependency] private DamageableSystem _damageable = default!; - [Dependency] private MobStateSystem _mobState = default!; - [Dependency] private MovementSpeedModifierSystem _movementSpeedModifier = default!; - [Dependency] private SharedJetpackSystem _jetpack = default!; - - private static readonly ProtoId HungerIconOverfedId = "HungerIconOverfed"; - private static readonly ProtoId HungerIconPeckishId = "HungerIconPeckish"; - private static readonly ProtoId HungerIconStarvingId = "HungerIconStarving"; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnShutdown); - SubscribeLocalEvent(OnRefreshMovespeed); - SubscribeLocalEvent(OnRejuvenate); - } - - private void OnMapInit(EntityUid uid, HungerComponent component, MapInitEvent args) - { - var amount = _random.Next( - (int) component.Thresholds[HungerThreshold.Peckish] + 10, - (int) component.Thresholds[HungerThreshold.Okay]); - SetHunger(uid, amount, component); - } - - private void OnShutdown(EntityUid uid, HungerComponent component, ComponentShutdown args) - { - _alerts.ClearAlertCategory(uid, component.HungerAlertCategory); - } - - private void OnRefreshMovespeed(EntityUid uid, HungerComponent component, RefreshMovementSpeedModifiersEvent args) - { - if (component.CurrentThreshold > HungerThreshold.Starving) - return; - - if (_jetpack.IsUserFlying(uid)) - return; - - args.ModifySpeed(component.StarvingSlowdownModifier, component.StarvingSlowdownModifier); - } - - private void OnRejuvenate(EntityUid uid, HungerComponent component, RejuvenateEvent args) - { - SetHunger(uid, component.Thresholds[HungerThreshold.Okay], component); - } - - /// - /// Gets the current hunger value of the given . - /// - public float GetHunger(HungerComponent component) - { - var dt = _timing.CurTime - component.LastAuthoritativeHungerChangeTime; - var value = component.LastAuthoritativeHungerValue - (float)dt.TotalSeconds * component.ActualDecayRate; - return ClampHungerWithinThresholds(component, value); - } - - /// - /// Adds to the current hunger of an entity by the specified value - /// - /// - /// - /// - public void ModifyHunger(EntityUid uid, float amount, HungerComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - SetHunger(uid, GetHunger(component) + amount, component); - } - - /// - /// Sets the current hunger of an entity to the specified value - /// - /// - /// - /// - public void SetHunger(EntityUid uid, float amount, HungerComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - SetAuthoritativeHungerValue((uid, component), amount); - UpdateCurrentThreshold(uid, component); - } - - /// - /// Sets and - /// , and dirties this entity. This "resets" the - /// starting point for 's calculation. - /// - /// The entity whose hunger will be set. - /// The value to set the entity's hunger to. - private void SetAuthoritativeHungerValue(Entity entity, float value) - { - entity.Comp.LastAuthoritativeHungerChangeTime = _timing.CurTime; - entity.Comp.LastAuthoritativeHungerValue = ClampHungerWithinThresholds(entity.Comp, value); - DirtyField(entity.Owner, entity.Comp, nameof(HungerComponent.LastAuthoritativeHungerChangeTime)); - DirtyField(entity.Owner, entity.Comp, nameof(HungerComponent.LastAuthoritativeHungerValue)); - } - - private void UpdateCurrentThreshold(EntityUid uid, HungerComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - var calculatedHungerThreshold = GetHungerThreshold(component); - if (calculatedHungerThreshold == component.CurrentThreshold) - return; - - component.CurrentThreshold = calculatedHungerThreshold; - DirtyField(uid, component, nameof(HungerComponent.CurrentThreshold)); - DoHungerThresholdEffects(uid, component); - } - - private void DoHungerThresholdEffects(EntityUid uid, HungerComponent? component = null, bool force = false) - { - if (!Resolve(uid, ref component)) - return; - - if (component.CurrentThreshold == component.LastThreshold && !force) - return; - - if (GetMovementThreshold(component.CurrentThreshold) != GetMovementThreshold(component.LastThreshold)) - { - _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); - } - - if (component.HungerThresholdAlerts.TryGetValue(component.CurrentThreshold, out var alertId)) - { - _alerts.ShowAlert(uid, alertId); - } - else - { - _alerts.ClearAlertCategory(uid, component.HungerAlertCategory); - } - - if (component.HungerThresholdDecayModifiers.TryGetValue(component.CurrentThreshold, out var modifier)) - { - component.ActualDecayRate = component.BaseDecayRate * modifier; - DirtyField(uid, component, nameof(HungerComponent.ActualDecayRate)); - SetAuthoritativeHungerValue((uid, component), GetHunger(component)); - } - - component.LastThreshold = component.CurrentThreshold; - DirtyField(uid, component, nameof(HungerComponent.LastThreshold)); - } - - private void DoContinuousHungerEffects(EntityUid uid, HungerComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - if (component.CurrentThreshold <= HungerThreshold.Starving && - component.StarvationDamage is { } damage && - !_mobState.IsDead(uid)) - { - _damageable.TryChangeDamage(uid, damage, true, false); - } - } - - /// - /// Gets the hunger threshold for an entity based on the amount of food specified. - /// If a specific amount isn't specified, just uses the current hunger of the entity - /// - /// - /// - /// - public HungerThreshold GetHungerThreshold(HungerComponent component, float? food = null) - { - food ??= GetHunger(component); - var result = HungerThreshold.Dead; - var value = component.Thresholds[HungerThreshold.Overfed]; - foreach (var threshold in component.Thresholds) - { - if (threshold.Value <= value && threshold.Value >= food) - { - result = threshold.Key; - value = threshold.Value; - } - } - - return result; - } - - /// - /// A check that returns if the entity is below a hunger threshold. - /// - public bool IsHungerBelowState(EntityUid uid, HungerThreshold threshold, float? food = null, HungerComponent? comp = null) - { - if (!Resolve(uid, ref comp)) - return false; // It's never going to go hungry, so it's probably fine to assume that it's not... you know, hungry. - - return GetHungerThreshold(comp, food) < threshold; - } - - private bool GetMovementThreshold(HungerThreshold threshold) - { - switch (threshold) - { - case HungerThreshold.Overfed: - case HungerThreshold.Okay: - return true; - case HungerThreshold.Peckish: - case HungerThreshold.Starving: - case HungerThreshold.Dead: - return false; - default: - throw new ArgumentOutOfRangeException(nameof(threshold), threshold, null); - } - } - - public bool TryGetStatusIconPrototype(HungerComponent component, [NotNullWhen(true)] out SatiationIconPrototype? prototype) - { - switch (component.CurrentThreshold) - { - case HungerThreshold.Overfed: - ProtoMan.Resolve(HungerIconOverfedId, out prototype); - break; - case HungerThreshold.Peckish: - ProtoMan.Resolve(HungerIconPeckishId, out prototype); - break; - case HungerThreshold.Starving: - ProtoMan.Resolve(HungerIconStarvingId, out prototype); - break; - default: - prototype = null; - break; - } - - return prototype != null; - } - - private static float ClampHungerWithinThresholds(HungerComponent component, float hungerValue) - { - return Math.Clamp(hungerValue, - component.Thresholds[HungerThreshold.Dead], - component.Thresholds[HungerThreshold.Overfed]); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var hunger)) - { - if (_timing.CurTime < hunger.NextThresholdUpdateTime) - continue; - hunger.NextThresholdUpdateTime = _timing.CurTime + hunger.ThresholdUpdateRate; - - UpdateCurrentThreshold(uid, hunger); - DoContinuousHungerEffects(uid, hunger); - } - } -} diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs index 0287109966..7740d9b1c5 100644 --- a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs +++ b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs @@ -235,9 +235,9 @@ public sealed partial class IngestionSystem foreach (var effect in entry.Effects) { // ignores any effect conditions, just cares about how much it can hydrate - if (effect is SatiateHunger hunger) + if (effect is Satiate satiate && satiate.SatiationType == SatiationSystem.Hunger) { - total += hunger.Factor * quantity.Quantity.Float(); + total += satiate.Factor * quantity.Quantity.Float(); } } } @@ -286,9 +286,9 @@ public sealed partial class IngestionSystem foreach (var effect in entry.Effects) { // ignores any effect conditions, just cares about how much it can hydrate - if (effect is SatiateThirst thirst) + if (effect is Satiate satiate && satiate.SatiationType == SatiationSystem.Thirst) { - total += thirst.Factor * quantity.Quantity.Float(); + total += satiate.Factor * quantity.Quantity.Float(); } } } diff --git a/Content.Shared/Nutrition/EntitySystems/Satiation.cs b/Content.Shared/Nutrition/EntitySystems/Satiation.cs new file mode 100644 index 0000000000..eda085217e --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/Satiation.cs @@ -0,0 +1,147 @@ +using Content.Shared.Nutrition.Prototypes; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Mapping; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Content.Shared.Nutrition.EntitySystems; + +/// +/// A need whose value decays over time. Examples include Thirst and Hunger. +/// +[DataDefinition, Serializable, NetSerializable, Access(typeof(SatiationSystem))] +public sealed partial class Satiation +{ + /// + /// This satiation's . + /// + [ViewVariables(VVAccess.ReadOnly)] + public ProtoId SatiationType; + + /// + /// This satiation's , which describes how it changes over time. + /// + [DataField(required: true), ViewVariables(VVAccess.ReadOnly)] + public ProtoId Prototype; + + + /// + /// The value of this satiation as of . + /// + /// + /// To get the current value at any arbitrary time, use + /// . + [DataField, ViewVariables(VVAccess.ReadOnly)] + public float LastAuthoritativeValue = float.MinValue; + + /// + /// The last time was modified. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadOnly)] + public TimeSpan LastAuthoritativeChangeTime; + + /// + /// The rate at which this satiation value is expected to decay. It is a combination of + /// and modifiers. + /// + [DataField, ViewVariables(VVAccess.ReadOnly)] + public float ActualDecayRate; + + /// + /// When is expected to change, if nothing but normal decay affects this satiation. + /// This is used to predict satiation updates on clients. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan? NextDecayRateModUpdateTime; + + /// + /// , but for satiation alerts. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan? NextAlertUpdateTime; +} + +/// +/// A combination of configuration () and state () which describes how a +/// value changes related to , as well as its current value. +/// +/// +/// uses this to describe and track damage descriptors based on satiations, applying +/// the damage regularly over time. describes the damage to apply at various levels of +/// satiation, and tracks the current damage to apply every update, meaning it doesn't need to be +/// looked up from on every tick. +/// +/// This should probably only ever be used in conjunction with +[DataDefinition, Serializable] +public sealed partial class SatiationThresholds +{ + /// + /// The values keyed by the satiation values at or below which the T value becomes "active". + /// + /// + [IncludeDataField] + public Dictionary Thresholds = []; + + /// + /// When this satiation is expected to decay from its current threshold to the next lower threshold. This + /// is null when there is no lower threshold to decay to. + /// + public TimeSpan? ProjectedThresholdChangeTime; + + /// + /// The current value, at least when maintained by something like + /// + /// + public T Current; +} + +[TypeSerializer] +public sealed partial class SatiationThresholdsSerializer : ITypeSerializer, MappingDataNode>, + ITypeCopier> +{ + public ValidationNode Validate( + ISerializationManager serializationManager, + MappingDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null + ) => serializationManager.ValidateNode>(node, context); + + public SatiationThresholds Read( + ISerializationManager serializationManager, + MappingDataNode node, + IDependencyCollection dependencies, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + ISerializationManager.InstantiationDelegate>? instanceProvider = null + ) => new() + { + Thresholds = serializationManager.Read>( + node, + context, + hookCtx.SkipHooks, + instanceProvider is { } ip ? () => ip().Thresholds : null, + true + ), + }; + + public DataNode Write( + ISerializationManager serializationManager, + SatiationThresholds value, + IDependencyCollection dependencies, + bool alwaysWrite = false, + ISerializationContext? context = null + ) => serializationManager.WriteValue(value.Thresholds, alwaysWrite, context, true); + + public void CopyTo( + ISerializationManager serializationManager, + SatiationThresholds source, + ref SatiationThresholds target, + IDependencyCollection dependencies, + SerializationHookContext hookCtx, + ISerializationContext? context = null + ) => serializationManager.CopyTo(source.Thresholds, ref target.Thresholds, context, true, true); +} diff --git a/Content.Shared/Nutrition/EntitySystems/SatiationDamageSystem.cs b/Content.Shared/Nutrition/EntitySystems/SatiationDamageSystem.cs new file mode 100644 index 0000000000..6c35423987 --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/SatiationDamageSystem.cs @@ -0,0 +1,49 @@ +using Content.Shared.Damage; +using Content.Shared.Damage.Systems; +using Content.Shared.Mobs.Systems; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.Prototypes; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.Nutrition.EntitySystems; + +/// +/// This system implements the behavior of +/// +public sealed partial class SatiationDamageSystem : + BaseSatiationEffectSystem +{ + [Dependency] private DamageableSystem _damageable = default!; + [Dependency] private MobStateSystem _mobState = default!; + [Dependency] private IGameTiming _timing = default!; + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var satiationDamageQuery = EntityQueryEnumerator(); + while (satiationDamageQuery.MoveNext(out var ent, out var comp)) + { + if (_timing.CurTime < comp.NextDamageTime || + _mobState.IsDead(ent)) + continue; + + comp.NextDamageTime = _timing.CurTime + comp.Frequency; + DirtyField(ent, comp, nameof(SatiationDamageComponent.NextDamageTime)); + + foreach (var (_, thresholds) in comp.Satiations) + { + if (thresholds.Current is not { } damage) + continue; + + _damageable.TryChangeDamage(ent, damage, interruptsDoAfters: false); + } + } + } + + protected override DamageSpecifier? DefaultValue() => null; + + protected override Dictionary, SatiationThresholds> GetThresholds( + SatiationDamageComponent comp) => comp.Satiations; +} diff --git a/Content.Shared/Nutrition/EntitySystems/SatiationSpeedModifierSystem.cs b/Content.Shared/Nutrition/EntitySystems/SatiationSpeedModifierSystem.cs new file mode 100644 index 0000000000..21f3f3d227 --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/SatiationSpeedModifierSystem.cs @@ -0,0 +1,34 @@ +using Content.Shared.Movement.Systems; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.Prototypes; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Nutrition.EntitySystems; + +public sealed partial class SatiationSpeedModifierSystem : + BaseSatiationEffectSystem +{ + [Dependency] private MovementSpeedModifierSystem _movementSpeedModifier = default!; + + protected override Dictionary, SatiationThresholds> GetThresholds( + SatiationSpeedModifierComponent comp) => comp.Satiations; + + protected override float DefaultValue() => 1f; + + protected override void AfterSatiationUpdate(Entity entity) + { + _movementSpeedModifier.RefreshMovementSpeedModifiers(entity.Owner); + } + + [SubscribeLocalEvent] + private static void OnRefreshMovementSpeed( + Entity entity, + ref RefreshMovementSpeedModifiersEvent args + ) + { + foreach (var (_, thresholds) in entity.Comp.Satiations) + { + args.ModifySpeed(thresholds.Current); + } + } +} diff --git a/Content.Shared/Nutrition/EntitySystems/SatiationSystem.Proxy.cs b/Content.Shared/Nutrition/EntitySystems/SatiationSystem.Proxy.cs new file mode 100644 index 0000000000..08b2f8e011 --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/SatiationSystem.Proxy.cs @@ -0,0 +1,260 @@ +using System.Linq; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.Prototypes; +using Content.Shared.StatusIcon; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Shared.Nutrition.EntitySystems; + +// This part provides functions for use in other systems. +public sealed partial class SatiationSystem +{ + /// + /// Gets 's current value of the satiation of . If this entity does + /// not have that satiation, returns null. + /// + public float? GetValueOrNull( + Entity entity, + [ForbidLiteral] ProtoId type + ) + { + if (GetAndResolveSatiationOfType(entity, type) is not var (satiation, proto)) + return null; + + return CalculateCurrentValue(satiation, proto); + } + + /// + /// Sets 's current satiation of to a value corresponding to + /// . If this entity does not have that satiation, or the key does not correspond to + /// any threshold, has no effect. + /// + public void SetValue( + Entity entity, + [ForbidLiteral] ProtoId type, + [ForbidLiteral] SatiationValue satiationValue + ) + { + if (GetAndResolveSatiationOfType(entity, type) is not var (satiation, proto) || + proto.GetValueOrNull(satiationValue) is not { } value) + return; + + SetAuthoritativeValue(entity, satiation, proto, value); + } + + /// + /// Sets 's current satiation of to . If + /// this entity does not have that satiation, has no effect. + /// + // [OverloadResolutionPriority(1)] // If you pass in an int, avoid instantiating a record to hold it. // Requires a newer language version :agony: + public void SetValue( + Entity entity, + [ForbidLiteral] ProtoId type, + float value + ) + { + if (GetAndResolveSatiationOfType(entity, type) is not var (satiation, proto)) + return; + + SetAuthoritativeValue(entity, satiation, proto, value); + } + + /// + /// Sets 's current satiation of to its current value plus + /// . If this entity does not have that satiation, has no effect. + /// + public void ModifyValue( + Entity entity, + [ForbidLiteral] ProtoId type, + float amount + ) + { + if (GetValueOrNull(entity, type) is { } currentValue) + { + SetValue(entity, type, currentValue + amount); + } + } + + /// + /// Returns whether or not the current value (plus optional ) is between + /// the values described by and . If + /// does not have a of the specified , returns false. If either + /// above- or below-key is null, any value is considered in-range compared to that key. + /// If either key is specified but not present in 's , + /// all values are considered out-of-range. + /// + public bool IsValueInRange( + Entity entity, + [ForbidLiteral] ProtoId type, + [ForbidLiteral] SatiationValue? above = null, + [ForbidLiteral] SatiationValue? below = null, + float hypotheticalValueDelta = 0 + ) + { + if (above is null && below is null) + { + DebugTools.Assert("Range is unbounded. Is this a programming error?"); + return true; + } + + if (GetAndResolveSatiationOfType(entity, type) is not var (satiation, proto)) + return false; + + // Resolve the bounds to integers we can actually compare against. + int? valueAbove = null; + if (above is { } a && (valueAbove = proto.GetValueOrNull(a)) is null) + return false; // `above` is not null, but we failed to resolve its value. + + int? valueAtOrBelow = null; + if (below is { } b && (valueAtOrBelow = proto.GetValueOrNull(b)) is null) + return false; // `atOrBelow` is not null, but we failed to resolve its value. + + if (valueAbove > valueAtOrBelow) + { + DebugTools.Assert("Range is empty. Is this a programming error?"); + return false; + } + + var currentValue = CalculateCurrentValue(satiation, proto); + if (hypotheticalValueDelta is not 0) + currentValue = proto.ClampSatiationWithinThresholds(currentValue + hypotheticalValueDelta); + + var isAboveBottom = valueAbove is null || currentValue > valueAbove; + var isAtOrBelowTop = valueAtOrBelow is null || currentValue < valueAtOrBelow; + ; + + return isAboveBottom && isAtOrBelowTop; + } + + /// + /// This function returns a value from based on the current value of + /// 's satiation of the given . The value in + /// with the lowest key greater than the current satiation value is returned. + /// + /// The entity whose satiation is considered + /// The type of satiation to consider + /// The values, keyed by thresholds + /// The value selected from + /// + /// The keying-threshold-value of the next threshold below the selected value. Most consumers will not have a use + /// for this value, but it is used by 's prediction of value decay. + /// + /// The type of values in + /// + /// True if a value was selected. False if is empty, if the current value of + /// the specified satiation is higher than all thresholds, or the given entity does not have a satiation of the + /// given type. + /// + public bool TryGetValueByThreshold( + Entity entity, + [ForbidLiteral] ProtoId type, + Dictionary valuesByThreshold, + out T? result, + out int? nextLowerThreshold + ) + { + result = default; + nextLowerThreshold = null; + if (GetValueOrNull(entity, type) is not { } currentValue || + GetAndResolveSatiationOfType(entity, type) is not var (_, proto)) + return false; + + using var valuesByDescendingThreshold = valuesByThreshold + // Resolve keys to threshold integers, discarding any keys which cannot be resolved. + .Select(it => proto.GetValueOrNull(it.Key) is { } value ? ((int, T)?)(value, it.Value) : null) + .OfType<(int, T)>() + // Order by descending threshold. + .OrderByDescending(it => it.Item1) + .GetEnumerator(); + if (!valuesByDescendingThreshold.MoveNext()) + { + // `values` is empty, so there are no values to return. + result = default; + nextLowerThreshold = null; + return false; + } + + if (currentValue > valuesByDescendingThreshold.Current.Item1) + { + // `currentSatiation` is higher than all thresholds, so we don't have a value, but we can return a next + // lower threshold. + result = default; + nextLowerThreshold = valuesByDescendingThreshold.Current.Item1; + return false; + } + + var nextHigher = valuesByDescendingThreshold.Current; + while (valuesByDescendingThreshold.MoveNext()) + { + var nextLower = valuesByDescendingThreshold.Current; + if (currentValue > nextLower.Item1) + { + // The current value is below `nextHigher` and above `nextLower`, so `nextHigher` is the correct threshold. + result = nextHigher.Item2; + nextLowerThreshold = nextLower.Item1; + return true; + } + + // Loop, setting `nextLower` to the next iteration's `nextHigher` + nextHigher = nextLower; + } + + // We've run out of thresholds below. + result = nextHigher.Item2; + nextLowerThreshold = null; + return true; + } + + /// + /// Looks up the appropriate for the given entity's of the + /// specified . If the entity does not have the specified satiation type, or if the satiation + /// does not have an icon for its current state, returns null. + /// + /// This should almost definitely never be used by anything other than the client's Overlay system + public StatusIconPrototype? GetStatusIconOrNull( + Entity entity, + [ForbidLiteral] ProtoId type + ) + { + if (entity.Comp.GetOrNull(type) is not { } satiation || + !ProtoMan.Resolve(satiation.Prototype, out var proto)) + return null; + + TryGetValueByThreshold(entity, type, proto.Icons, out var iconProtoId, out _); + return ProtoMan.Resolve(iconProtoId, out var icon) ? icon : null; + } + + #region Commands + + /// + /// Returns the all of the key strings of the given + /// for , or empty if no such type exists. + /// + /// + /// It is expected that is validated with before calling this. If it fails to resolve, an + /// error will be logged. + /// + public IEnumerable GetKeysForType( + Entity entity, + [ForbidLiteral] ProtoId type + ) + { + return GetAndResolveSatiationOfType(entity, type)?.Proto.Thresholds.Keys ?? Enumerable.Empty(); + } + + /// + /// Returns the of the given for + /// , or null if no such type exists. + /// + /// + /// It is expected that is validated with before calling this. If it fails to resolve, an + /// error will be logged. + /// + public int? GetMaximumValue( + Entity entity, + [ForbidLiteral] ProtoId type + ) => GetAndResolveSatiationOfType(entity, type)?.Proto.MaximumValue; + + #endregion +} diff --git a/Content.Shared/Nutrition/EntitySystems/SatiationSystem.cs b/Content.Shared/Nutrition/EntitySystems/SatiationSystem.cs new file mode 100644 index 0000000000..d4883868e7 --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/SatiationSystem.cs @@ -0,0 +1,253 @@ +using Content.Shared.Alert; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.Prototypes; +using Content.Shared.Random.Helpers; +using Content.Shared.Rejuvenate; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.Nutrition.EntitySystems; + +/// +/// This system manages the . Broadly, what that means is that it handles the decay of +/// satiations in , and external changes to satiations through accessors like +/// . +/// +public sealed partial class SatiationSystem : EntitySystem +{ + [Dependency] private AlertsSystem _alerts = default!; + [Dependency] private IGameTiming _timing = default!; + + [Dependency] private EntityQuery _satiationQuery; + + /// + /// The ID of the Hunger satiation type. Provided because it is so commonly used in Content. + /// + public static readonly ProtoId Hunger = "Hunger"; + + /// + /// The ID of the Thirst satiation type. Provided because it is so commonly used in Content. + /// + public static readonly ProtoId Thirst = "Thirst"; + + /// + public override void Update(float frameTime) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var component)) + { + Entity entity = (uid, component); + foreach (var (satiation, proto) in GetSatiationsAndTypes(entity)) + { + if (_timing.CurTime >= satiation.NextAlertUpdateTime) + { + UpdateAlerts(entity, satiation, proto); + } + + if (_timing.CurTime >= satiation.NextDecayRateModUpdateTime) + { + SetAuthoritativeValue(entity, satiation, proto, CalculateCurrentValue(satiation, proto)); + } + } + } + } + + /// + /// Sets starting satiation values. + /// + [SubscribeLocalEvent] + private void OnMapInit(Entity entity, ref MapInitEvent args) + { + foreach (var (type, satiation) in entity.Comp.Satiations) + { + if (!ProtoMan.Resolve(satiation.Prototype, out var proto)) + continue; + + satiation.SatiationType = type; + + // TODO: Replace with RandomPredicted once the engine PR is merged + var rand = SharedRandomExtensions.PredictedRandom(_timing, GetNetEntity(entity)); + var value = rand.NextFloat(proto.StartingValueMinimum, proto.StartingValueMaximum); + + SetAuthoritativeValue(entity, satiation, proto, value); + } + + DirtyField(entity.AsNullable(), nameof(SatiationComponent.Satiations)); + } + + /// + /// Clears alerts. + /// + [SubscribeLocalEvent] + private void OnShutdown(Entity entity, ref ComponentShutdown args) + { + foreach (var (_, proto) in GetSatiationsAndTypes(entity)) + { + _alerts.ClearAlertCategory(entity.Owner, proto.AlertCategory); + } + } + + /// + /// Sets all satiations to their maximums. + /// + [SubscribeLocalEvent] + private void OnRejuvenate(Entity entity, ref RejuvenateEvent args) + { + foreach (var type in entity.Comp.Satiations.Keys) + { + SetValue(entity, type, satiationValue: int.MaxValue); + } + } + + /// + /// This helper resolves and returns the corresponding from + /// along with its . + /// Returns null if the prototype fails to resolve, or if the component does not have the specified satiation. + /// + private (Satiation Satiation, SatiationPrototype Proto)? GetAndResolveSatiationOfType( + SatiationComponent satiations, + [ForbidLiteral] ProtoId type + ) + { + if (satiations.GetOrNull(type) is not { } satiation || + !ProtoMan.Resolve(satiation.Prototype, out var proto)) + return null; + + return (satiation, proto); + } + + /// + /// Similar to , this helper returns all s on + /// along with their corresponding s. + /// + private IEnumerable<(Satiation, SatiationPrototype)> GetSatiationsAndTypes(SatiationComponent satiations) + { + foreach (var satiation in satiations.Satiations.Values) + { + if (!ProtoMan.Resolve(satiation.Prototype, out var proto)) + continue; + + yield return (satiation, proto); + } + } + + /// + /// Calculates the current value of the given by linearly extrapolating the change of the + /// value based on , + /// and + /// + private float CalculateCurrentValue(Satiation satiation, SatiationPrototype proto) + { + var dt = _timing.CurTime - satiation.LastAuthoritativeChangeTime; + var value = satiation.LastAuthoritativeValue - (float)dt.TotalSeconds * satiation.ActualDecayRate; + return proto.ClampSatiationWithinThresholds(value); + } + + /// + /// The beating heart of this system, this function sets the given 's + /// to . This involves + /// updating obvious fields on the , but since changes to the value also affect the + /// current threshold, we need to consider all of the effects that has as well. + /// + private void SetAuthoritativeValue( + Entity entity, + Satiation satiation, + SatiationPrototype proto, + float value + ) + { + // Update the authoritative value itself. + satiation.LastAuthoritativeChangeTime = _timing.CurTime; + satiation.LastAuthoritativeValue = proto.ClampSatiationWithinThresholds(value); + + if (!TryGetValueByThreshold( + entity, + satiation.SatiationType, + proto.DecayModifiers, + out var currentDecayMod, + out var nextLowerThreshold + )) + { + currentDecayMod = 1f; + } + + satiation.ActualDecayRate = proto.BaseDecayRate * currentDecayMod; + + if (nextLowerThreshold is { } t) + { + satiation.NextDecayRateModUpdateTime = + _timing.CurTime + TimeSpan.FromSeconds((value - t) / satiation.ActualDecayRate); + } + else + { + satiation.NextDecayRateModUpdateTime = null; + } + + var updateEvent = new SatiationUpdateEvent(satiation.SatiationType); + RaiseLocalEvent(entity, ref updateEvent); + + DirtyField(entity.AsNullable(), nameof(SatiationComponent.Satiations)); + } + + /// + /// This is basically a reimplementation of . + /// + [SubscribeLocalEvent] + private void UpdateAlertsOnSatiationUpdated(Entity entity, ref SatiationUpdateEvent args) + { + if (entity.Comp.GetOrNull(args.Type) is not { } satiation || + !ProtoMan.Resolve(satiation.Prototype, out var proto)) + return; + + UpdateAlerts(entity, satiation, proto); + } + + private void UpdateAlerts( + Entity entity, + Satiation satiation, + SatiationPrototype proto + ) + { + if (TryGetValueByThreshold( + entity, + satiation.SatiationType, + proto.Alerts, + out var result, + out var nextLowerThreshold)) + { + if (result is { } alert) + { + _alerts.ShowAlert(entity.Owner, alert); + satiation.NextAlertUpdateTime = nextLowerThreshold is { } lower + ? GetTimeToDecay(entity, satiation.SatiationType, lower) + : null; + } + else + { + _alerts.ClearAlertCategory(entity.Owner, proto.AlertCategory); + satiation.NextAlertUpdateTime = null; + } + } + else + { + _alerts.ClearAlertCategory(entity.Owner, proto.AlertCategory); + satiation.NextAlertUpdateTime = null; + } + } + + public TimeSpan? GetTimeToDecay(Entity entity, + ProtoId type, + int threshold + ) + { + if (GetValueOrNull(entity, type) is not { } value || + entity.Comp.GetOrNull(type) is not { } satiation) + return null; + + return _timing.CurTime + TimeSpan.FromSeconds((value - threshold) / satiation.ActualDecayRate); + } +} + +// Best effort on change to authoritative value or decay rate +[ByRefEvent] +public readonly record struct SatiationUpdateEvent(ProtoId Type); diff --git a/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs b/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs deleted file mode 100644 index fa0375d4ee..0000000000 --- a/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs +++ /dev/null @@ -1,221 +0,0 @@ -using Content.Shared.Alert; -using Content.Shared.Movement.Components; -using Content.Shared.Movement.Systems; -using Content.Shared.Nutrition.Components; -using Content.Shared.Rejuvenate; -using Content.Shared.StatusIcon; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using Robust.Shared.Timing; -using System.Diagnostics.CodeAnalysis; - -namespace Content.Shared.Nutrition.EntitySystems; - -[UsedImplicitly] -public sealed partial class ThirstSystem : EntitySystem -{ - [Dependency] private IGameTiming _timing = default!; - [Dependency] private IRobustRandom _random = default!; - [Dependency] private AlertsSystem _alerts = default!; - [Dependency] private MovementSpeedModifierSystem _movement = default!; - [Dependency] private SharedJetpackSystem _jetpack = default!; - - private static readonly ProtoId ThirstIconOverhydratedId = "ThirstIconOverhydrated"; - private static readonly ProtoId ThirstIconThirstyId = "ThirstIconThirsty"; - private static readonly ProtoId ThirstIconParchedId = "ThirstIconParched"; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnRefreshMovespeed); - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnRejuvenate); - } - - private void OnMapInit(EntityUid uid, ThirstComponent component, MapInitEvent args) - { - // Do not change behavior unless starting value is explicitly defined - if (component.CurrentThirst < 0) - { - component.CurrentThirst = _random.Next( - (int) component.ThirstThresholds[ThirstThreshold.Thirsty] + 10, - (int) component.ThirstThresholds[ThirstThreshold.Okay] - 1); - - DirtyField(uid, component, nameof(ThirstComponent.CurrentThirst)); - } - component.NextUpdateTime = _timing.CurTime; - component.CurrentThirstThreshold = GetThirstThreshold(component, component.CurrentThirst); - component.LastThirstThreshold = ThirstThreshold.Okay; // TODO: Potentially change this -> Used Okay because no effects. - // TODO: Check all thresholds make sense and throw if they don't. - UpdateEffects(uid, component); - - DirtyFields(uid, component, null, nameof(ThirstComponent.NextUpdateTime), nameof(ThirstComponent.CurrentThirstThreshold), nameof(ThirstComponent.LastThirstThreshold)); - - TryComp(uid, out MovementSpeedModifierComponent? moveMod); - _movement.RefreshMovementSpeedModifiers((uid, moveMod)); - } - - private void OnRefreshMovespeed(EntityUid uid, ThirstComponent component, RefreshMovementSpeedModifiersEvent args) - { - // TODO: This should really be taken care of somewhere else - if (_jetpack.IsUserFlying(uid)) - return; - - var mod = component.CurrentThirstThreshold <= ThirstThreshold.Parched ? 0.75f : 1.0f; - args.ModifySpeed(mod, mod); - } - - private void OnRejuvenate(EntityUid uid, ThirstComponent component, RejuvenateEvent args) - { - SetThirst(uid, component, component.ThirstThresholds[ThirstThreshold.Okay]); - } - - private ThirstThreshold GetThirstThreshold(ThirstComponent component, float amount) - { - ThirstThreshold result = ThirstThreshold.Dead; - var value = component.ThirstThresholds[ThirstThreshold.OverHydrated]; - foreach (var threshold in component.ThirstThresholds) - { - if (threshold.Value <= value && threshold.Value >= amount) - { - result = threshold.Key; - value = threshold.Value; - } - } - - return result; - } - - public void ModifyThirst(EntityUid uid, ThirstComponent component, float amount) - { - SetThirst(uid, component, component.CurrentThirst + amount); - } - - public void SetThirst(EntityUid uid, ThirstComponent component, float amount) - { - component.CurrentThirst = Math.Clamp(amount, - component.ThirstThresholds[ThirstThreshold.Dead], - component.ThirstThresholds[ThirstThreshold.OverHydrated] - ); - - DirtyField(uid, component, nameof(ThirstComponent.CurrentThirst)); - } - - private bool IsMovementThreshold(ThirstThreshold threshold) - { - switch (threshold) - { - case ThirstThreshold.Dead: - case ThirstThreshold.Parched: - return true; - case ThirstThreshold.Thirsty: - case ThirstThreshold.Okay: - case ThirstThreshold.OverHydrated: - return false; - default: - throw new ArgumentOutOfRangeException(nameof(threshold), threshold, null); - } - } - - public bool TryGetStatusIconPrototype(ThirstComponent component, [NotNullWhen(true)] out SatiationIconPrototype? prototype) - { - switch (component.CurrentThirstThreshold) - { - case ThirstThreshold.OverHydrated: - ProtoMan.Resolve(ThirstIconOverhydratedId, out prototype); - break; - - case ThirstThreshold.Thirsty: - ProtoMan.Resolve(ThirstIconThirstyId, out prototype); - break; - - case ThirstThreshold.Parched: - ProtoMan.Resolve(ThirstIconParchedId, out prototype); - break; - - default: - prototype = null; - break; - } - - return prototype != null; - } - - private void UpdateEffects(EntityUid uid, ThirstComponent component) - { - if (IsMovementThreshold(component.LastThirstThreshold) != IsMovementThreshold(component.CurrentThirstThreshold) && - TryComp(uid, out MovementSpeedModifierComponent? movementSlowdownComponent)) - { - _movement.RefreshMovementSpeedModifiers((uid, movementSlowdownComponent)); - } - - // Update UI - if (ThirstComponent.ThirstThresholdAlertTypes.TryGetValue(component.CurrentThirstThreshold, out var alertId)) - { - _alerts.ShowAlert(uid, alertId); - } - else - { - _alerts.ClearAlertCategory(uid, component.ThirstyCategory); - } - - DirtyField(uid, component, nameof(ThirstComponent.LastThirstThreshold)); - DirtyField(uid, component, nameof(ThirstComponent.ActualDecayRate)); - - switch (component.CurrentThirstThreshold) - { - case ThirstThreshold.OverHydrated: - component.LastThirstThreshold = component.CurrentThirstThreshold; - component.ActualDecayRate = component.BaseDecayRate * 1.2f; - return; - - case ThirstThreshold.Okay: - component.LastThirstThreshold = component.CurrentThirstThreshold; - component.ActualDecayRate = component.BaseDecayRate; - return; - - case ThirstThreshold.Thirsty: - // Same as okay except with UI icon saying drink soon. - component.LastThirstThreshold = component.CurrentThirstThreshold; - component.ActualDecayRate = component.BaseDecayRate * 0.8f; - return; - case ThirstThreshold.Parched: - _movement.RefreshMovementSpeedModifiers(uid); - component.LastThirstThreshold = component.CurrentThirstThreshold; - component.ActualDecayRate = component.BaseDecayRate * 0.6f; - return; - - case ThirstThreshold.Dead: - return; - - default: - Log.Error($"No thirst threshold found for {component.CurrentThirstThreshold}"); - throw new ArgumentOutOfRangeException($"No thirst threshold found for {component.CurrentThirstThreshold}"); - } - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var thirst)) - { - if (_timing.CurTime < thirst.NextUpdateTime) - continue; - - thirst.NextUpdateTime += thirst.UpdateRate; - - ModifyThirst(uid, thirst, -thirst.ActualDecayRate); - var calculatedThirstThreshold = GetThirstThreshold(thirst, thirst.CurrentThirst); - - if (calculatedThirstThreshold == thirst.CurrentThirstThreshold) - continue; - - thirst.CurrentThirstThreshold = calculatedThirstThreshold; - UpdateEffects(uid, thirst); - } - } -} diff --git a/Content.Shared/Nutrition/Prototypes/SatiationPrototype.cs b/Content.Shared/Nutrition/Prototypes/SatiationPrototype.cs new file mode 100644 index 0000000000..2a7dc4ea55 --- /dev/null +++ b/Content.Shared/Nutrition/Prototypes/SatiationPrototype.cs @@ -0,0 +1,148 @@ +using System.Runtime.CompilerServices; +using Content.Shared.Alert; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.StatusIcon; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; + +namespace Content.Shared.Nutrition.Prototypes; + +/// +/// A specific variety of satiation. For example, an animal which is always hungry would use one prototype while a +/// Diona which rarely gets hungry would use a different one. +/// +[Prototype] +public sealed partial class SatiationPrototype : IPrototype, IInheritingPrototype +{ + /// + [IdDataField] + public string ID { get; private set; } = default!; + + /// + [ParentDataField(typeof(AbstractPrototypeIdArraySerializer))] + public string[]? Parents { get; private set; } + + /// + [AbstractDataField] + public bool Abstract { get; private set; } + + /// + /// The base rate at which this satiation decreases per second. + /// + [DataField(required: true)] + public float BaseDecayRate; + + /// + /// The highest value this satiation can have. Increases beyond this value are clamped to this value. + /// + [DataField(required: true)] + public int MaximumValue; + + /// + /// The definition of key strings referentiable by s. Any reference to a key in this map + /// by a will be resolved to the numeric value associated to that key here before use. + ///
+ /// Note hat different satiations can use the same keys without issue. Indeed, the intention is that a "base" + /// satiation can define values and modifiers on itself using these keys, and then inheriting satiation prototypes + /// can simply change the numeric values associated with those keys without needing to redefine the values and + /// modifiers. + ///
+ [DataField(customTypeSerializer: typeof(DictionarySerializer))] + public Dictionary Thresholds = []; + + /// + /// The lowest possible value this satiation can be initialized to. + /// + [DataField("startingValueMinimum", required: true)] + private SatiationValue _startingValueMinimum = default!; + + /// + public int StartingValueMinimum => GetValueOrNull(_startingValueMinimum) ?? 0; + + /// + /// The highest possible value this satiation can be initialized to. + /// + [DataField("startingValueMaximum", required: true)] + private SatiationValue _startingValueMaximum = default!; + + /// + public int StartingValueMaximum => GetValueOrNull(_startingValueMaximum) ?? MaximumValue; + + /// + /// Modifiers to based on the current threshold. + /// + /// + [DataField(customTypeSerializer: typeof(DictionarySerializer))] + public Dictionary DecayModifiers = []; + + #region Alerts + + /// + /// The which all should belong to. + /// + [DataField(required: true)] + public ProtoId AlertCategory; + + /// + /// Alerts to show when in the corresponding threshold. + /// + [DataField(customTypeSerializer: typeof(DictionarySerializer?>))] + public Dictionary?> Alerts = []; + + /// + /// Icons to show to accompany when in the corresponding threshold. + /// + [DataField(customTypeSerializer: typeof(DictionarySerializer?>))] + public Dictionary?> Icons = []; + + #endregion + + + /// + /// Clamps between this prototype's highest and lowest values. + /// + public float ClampSatiationWithinThresholds(float value) => Math.Clamp(value, 0, MaximumValue); + + /// + /// Attempts to get an integer value from the given . If + /// is an immediate value, use its contained integer value. If it is a key, + /// attempts to look up the integer value of that key in ; in the case that a key not + /// present in this proto type is requested, returns null. + /// + public int? GetValueOrNull(SatiationValue satiationValue) + { + if (satiationValue.Key is { } key) + return Thresholds.TryGetValue(key, out var v) ? v : null; + + return satiationValue.Value; + } +} + +/// +/// This type is a union of int and string for use with satiations. When it contains an immediate integer +/// value, that value is used. When it contains a string key, that key is looked up in a +/// to resolve its integer value before use. +/// +/// +/// Values of this type should not be created directly. Instead, rely on the implicit conversion operators. Similarly, +/// the fields in this type should not have their values modified. +/// +/// +// TODO Replace with an actual union type once that's available in C#15: https://devblogs.microsoft.com/dotnet/csharp-15-union-types/ +[DataRecord, Serializable, NetSerializable] +public partial record struct SatiationValue() +{ + [DataField, Access] + public int Value = -1; + + [DataField, Access] + public string? Key = null; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator SatiationValue(int value) => new() { Value = value }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator SatiationValue(string key) => new() { Key = key }; +} diff --git a/Content.Shared/Nutrition/Prototypes/SatiationTypePrototype.cs b/Content.Shared/Nutrition/Prototypes/SatiationTypePrototype.cs new file mode 100644 index 0000000000..f85dbf6223 --- /dev/null +++ b/Content.Shared/Nutrition/Prototypes/SatiationTypePrototype.cs @@ -0,0 +1,23 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Nutrition.Prototypes; + +/// +/// Types of satiation, eg. Hunger, Thirst. +/// +[Prototype] +public sealed partial class SatiationTypePrototype : IPrototype +{ + /// + [IdDataField] + public string ID { get; private set; } = default!; + + /// + /// The localization of the name of this type of satiation. + /// + [DataField("name")] + public LocId NameLoc; + + [ViewVariables(VVAccess.ReadOnly)] + public string Name => Loc.GetString(NameLoc); +} diff --git a/Content.Shared/Nutrition/Prototypes/SatiationValueSerializer.cs b/Content.Shared/Nutrition/Prototypes/SatiationValueSerializer.cs new file mode 100644 index 0000000000..5d4bd7e68d --- /dev/null +++ b/Content.Shared/Nutrition/Prototypes/SatiationValueSerializer.cs @@ -0,0 +1,48 @@ +using JetBrains.Annotations; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.Markdown.Value; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Content.Shared.Nutrition.Prototypes; + +/// +/// This serializer for allows YAMLers to use either an int or a string directly where a +/// is expected. +/// +// TODO Replace with an actual union type once that's available in C#15: https://devblogs.microsoft.com/dotnet/csharp-15-union-types/ +[UsedImplicitly, TypeSerializer] +public sealed class SatiationValueSerializer : ITypeSerializer +{ + public ValidationNode Validate( + ISerializationManager serializationManager, + ValueDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null + ) => int.TryParse(node.Value, out _) + ? serializationManager.ValidateNode(node, context) + : serializationManager.ValidateNode(node, context); + + public SatiationValue Read( + ISerializationManager serializationManager, + ValueDataNode node, + IDependencyCollection dependencies, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + ISerializationManager.InstantiationDelegate? instanceProvider = null + ) => int.TryParse(node.Value, out _) + ? serializationManager.Read(node, context) + : serializationManager.Read(node, context, notNullableOverride: true); + + public DataNode Write( + ISerializationManager serializationManager, + SatiationValue value, + IDependencyCollection dependencies, + bool alwaysWrite = false, + ISerializationContext? context = null + ) => value.Key is not null + ? serializationManager.WriteValue(value.Key, notNullableOverride: true) + : serializationManager.WriteValue(value.Value); +} diff --git a/Content.Shared/Overlays/ShowHungerIconsComponent.cs b/Content.Shared/Overlays/ShowHungerIconsComponent.cs deleted file mode 100644 index b3841bd80f..0000000000 --- a/Content.Shared/Overlays/ShowHungerIconsComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Robust.Shared.GameStates; - -namespace Content.Shared.Overlays; - -/// -/// This component allows you to see the hungriness of mobs. -/// -[RegisterComponent, NetworkedComponent] -public sealed partial class ShowHungerIconsComponent : Component { } diff --git a/Content.Shared/Overlays/ShowSatiationIconsComponent.cs b/Content.Shared/Overlays/ShowSatiationIconsComponent.cs new file mode 100644 index 0000000000..f3a9fe1889 --- /dev/null +++ b/Content.Shared/Overlays/ShowSatiationIconsComponent.cs @@ -0,0 +1,18 @@ +using Content.Shared.Nutrition.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Overlays; + +/// +/// This component allows the owner to see the satiation of mobs. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class ShowSatiationIconsComponent : Component +{ + /// + /// The s to be shown to the owner of this component. + /// + [DataField] + public List> ShownTypes = []; +} diff --git a/Content.Shared/Overlays/ShowThirstIconsComponent.cs b/Content.Shared/Overlays/ShowThirstIconsComponent.cs deleted file mode 100644 index 1914034e9e..0000000000 --- a/Content.Shared/Overlays/ShowThirstIconsComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Robust.Shared.GameStates; - -namespace Content.Shared.Overlays; - -/// -/// This component allows you to see the thirstiness of mobs. -/// -[RegisterComponent, NetworkedComponent] -public sealed partial class ShowThirstIconsComponent : Component { } diff --git a/Content.Shared/Sericulture/SericultureComponent.cs b/Content.Shared/Sericulture/SericultureComponent.cs index a07e96a34c..56d3b9d7fc 100644 --- a/Content.Shared/Sericulture/SericultureComponent.cs +++ b/Content.Shared/Sericulture/SericultureComponent.cs @@ -1,7 +1,7 @@ -using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Prototypes; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Shared.Sericulture; @@ -57,10 +57,8 @@ public sealed partial class SericultureComponent : Component public float HungerCost = 5f; /// - /// The lowest hunger threshold that this mob can be in before it's allowed to spin silk. + /// If the entity's hunger satiation is below this value, it cannot spin web. /// - [DataField("minHungerThreshold")] - [ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - public HungerThreshold MinHungerThreshold = HungerThreshold.Okay; + [DataField(required: true), AutoNetworkedField] + public SatiationValue MinHungerThreshold; } diff --git a/Content.Shared/Sericulture/SericultureSystem.cs b/Content.Shared/Sericulture/SericultureSystem.cs index 70410489ec..bb8081153b 100644 --- a/Content.Shared/Sericulture/SericultureSystem.cs +++ b/Content.Shared/Sericulture/SericultureSystem.cs @@ -6,7 +6,9 @@ using Robust.Shared.Serialization; using Content.Shared.Popups; using Robust.Shared.Network; using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.Prototypes; using Content.Shared.Stacks; +using Robust.Shared.Prototypes; namespace Content.Shared.Sericulture; @@ -21,7 +23,7 @@ public abstract partial class SharedSericultureSystem : EntitySystem // Systems [Dependency] private SharedActionsSystem _actionsSystem = default!; [Dependency] private SharedDoAfterSystem _doAfterSystem = default!; - [Dependency] private HungerSystem _hungerSystem = default!; + [Dependency] private SatiationSystem _satiation = default!; [Dependency] private SharedPopupSystem _popupSystem = default!; [Dependency] private SharedStackSystem _stackSystem = default!; @@ -70,18 +72,16 @@ public abstract partial class SharedSericultureSystem : EntitySystem private void OnSericultureStart(EntityUid uid, SericultureComponent comp, SericultureActionEvent args) { - if (!TryComp(uid, out var hungerComp) - || _hungerSystem.IsHungerBelowState(uid, - comp.MinHungerThreshold, - _hungerSystem.GetHunger(hungerComp) - comp.HungerCost, - hungerComp)) + if (!TryComp(uid, out var satiationComponent) || + !_satiation.IsValueInRange((uid, satiationComponent), SatiationSystem.Hunger, above: comp.MinHungerThreshold, hypotheticalValueDelta: -comp.HungerCost)) { _popupSystem.PopupEntity(Loc.GetString(comp.PopupText), uid, uid); return; } var doAfter = new DoAfterArgs(EntityManager, uid, comp.ProductionLength, new SericultureDoAfterEvent(), uid) - { // I'm not sure if more things should be put here, but imo ideally it should probably be set in the component/YAML. Not sure if this is currently possible. + { + // I'm not sure if more things should be put here, but imo ideally it should probably be set in the component/YAML. Not sure if this is currently possible. BreakOnMove = true, BlockDuplicate = true, BreakOnDamage = true, @@ -91,24 +91,20 @@ public abstract partial class SharedSericultureSystem : EntitySystem _doAfterSystem.TryStartDoAfter(doAfter); } - private void OnSericultureDoAfter(EntityUid uid, SericultureComponent comp, SericultureDoAfterEvent args) { if (args.Cancelled || args.Handled || comp.Deleted) return; - if (!TryComp(uid, - out var hungerComp) // A check, just incase the doafter is somehow performed when the entity is not in the right hunger state. - || _hungerSystem.IsHungerBelowState(uid, - comp.MinHungerThreshold, - _hungerSystem.GetHunger(hungerComp) - comp.HungerCost, - hungerComp)) + // A check, just incase the doafter is somehow performed when the entity is not in the right hunger state. + if (!TryComp(uid, out var satiationComponent) || + !_satiation.IsValueInRange((uid, satiationComponent), SatiationSystem.Hunger, above: comp.MinHungerThreshold, hypotheticalValueDelta: -comp.HungerCost)) { _popupSystem.PopupEntity(Loc.GetString(comp.PopupText), uid, uid); return; } - _hungerSystem.ModifyHunger(uid, -comp.HungerCost, hungerComp); + _satiation.ModifyValue((uid, satiationComponent), SatiationSystem.Hunger, -comp.HungerCost); if (!_netManager.IsClient) // Have to do this because spawning stuff in shared is CBT. { @@ -124,11 +120,10 @@ public abstract partial class SharedSericultureSystem : EntitySystem /// /// Should be relayed upon using the action. /// -public sealed partial class SericultureActionEvent : InstantActionEvent { } +public sealed partial class SericultureActionEvent : InstantActionEvent; /// /// Is relayed at the end of the sericulturing doafter. /// [Serializable, NetSerializable] -public sealed partial class SericultureDoAfterEvent : SimpleDoAfterEvent { } - +public sealed partial class SericultureDoAfterEvent : SimpleDoAfterEvent; diff --git a/Content.Shared/Whitelist/EntityWhitelistSystem.cs b/Content.Shared/Whitelist/EntityWhitelistSystem.cs index 9a885c7708..c6515a92f5 100644 --- a/Content.Shared/Whitelist/EntityWhitelistSystem.cs +++ b/Content.Shared/Whitelist/EntityWhitelistSystem.cs @@ -7,7 +7,8 @@ namespace Content.Shared.Whitelist; public sealed partial class EntityWhitelistSystem : EntitySystem { [Dependency] private TagSystem _tag = default!; - [Dependency] private EntityQuery _itemQuery; + + [Dependency] private EntityQuery _itemQuery = default!; private CompName _itemComponentName = default; private CompName _tagComponentName = default; diff --git a/Resources/Locale/en-US/guidebook/entity-effects/conditions.ftl b/Resources/Locale/en-US/guidebook/entity-effects/conditions.ftl index 1d017423d3..7796d9b99a 100644 --- a/Resources/Locale/en-US/guidebook/entity-effects/conditions.ftl +++ b/Resources/Locale/en-US/guidebook/entity-effects/conditions.ftl @@ -25,12 +25,12 @@ entity-condition-guidebook-group-damage = } } -entity-condition-guidebook-total-hunger = +entity-condition-guidebook-total-satiation = { $max -> - [2147483648] the target has at least {NATURALFIXED($min, 2)} total hunger + [2147483648] the target has at least {NATURALFIXED($min, 2)} total {$type} *[other] { $min -> - [0] the target has at most {NATURALFIXED($max, 2)} total hunger - *[other] the target has between {NATURALFIXED($min, 2)} and {NATURALFIXED($max, 2)} total hunger + [0] the target has at most {NATURALFIXED($max, 2)} total {$type} + *[other] the target has between {NATURALFIXED($min, 2)} and {NATURALFIXED($max, 2)} total {$type} } } diff --git a/Resources/Locale/en-US/guidebook/entity-effects/effects.ftl b/Resources/Locale/en-US/guidebook/entity-effects/effects.ftl index ac569f1c64..6602db9b38 100644 --- a/Resources/Locale/en-US/guidebook/entity-effects/effects.ftl +++ b/Resources/Locale/en-US/guidebook/entity-effects/effects.ftl @@ -67,22 +67,13 @@ entity-effect-guidebook-smoke-area = *[other] create } large quantities of smoke -entity-effect-guidebook-satiate-thirst = +entity-effect-guidebook-satiate = { $chance -> [1] Satiates *[other] satiate } { $relative -> - [1] thirst averagely - *[other] thirst at {NATURALFIXED($relative, 3)}x the average rate - } - -entity-effect-guidebook-satiate-hunger = - { $chance -> - [1] Satiates - *[other] satiate - } { $relative -> - [1] hunger averagely - *[other] hunger at {NATURALFIXED($relative, 3)}x the average rate + [1] {$type} averagely + *[other] {$type} at {NATURALFIXED($relative, 3)}x the average rate } entity-effect-guidebook-health-change = diff --git a/Resources/Locale/en-US/nutrition/components/examinable-hunger-component.ftl b/Resources/Locale/en-US/nutrition/components/examinable-hunger-component.ftl deleted file mode 100644 index c42b56ef88..0000000000 --- a/Resources/Locale/en-US/nutrition/components/examinable-hunger-component.ftl +++ /dev/null @@ -1,5 +0,0 @@ -examinable-hunger-component-examine-overfed = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "look", "looks")} stuffed! -examinable-hunger-component-examine-okay = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "look", "looks")} content. -examinable-hunger-component-examine-peckish = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "look", "looks")} hungry. -examinable-hunger-component-examine-starving = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "look", "looks")} starved! -examinable-hunger-component-examine-none = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "seem", "seems")} not to get hungry. diff --git a/Resources/Locale/en-US/nutrition/components/examinable-satiation-component.ftl b/Resources/Locale/en-US/nutrition/components/examinable-satiation-component.ftl new file mode 100644 index 0000000000..140a05d6c2 --- /dev/null +++ b/Resources/Locale/en-US/nutrition/components/examinable-satiation-component.ftl @@ -0,0 +1,11 @@ +examinable-satiation-component-examine-hunger-full = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "look", "looks")} stuffed! +examinable-satiation-component-examine-hunger-okay = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "look", "looks")} content. +examinable-satiation-component-examine-hunger-concerned = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "look", "looks")} hungry. +examinable-satiation-component-examine-hunger-desperate = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "look", "looks")} starved! +examinable-satiation-component-examine-hunger-none = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "seem", "seems")} not to get hungry. + +examinable-satiation-component-examine-thirst-full = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "look", "looks")} well hydrated! +examinable-satiation-component-examine-thirst-okay = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "look", "looks")} quenched. +examinable-satiation-component-examine-thirst-concerned = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "look", "looks")} thirsty. +examinable-satiation-component-examine-thirst-desperate = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "look", "looks")} parched! +examinable-satiation-component-examine-thirst-none = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "seem", "seems")} not to get thirsty. diff --git a/Resources/Locale/en-US/nutrition/nutrition-commands.ftl b/Resources/Locale/en-US/nutrition/nutrition-commands.ftl index 2b1259e9b2..e45ea57575 100644 --- a/Resources/Locale/en-US/nutrition/nutrition-commands.ftl +++ b/Resources/Locale/en-US/nutrition/nutrition-commands.ftl @@ -1,12 +1,12 @@ -cmd-nutrition-error-player = You cannot use this command unless you are a player. -cmd-nutrition-error-entity = You cannot use this command without an entity. -cmd-nutrition-error-component = Your entity does not have a {$comp} component. +cmd-nutrition-satiation-need = {$satiation} satiation need -cmd-hungry-desc = makes you hungry -cmd-hungry-help = sets your hungry level to starving -cmd-setnutrit-desc = modify hunger and thirst -cmd-setnutrit-help = set your hunger or thirst to one of the built-in thresholds -cmd-setnutrit-error-invalid-threshold = invalid {$thresholdType} `{$thresholdString}` +cmd-setsatiation-desc = Sets your satiation need. +cmd-setsatiation-help = Sets your specified satiation need to the specified threshold. -cmd-thirsty-desc = makes you thirsty -cmd-thirsty-help = sets your thirst level to partched +cmd-nutrition-setsatiation-prototype-error = Invalid satiation \"{$satiation-proto-id}\" +cmd-nutrition-setsatiation-no-matching-key-error = Unrecognized key \"{$key}\" for satiation \"{$satiation-proto-id}\" +cmd-nutrition-setsatiation-hint-key = Satiation Value Key +cmd-nutrition-setsatiation-hint-max-value = Maximum Value + +cmd-unsatiate-desc = Makes your satiation needs desperate. +cmd-unsatiate-help = Sets the specified satiation needs to the "desperate" threshold. Affects specified satiation types, or all types if none are specified. diff --git a/Resources/Locale/en-US/nutrition/satiation-types.ftl b/Resources/Locale/en-US/nutrition/satiation-types.ftl new file mode 100644 index 0000000000..ef178fc532 --- /dev/null +++ b/Resources/Locale/en-US/nutrition/satiation-types.ftl @@ -0,0 +1,2 @@ +satiation-type-hunger = hunger +satiation-type-thirst = thirst diff --git a/Resources/Prototypes/Body/Satiation/Hunger/examination.yml b/Resources/Prototypes/Body/Satiation/Hunger/examination.yml new file mode 100644 index 0000000000..302a1af873 --- /dev/null +++ b/Resources/Prototypes/Body/Satiation/Hunger/examination.yml @@ -0,0 +1,12 @@ +- type: entity + abstract: true + id: BaseExaminableHunger + components: + - type: ExaminableSatiation + satiations: + Hunger: + Dead: "examinable-satiation-component-examine-hunger-desperate" + Starving: "examinable-satiation-component-examine-hunger-desperate" + Peckish: "examinable-satiation-component-examine-hunger-concerned" + Okay: "examinable-satiation-component-examine-hunger-okay" + Overfed: "examinable-satiation-component-examine-hunger-full" diff --git a/Resources/Prototypes/Body/Satiation/Hunger/hunger.yml b/Resources/Prototypes/Body/Satiation/Hunger/hunger.yml new file mode 100644 index 0000000000..90f87fb67e --- /dev/null +++ b/Resources/Prototypes/Body/Satiation/Hunger/hunger.yml @@ -0,0 +1,104 @@ +- type: satiation + id: NormalSatiationHunger + baseDecayRate: 0.01666666666 + maximumValue: 200 + thresholds: + Overfed: 200 + Okay: 150 + Peckish: 100 + Starving: 50 + Dead: 0 + startingValueMinimum: Peckish + startingValueMaximum: Okay + decayModifiers: + Overfed: 1.2 + Okay: 1.0 + Peckish: 0.8 + Starving: 0.6 + alertCategory: Hunger + alerts: + Peckish: Peckish + Starving: Starving + icons: + Overfed: HungerIconOverfed + Okay: null + Peckish: HungerIconPeckish + Starving: HungerIconStarving + +- type: satiation + id: SimpleMobBaseHunger + parent: NormalSatiationHunger + thresholds: # only animals and rats are derived from this prototype so let's override it here and in rats' proto + Overfed: 100 + Okay: 50 + Peckish: 25 + Starving: 10 + baseDecayRate: 0.00925925925926 # it is okay for animals to eat and drink less than humans, but more frequently + maximumValue: 100 + +- type: satiation + id: SnailHunger + parent: SimpleMobBaseHunger + thresholds: + Overfed: 35 + Okay: 25 + Peckish: 15 + Starving: 10 + baseDecayRate: 0.1 + maximumValue: 35 + +- type: satiation + id: CockroachHunger + parent: SimpleMobBaseHunger + baseDecayRate: 0.25 + +- type: satiation + id: MouseHunger + parent: SimpleMobBaseHunger + thresholds: + Overfed: 35 + Okay: 25 + Peckish: 15 + Starving: 10 + baseDecayRate: 0.5 # I'm very hungry! Give me. The cheese. + maximumValue: 35 + +- type: satiation + id: MouseAdmemeHunger + parent: MouseHunger + baseDecayRate: 10 # always hungry + +- type: satiation + id: HamsterHunger + parent: SimpleMobBaseHunger + baseDecayRate: 0.3 + +- type: satiation + id: RegalRatHunger + parent: SimpleMobBaseHunger + thresholds: + Overfed: 200 + Okay: 150 + Peckish: 100 + Starving: 50 + baseDecayRate: 0.01666666666 + maximumValue: 200 + +- type: satiation + id: ScurretHunger + parent: SimpleMobBaseHunger + baseDecayRate: 0.05 # They get a lil' hungy + +- type: satiation + id: DionaHunger + parent: NormalSatiationHunger + baseDecayRate: 0.0083 + +- type: entity + abstract: true + id: BaseHungerSlowdown + component: + - type: SatiationSpeedModifier + satiations: + Hunger: + Peckish: 0.75 diff --git a/Resources/Prototypes/Body/Satiation/Thirst/thirst.yml b/Resources/Prototypes/Body/Satiation/Thirst/thirst.yml new file mode 100644 index 0000000000..8e066e19c5 --- /dev/null +++ b/Resources/Prototypes/Body/Satiation/Thirst/thirst.yml @@ -0,0 +1,69 @@ +- type: satiation + id: NormalSatiationThirst + baseDecayRate: 0.1 + maximumValue: 600 + thresholds: + Overhydrated: 600 + Okay: 450 + Thirsty: 300 + Parched: 150 + Dead: 0 + startingValueMinimum: Thirsty + startingValueMaximum: Okay + decayModifiers: + Overhydrated: 1.2 + Okay: 1.0 + Thirsty: 0.8 + Parched: 0.6 + alertCategory: Thirst + alerts: + Thirsty: Thirsty + Parched: Parched + icons: + Overhydrated: ThirstIconOverhydrated + Okay: null + Thirsty: ThirstIconThirsty + Parched: ThirstIconParched + +- type: satiation + id: MouseThirst + parent: NormalSatiationThirst + baseDecayRate: 0.04 + maximumValue: 35 + thresholds: + Overhydrated: 35 + Okay: 25 + Thirsty: 15 + Parched: 10 + Dead: 0 + +- type: satiation + id: MouseAdmemeThirst + parent: MouseThirst + baseDecayRate: 10 + +- type: satiation + id: SimpleMobBaseThirst + parent: NormalSatiationThirst + baseDecayRate: 0.04 + maximumValue: 200 + thresholds: + Overhydrated: 200 + Okay: 150 + Thirsty: 100 + Parched: 50 + Dead: 0 + +- type: satiation + id: DionaThirst + parent: NormalSatiationThirst + baseDecayRate: 0.0083 + +- type: entity + abstract: true + id: BaseThirstSlowdown + component: + - type: SatiationSpeedModifier + satiations: + Thirst: + Parched: 0.75 diff --git a/Resources/Prototypes/Body/Satiation/types.yml b/Resources/Prototypes/Body/Satiation/types.yml new file mode 100644 index 0000000000..a747701c9d --- /dev/null +++ b/Resources/Prototypes/Body/Satiation/types.yml @@ -0,0 +1,7 @@ +- type: satiationType + id: Hunger + name: satiation-type-hunger + +- type: satiationType + id: Thirst + name: satiation-type-thirst diff --git a/Resources/Prototypes/Body/Species/arachnid.yml b/Resources/Prototypes/Body/Species/arachnid.yml index 602f4857c9..0f10c13178 100644 --- a/Resources/Prototypes/Body/Species/arachnid.yml +++ b/Resources/Prototypes/Body/Species/arachnid.yml @@ -85,6 +85,7 @@ productionLength: 2 entityProduced: MaterialWebSilk1 hungerCost: 4 # Should total to 25 total silk on full hunger + minHungerThreshold: Peckish - type: Tag tags: - CanPilot diff --git a/Resources/Prototypes/Body/Species/diona.yml b/Resources/Prototypes/Body/Species/diona.yml index 9ba950c9e4..507efacfd5 100644 --- a/Resources/Prototypes/Body/Species/diona.yml +++ b/Resources/Prototypes/Body/Species/diona.yml @@ -79,10 +79,12 @@ damageRecovery: types: Asphyxiation: -1.0 - - type: Hunger - baseDecayRate: 0.0083 - - type: Thirst - baseDecayRate: 0.0083 + - type: Satiation + satiations: + Hunger: + prototype: DionaHunger + Thirst: + prototype: DionaThirst - type: Damageable damageModifierSet: Diona - type: DamageVisuals diff --git a/Resources/Prototypes/Body/species_base.yml b/Resources/Prototypes/Body/species_base.yml index 698f618cad..7bf6559254 100644 --- a/Resources/Prototypes/Body/species_base.yml +++ b/Resources/Prototypes/Body/species_base.yml @@ -161,11 +161,17 @@ - MobFlammable - BaseSpeciesMob - BaseMobDestructible + - BaseHungerSlowdown + - BaseThirstSlowdown id: BaseSpeciesMobOrganic save: false components: - - type: Hunger - - type: Thirst + - type: Satiation + satiations: + Hunger: + prototype: NormalSatiationHunger + Thirst: + prototype: NormalSatiationThirst - type: Barotrauma damage: types: diff --git a/Resources/Prototypes/Entities/Clothing/Eyes/hud.yml b/Resources/Prototypes/Entities/Clothing/Eyes/hud.yml index 6c49e05391..78cef66ec9 100644 --- a/Resources/Prototypes/Entities/Clothing/Eyes/hud.yml +++ b/Resources/Prototypes/Entities/Clothing/Eyes/hud.yml @@ -99,7 +99,9 @@ sprite: Clothing/Eyes/Hud/beergoggles.rsi - type: Clothing sprite: Clothing/Eyes/Hud/beergoggles.rsi - - type: ShowThirstIcons + - type: ShowSatiationIcons + shownTypes: + - Thirst - type: SolutionScanner - type: Tag tags: @@ -116,7 +118,9 @@ sprite: Clothing/Eyes/Hud/friedonion.rsi - type: Clothing sprite: Clothing/Eyes/Hud/friedonion.rsi - - type: ShowHungerIcons + - type: ShowSatiationIcons + shownTypes: + - Hunger - type: Edible - type: Solution solution: @@ -138,8 +142,10 @@ sprite: Clothing/Eyes/Hud/onionbeer.rsi - type: Clothing sprite: Clothing/Eyes/Hud/onionbeer.rsi - - type: ShowHungerIcons - - type: ShowThirstIcons + - type: ShowSatiationIcons + shownTypes: + - Hunger + - Thirst - type: entity parent: [ClothingEyesHudBase, ShowMedicalIcons] @@ -151,7 +157,9 @@ sprite: Clothing/Eyes/Hud/medonion.rsi - type: Clothing sprite: Clothing/Eyes/Hud/medonion.rsi - - type: ShowHungerIcons + - type: ShowSatiationIcons + shownTypes: + - Hunger - type: entity parent: [ClothingEyesHudBase, ShowMedicalIcons] @@ -163,8 +171,10 @@ sprite: Clothing/Eyes/Hud/medonionbeer.rsi - type: Clothing sprite: Clothing/Eyes/Hud/medonionbeer.rsi - - type: ShowHungerIcons - - type: ShowThirstIcons + - type: ShowSatiationIcons + shownTypes: + - Hunger + - Thirst - type: entity parent: [ClothingEyesHudBase, ShowSecurityIcons, ShowMedicalIcons, BaseSecurityCommandContraband] @@ -210,8 +220,10 @@ damageContainers: - Biological - Inorganic - - type: ShowHungerIcons - - type: ShowThirstIcons + - type: ShowSatiationIcons + shownTypes: + - Hunger + - Thirst - type: ShowSyndicateIcons - type: entity diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index f1677e6d96..068713b9b2 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -199,7 +199,7 @@ - type: entity name: chicken - parent: [SimpleMobBase, BaseSlicingRefinable, BaseMobDestructible] + parent: [SimpleMobBase, BaseSlicingRefinable, BaseMobDestructible, BaseExaminableHunger] id: MobChicken description: Comes before an egg, and IS a dinosaur! components: @@ -239,6 +239,7 @@ breedChance: 0.05 birthPopup: reproductive-laid-egg-popup makeOffspringInfant: false + minHungerThreshold: Peckish partnerWhitelist: tags: - Chicken @@ -274,7 +275,6 @@ - type: EggLayer eggSpawn: - id: FoodEgg - - type: ExaminableHunger - type: ReplacementAccent accent: chicken - type: SentienceTarget @@ -391,8 +391,10 @@ - horrible - terrible - chewy - - type: Hunger - baseDecayRate: 0.25 + - type: Satiation + satiations: + Hunger: + prototype: CockroachHunger - type: Extractable grindableSolutionName: food - type: Solution @@ -660,7 +662,7 @@ - type: entity name: mallard duck #Quack - parent: [SimpleMobBase, BaseSlicingRefinable, BaseMobDestructible] + parent: [SimpleMobBase, BaseSlicingRefinable, BaseMobDestructible, BaseExaminableHunger] id: MobDuckMallard description: An adorable mallard duck, it's fluffy and soft! components: @@ -690,6 +692,7 @@ breedChance: 0.05 birthPopup: reproductive-laid-egg-popup makeOffspringInfant: false + minHungerThreshold: Peckish partnerWhitelist: tags: - Duck @@ -725,7 +728,6 @@ - type: EggLayer eggSpawn: - id: FoodEgg - - type: ExaminableHunger - type: ReplacementAccent accent: duck - type: SentienceTarget @@ -863,7 +865,7 @@ - type: entity name: cow - parent: [ BaseMobRuminant, SimpleMobBase, BaseSlicingRefinable, BaseMobDestructible ] + parent: [ BaseMobRuminant, SimpleMobBase, BaseSlicingRefinable, BaseMobDestructible, BaseExaminableHunger ] id: MobCow description: Moo. components: @@ -878,6 +880,7 @@ - DoorBumpOpener - Cow - type: Reproductive + minHungerThreshold: Peckish partnerWhitelist: tags: - Cow @@ -908,10 +911,10 @@ solutions: - SolutionUdder - type: Udder + minHungerThreshold: Peckish reagentId: Milk quantityPerUpdate: 25 growthDelay: 30 - - type: ExaminableHunger - type: ToolRefinable refineResult: - id: FoodMeat @@ -1022,7 +1025,7 @@ - type: entity name: goat - parent: [ BaseMobRuminant, SimpleMobBase, BaseSlicingRefinable, BaseMobDestructible ] + parent: [ BaseMobRuminant, SimpleMobBase, BaseSlicingRefinable, BaseMobDestructible, BaseExaminableHunger ] id: MobGoat description: Her spine consists of long sharp segments, no wonder she is so grumpy. components: @@ -1050,6 +1053,7 @@ - DoorBumpOpener - Goat - type: Reproductive + minHungerThreshold: Peckish partnerWhitelist: tags: - Goat @@ -1070,11 +1074,12 @@ - SolutionUdderGoat - SolutionWool - type: Udder + minHungerThreshold: Peckish reagentId: MilkGoat quantityPerUpdate: 25 growthDelay: 20 - - type: ExaminableHunger - type: Wooly + minHungerThreshold: Peckish - type: Edible destroyOnEmpty: false solution: wool @@ -1134,7 +1139,7 @@ - type: entity abstract: true - parent: [BaseMobRuminant, BaseSlicingRefinable] + parent: [BaseMobRuminant, BaseSlicingRefinable, BaseExaminableHunger] id: MobSheepBase name: sheepBase description: A sheep. @@ -1203,8 +1208,9 @@ reagentId: MilkSheep quantityPerUpdate: 25 growthDelay: 20 - - type: ExaminableHunger + minHungerThreshold: Peckish - type: Wooly + minHungerThreshold: Peckish - type: Shearable targetSolutionName: wool shearedProductId: MaterialCotton1 # Cotton is spawned because there is no wool item. @@ -2006,23 +2012,12 @@ - type: FlavorProfile flavors: - meaty - - type: Thirst - startingThirst: 25 # spawn with Okay thirst state - thresholds: - OverHydrated: 35 - Okay: 25 - Thirsty: 15 - Parched: 10 - Dead: 0 - baseDecayRate: 0.04 - - type: Hunger - thresholds: - Overfed: 35 - Okay: 25 - Peckish: 15 - Starving: 10 - Dead: 0 - baseDecayRate: 0.5 # I'm very hungry! Give me. The cheese. + - type: Satiation + satiations: + Hunger: + prototype: MouseHunger + Thirst: + prototype: MouseThirst - type: Extractable grindableSolutionName: food - type: ToolRefinable @@ -3707,8 +3702,10 @@ - type: FlavorProfile flavors: - meaty - - type: Hunger - baseDecayRate: 0.3 + - type: Satiation + satiations: + Hunger: + prototype: HamsterHunger - type: Extractable grindableSolutionName: food - type: Solution @@ -3816,6 +3813,7 @@ - Pig - VimPilot - type: Reproductive + minHungerThreshold: Peckish partnerWhitelist: tags: - Pig diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml index 68c79513a4..44b97bfcdb 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml @@ -1,7 +1,7 @@ - type: entity name: rat king id: MobRatKing - parent: [ BaseMobRat, SimpleMobBase, MobCombat, BaseSlicingRefinable, MobNightVision, BaseMobDestructible ] + parent: [ BaseMobRat, SimpleMobBase, MobCombat, BaseSlicingRefinable, MobNightVision, BaseMobDestructible, BaseThirstSlowdown ] description: He's da rat. He make da roolz. components: - type: CombatMode @@ -61,22 +61,12 @@ types: Slash: 12 Piercing: 8 - - type: Hunger # probably should be prototyped - thresholds: - Overfed: 200 - Okay: 150 - Peckish: 100 - Starving: 50 - Dead: 0 - baseDecayRate: 0.01666666666 - - type: Thirst - thresholds: - OverHydrated: 600 - Okay: 450 - Thirsty: 300 - Parched: 150 - Dead: 0 - baseDecayRate: 0.1 + - type: Satiation + satiations: + Hunger: + prototype: RegalRatHunger + Thirst: + prototype: NormalSatiationThirst - type: DamageStateVisuals states: Alive: @@ -173,7 +163,7 @@ - type: entity name: rat servant id: MobRatServant - parent: [ BaseMobRat, SimpleMobBase, MobCombat, BaseSlicingRefinable ] + parent: [ BaseMobRat, SimpleMobBase, MobCombat, BaseSlicingRefinable, BaseThirstSlowdown ] description: He's da mini rat. He don't make da roolz. categories: [ HideSpawnMenu ] #Must be configured to a King or the AI breaks. components: @@ -255,22 +245,12 @@ types: Slash: 5 Piercing: 3 - - type: Hunger # probably should be prototyped - thresholds: - Overfed: 200 - Okay: 150 - Peckish: 100 - Starving: 50 - Dead: 0 - baseDecayRate: 0.01666666666 - - type: Thirst - thresholds: - OverHydrated: 600 - Okay: 450 - Thirsty: 300 - Parched: 150 - Dead: 0 - baseDecayRate: 0.1 + - type: Satiation + satiations: + Hunger: + prototype: RegalRatHunger + Thirst: + prototype: NormalSatiationThirst - type: DamageStateVisuals states: Alive: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/scurret.yml b/Resources/Prototypes/Entities/Mobs/NPCs/scurret.yml index 4ceddc730a..e3b6267bf6 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/scurret.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/scurret.yml @@ -133,8 +133,10 @@ - type: InventorySlots - type: Edible # Why? I'm not sure but I also am afraid of the answer. - - type: Hunger - baseDecayRate: 0.05 # They get a lil' hungy + - type: Satiation + satiations: + Hunger: + prototype: ScurretHunger - type: BodyEmotes # Grants them clapping and so on. soundsId: Scurret - type: Deathgasp diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml index 1fda20dbf3..83e61a70d4 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml @@ -39,6 +39,8 @@ - MobBloodstream - MobFlammable - BaseMobAnimal + - BaseHungerSlowdown + - BaseThirstSlowdown id: SimpleSpaceMobBase # Mob without barotrauma, freezing and asphyxiation (for space carps!?) suffix: AI components: @@ -67,26 +69,18 @@ - MobRespirator - MobAtmosStandard - SimpleSpaceMobBase + - BaseHungerSlowdown + - BaseThirstSlowdown id: SimpleMobBase # for air breathers suffix: AI abstract: true components: - - type: Hunger - thresholds: # only animals and rats are derived from this prototype so let's override it here and in rats' proto - Overfed: 100 - Okay: 50 - Peckish: 25 - Starving: 10 - Dead: 0 - baseDecayRate: 0.00925925925926 # it is okay for animals to eat and drink less than humans, but more frequently - - type: Thirst - thresholds: - OverHydrated: 200 - Okay: 150 - Thirsty: 100 - Parched: 50 - Dead: 0 - baseDecayRate: 0.04 + - type: Satiation + satiations: + Hunger: + prototype: SimpleMobBaseHunger + Thirst: + prototype: SimpleMobBaseThirst - type: StatusEffects allowed: - Electrocution diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml index 705bbfe04e..c5b429de5d 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml @@ -571,23 +571,12 @@ Quantity: 3 - ReagentId: Water Quantity: 4 #It makes saline if you add salt! - - type: Thirst - startingThirst: 25 # spawn with Okay thirst state - thresholds: - OverHydrated: 35 - Okay: 25 - Thirsty: 15 - Parched: 10 - Dead: 0 - baseDecayRate: 0.04 - - type: Hunger - thresholds: - Overfed: 35 - Okay: 25 - Peckish: 15 - Starving: 10 - Dead: 0 - baseDecayRate: 0.1 + - type: Satiation + satiations: + Hunger: + prototype: SnailHunger + Thirst: + prototype: MouseThirst - type: Extractable grindableSolutionName: food - type: Tag diff --git a/Resources/Prototypes/Entities/Structures/Machines/fatextractor.yml b/Resources/Prototypes/Entities/Structures/Machines/fatextractor.yml index bf2e5a3f62..3f5abfc9bf 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/fatextractor.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/fatextractor.yml @@ -5,6 +5,7 @@ description: Safely and efficiently extracts excess fat from a subject. components: - type: FatExtractor + minHungerThreshold: Peckish processSound: path: /Audio/Machines/microwave_loop.ogg params: diff --git a/Resources/Prototypes/NPCs/nutrition.yml b/Resources/Prototypes/NPCs/nutrition.yml index b95552fa13..3a4e762526 100644 --- a/Resources/Prototypes/NPCs/nutrition.yml +++ b/Resources/Prototypes/NPCs/nutrition.yml @@ -5,8 +5,9 @@ - tasks: - !type:HTNPrimitiveTask preconditions: - - !type:HungryPrecondition - minHungerState: Peckish # See HungerThreshold enum + - !type:SatiationPrecondition + satiationType: Hunger + below: Peckish operator: !type:UtilityOperator proto: NearbyFood @@ -35,8 +36,9 @@ - tasks: - !type:HTNPrimitiveTask preconditions: - - !type:ThirstyPrecondition - minThirstState: Parched # See ThirstThreshold enum + - !type:SatiationPrecondition + satiationType: Thirst + below: Parched operator: !type:UtilityOperator proto: NearbyDrink diff --git a/Resources/Prototypes/NPCs/utility_queries.yml b/Resources/Prototypes/NPCs/utility_queries.yml index 3c1d3fe5f2..32956d2409 100644 --- a/Resources/Prototypes/NPCs/utility_queries.yml +++ b/Resources/Prototypes/NPCs/utility_queries.yml @@ -39,6 +39,7 @@ curve: !type:PresetCurve preset: TargetDistance - !type:FoodValueCon + hungerThreshold: Peckish curve: !type:QuadraticCurve slope: 1.0 exponent: 0.4 @@ -58,6 +59,7 @@ curve: !type:PresetCurve preset: TargetDistance - !type:DrinkValueCon + thirstThreshold: Thirsty curve: !type:QuadraticCurve slope: 1.0 exponent: 0.4 diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml b/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml index 5535c3c9bf..683302c233 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml @@ -273,7 +273,8 @@ metabolites: Ethanol: 0.1 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 - !type:HealthChange damage: @@ -429,7 +430,8 @@ metabolites: Ethanol: 0.6 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 3 fizziness: 0.8 @@ -872,9 +874,11 @@ metabolites: Ethanol: 0.1 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger factor: -2 - !type:EvenHealthChange damage: @@ -1367,7 +1371,8 @@ metabolites: Ethanol: 0.5 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 - !type:HealthChange damage: @@ -1520,7 +1525,8 @@ effects: - !type:ModifyStatusEffect effectProto: StatusEffectRatvarianLanguage - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 - type: reagent @@ -1542,7 +1548,8 @@ effects: - !type:ModifyStatusEffect effectProto: StatusEffectMuted - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 - type: reagent @@ -1825,7 +1832,8 @@ Ethanol: 0.2 Theobromine: 0.1 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 1 - !type:ModifyStatusEffect effectProto: StatusEffectDrowsiness @@ -1853,7 +1861,8 @@ Ethanol: 0.3 Theobromine: 0.1 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 1 - !type:ModifyStatusEffect effectProto: StatusEffectDrowsiness @@ -1881,7 +1890,8 @@ Ethanol: 0.2 Theobromine: 0.1 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 1 - !type:ModifyStatusEffect effectProto: StatusEffectDrowsiness @@ -1909,7 +1919,8 @@ Ethanol: 0.3 Theobromine: 0.1 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 1 - !type:ModifyStatusEffect effectProto: StatusEffectDrowsiness @@ -1937,7 +1948,8 @@ Ethanol: 0.14 Theobromine: 0.1 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 1 - !type:ModifyStatusEffect effectProto: StatusEffectDrowsiness @@ -1965,7 +1977,8 @@ Ethanol: 0.3 Theobromine: 0.1 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 1 - !type:ModifyStatusEffect effectProto: StatusEffectDrowsiness @@ -2052,7 +2065,8 @@ effects: - !type:ModifyStatusEffect effectProto: StatusEffectMuted - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 - type: reagent @@ -2103,7 +2117,8 @@ damage: types: Poison: .5 - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 1 - type: reagent @@ -2258,7 +2273,8 @@ metabolites: Ethanol: 0.9 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 3 - !type:Drunk boozePower: 10 diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/base_drink.yml b/Resources/Prototypes/Reagents/Consumable/Drink/base_drink.yml index d0ea31063c..880bf6f2cb 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/base_drink.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/base_drink.yml @@ -7,7 +7,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 3 reactiveEffects: Extinguish: @@ -28,7 +29,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 plantMetabolism: - !type:PlantAdjustNutrition @@ -53,7 +55,8 @@ metabolites: Ethanol: 0.12 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 reactiveEffects: Flammable: diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml b/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml index 820a792d52..e9bc523cbd 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml @@ -12,7 +12,8 @@ metabolites: Theobromine: 0.1 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 - !type:ModifyStatusEffect effectProto: StatusEffectDrowsiness @@ -50,7 +51,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 1 - type: reagent @@ -64,7 +66,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 4 # Coconut water is 94% water - type: reagent @@ -78,7 +81,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 - type: reagent @@ -98,7 +102,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 - !type:ModifyStatusEffect effectProto: StatusEffectDrowsiness @@ -156,7 +161,8 @@ metabolites: Theobromine: 0.1 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 - !type:ModifyStatusEffect effectProto: StatusEffectDrowsiness @@ -210,7 +216,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst - !type:HealthChange damage: types: @@ -234,7 +241,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 - type: reagent @@ -254,7 +262,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 4 - type: reagent @@ -281,7 +290,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 4 - type: reagent @@ -304,7 +314,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: -2 - type: reagent @@ -316,7 +327,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 # TODO heal mimes @@ -338,7 +350,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 6 - !type:ModifyStatusEffect effectProto: StatusEffectDrowsiness @@ -377,7 +390,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 - !type:ModifyStatusEffect effectProto: StatusEffectDrowsiness @@ -437,7 +451,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 4 Respiration: effects: @@ -467,7 +482,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 3 # Slightly worse for quenching thirst than just straight water plantMetabolism: - !type:PlantAdjustWater @@ -490,7 +506,8 @@ metabolisms: Digestion: effects: - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger factor: 1 - type: reagent @@ -504,7 +521,8 @@ metabolisms: Digestion: effects: - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger factor: 4 - type: reagent @@ -526,7 +544,8 @@ metabolites: Theobromine: 0.1 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 4 - type: reagent @@ -540,7 +559,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 1 fizziness: 0.3 @@ -596,7 +616,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 0.6 - type: reagent diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/soda.yml b/Resources/Prototypes/Reagents/Consumable/Drink/soda.yml index 73bff27e67..fb99612ae6 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/soda.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/soda.yml @@ -16,7 +16,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 - !type:ModifyStatusEffect effectProto: StatusEffectDrowsiness @@ -40,7 +41,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 4 - type: reagent @@ -80,7 +82,8 @@ metabolites: Theobromine: 0.2 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 - !type:ModifyStatusEffect effectProto: StatusEffectDrowsiness @@ -205,7 +208,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 4 - type: reagent @@ -266,7 +270,8 @@ metabolites: Theobromine: 0.2 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 2 - !type:HealthChange conditions: diff --git a/Resources/Prototypes/Reagents/Consumable/Food/condiments.yml b/Resources/Prototypes/Reagents/Consumable/Food/condiments.yml index 61255df5e6..313e870e30 100644 --- a/Resources/Prototypes/Reagents/Consumable/Food/condiments.yml +++ b/Resources/Prototypes/Reagents/Consumable/Food/condiments.yml @@ -129,9 +129,11 @@ metabolisms: Digestion: effects: - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger factor: 0.5 - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: -0.5 # high salt content - type: reagent @@ -162,7 +164,8 @@ Digestion: effects: # eating salt on its own kinda sucks, kids - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: -0.5 - type: reagent @@ -181,7 +184,8 @@ Digestion: # 12 diona blood for 1 unit of syrup, this stuff better be worthwhile. effects: - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger factor: 6.0 #Stronger than cookedramen footstepSound: collection: FootstepSlime diff --git a/Resources/Prototypes/Reagents/Consumable/Food/food.yml b/Resources/Prototypes/Reagents/Consumable/Food/food.yml index cb43938adf..2193bdd755 100644 --- a/Resources/Prototypes/Reagents/Consumable/Food/food.yml +++ b/Resources/Prototypes/Reagents/Consumable/Food/food.yml @@ -10,7 +10,8 @@ metabolisms: Metabolites: effects: - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger plantMetabolism: - !type:PlantAdjustNutrition amount: 1.5 @@ -37,7 +38,8 @@ # Helps you stop bleeding to an extent. - !type:ModifyBleed amount: -0.25 - - !type:SatiateHunger #Numbers are balanced with this in mind + it helps limit how much healing you can get from food + - !type:Satiate #Numbers are balanced with this in mind + it helps limit how much healing you can get from food + satiationType: Hunger # Lets plants benefit too plantMetabolism: - !type:PlantAdjustNutrition @@ -63,7 +65,8 @@ Brute: -0.4 - !type:ModifyBloodLevel amount: 1 # weaker than iron but pretty good all things considered - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger pricePerUnit: 3 - type: reagent @@ -78,7 +81,8 @@ metabolisms: Metabolites: effects: - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger conditions: - !type:ReagentCondition #Only satiates when eaten with nutriment reagent: Nutriment diff --git a/Resources/Prototypes/Reagents/Consumable/Food/ingredients.yml b/Resources/Prototypes/Reagents/Consumable/Food/ingredients.yml index 285458baa7..3c874be6fa 100644 --- a/Resources/Prototypes/Reagents/Consumable/Food/ingredients.yml +++ b/Resources/Prototypes/Reagents/Consumable/Food/ingredients.yml @@ -13,7 +13,8 @@ Sugar: 0.4 Nutriment: 0.2 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: -0.2 #its flour - type: reagent @@ -29,7 +30,8 @@ metabolites: Nutriment: 0.2 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: -0.2 #its cornflour - type: reagent @@ -223,7 +225,8 @@ metabolisms: Digestion: effects: - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger conditions: - !type:ReagentCondition #Only satiates when eaten with nutriment reagent: Nutriment @@ -266,7 +269,8 @@ metabolites: Nutriment: 0.2 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: -0.2 - type: reagent diff --git a/Resources/Prototypes/Reagents/biological.yml b/Resources/Prototypes/Reagents/biological.yml index fd3e2889eb..097f95b4ed 100644 --- a/Resources/Prototypes/Reagents/biological.yml +++ b/Resources/Prototypes/Reagents/biological.yml @@ -19,7 +19,8 @@ metabolites: UncookedAnimalProteins: 0.2 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 1.0 conditions: - !type:MetabolizerTypeCondition @@ -74,7 +75,8 @@ Digestion: # Delicious! effects: - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger factor: 1 conditions: - !type:MetabolizerTypeCondition @@ -106,9 +108,11 @@ Digestion: # Sweet! effects: - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger factor: 1 - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 1 footstepSound: collection: FootstepSlime @@ -160,7 +164,8 @@ Digestion: # Disgusting! effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: -0.5 Bloodstream: effects: @@ -183,7 +188,8 @@ metabolisms: Bloodstream: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 1.5 # Dragon doesn't require airloss healing, so omnizine is still best for humans. - !type:ModifyBloodLevel @@ -237,7 +243,8 @@ metabolites: Nutriment: 0.1 effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 0.5 footstepSound: collection: FootstepBlood @@ -255,7 +262,8 @@ metabolisms: Digestion: effects: - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger factor: 1.5 - !type:HealthChange damage: diff --git a/Resources/Prototypes/Reagents/botany.yml b/Resources/Prototypes/Reagents/botany.yml index d5b4dd75e9..9dea63ecab 100644 --- a/Resources/Prototypes/Reagents/botany.yml +++ b/Resources/Prototypes/Reagents/botany.yml @@ -12,7 +12,8 @@ metabolisms: Digestion: effects: - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger conditions: - !type:MetabolizerTypeCondition type: [Plant] @@ -36,7 +37,8 @@ metabolisms: Bloodstream: effects: - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger conditions: - !type:MetabolizerTypeCondition type: [Plant] diff --git a/Resources/Prototypes/Reagents/chemicals.yml b/Resources/Prototypes/Reagents/chemicals.yml index 368a85a959..8a6848e935 100644 --- a/Resources/Prototypes/Reagents/chemicals.yml +++ b/Resources/Prototypes/Reagents/chemicals.yml @@ -181,7 +181,8 @@ metabolisms: Bloodstream: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: -5 # This is basically industrial dessicant - !type:PopupMessage visualType: MediumCaution diff --git a/Resources/Prototypes/Reagents/fun.yml b/Resources/Prototypes/Reagents/fun.yml index d7741365db..7ac15c2d10 100644 --- a/Resources/Prototypes/Reagents/fun.yml +++ b/Resources/Prototypes/Reagents/fun.yml @@ -37,7 +37,8 @@ metabolisms: Digestion: effects: - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger conditions: - !type:MetabolizerTypeCondition type: [ Moth ] diff --git a/Resources/Prototypes/Reagents/medicine.yml b/Resources/Prototypes/Reagents/medicine.yml index d122082f66..d9ec5b8a37 100644 --- a/Resources/Prototypes/Reagents/medicine.yml +++ b/Resources/Prototypes/Reagents/medicine.yml @@ -484,7 +484,8 @@ Heat: -0.33 Shock: -0.33 Cold: -0.33 - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: -10 conditions: - !type:ReagentCondition @@ -723,7 +724,8 @@ metabolisms: Bloodstream: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 6 - !type:ModifyBloodLevel amount: 6 @@ -861,7 +863,8 @@ Bloodstream: effects: # what the hell, this isn't satiating at all!! - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger factor: -1 # Should heal quite literally everything, use in very small amounts @@ -1155,7 +1158,8 @@ damage: types: Blunt: -2.25 # 9 per u - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger factor: -1 - !type:HealthChange conditions: @@ -1178,7 +1182,8 @@ metabolisms: Digestion: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 3 - !type:HealthChange conditions: diff --git a/Resources/Prototypes/Reagents/narcotics.yml b/Resources/Prototypes/Reagents/narcotics.yml index 8a3a22ea35..9eb6d1f348 100644 --- a/Resources/Prototypes/Reagents/narcotics.yml +++ b/Resources/Prototypes/Reagents/narcotics.yml @@ -202,9 +202,11 @@ Asphyxiation: -2 # Side effects - !type:ResetNarcolepsy - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger factor: 1 - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 1 - type: reagent diff --git a/Resources/Prototypes/Reagents/pyrotechnic.yml b/Resources/Prototypes/Reagents/pyrotechnic.yml index c5f338c4ef..2964c4bc88 100644 --- a/Resources/Prototypes/Reagents/pyrotechnic.yml +++ b/Resources/Prototypes/Reagents/pyrotechnic.yml @@ -167,7 +167,8 @@ metabolisms: Bloodstream: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 1 conditions: - !type:MetabolizerTypeCondition @@ -209,7 +210,8 @@ metabolisms: Bloodstream: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: 1 conditions: - !type:MetabolizerTypeCondition diff --git a/Resources/Prototypes/Reagents/toxins.yml b/Resources/Prototypes/Reagents/toxins.yml index 8f407e30b9..501ec30973 100644 --- a/Resources/Prototypes/Reagents/toxins.yml +++ b/Resources/Prototypes/Reagents/toxins.yml @@ -178,7 +178,8 @@ metabolisms: Bloodstream: effects: - - !type:SatiateThirst + - !type:Satiate + satiationType: Thirst factor: -1.5 - !type:HealthChange ignoreResistances: true @@ -696,12 +697,14 @@ effects: - !type:HealthChange conditions: - - !type:HungerCondition + - !type:SatiationCondition + satiationType: Hunger max: 50 damage: types: Poison: 2 - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger factor: -6 # inspired by the spider neurotoxin GsMtx-4 @@ -757,7 +760,8 @@ metabolisms: Digestion: effects: - - !type:SatiateHunger + - !type:Satiate + satiationType: Hunger factor: 1 conditions: - !type:MetabolizerTypeCondition