Refactor hunger and thirst systems (#34166)

* 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

* 我做泥團子 😔

* 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 <ajcm_mail@disroot.org>
This commit is contained in:
Centronias 2026-08-08 15:44:24 -07:00 • committed by GitHub
parent 6f6a5694c8
commit 8ad82dc665
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
96 changed files with 2701 additions and 1529 deletions

View file

@ -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<ShowHungerIconsComponent>
{
[Dependency] private HungerSystem _hunger = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HungerComponent, GetStatusIconsEvent>(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);
}
}

View file

@ -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;
/// <summary>
/// This system handles the inclusion of satiation status icons for entities with the <see cref="ShowSatiationIconsComponent"/>
/// </summary>
public sealed partial class ShowSatiationIconsSystem : EquipmentHudSystem<ShowSatiationIconsComponent>
{
[Dependency] private SatiationSystem _satiation = default!;
private HashSet<ProtoId<SatiationTypePrototype>> _types = [];
/// <inheritdoc />
protected override void UpdateInternal(RefreshEquipmentHudEvent<ShowSatiationIconsComponent> 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);
}
}
/// <inheritdoc />
protected override void DeactivateInternal()
{
base.DeactivateInternal();
_types.Clear();
}
[SubscribeLocalEvent]
private void OnGetStatusIconsEvent(Entity<SatiationComponent> entity, ref GetStatusIconsEvent args)
{
if (!IsActive)
{
return;
}
foreach (var shownTypeId in _types)
{
if (_satiation.GetStatusIconOrNull(entity, shownTypeId) is { } iconId)
{
args.StatusIcons.Add(iconId);
}
}
}
}

View file

@ -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<ShowThirstIconsComponent>
{
[Dependency] private ThirstSystem _thirst = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ThirstComponent, GetStatusIconsEvent>(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);
}
}

View file

@ -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<HungerComponent>(Player);
var thirstComponent = Comp<ThirstComponent>(Player);
var hungerSystem = SEntMan.System<HungerSystem>();
var thirstSystem = SEntMan.System<ThirstSystem>();
var satiationComponent = Comp<SatiationComponent>(Player);
var entity = new Entity<SatiationComponent>(SPlayer, satiationComponent);
var satiationSystem = SEntMan.System<SatiationSystem>();
var ingestionSystem = SEntMan.System<IngestionSystem>();
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));

View file

@ -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<SatiationTypePrototype> 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<SatiationComponent>(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<SatiationComponent>(SSpawn(TestProto));
var dict = new Dictionary<SatiationValue, int>
{
// 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<SatiationComponent>(SSpawn(TestProto));
var dict = new Dictionary<SatiationValue, int>
{
// 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<SatiationComponent>(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)
{
/// <summary>
/// Returns the receiver <see cref="Constraint"/> when <c>#define DEBUG</c> is false, otherwise returns a
/// <see cref="ThrowsConstraint"/> with exception type <see cref="DebugAssertException"/>. This is useful when
/// something throws in Debug, but fails gracefully otherwise.
/// </summary>
// 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<DebugAssertException>() : constraint;
#pragma warning restore CS0162 // Unreachable code detected
}
}

View file

@ -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<HungerComponent>(uid, out var hunger))
if (TryComp<SatiationComponent>(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))

View file

@ -1,26 +0,0 @@
using Content.Shared.Hands.Components;
using Content.Shared.Nutrition.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.NPC.HTN.Preconditions;
/// <summary>
/// Returns true if the active hand entity has the specified components.
/// </summary>
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<EntityUid>(NPCBlackboard.Owner, out var owner, _entManager))
{
return false;
}
return _entManager.TryGetComponent<HungerComponent>(owner, out var hunger) ? hunger.CurrentThreshold <= MinHungerState : false;
}
}

View file

@ -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;
/// <summary>
/// IsMet if has a satiation component whose satiation for <see cref="BaseSatiationPrecondition.SatiationType"/> meets
/// the criteria specified by <see cref="Above"/> and <see cref="Below"/>.
/// </summary>
/// <seealso cref="SatiationSystem.IsValueInRange"/>
public sealed partial class BaseSatiationPrecondition : HTNPrecondition
{
[Dependency] private IEntityManager _entManager = default!;
/// <summary>
/// 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.
/// </summary>
[DataField]
public SatiationValue? Above;
/// <summary>
/// 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.
/// </summary>
[DataField]
public SatiationValue? Below;
/// <summary>
/// The type of the satiation considered by this condition. If the agent does not have this satiation type, this
/// condition can never be met.
/// </summary>
[DataField]
public ProtoId<SatiationTypePrototype> SatiationType;
/// <inheritdoc/>
public override bool IsMet(NPCBlackboard blackboard)
{
if (!blackboard.TryGetValue<EntityUid>(NPCBlackboard.Owner, out var owner, _entManager) ||
!_entManager.TryGetComponent<SatiationComponent>(owner, out var satiation))
return false;
return _entManager.System<SatiationSystem>()
.IsValueInRange(
(owner, satiation),
SatiationType,
Above,
Below
);
}
}

View file

@ -1,26 +0,0 @@
using Content.Shared.Hands.Components;
using Content.Shared.Nutrition.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.NPC.HTN.Preconditions;
/// <summary>
/// Returns true if the active hand entity has the specified components.
/// </summary>
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<EntityUid>(NPCBlackboard.Owner, out var owner, _entManager))
{
return false;
}
return _entManager.TryGetComponent<ThirstComponent>(owner, out var thirst) ? thirst.CurrentThirstThreshold <= MinThirstState : false;
}
}

View file

@ -1,6 +1,12 @@
using Content.Shared.Nutrition.Prototypes;
namespace Content.Server.NPC.Queries.Considerations;
public sealed partial class DrinkValueCon : UtilityConsideration
{
/// <summary>
/// This consideration will return 0 if the entity's thirst is above this value.
/// </summary>
[DataField(required: true)]
public SatiationValue ThirstThreshold;
}

View file

@ -1,6 +1,12 @@
using Content.Shared.Nutrition.Prototypes;
namespace Content.Server.NPC.Queries.Considerations;
public sealed partial class FoodValueCon : UtilityConsideration
{
/// <summary>
/// This consideration will return 0 if the entity's hunger is above this value.
/// </summary>
[DataField(required: true)]
public SatiationValue HungerThreshold;
}

View file

@ -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<PuddleComponent> _puddleQuery = default!;
@ -164,7 +165,7 @@ public sealed partial class NPCUtilitySystem : EntitySystem
var owner = blackboard.GetValue<EntityUid>(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<IgnoreBadFoodComponent>(owner);
// only eat when hungry or if it will eat anything
if (TryComp<HungerComponent>(owner, out var hunger) && hunger.CurrentThreshold > HungerThreshold.Okay && avoidBadFood)
if (TryComp<SatiationComponent>(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<ThirstComponent>(owner, out var thirst) && thirst.CurrentThirstThreshold > ThirstThreshold.Okay)
if (TryComp<SatiationComponent>(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();

View file

@ -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;
/// <summary>
/// A minium hunger threshold for extracting nutrition.
/// A minium hunger threshold for extracting nutrition, as specified by a <see cref="SatiationPrototype.Thresholds"/>.
/// Ignored when emagged.
/// </summary>
[DataField("minHungerThreshold")]
public HungerThreshold MinHungerThreshold = HungerThreshold.Okay;
[DataField(required: true)]
public SatiationValue MinHungerThreshold;
}

View file

@ -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<InteractionPopupComponent>(uid, out var interactionPopup))
_audio.PlayPvs(interactionPopup.InteractSuccessSound, uid);
_hunger.ModifyHunger(uid, -component.HungerPerBirth);
_hunger.ModifyHunger(partner, -component.HungerPerBirth);
if (TryComp<SatiationComponent>(uid, out var satiation))
{
_satiation.ModifyValue((uid, satiation), SatiationSystem.Hunger, -component.HungerPerBirth);
}
if (TryComp<SatiationComponent>(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<HungerComponent>(uid, out var hunger) && _hunger.GetHungerThreshold(hunger) < HungerThreshold.Okay)
return false;
// If no satiations, no limit to reproduction
if (!TryComp<SatiationComponent>(uid, out var satiation))
{
return true;
}
if (TryComp<ThirstComponent>(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<ActorComponent>(uid) || TryComp<MindContainerComponent>(uid, out var mind) && mind.HasMind)

View file

@ -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<SatiationComponent>? 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<HungerComponent>(occupant, out var hunger))
if (!TryComp<SatiationComponent>(firstEntity, out var satiation))
return false;
if (_hunger.GetHunger(hunger) < component.NutritionPerSecond)
Entity<SatiationComponent> 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<EmaggedComponent>(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)
{

View file

@ -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<HungerSystem>().SetHunger(playerEntity, hungryThreshold, hunger);
}
}

View file

@ -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<HungerSystem>().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<ThirstSystem>().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<HungerThreshold>(), nameof(HungerThreshold)),
"thirst" => CompletionResult.FromHintOptions(Enum.GetNames<ThirstThreshold>(), nameof(ThirstThreshold)),
_ => CompletionResult.Empty,
};
}
default:
return CompletionResult.Empty;
}
}
}

View file

@ -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;
/// <summary>
/// Sets the running player's entity's satiation of a given type to a given value or threshold.
/// </summary>
[AdminCommand(AdminFlags.Debug)]
public sealed partial class SetSatiation : LocalizedEntityCommands
{
[Dependency] private IPrototypeManager _proto = default!;
[Dependency] private SatiationSystem _satiation = default!;
/// <inheritdoc/>
public override string Command => "setsatiation";
/// <inheritdoc/>
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<SatiationComponent>(playerEntity, out var comp))
{
shell.WriteError(Loc.GetString("shell-entity-with-uid-lacks-component",
("uid", playerEntity.Id),
("componentName", nameof(SatiationComponent))));
return;
}
ProtoId<SatiationTypePrototype> 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);
}
/// <inheritdoc/>
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (shell.Player?.AttachedEntity is not { } playerEntity ||
!EntityManager.TryGetComponent<SatiationComponent>(playerEntity, out var comp))
return CompletionResult.Empty;
var entity = new Entity<SatiationComponent>(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<SatiationPrototype>(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;
}
}
}

View file

@ -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<ThirstSystem>().SetThirst(playerEntity, thirst, thirstyThreshold);
}
}

View file

@ -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;
/// <summary>
/// This command sets the specified <see cref="Satiation">Satiation(s)</see> to 10% of its maximum.
/// </summary>
[AdminCommand(AdminFlags.Debug)]
public sealed partial class Unsatiate : LocalizedEntityCommands
{
[Dependency] private IPrototypeManager _protoMan = default!;
[Dependency] private SatiationSystem _satiation = default!;
/// <inheritdoc/>
public override string Command => "unsatiate";
/// <inheritdoc/>
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<SatiationComponent> 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<SatiationComponent> entity, string[] args)
{
// Check all of the types given before modifying any of them.
foreach (var arg in args)
{
if (_protoMan.TryIndex<SatiationPrototype>(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);
}
}
/// <inheritdoc/>
// 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<SatiationComponent>(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));
}
}

View file

@ -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<HungerComponent>(uid, out var hunger))
if (!TryComp<SatiationComponent>(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<RatKingServantComponent>(servant);
comp.King = uid;
@ -74,17 +76,17 @@ namespace Content.Server.RatKing
if (args.Handled)
return;
if (!TryComp<HungerComponent>(uid, out var hunger))
if (!TryComp<SatiationComponent>(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);

View file

@ -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<RespiratorComponent>(target);
RemComp<BarotraumaComponent>(target);
RemComp<HungerComponent>(target);
RemComp<ThirstComponent>(target);
RemComp<SatiationComponent>(target);
RemComp<ReproductiveComponent>(target);
RemComp<ReproductivePartnerComponent>(target);
RemComp<LegsParalyzedComponent>(target);

View file

@ -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;
/// <summary>
/// If the entity's hunger satiation is below this value, it cannot be milked.
/// </summary>
[DataField(required: true), AutoNetworkedField]
public SatiationValue MinHungerThreshold;
/// <summary>
/// How long to wait before producing.
/// </summary>

View file

@ -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;
/// </summary>
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<SatiationComponent>(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 !?

View file

@ -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;
/// <summary>
/// If the entity's hunger satiation is below this value, it cannot be milked.
/// </summary>
[DataField(required: true), AutoNetworkedField]
public SatiationValue MinHungerThreshold;
/// <summary>
/// How long to wait before growing wool.
/// </summary>

View file

@ -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;
/// </summary>
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<SatiationComponent>(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 _);

View file

@ -1,39 +0,0 @@
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityConditions.Conditions.Body;
/// <summary>
/// Returns true if this entity's hunger is within a specified minimum and maximum.
/// </summary>
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
public sealed partial class TotalHungerEntityConditionSystem : EntityConditionSystem<HungerComponent, HungerCondition>
{
[Dependency] private HungerSystem _hunger = default!;
protected override void Condition(Entity<HungerComponent> entity, ref EntityConditionEvent<HungerCondition> args)
{
var total = _hunger.GetHunger(entity.Comp);
args.Result = total >= args.Condition.Min && total <= args.Condition.Max;
}
}
/// <inheritdoc cref="EntityCondition"/>
public sealed partial class HungerCondition : EntityConditionBase<HungerCondition>
{
/// <summary>
/// Minimum hunger required to fulfill this condition.
/// </summary>
[DataField]
public float Min;
/// <summary>
/// Maximum hunger required to fulfill this condition.
/// </summary>
[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));
}

View file

@ -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;
/// <summary>
/// A condition which passes if the specified <see cref="SatiationTypePrototype"/> is between the specified
/// <see cref="SatiationCondition.Min"/> and <see cref="SatiationCondition.Max"/>. If the entity does not have the
/// specified satiation, the condition evaluates to false.
/// </summary>
public sealed partial class SatiationEntityConditionSystem : EntityConditionSystem<SatiationComponent, SatiationCondition>
{
[Dependency] private SatiationSystem _satiation = default!;
/// <inheritdoc/>
protected override void Condition(Entity<SatiationComponent> entity,
ref EntityConditionEvent<SatiationCondition> 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);
}
}
/// <inheritdoc cref="EntityCondition"/>
public sealed partial class SatiationCondition : EntityConditionBase<SatiationCondition>
{
/// <summary>
/// The value above which this condition will fail. If <see cref="MaxInclusive"/> is false, the condition will fail
/// if at that value as well.
/// </summary>
[DataField]
public float Max = float.PositiveInfinity;
/// <summary>
/// The value below which this condition will fail. If <see cref="MinInclusive"/> is false, the condition will fail
/// if at that value as well.
/// </summary>
[DataField]
public float Min = 0;
/// <summary>
/// If <c>true</c>, values exactly equal to <see cref="Max"/> will NOT fail.
/// </summary>
[DataField]
public bool MaxInclusive = false;
/// <summary>
/// If <c>true</c>, values exactly equal to <see cref="Min"/> will NOT fail.
/// </summary>
[DataField]
public bool MinInclusive = false;
/// <summary>
/// The type of satiation whose value will be considered.
/// </summary>
[DataField(required: true)]
public ProtoId<SatiationTypePrototype> SatiationType;
/// <inheritdoc/>
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));
}
}

View file

@ -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...
/// <summary>
/// Modifies the thirst level of a given entity, multiplied by scale.
/// Attempts to find a <see cref="SatiationComponent"/> on the target, and update its value of the specified
/// <see cref="SatiationTypePrototype"/>.
/// </summary>
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class SatiateThirstEntityEffectsSystem : EntityEffectSystem<ThirstComponent, SatiateThirst>
public sealed partial class SatiateEntityEffectsSystem : EntityEffectSystem<SatiationComponent, Satiate>
{
[Dependency] private ThirstSystem _thirst = default!;
protected override void Effect(Entity<ThirstComponent> entity, ref EntityEffectEvent<SatiateThirst> args)
[Dependency] private SatiationSystem _satiation = default!;
protected override void Effect(Entity<SatiationComponent> entity, ref EntityEffectEvent<Satiate> args)
{
_thirst.ModifyThirst(entity, entity.Comp, args.Effect.Factor * args.Scale);
_satiation.ModifyValue(entity, args.Effect.SatiationType, args.Effect.Factor * args.Scale);
}
}
/// <summary>
/// Modifies the hunger level of a given entity, multiplied by scale.
/// </summary>
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class SatiateHungerEntityEffectsSystem : EntityEffectSystem<HungerComponent, SatiateHunger>
{
[Dependency] private HungerSystem _hunger = default!;
protected override void Effect(Entity<HungerComponent> entity, ref EntityEffectEvent<SatiateHunger> args)
{
_hunger.ModifyHunger(entity, args.Effect.Factor * args.Scale, entity.Comp);
}
}
/// <summary>
/// A type of <see cref="EntityEffectBase{T}"/> made for satiation effects.
/// </summary>
/// <typeparam name="T">The effect inheriting this BaseEffect</typeparam>
/// <inheritdoc cref="EntityEffect"/>
public abstract partial class Satiate<T> : EntityEffectBase<T> where T : EntityEffectBase<T>
public sealed partial class Satiate : EntityEffectBase<Satiate>
{
public const float AverageSatiation = 3f; // Magic number. Not sure how it was calculated since I didn't make it.
/// <summary>
/// Change in satiation.
/// How much the specified <see cref="SatiationType"/> is modified by this effect.
/// </summary>
[DataField]
public float Factor = 1.5f;
}
/// <inheritdoc cref="Satiate{T}"/>
public sealed partial class SatiateThirst : Satiate<SatiateThirst>
{
public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> Loc.GetString("entity-effect-guidebook-satiate-thirst", ("chance", Probability), ("relative", Factor / AverageSatiation));
}
/// <summary>
/// The type of satiation to modify.
/// </summary>
[DataField(required: true)]
public ProtoId<SatiationTypePrototype> SatiationType;
/// <inheritdoc cref="Satiate{T}"/>
public sealed partial class SatiateHunger : Satiate<SatiateHunger>
{
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)
);
}
}

View file

@ -104,8 +104,7 @@ public partial class InventorySystem
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowJobIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowHealthBarsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowHealthIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowHungerIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowThirstIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowSatiationIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowMindShieldIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowSyndicateIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowCriminalRecordIconsComponent>>(RefRelayInventoryEvent);

View file

@ -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<HungerComponent>(uid, out var hunger))
_hunger.ModifyHunger(uid, hungerAdded, hunger);
if (TryComp<ThirstComponent>(uid, out var thirst))
_thirst.ModifyThirst(uid, thirst, thirstAdded);
if (TryComp<SatiationComponent>(uid, out var satiation))
{
Entity<SatiationComponent> 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;

View file

@ -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;
/// <summary>
/// If the entity's hunger satiation is below this value, it cannot reproduce.
/// </summary>
[DataField]
public SatiationValue? MinHungerThreshold;
/// <summary>
/// If the entity's thirst satiation is below this value, it cannot reproduce.
/// </summary>
[DataField]
public SatiationValue? MinThirstThreshold;
/// <summary>
/// Popup shown when an entity gives birth.
/// Configurable for things like laying eggs.

View file

@ -1,31 +0,0 @@
using Content.Shared.Nutrition.EntitySystems;
using Robust.Shared.GameStates;
namespace Content.Shared.Nutrition.Components;
/// <summary>
/// Adds text to the entity's description box based on its current hunger threshold.
/// </summary>
[RegisterComponent, NetworkedComponent]
[Access(typeof(ExaminableHungerSystem))]
public sealed partial class ExaminableHungerComponent : Component
{
/// <summary>
/// Dictionary of hunger thresholds to LocIds of the messages to display.
/// </summary>
[DataField]
public Dictionary<HungerThreshold, LocId> 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"}
};
/// <summary>
/// LocId of a fallback message to display if the entity has no <see cref="HungerComponent"/>
/// or does not have a value in <see cref="Descriptions"/> for the current threshold.
/// </summary>
public LocId NoHungerDescription = "examinable-hunger-component-examine-none";
}

View file

@ -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;
/// <summary>
/// Component which specifies what <see cref="ExaminableSatiationComponent"/> shows in examine descriptions.
/// </summary>
/// <seealso cref="ExaminableSatiationSystem"/>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)]
[Access(typeof(ExaminableSatiationSystem))]
public sealed partial class ExaminableSatiationComponent : Component
{
/// <summary>
/// Examination localization string IDs per threshold per satiation type.
/// </summary>
[DataField(required: true), AutoNetworkedField]
public Dictionary<ProtoId<SatiationTypePrototype>, SatiationThresholds<LocId?>> Satiations;
}

View file

@ -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
{
/// <summary>
/// The hunger value as authoritatively set by the server as of <see cref="LastAuthoritativeHungerChangeTime"/>.
/// This value should be updated relatively infrequently. To get the current hunger, which changes with each update,
/// use <see cref="HungerSystem.GetHunger"/>.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
[AutoNetworkedField]
public float LastAuthoritativeHungerValue;
/// <summary>
/// The time at which <see cref="LastAuthoritativeHungerValue"/> was last updated.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
public TimeSpan LastAuthoritativeHungerChangeTime;
/// <summary>
/// The base amount at which <see cref="LastAuthoritativeHungerValue"/> decays.
/// </summary>
/// <remarks>Any time this is modified, <see cref="HungerSystem.SetAuthoritativeHungerValue"/> should be called.</remarks>
[DataField("baseDecayRate"), ViewVariables(VVAccess.ReadWrite)]
public float BaseDecayRate = 0.01666666666f;
/// <summary>
/// The actual amount at which <see cref="LastAuthoritativeHungerValue"/> decays.
/// Affected by <seealso cref="CurrentThreshold"/>
/// </summary>
/// <remarks>Any time this is modified, <see cref="HungerSystem.SetAuthoritativeHungerValue"/> should be called.</remarks>
[DataField("actualDecayRate"), ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
public float ActualDecayRate;
/// <summary>
/// The last threshold this entity was at.
/// Stored in order to prevent recalculating
/// </summary>
[DataField("lastThreshold"), ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
public HungerThreshold LastThreshold;
/// <summary>
/// The current hunger threshold the entity is at
/// </summary>
/// <remarks>Any time this is modified, <see cref="HungerSystem.SetAuthoritativeHungerValue"/> should be called.</remarks>
[DataField("currentThreshold"), ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
public HungerThreshold CurrentThreshold;
/// <summary>
/// A dictionary relating HungerThreshold to the amount of <see cref="HungerSystem.GetHunger">current hunger</see> needed for each one
/// </summary>
[DataField("thresholds", customTypeSerializer: typeof(DictionarySerializer<HungerThreshold, float>))]
[AutoNetworkedField]
public Dictionary<HungerThreshold, float> Thresholds = new()
{
{ HungerThreshold.Overfed, 200.0f },
{ HungerThreshold.Okay, 150.0f },
{ HungerThreshold.Peckish, 100.0f },
{ HungerThreshold.Starving, 50.0f },
{ HungerThreshold.Dead, 0.0f }
};
/// <summary>
/// A dictionary relating hunger thresholds to corresponding alerts.
/// </summary>
[DataField("hungerThresholdAlerts")]
[AutoNetworkedField]
public Dictionary<HungerThreshold, ProtoId<AlertPrototype>> HungerThresholdAlerts = new()
{
{ HungerThreshold.Peckish, "Peckish" },
{ HungerThreshold.Starving, "Starving" },
{ HungerThreshold.Dead, "Starving" }
};
[DataField]
public ProtoId<AlertCategoryPrototype> HungerAlertCategory = "Hunger";
/// <summary>
/// A dictionary relating HungerThreshold to how much they modify <see cref="BaseDecayRate"/>.
/// </summary>
[DataField("hungerThresholdDecayModifiers", customTypeSerializer: typeof(DictionarySerializer<HungerThreshold, float>))]
[AutoNetworkedField]
public Dictionary<HungerThreshold, float> HungerThresholdDecayModifiers = new()
{
{ HungerThreshold.Overfed, 1.2f },
{ HungerThreshold.Okay, 1f },
{ HungerThreshold.Peckish, 0.8f },
{ HungerThreshold.Starving, 0.6f },
{ HungerThreshold.Dead, 0.6f }
};
/// <summary>
/// The amount of slowdown applied when an entity is starving
/// </summary>
[DataField("starvingSlowdownModifier"), ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
public float StarvingSlowdownModifier = 0.75f;
/// <summary>
/// Damage dealt when your current threshold is at HungerThreshold.Dead
/// </summary>
[DataField("starvationDamage")]
public DamageSpecifier? StarvationDamage;
/// <summary>
/// The time when the hunger threshold will update next.
/// </summary>
[DataField("nextUpdateTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
[AutoPausedField]
public TimeSpan NextThresholdUpdateTime;
/// <summary>
/// The time between each hunger threshold update.
/// </summary>
[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,
}

View file

@ -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;
/// <summary>
/// A component which is basically just a collection of <see cref="Satiation"/>s keyed by their
/// <see cref="SatiationTypePrototype"/>s.
/// </summary>
[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
{
/// <summary>
/// The actual <see cref="Satiation"/>s this entity has, keyed by their <see cref="SatiationTypePrototype">type</see>.
/// </summary>
[DataField(required: true)]
[AutoNetworkedField]
public Dictionary<ProtoId<SatiationTypePrototype>, Satiation> Satiations = [];
/// <summary>
/// Checks if this has a <see cref="Satiation"/> of the specified <paramref name="type"/>.
/// </summary>
[Access(Other = AccessPermissions.Execute)]
public bool Has(ProtoId<SatiationTypePrototype> type) => GetOrNull(type) != null;
/// <summary>
/// Gets the <see cref="Satiation"/> of the given <paramref name="type"/> on this component, or
/// <c>null</c> if no such satiation exists.
/// </summary>
[Access(Other = AccessPermissions.Execute)]
public Satiation? GetOrNull(ProtoId<SatiationTypePrototype> type) => Satiations.GetValueOrDefault(type);
}

View file

@ -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;
/// <summary>
/// This component causes its entity to continuously take damage based on the entity's current satiation.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause]
[Access(typeof(SatiationDamageSystem))]
public sealed partial class SatiationDamageComponent : Component
{
/// <summary>
/// Damage values by satiation threshold, for a satiation type.
/// </summary>
[DataField(required: true), AutoNetworkedField]
public Dictionary<ProtoId<SatiationTypePrototype>, SatiationThresholds<DamageSpecifier?>> Satiations;
/// <summary>
/// How often the damage is applied.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan Frequency = TimeSpan.FromSeconds(1);
/// <summary>
/// Runtime data, indicating when the next damage application will occur.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
public TimeSpan NextDamageTime;
}

View file

@ -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;
/// <summary>
/// This component causes its entity to have movement speed modifiers applied based on the entity's current satiations.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true),
Access(typeof(SatiationSpeedModifierSystem))]
public sealed partial class SatiationSpeedModifierComponent : Component
{
/// <summary>
/// Speed modifiers, expressed as <see cref="float"/>s, contained within <see cref="SatiationThresholds{T}"/>,
/// keyed by the <see cref="SatiationTypePrototype">satiation types</see> they are thresholds for.
/// </summary>
[DataField(required: true), AutoNetworkedField]
public Dictionary<ProtoId<SatiationTypePrototype>, SatiationThresholds<float>> Satiations;
}

View file

@ -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;
/// <summary>
/// The time when the hunger will update next.
/// </summary>
[DataField("nextUpdateTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
[AutoPausedField]
public TimeSpan NextUpdateTime;
/// <summary>
/// The time between each update.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public TimeSpan UpdateRate = TimeSpan.FromSeconds(1);
[DataField("thresholds")]
[AutoNetworkedField]
public Dictionary<ThirstThreshold, float> 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<AlertCategoryPrototype> ThirstyCategory = "Thirst";
public static readonly Dictionary<ThirstThreshold, ProtoId<AlertPrototype>> 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,
}

View file

@ -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;
/// <summary>
/// This abstract system provides a convenient interface for implementing effects which react to changes in
/// <see cref="Satiation"/> thresholds.
/// </summary>
/// <example>
/// <see cref="SatiationDamageSystem"/> 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.
/// </example>
/// <typeparam name="TComp">The type of component this system interacts with</typeparam>
/// <typeparam name="T">
/// The type of value this system maintains. It must be contained within a <see cref="SatiationThresholds{T}"/>
/// <see cref="GetThresholds">accessible via <typeparamref name="TComp">TComp</typeparamref></see>.
/// </typeparam>
/// <remarks>Note that this <b>is not</b> related to <see cref="EntityEffects"/></remarks>
public abstract partial class BaseSatiationEffectSystem<TComp, T> : EntitySystem where TComp : Component
{
[Dependency] private SatiationSystem _satiation = default!;
[Dependency] private EntityQuery<SatiationComponent> _satiationQuery;
[Dependency] private IGameTiming _timing = default!;
/// <summary>
/// How to access <see cref="SatiationThresholds{T}"/> via a <typeparamref name="TComp"/>.
/// </summary>
protected abstract Dictionary<ProtoId<SatiationTypePrototype>, SatiationThresholds<T>> GetThresholds(TComp comp);
/// <summary>
/// The default <typeparamref name="T"/> value to set our maintained value to in the case that our current satiation
/// is outside of <see cref="GetThresholds"/>.
/// </summary>
protected abstract T DefaultValue();
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TComp, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<TComp, SatiationUpdateEvent>(OnSatiationUpdate);
}
/// <inheritdoc/>
// Updates maintained values when reaching the projected threshold time.
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<TComp, SatiationComponent>();
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<TComp> 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<SatiationComponent>(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<TComp> entity, ref SatiationUpdateEvent args)
{
if (!_satiationQuery.TryComp(entity, out var comp))
return;
UpdateSatiation(entity, comp, args.Type);
}
/// <summary>
/// Updates the maintained <typeparamref name="T"/> based on the entity's satiation.
/// </summary>
private void UpdateSatiation(Entity<TComp> entity, SatiationComponent comp, ProtoId<SatiationTypePrototype> 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);
}
/// <summary>
/// This function is called after <see cref="UpdateSatiation"/> completes its work maintaining <typeparamref name="T"/>.
/// </summary>
protected virtual void AfterSatiationUpdate(Entity<TComp> entity) { }
}

View file

@ -1,40 +0,0 @@
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
using Content.Shared.Nutrition.Components;
namespace Content.Shared.Nutrition.EntitySystems;
/// <inheritdoc cref="ExaminableHungerComponent"/>
public sealed partial class ExaminableHungerSystem : EntitySystem
{
[Dependency] private HungerSystem _hunger = default!;
[Dependency] private EntityQuery<HungerComponent> _hungerQuery = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ExaminableHungerComponent, ExaminedEvent>(OnExamine);
}
/// <summary>
/// Defines the text provided on examine.
/// Changes depending on the amount of hunger the target has.
/// </summary>
private void OnExamine(Entity<ExaminableHungerComponent> 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);
}
}

View file

@ -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;
/// <summary>
/// Makes descriptions specified in <see cref="ExaminableSatiationComponent"/> show up in response to
/// <see cref="ExaminedEvent"/>s.
/// </summary>
/// <seealso cref="ExaminableSatiationComponent"/>
public sealed partial class ExaminableSatiationSystem : BaseSatiationEffectSystem<ExaminableSatiationComponent, LocId?>
{
protected override Dictionary<ProtoId<SatiationTypePrototype>, SatiationThresholds<LocId?>> GetThresholds(
ExaminableSatiationComponent comp
) => comp.Satiations;
protected override LocId? DefaultValue() => null;
[SubscribeLocalEvent]
private void OnExamine(Entity<ExaminableSatiationComponent> 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)));
}
}
}

View file

@ -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<SatiationIconPrototype> HungerIconOverfedId = "HungerIconOverfed";
private static readonly ProtoId<SatiationIconPrototype> HungerIconPeckishId = "HungerIconPeckish";
private static readonly ProtoId<SatiationIconPrototype> HungerIconStarvingId = "HungerIconStarving";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HungerComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<HungerComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<HungerComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
SubscribeLocalEvent<HungerComponent, RejuvenateEvent>(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);
}
/// <summary>
/// Gets the current hunger value of the given <see cref="HungerComponent"/>.
/// </summary>
public float GetHunger(HungerComponent component)
{
var dt = _timing.CurTime - component.LastAuthoritativeHungerChangeTime;
var value = component.LastAuthoritativeHungerValue - (float)dt.TotalSeconds * component.ActualDecayRate;
return ClampHungerWithinThresholds(component, value);
}
/// <summary>
/// Adds to the current hunger of an entity by the specified value
/// </summary>
/// <param name="uid"></param>
/// <param name="amount"></param>
/// <param name="component"></param>
public void ModifyHunger(EntityUid uid, float amount, HungerComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
SetHunger(uid, GetHunger(component) + amount, component);
}
/// <summary>
/// Sets the current hunger of an entity to the specified value
/// </summary>
/// <param name="uid"></param>
/// <param name="amount"></param>
/// <param name="component"></param>
public void SetHunger(EntityUid uid, float amount, HungerComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
SetAuthoritativeHungerValue((uid, component), amount);
UpdateCurrentThreshold(uid, component);
}
/// <summary>
/// Sets <see cref="HungerComponent.LastAuthoritativeHungerValue"/> and
/// <see cref="HungerComponent.LastAuthoritativeHungerChangeTime"/>, and dirties this entity. This "resets" the
/// starting point for <see cref="GetHunger"/>'s calculation.
/// </summary>
/// <param name="entity">The entity whose hunger will be set.</param>
/// <param name="value">The value to set the entity's hunger to.</param>
private void SetAuthoritativeHungerValue(Entity<HungerComponent> 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);
}
}
/// <summary>
/// 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
/// </summary>
/// <param name="component"></param>
/// <param name="food"></param>
/// <returns></returns>
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;
}
/// <summary>
/// A check that returns if the entity is below a hunger threshold.
/// </summary>
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<HungerComponent>();
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);
}
}
}

View file

@ -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();
}
}
}

View file

@ -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;
/// <summary>
/// A need whose value decays over time. Examples include Thirst and Hunger.
/// </summary>
[DataDefinition, Serializable, NetSerializable, Access(typeof(SatiationSystem))]
public sealed partial class Satiation
{
/// <summary>
/// This satiation's <see cref="SatiationTypePrototype"/>.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public ProtoId<SatiationTypePrototype> SatiationType;
/// <summary>
/// This satiation's <see cref="SatiationPrototype"/>, which describes how it changes over time.
/// </summary>
[DataField(required: true), ViewVariables(VVAccess.ReadOnly)]
public ProtoId<SatiationPrototype> Prototype;
/// <summary>
/// The value of this satiation as of <see cref="LastAuthoritativeChangeTime"/>.
/// </summary>
/// <remarks>
/// To get the current value at any arbitrary time, use <see cref="SatiationSystem.GetValueOrNull"/>
/// </remarks>.
[DataField, ViewVariables(VVAccess.ReadOnly)]
public float LastAuthoritativeValue = float.MinValue;
/// <summary>
/// The last time <see cref="LastAuthoritativeValue"/> was modified.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadOnly)]
public TimeSpan LastAuthoritativeChangeTime;
/// <summary>
/// The rate at which this satiation value is expected to decay. It is a combination of
/// <see cref="SatiationPrototype.BaseDecayRate"/> and modifiers.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public float ActualDecayRate;
/// <summary>
/// When <see cref="ActualDecayRate"/> is expected to change, if nothing but normal decay affects this satiation.
/// This is used to predict satiation updates on clients.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan? NextDecayRateModUpdateTime;
/// <summary>
/// <see cref="NextDecayRateModUpdateTime"/>, but for satiation alerts.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan? NextAlertUpdateTime;
}
/// <summary>
/// A combination of configuration (<see cref="Thresholds"/>) and state (<see cref="Current"/>) which describes how a
/// <typeparamref name="T"/> value changes related to <see cref="Satiation"/>, as well as its current value.
/// </summary>
/// <example>
/// <see cref="SatiationDamageSystem"/> uses this to describe and track damage descriptors based on satiations, applying
/// the damage regularly over time. <see cref="Thresholds"/> describes the damage to apply at various levels of
/// satiation, and <see cref="Current"/> tracks the current damage to apply every update, meaning it doesn't need to be
/// looked up from <see cref="Thresholds"/> on every tick.
/// </example>
/// <remarks>This should probably only ever be used in conjunction with <see cref="BaseSatiationEffectSystem{TComp,T}"/></remarks>
[DataDefinition, Serializable]
public sealed partial class SatiationThresholds<T>
{
/// <summary>
/// The <typeparamref name="T"/> values keyed by the satiation values at or below which the T value becomes "active".
/// </summary>
/// <seealso cref="SatiationSystem.TryGetValueByThreshold"/>
[IncludeDataField]
public Dictionary<SatiationValue, T> Thresholds = [];
/// <summary>
/// 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.
/// </summary>
public TimeSpan? ProjectedThresholdChangeTime;
/// <summary>
/// The current <typeparamref name="T"/> value, at least when maintained by something like
/// <see cref="BaseSatiationEffectSystem{TComp,T}"/>
/// </summary>
public T Current;
}
[TypeSerializer]
public sealed partial class SatiationThresholdsSerializer<T> : ITypeSerializer<SatiationThresholds<T>, MappingDataNode>,
ITypeCopier<SatiationThresholds<T>>
{
public ValidationNode Validate(
ISerializationManager serializationManager,
MappingDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context = null
) => serializationManager.ValidateNode<Dictionary<SatiationValue, T>>(node, context);
public SatiationThresholds<T> Read(
ISerializationManager serializationManager,
MappingDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context = null,
ISerializationManager.InstantiationDelegate<SatiationThresholds<T>>? instanceProvider = null
) => new()
{
Thresholds = serializationManager.Read<Dictionary<SatiationValue, T>>(
node,
context,
hookCtx.SkipHooks,
instanceProvider is { } ip ? () => ip().Thresholds : null,
true
),
};
public DataNode Write(
ISerializationManager serializationManager,
SatiationThresholds<T> value,
IDependencyCollection dependencies,
bool alwaysWrite = false,
ISerializationContext? context = null
) => serializationManager.WriteValue(value.Thresholds, alwaysWrite, context, true);
public void CopyTo(
ISerializationManager serializationManager,
SatiationThresholds<T> source,
ref SatiationThresholds<T> target,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context = null
) => serializationManager.CopyTo(source.Thresholds, ref target.Thresholds, context, true, true);
}

View file

@ -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;
/// <summary>
/// This system implements the behavior of <see cref="SatiationDamageComponent"/>
/// </summary>
public sealed partial class SatiationDamageSystem :
BaseSatiationEffectSystem<SatiationDamageComponent, DamageSpecifier?>
{
[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<SatiationDamageComponent>();
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<ProtoId<SatiationTypePrototype>, SatiationThresholds<DamageSpecifier?>> GetThresholds(
SatiationDamageComponent comp) => comp.Satiations;
}

View file

@ -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<SatiationSpeedModifierComponent, float>
{
[Dependency] private MovementSpeedModifierSystem _movementSpeedModifier = default!;
protected override Dictionary<ProtoId<SatiationTypePrototype>, SatiationThresholds<float>> GetThresholds(
SatiationSpeedModifierComponent comp) => comp.Satiations;
protected override float DefaultValue() => 1f;
protected override void AfterSatiationUpdate(Entity<SatiationSpeedModifierComponent> entity)
{
_movementSpeedModifier.RefreshMovementSpeedModifiers(entity.Owner);
}
[SubscribeLocalEvent]
private static void OnRefreshMovementSpeed(
Entity<SatiationSpeedModifierComponent> entity,
ref RefreshMovementSpeedModifiersEvent args
)
{
foreach (var (_, thresholds) in entity.Comp.Satiations)
{
args.ModifySpeed(thresholds.Current);
}
}
}

View file

@ -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
{
/// <summary>
/// Gets <paramref name="entity"/>'s current value of the satiation of <paramref name="type"/>. If this entity does
/// not have that satiation, returns null.
/// </summary>
public float? GetValueOrNull(
Entity<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> type
)
{
if (GetAndResolveSatiationOfType(entity, type) is not var (satiation, proto))
return null;
return CalculateCurrentValue(satiation, proto);
}
/// <summary>
/// Sets <paramref name="entity"/>'s current satiation of <paramref name="type"/> to a value corresponding to
/// <paramref name="satiationValue"/>. If this entity does not have that satiation, or the key does not correspond to
/// any threshold, has no effect.
/// </summary>
public void SetValue(
Entity<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> 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);
}
/// <summary>
/// Sets <paramref name="entity"/>'s current satiation of <paramref name="type"/> to <paramref name="value"/>. If
/// this entity does not have that satiation, has no effect.
/// </summary>
// [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<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> type,
float value
)
{
if (GetAndResolveSatiationOfType(entity, type) is not var (satiation, proto))
return;
SetAuthoritativeValue(entity, satiation, proto, value);
}
/// <summary>
/// Sets <paramref name="entity"/>'s current satiation of <paramref name="type"/> to its current value plus
/// <paramref name="amount"/>. If this entity does not have that satiation, has no effect.
/// </summary>
public void ModifyValue(
Entity<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> type,
float amount
)
{
if (GetValueOrNull(entity, type) is { } currentValue)
{
SetValue(entity, type, currentValue + amount);
}
}
/// <summary>
/// Returns whether or not the current value (plus optional <paramref name="hypotheticalValueDelta"/>) is between
/// the values described by <paramref name="above"/> and <paramref name="below"/>. If <paramref name="entity"/>
/// does not have a <see cref="Satiation"/> of the specified <paramref name="type"/>, 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 <paramref name="type"/>'s <see cref="SatiationPrototype.Thresholds"/>,
/// all values are considered out-of-range.
/// </summary>
public bool IsValueInRange(
Entity<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> 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;
}
/// <summary>
/// This function returns a value from <see cref="valuesByThreshold"/> based on the current value of
/// <paramref name="entity"/>'s satiation of the given <paramref name="type"/>. The value in
/// <paramref name="valuesByThreshold"/> with the lowest key greater than the current satiation value is returned.
/// </summary>
/// <param name="entity">The entity whose satiation is considered</param>
/// <param name="type">The type of satiation to consider</param>
/// <param name="valuesByThreshold">The values, keyed by <see cref="SatiationValue"/> thresholds</param>
/// <param name="result">The value selected from <paramref name="valuesByThreshold"/></param>
/// <param name="nextLowerThreshold">
/// 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 <see cref="BaseSatiationEffectSystem{TComp,T}"/>'s prediction of value decay.
/// </param>
/// <typeparam name="T">The type of values in <paramref name="valuesByThreshold"/></typeparam>
/// <returns>
/// True if a value was selected. False if <paramref name="valuesByThreshold"/> 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.
/// </returns>
public bool TryGetValueByThreshold<T>(
Entity<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> type,
Dictionary<SatiationValue, T> 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;
}
/// <summary>
/// Looks up the <see cref="StatusIconPrototype"/> appropriate for the given entity's <see cref="Satiation"/> of the
/// specified <paramref name="type"/>. 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.
/// </summary>
/// <remarks>This should almost definitely never be used by anything other than the client's Overlay system</remarks>
public StatusIconPrototype? GetStatusIconOrNull(
Entity<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> 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
/// <summary>
/// Returns the all of the <see cref="SatiationPrototype.Thresholds">key strings</see> of the given
/// <paramref name="type"/> for <paramref name="entity"/>, or empty if no such type exists.
/// </summary>
/// <remarks>
/// It is expected that <paramref name="type"/> is validated with before calling this. If it fails to resolve, an
/// error will be logged.
/// </remarks>
public IEnumerable<string> GetKeysForType(
Entity<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> type
)
{
return GetAndResolveSatiationOfType(entity, type)?.Proto.Thresholds.Keys ?? Enumerable.Empty<string>();
}
/// <summary>
/// Returns the <see cref="SatiationPrototype.MaximumValue"/> of the given <paramref name="type"/> for
/// <paramref name="entity"/>, or null if no such type exists.
/// </summary>
/// <remarks>
/// It is expected that <paramref name="type"/> is validated with before calling this. If it fails to resolve, an
/// error will be logged.
/// </remarks>
public int? GetMaximumValue(
Entity<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> type
) => GetAndResolveSatiationOfType(entity, type)?.Proto.MaximumValue;
#endregion
}

View file

@ -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;
/// <summary>
/// This system manages the <see cref="SatiationComponent"/>. Broadly, what that means is that it handles the decay of
/// satiations in <see cref="Update"/>, and external changes to satiations through accessors like
/// <see cref="ModifyValue"/>.
/// </summary>
public sealed partial class SatiationSystem : EntitySystem
{
[Dependency] private AlertsSystem _alerts = default!;
[Dependency] private IGameTiming _timing = default!;
[Dependency] private EntityQuery<SatiationComponent> _satiationQuery;
/// <summary>
/// The ID of the <c>Hunger</c> satiation type. Provided because it is so commonly used in Content.
/// </summary>
public static readonly ProtoId<SatiationTypePrototype> Hunger = "Hunger";
/// <summary>
/// The ID of the <c>Thirst</c> satiation type. Provided because it is so commonly used in Content.
/// </summary>
public static readonly ProtoId<SatiationTypePrototype> Thirst = "Thirst";
/// <inheritdoc/>
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<SatiationComponent>();
while (query.MoveNext(out var uid, out var component))
{
Entity<SatiationComponent> 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));
}
}
}
}
/// <summary>
/// Sets starting satiation values.
/// </summary>
[SubscribeLocalEvent]
private void OnMapInit(Entity<SatiationComponent> 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));
}
/// <summary>
/// Clears alerts.
/// </summary>
[SubscribeLocalEvent]
private void OnShutdown(Entity<SatiationComponent> entity, ref ComponentShutdown args)
{
foreach (var (_, proto) in GetSatiationsAndTypes(entity))
{
_alerts.ClearAlertCategory(entity.Owner, proto.AlertCategory);
}
}
/// <summary>
/// Sets all satiations to their maximums.
/// </summary>
[SubscribeLocalEvent]
private void OnRejuvenate(Entity<SatiationComponent> entity, ref RejuvenateEvent args)
{
foreach (var type in entity.Comp.Satiations.Keys)
{
SetValue(entity, type, satiationValue: int.MaxValue);
}
}
/// <summary>
/// This helper resolves <paramref name="type"/> and returns the corresponding <see cref="Satiation"/> from
/// <paramref name="satiations"/> along with its <see cref="SatiationPrototype"/>.
/// Returns null if the prototype fails to resolve, or if the component does not have the specified satiation.
/// </summary>
private (Satiation Satiation, SatiationPrototype Proto)? GetAndResolveSatiationOfType(
SatiationComponent satiations,
[ForbidLiteral] ProtoId<SatiationTypePrototype> type
)
{
if (satiations.GetOrNull(type) is not { } satiation ||
!ProtoMan.Resolve(satiation.Prototype, out var proto))
return null;
return (satiation, proto);
}
/// <summary>
/// Similar to <see cref="GetAndResolveSatiationOfType"/>, this helper returns all <see cref="Satiation"/>s on
/// <paramref name="satiations"/> along with their corresponding <see cref="SatiationPrototype"/>s.
/// </summary>
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);
}
}
/// <summary>
/// Calculates the current value of the given <see cref="Satiation"/> by linearly extrapolating the change of the
/// value based on <see cref="Satiation.LastAuthoritativeValue"/>, <see cref="Satiation.LastAuthoritativeChangeTime"/>
/// and <see cref="Satiation.ActualDecayRate"/>
/// </summary>
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);
}
/// <summary>
/// The beating heart of this system, this function sets the given <paramref name="entity"/>'s
/// <paramref name="satiation"/> to <paramref name="value"/>. This involves
/// updating obvious fields on the <see cref="SatiationComponent"/>, but since changes to the value also affect the
/// current threshold, we need to consider all of the effects that has as well.
/// </summary>
private void SetAuthoritativeValue(
Entity<SatiationComponent> 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));
}
/// <remarks>
/// This is basically a reimplementation of <see cref="BaseSatiationEffectSystem{TComp,T}.OnSatiationUpdate"/>.
/// </remarks>
[SubscribeLocalEvent]
private void UpdateAlertsOnSatiationUpdated(Entity<SatiationComponent> 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<SatiationComponent> 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<SatiationComponent> entity,
ProtoId<SatiationTypePrototype> 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<SatiationTypePrototype> Type);

View file

@ -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<SatiationIconPrototype> ThirstIconOverhydratedId = "ThirstIconOverhydrated";
private static readonly ProtoId<SatiationIconPrototype> ThirstIconThirstyId = "ThirstIconThirsty";
private static readonly ProtoId<SatiationIconPrototype> ThirstIconParchedId = "ThirstIconParched";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ThirstComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
SubscribeLocalEvent<ThirstComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ThirstComponent, RejuvenateEvent>(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<ThirstComponent>();
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);
}
}
}

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
[Prototype]
public sealed partial class SatiationPrototype : IPrototype, IInheritingPrototype
{
/// <inheritdoc/>
[IdDataField]
public string ID { get; private set; } = default!;
/// <inheritdoc/>
[ParentDataField(typeof(AbstractPrototypeIdArraySerializer<SatiationPrototype>))]
public string[]? Parents { get; private set; }
/// <inheritdoc/>
[AbstractDataField]
public bool Abstract { get; private set; }
/// <summary>
/// The base rate at which this satiation decreases per second.
/// </summary>
[DataField(required: true)]
public float BaseDecayRate;
/// <summary>
/// The highest value this satiation can have. Increases beyond this value are clamped to this value.
/// </summary>
[DataField(required: true)]
public int MaximumValue;
/// <summary>
/// The definition of key strings referentiable by <see cref="SatiationValue"/>s. Any reference to a key in this map
/// by a <see cref="SatiationValue"/> will be resolved to the numeric value associated to that key here before use.
/// <br/>
/// 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.
/// </summary>
[DataField(customTypeSerializer: typeof(DictionarySerializer<string, int>))]
public Dictionary<string, int> Thresholds = [];
/// <summary>
/// The lowest possible value this satiation can be initialized to.
/// </summary>
[DataField("startingValueMinimum", required: true)]
private SatiationValue _startingValueMinimum = default!;
/// <inheritdoc cref="_startingValueMinimum" />
public int StartingValueMinimum => GetValueOrNull(_startingValueMinimum) ?? 0;
/// <summary>
/// The highest possible value this satiation can be initialized to.
/// </summary>
[DataField("startingValueMaximum", required: true)]
private SatiationValue _startingValueMaximum = default!;
/// <inheritdoc cref="_startingValueMaximum" />
public int StartingValueMaximum => GetValueOrNull(_startingValueMaximum) ?? MaximumValue;
/// <summary>
/// Modifiers to <see cref="BaseDecayRate"/> based on the current threshold.
/// </summary>
/// <seealso cref="Satiation.ActualDecayRate"/>
[DataField(customTypeSerializer: typeof(DictionarySerializer<SatiationValue, float>))]
public Dictionary<SatiationValue, float> DecayModifiers = [];
#region Alerts
/// <summary>
/// The <see cref="AlertCategory"/> which all <see cref="Alerts"/> should belong to.
/// </summary>
[DataField(required: true)]
public ProtoId<AlertCategoryPrototype> AlertCategory;
/// <summary>
/// Alerts to show when in the corresponding threshold.
/// </summary>
[DataField(customTypeSerializer: typeof(DictionarySerializer<SatiationValue, ProtoId<AlertPrototype>?>))]
public Dictionary<SatiationValue, ProtoId<AlertPrototype>?> Alerts = [];
/// <summary>
/// Icons to show to accompany <see cref="Alerts"/> when in the corresponding threshold.
/// </summary>
[DataField(customTypeSerializer: typeof(DictionarySerializer<SatiationValue, ProtoId<SatiationIconPrototype>?>))]
public Dictionary<SatiationValue, ProtoId<SatiationIconPrototype>?> Icons = [];
#endregion
/// <summary>
/// Clamps <paramref name="value"/> between this prototype's highest and lowest values.
/// </summary>
public float ClampSatiationWithinThresholds(float value) => Math.Clamp(value, 0, MaximumValue);
/// <summary>
/// Attempts to get an integer value from the given <paramref name="satiationValue"/>. If
/// <paramref name="satiationValue"/> 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 <see cref="Thresholds"/>; in the case that a key not
/// present in this proto type is requested, returns null.
/// </summary>
public int? GetValueOrNull(SatiationValue satiationValue)
{
if (satiationValue.Key is { } key)
return Thresholds.TryGetValue(key, out var v) ? v : null;
return satiationValue.Value;
}
}
/// <summary>
/// This type is a union of <c>int</c> and <c>string</c> 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
/// <see cref="SatiationPrototype"/> to resolve its integer value before use.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <seealso cref="SatiationPrototype.GetValueOrNull"/>
// 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 };
}

View file

@ -0,0 +1,23 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Nutrition.Prototypes;
/// <summary>
/// Types of satiation, eg. Hunger, Thirst.
/// </summary>
[Prototype]
public sealed partial class SatiationTypePrototype : IPrototype
{
/// <inheritdoc/>
[IdDataField]
public string ID { get; private set; } = default!;
/// <summary>
/// The localization of the name of this type of satiation.
/// </summary>
[DataField("name")]
public LocId NameLoc;
[ViewVariables(VVAccess.ReadOnly)]
public string Name => Loc.GetString(NameLoc);
}

View file

@ -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;
/// <summary>
/// This serializer for <see cref="SatiationValue"/> allows YAMLers to use either an int or a string directly where a
/// <see cref="SatiationValue"/> is expected.
/// </summary>
// 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<SatiationValue, ValueDataNode>
{
public ValidationNode Validate(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context = null
) => int.TryParse(node.Value, out _)
? serializationManager.ValidateNode<int>(node, context)
: serializationManager.ValidateNode<string>(node, context);
public SatiationValue Read(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context = null,
ISerializationManager.InstantiationDelegate<SatiationValue>? instanceProvider = null
) => int.TryParse(node.Value, out _)
? serializationManager.Read<int>(node, context)
: serializationManager.Read<string>(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);
}

View file

@ -1,9 +0,0 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Overlays;
/// <summary>
/// This component allows you to see the hungriness of mobs.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ShowHungerIconsComponent : Component { }

View file

@ -0,0 +1,18 @@
using Content.Shared.Nutrition.Prototypes;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Overlays;
/// <summary>
/// This component allows the owner to see the satiation of mobs.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ShowSatiationIconsComponent : Component
{
/// <summary>
/// The <see cref="SatiationTypePrototype"/>s to be shown to the owner of this component.
/// </summary>
[DataField]
public List<ProtoId<SatiationTypePrototype>> ShownTypes = [];
}

View file

@ -1,9 +0,0 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Overlays;
/// <summary>
/// This component allows you to see the thirstiness of mobs.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ShowThirstIconsComponent : Component { }

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
[DataField("minHungerThreshold")]
[ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
public HungerThreshold MinHungerThreshold = HungerThreshold.Okay;
[DataField(required: true), AutoNetworkedField]
public SatiationValue MinHungerThreshold;
}

View file

@ -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<HungerComponent>(uid, out var hungerComp)
|| _hungerSystem.IsHungerBelowState(uid,
comp.MinHungerThreshold,
_hungerSystem.GetHunger(hungerComp) - comp.HungerCost,
hungerComp))
if (!TryComp<SatiationComponent>(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<HungerComponent>(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<SatiationComponent>(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
/// <summary>
/// Should be relayed upon using the action.
/// </summary>
public sealed partial class SericultureActionEvent : InstantActionEvent { }
public sealed partial class SericultureActionEvent : InstantActionEvent;
/// <summary>
/// Is relayed at the end of the sericulturing doafter.
/// </summary>
[Serializable, NetSerializable]
public sealed partial class SericultureDoAfterEvent : SimpleDoAfterEvent { }
public sealed partial class SericultureDoAfterEvent : SimpleDoAfterEvent;

View file

@ -7,7 +7,8 @@ namespace Content.Shared.Whitelist;
public sealed partial class EntityWhitelistSystem : EntitySystem
{
[Dependency] private TagSystem _tag = default!;
[Dependency] private EntityQuery<ItemComponent> _itemQuery;
[Dependency] private EntityQuery<ItemComponent> _itemQuery = default!;
private CompName _itemComponentName = default;
private CompName _tagComponentName = default;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,2 @@
satiation-type-hunger = hunger
satiation-type-thirst = thirst

View file

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

View file

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

View file

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

View file

@ -0,0 +1,7 @@
- type: satiationType
id: Hunger
name: satiation-type-hunger
- type: satiationType
id: Thirst
name: satiation-type-thirst

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -181,7 +181,8 @@
metabolisms:
Bloodstream:
effects:
- !type:SatiateThirst
- !type:Satiate
satiationType: Thirst
factor: -5 # This is basically industrial dessicant
- !type:PopupMessage
visualType: MediumCaution

View file

@ -37,7 +37,8 @@
metabolisms:
Digestion:
effects:
- !type:SatiateHunger
- !type:Satiate
satiationType: Hunger
conditions:
- !type:MetabolizerTypeCondition
type: [ Moth ]

View file

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

View file

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

View file

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

View file

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