From: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> Date: Mon, 3 Apr 2023 02:42:30 +0000 (-0400) Subject: Hunger ECS (#14939) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=0f0b53423900140273b1d918900b78e2dfc03925;p=space-station-14.git Hunger ECS (#14939) --- diff --git a/Content.Client/Nutrition/Components/HungerComponent.cs b/Content.Client/Nutrition/Components/HungerComponent.cs deleted file mode 100644 index db1b3383e3..0000000000 --- a/Content.Client/Nutrition/Components/HungerComponent.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Content.Shared.Movement.Components; -using Content.Shared.Movement.Systems; -using Content.Shared.Nutrition.Components; -using Robust.Shared.GameObjects; - -namespace Content.Client.Nutrition.Components -{ - [RegisterComponent] - [ComponentReference(typeof(SharedHungerComponent))] - public sealed class HungerComponent : SharedHungerComponent - { - private HungerThreshold _currentHungerThreshold; - public override HungerThreshold CurrentHungerThreshold => _currentHungerThreshold; - - public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) - { - base.HandleComponentState(curState, nextState); - - if (curState is not HungerComponentState hunger) - { - return; - } - - _currentHungerThreshold = hunger.CurrentThreshold; - } - } -} diff --git a/Content.Server/Animals/Systems/EggLayerSystem.cs b/Content.Server/Animals/Systems/EggLayerSystem.cs index d8fde67497..a9ab3c5f29 100644 --- a/Content.Server/Animals/Systems/EggLayerSystem.cs +++ b/Content.Server/Animals/Systems/EggLayerSystem.cs @@ -1,11 +1,11 @@ using Content.Server.Actions; using Content.Server.Animals.Components; -using Content.Server.Nutrition.Components; using Content.Server.Popups; using Content.Shared.Actions.ActionTypes; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Storage; using Robust.Server.GameObjects; -using Robust.Shared.Audio; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -14,9 +14,11 @@ namespace Content.Server.Animals.Systems; public sealed class EggLayerSystem : EntitySystem { - [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ActionsSystem _actions = default!; + [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private readonly HungerSystem _hunger = default!; [Dependency] private readonly PopupSystem _popup = default!; public override void Initialize() @@ -31,11 +33,12 @@ public sealed class EggLayerSystem : EntitySystem { base.Update(frameTime); - foreach (var eggLayer in EntityQuery()) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var eggLayer)) { // Players should be using the action. - if (HasComp(eggLayer.Owner)) - return; + if (HasComp(uid)) + continue; eggLayer.AccumulatedFrametime += frameTime; @@ -45,7 +48,7 @@ public sealed class EggLayerSystem : EntitySystem eggLayer.AccumulatedFrametime -= eggLayer.CurrentEggLayCooldown; eggLayer.CurrentEggLayCooldown = _random.NextFloat(eggLayer.EggLayCooldownMin, eggLayer.EggLayCooldownMax); - TryLayEgg(eggLayer.Owner, eggLayer); + TryLayEgg(uid, eggLayer); } } @@ -77,7 +80,7 @@ public sealed class EggLayerSystem : EntitySystem return false; } - hunger.CurrentHunger -= component.HungerUsage; + _hunger.ModifyHunger(uid, -component.HungerUsage, hunger); } foreach (var ent in EntitySpawnCollection.GetSpawns(component.EggSpawn, _random)) @@ -86,7 +89,7 @@ public sealed class EggLayerSystem : EntitySystem } // Sound + popups - SoundSystem.Play(component.EggLaySound.GetSound(), Filter.Pvs(uid), uid, component.EggLaySound.Params); + _audio.PlayPvs(component.EggLaySound, uid); _popup.PopupEntity(Loc.GetString("action-popup-lay-egg-user"), uid, uid); _popup.PopupEntity(Loc.GetString("action-popup-lay-egg-others", ("entity", uid)), uid, Filter.PvsExcept(uid), true); diff --git a/Content.Server/Animals/Systems/UdderSystem.cs b/Content.Server/Animals/Systems/UdderSystem.cs index ad0e6be27f..dd774b8999 100644 --- a/Content.Server/Animals/Systems/UdderSystem.cs +++ b/Content.Server/Animals/Systems/UdderSystem.cs @@ -1,11 +1,13 @@ using Content.Server.Animals.Components; using Content.Server.Chemistry.Components.SolutionManager; using Content.Server.Chemistry.EntitySystems; +using Content.Server.DoAfter; using Content.Server.Nutrition.Components; using Content.Server.Popups; using Content.Shared.DoAfter; using Content.Shared.IdentityManagement; using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Popups; using Content.Shared.Udder; using Content.Shared.Verbs; @@ -18,6 +20,7 @@ namespace Content.Server.Animals.Systems internal sealed class UdderSystem : EntitySystem { [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; + [Dependency] private readonly HungerSystem _hunger = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; @@ -41,10 +44,8 @@ namespace Content.Server.Animals.Systems // Actually there is food digestion so no problem with instant reagent generation "OnFeed" if (EntityManager.TryGetComponent(udder.Owner, out var hunger)) { - hunger.HungerThresholds.TryGetValue(HungerThreshold.Peckish, out var targetThreshold); - // Is there enough nutrition to produce reagent? - if (hunger.CurrentHunger < targetThreshold) + if (_hunger.GetHungerThreshold(hunger) < HungerThreshold.Peckish) continue; } diff --git a/Content.Server/Chemistry/ReagentEffects/SatiateHunger.cs b/Content.Server/Chemistry/ReagentEffects/SatiateHunger.cs index f00b4fe5cd..7ef11578ad 100644 --- a/Content.Server/Chemistry/ReagentEffects/SatiateHunger.cs +++ b/Content.Server/Chemistry/ReagentEffects/SatiateHunger.cs @@ -1,5 +1,7 @@ using Content.Server.Nutrition.Components; using Content.Shared.Chemistry.Reagent; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; namespace Content.Server.Chemistry.ReagentEffects { @@ -17,8 +19,10 @@ namespace Content.Server.Chemistry.ReagentEffects //Remove reagent at set rate, satiate hunger if a HungerComponent can be found public override void Effect(ReagentEffectArgs args) { - if (args.EntityManager.TryGetComponent(args.SolutionEntity, out HungerComponent? hunger)) - hunger.UpdateFood(NutritionFactor * (float) args.Quantity); + var entman = args.EntityManager; + if (!entman.TryGetComponent(args.SolutionEntity, out HungerComponent? hunger)) + return; + entman.System().ModifyHunger(args.SolutionEntity, NutritionFactor * (float) args.Quantity, hunger); } } } diff --git a/Content.Server/Medical/VomitSystem.cs b/Content.Server/Medical/VomitSystem.cs index ed02bee666..af7989d7ae 100644 --- a/Content.Server/Medical/VomitSystem.cs +++ b/Content.Server/Medical/VomitSystem.cs @@ -7,22 +7,24 @@ using Content.Server.Nutrition.Components; using Content.Server.Nutrition.EntitySystems; using Content.Server.Popups; using Content.Server.Stunnable; -using Content.Shared.Audio; using Content.Shared.IdentityManagement; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; using Content.Shared.StatusEffect; +using Robust.Server.GameObjects; using Robust.Shared.Audio; -using Robust.Shared.Player; namespace Content.Server.Medical { public sealed class VomitSystem : EntitySystem { - - [Dependency] private readonly StunSystem _stunSystem = default!; - [Dependency] private readonly SolutionContainerSystem _solutionSystem = default!; - [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly BodySystem _bodySystem = default!; - [Dependency] private readonly ThirstSystem _thirstSystem = default!; + [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private readonly BodySystem _body = default!; + [Dependency] private readonly HungerSystem _hunger = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly StunSystem _stun = default!; + [Dependency] private readonly ThirstSystem _thirst = default!; /// /// Make an entity vomit, if they have a stomach. @@ -30,23 +32,22 @@ namespace Content.Server.Medical public void Vomit(EntityUid uid, float thirstAdded = -40f, float hungerAdded = -40f) { // Main requirement: You have a stomach - var stomachList = _bodySystem.GetBodyOrganComponents(uid); + var stomachList = _body.GetBodyOrganComponents(uid); if (stomachList.Count == 0) - { return; - } + // Vomiting makes you hungrier and thirstier if (TryComp(uid, out var hunger)) - hunger.UpdateFood(hungerAdded); + _hunger.ModifyHunger(uid, hungerAdded, hunger); if (TryComp(uid, out var thirst)) - _thirstSystem.UpdateThirst(thirst, thirstAdded); + _thirst.UpdateThirst(thirst, thirstAdded); // It fully empties the stomach, this amount from the chem stream is relatively small - float solutionSize = (Math.Abs(thirstAdded) + Math.Abs(hungerAdded)) / 6; + var solutionSize = (MathF.Abs(thirstAdded) + MathF.Abs(hungerAdded)) / 6; // Apply a bit of slowdown if (TryComp(uid, out var status)) - _stunSystem.TrySlowdown(uid, TimeSpan.FromSeconds(solutionSize), true, 0.5f, 0.5f, status); + _stun.TrySlowdown(uid, TimeSpan.FromSeconds(solutionSize), true, 0.5f, 0.5f, status); var puddle = EntityManager.SpawnEntity("PuddleVomit", Transform(uid).Coordinates); @@ -56,23 +57,23 @@ namespace Content.Server.Medical var puddleComp = Comp(puddle); - SoundSystem.Play("/Audio/Effects/Fluids/splat.ogg", Filter.Pvs(uid), uid, AudioHelpers.WithVariation(0.2f).WithVolume(-4f)); + _audio.PlayPvs("/Audio/Effects/Fluids/splat.ogg", uid, AudioParams.Default.WithVariation(0.2f).WithVolume(-4f)); - _popupSystem.PopupEntity(Loc.GetString("disease-vomit", ("person", Identity.Entity(uid, EntityManager))), uid); + _popup.PopupEntity(Loc.GetString("disease-vomit", ("person", Identity.Entity(uid, EntityManager))), uid); // Get the solution of the puddle we spawned - if (!_solutionSystem.TryGetSolution(puddle, puddleComp.SolutionName, out var puddleSolution)) + if (!_solutionContainer.TryGetSolution(puddle, puddleComp.SolutionName, out var puddleSolution)) return; // Empty the stomach out into it foreach (var stomach in stomachList) { - if (_solutionSystem.TryGetSolution(stomach.Comp.Owner, StomachSystem.DefaultSolutionName, out var sol)) - _solutionSystem.TryAddSolution(puddle, puddleSolution, sol); + if (_solutionContainer.TryGetSolution(stomach.Comp.Owner, StomachSystem.DefaultSolutionName, out var sol)) + _solutionContainer.TryAddSolution(puddle, puddleSolution, sol); } // And the small bit of the chem stream from earlier if (TryComp(uid, out var bloodStream)) { var temp = bloodStream.ChemicalSolution.SplitSolution(solutionSize); - _solutionSystem.TryAddSolution(puddle, puddleSolution, temp); + _solutionContainer.TryAddSolution(puddle, puddleSolution, temp); } } } diff --git a/Content.Server/Nutrition/Components/HungerComponent.cs b/Content.Server/Nutrition/Components/HungerComponent.cs deleted file mode 100644 index a02b620e4e..0000000000 --- a/Content.Server/Nutrition/Components/HungerComponent.cs +++ /dev/null @@ -1,197 +0,0 @@ -using Content.Shared.Alert; -using Content.Shared.Movement.Components; -using Content.Shared.Movement.Systems; -using Content.Shared.Nutrition.Components; -using Robust.Shared.Random; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; - -namespace Content.Server.Nutrition.Components -{ - [RegisterComponent] - [ComponentReference(typeof(SharedHungerComponent))] - public sealed class HungerComponent : SharedHungerComponent - { - [Dependency] private readonly IEntityManager _entMan = default!; - [Dependency] private readonly IRobustRandom _random = default!; - - // Base stuff - [ViewVariables(VVAccess.ReadWrite)] - public float BaseDecayRate - { - get => _baseDecayRate; - set => _baseDecayRate = value; - } - [DataField("baseDecayRate")] - private float _baseDecayRate = 0.01666666666f; - - [ViewVariables(VVAccess.ReadWrite)] - public float ActualDecayRate - { - get => _actualDecayRate; - set => _actualDecayRate = value; - } - private float _actualDecayRate; - - // Hunger - [ViewVariables(VVAccess.ReadOnly)] - public override HungerThreshold CurrentHungerThreshold => _currentHungerThreshold; - private HungerThreshold _currentHungerThreshold; - - private HungerThreshold _lastHungerThreshold; - - [ViewVariables(VVAccess.ReadWrite)] - public float CurrentHunger - { - get => _currentHunger; - set => _currentHunger = value; - } - [DataField("startingHunger")] - private float _currentHunger = -1f; - - [ViewVariables(VVAccess.ReadOnly)] - public Dictionary HungerThresholds => _hungerThresholds; - - [DataField("thresholds", customTypeSerializer: typeof(DictionarySerializer))] - private Dictionary _hungerThresholds = new() - { - { HungerThreshold.Overfed, 200.0f }, - { HungerThreshold.Okay, 150.0f }, - { HungerThreshold.Peckish, 100.0f }, - { HungerThreshold.Starving, 50.0f }, - { HungerThreshold.Dead, 0.0f }, - }; - - public static readonly Dictionary HungerThresholdAlertTypes = new() - { - { HungerThreshold.Peckish, AlertType.Peckish }, - { HungerThreshold.Starving, AlertType.Starving }, - { HungerThreshold.Dead, AlertType.Starving }, - }; - - public void HungerThresholdEffect(bool force = false) - { - if (_currentHungerThreshold != _lastHungerThreshold || force) - { - // Revert slow speed if required - if (_lastHungerThreshold == HungerThreshold.Starving && _currentHungerThreshold != HungerThreshold.Dead && - _entMan.TryGetComponent(Owner, out MovementSpeedModifierComponent? movementSlowdownComponent)) - { - EntitySystem.Get().RefreshMovementSpeedModifiers(Owner); - } - - // Update UI - if (HungerThresholdAlertTypes.TryGetValue(_currentHungerThreshold, out var alertId)) - { - EntitySystem.Get().ShowAlert(Owner, alertId); - } - else - { - EntitySystem.Get().ClearAlertCategory(Owner, AlertCategory.Hunger); - } - - switch (_currentHungerThreshold) - { - case HungerThreshold.Overfed: - _lastHungerThreshold = _currentHungerThreshold; - _actualDecayRate = _baseDecayRate * 1.2f; - return; - - case HungerThreshold.Okay: - _lastHungerThreshold = _currentHungerThreshold; - _actualDecayRate = _baseDecayRate; - return; - - case HungerThreshold.Peckish: - // Same as okay except with UI icon saying eat soon. - _lastHungerThreshold = _currentHungerThreshold; - _actualDecayRate = _baseDecayRate * 0.8f; - return; - - case HungerThreshold.Starving: - // TODO: If something else bumps this could cause mega-speed. - // If some form of speed update system if multiple things are touching it use that. - EntitySystem.Get().RefreshMovementSpeedModifiers(Owner); - _lastHungerThreshold = _currentHungerThreshold; - _actualDecayRate = _baseDecayRate * 0.6f; - return; - - case HungerThreshold.Dead: - return; - default: - Logger.ErrorS("hunger", $"No hunger threshold found for {_currentHungerThreshold}"); - throw new ArgumentOutOfRangeException($"No hunger threshold found for {_currentHungerThreshold}"); - } - } - } - - protected override void Startup() - { - base.Startup(); - // Do not change behavior unless starting hunger is explicitly defined - if (_currentHunger < 0) - { - // Similar functionality to SS13. Should also stagger people going to the chef. - _currentHunger = _random.Next( - (int) _hungerThresholds[HungerThreshold.Peckish] + 10, - (int) _hungerThresholds[HungerThreshold.Okay] - 1); - } - - _currentHungerThreshold = GetHungerThreshold(_currentHunger); - _lastHungerThreshold = HungerThreshold.Okay; // TODO: Potentially change this -> Used Okay because no effects. - HungerThresholdEffect(true); - Dirty(); - } - - public HungerThreshold GetHungerThreshold(float food) - { - HungerThreshold result = HungerThreshold.Dead; - var value = HungerThresholds[HungerThreshold.Overfed]; - foreach (var threshold in _hungerThresholds) - { - if (threshold.Value <= value && threshold.Value >= food) - { - result = threshold.Key; - value = threshold.Value; - } - } - - return result; - } - - public void UpdateFood(float amount) - { - _currentHunger = Math.Clamp(_currentHunger + amount, HungerThresholds[HungerThreshold.Dead], HungerThresholds[HungerThreshold.Overfed]); - } - - // TODO: If mob is moving increase rate of consumption? - // Should use a multiplier as something like a disease would overwrite decay rate. - public void OnUpdate(float frametime) - { - UpdateFood(- frametime * ActualDecayRate); - UpdateCurrentThreshold(); - } - - private void UpdateCurrentThreshold() - { - var calculatedHungerThreshold = GetHungerThreshold(_currentHunger); - // _trySound(calculatedThreshold); - if (calculatedHungerThreshold != _currentHungerThreshold) - { - _currentHungerThreshold = calculatedHungerThreshold; - HungerThresholdEffect(); - Dirty(); - } - } - - public void ResetFood() - { - _currentHunger = HungerThresholds[HungerThreshold.Okay]; - UpdateCurrentThreshold(); - } - - public override ComponentState GetComponentState() - { - return new HungerComponentState(_currentHungerThreshold); - } - } -} diff --git a/Content.Server/Nutrition/EntitySystems/HungerSystem.cs b/Content.Server/Nutrition/EntitySystems/HungerSystem.cs deleted file mode 100644 index b367cbedd6..0000000000 --- a/Content.Server/Nutrition/EntitySystems/HungerSystem.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Content.Server.Nutrition.Components; -using Content.Shared.Rejuvenate; -using JetBrains.Annotations; - -namespace Content.Server.Nutrition.EntitySystems -{ - [UsedImplicitly] - public sealed class HungerSystem : EntitySystem - { - private float _accumulatedFrameTime; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnRejuvenate); - } - - public override void Update(float frameTime) - { - _accumulatedFrameTime += frameTime; - - if (_accumulatedFrameTime > 1) - { - foreach (var comp in EntityManager.EntityQuery()) - { - comp.OnUpdate(_accumulatedFrameTime); - } - - _accumulatedFrameTime -= 1; - } - } - - private void OnRejuvenate(EntityUid uid, HungerComponent component, RejuvenateEvent args) - { - component.ResetFood(); - } - } -} diff --git a/Content.Server/Nutrition/Hungry.cs b/Content.Server/Nutrition/Hungry.cs index 7c722be9bf..c27f302a8d 100644 --- a/Content.Server/Nutrition/Hungry.cs +++ b/Content.Server/Nutrition/Hungry.cs @@ -2,6 +2,7 @@ using Content.Server.Administration; using Content.Server.Nutrition.Components; using Content.Shared.Administration; using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; using Robust.Server.Player; using Robust.Shared.Console; @@ -37,8 +38,8 @@ namespace Content.Server.Nutrition return; } - var hungryThreshold = hunger.HungerThresholds[HungerThreshold.Starving]; - hunger.CurrentHunger = hungryThreshold; + var hungryThreshold = hunger.Thresholds[HungerThreshold.Starving]; + _entities.System().SetHunger(playerEntity, hungryThreshold, hunger); } } } diff --git a/Content.Server/RatKing/RatKingSystem.cs b/Content.Server/RatKing/RatKingSystem.cs index 55b64384ff..6dee181d59 100644 --- a/Content.Server/RatKing/RatKingSystem.cs +++ b/Content.Server/RatKing/RatKingSystem.cs @@ -4,6 +4,8 @@ using Content.Server.Nutrition.Components; using Content.Server.Popups; using Content.Shared.Actions; using Content.Shared.Atmos; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; using Robust.Server.GameObjects; using Robust.Shared.Player; @@ -11,9 +13,10 @@ namespace Content.Server.RatKing { public sealed class RatKingSystem : EntitySystem { - [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly ActionsSystem _action = default!; [Dependency] private readonly AtmosphereSystem _atmos = default!; + [Dependency] private readonly HungerSystem _hunger = default!; + [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly TransformSystem _xform = default!; public override void Initialize() @@ -50,7 +53,7 @@ namespace Content.Server.RatKing return; } args.Handled = true; - hunger.CurrentHunger -= component.HungerPerArmyUse; + _hunger.ModifyHunger(uid, -component.HungerPerArmyUse, hunger); Spawn(component.ArmyMobSpawnId, Transform(uid).Coordinates); //spawn the little mouse boi } @@ -73,7 +76,7 @@ namespace Content.Server.RatKing return; } args.Handled = true; - hunger.CurrentHunger -= component.HungerPerDomainUse; + _hunger.ModifyHunger(uid, -component.HungerPerDomainUse, hunger); _popup.PopupEntity(Loc.GetString("rat-king-domain-popup"), uid); @@ -84,6 +87,13 @@ namespace Content.Server.RatKing } } - public sealed class RatKingRaiseArmyActionEvent : InstantActionEvent { }; - public sealed class RatKingDomainActionEvent : InstantActionEvent { }; + public sealed class RatKingRaiseArmyActionEvent : InstantActionEvent + { + + } + + public sealed class RatKingDomainActionEvent : InstantActionEvent + { + + } }; diff --git a/Content.Server/Zombies/ZombifyOnDeathSystem.cs b/Content.Server/Zombies/ZombifyOnDeathSystem.cs index 1994b98a7a..b040b0ae77 100644 --- a/Content.Server/Zombies/ZombifyOnDeathSystem.cs +++ b/Content.Server/Zombies/ZombifyOnDeathSystem.cs @@ -31,6 +31,7 @@ using Content.Shared.Movement.Systems; using Content.Shared.Weapons.Melee; using Content.Server.Chat; using Content.Server.Chat.Systems; +using Content.Shared.Nutrition.Components; namespace Content.Server.Zombies { diff --git a/Content.Shared/Nutrition/Components/HungerComponent.cs b/Content.Shared/Nutrition/Components/HungerComponent.cs new file mode 100644 index 0000000000..baca5928a4 --- /dev/null +++ b/Content.Shared/Nutrition/Components/HungerComponent.cs @@ -0,0 +1,151 @@ +using Content.Shared.Alert; +using Content.Shared.Damage; +using Content.Shared.Nutrition.EntitySystems; +using Robust.Shared.GameStates; +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))] +public sealed class HungerComponent : Component +{ + /// + /// The current hunger amount of the entity + /// + [DataField("currentHunger"), ViewVariables(VVAccess.ReadWrite)] + public float CurrentHunger; + + /// + /// The base amount at which decays. + /// + [DataField("baseDecayRate"), ViewVariables(VVAccess.ReadWrite)] + public float BaseDecayRate = 0.01666666666f; + + /// + /// The actual amount at which decays. + /// Affected by + /// + [DataField("actualDecayRate"), ViewVariables(VVAccess.ReadWrite)] + public float ActualDecayRate; + + /// + /// The last threshold this entity was at. + /// Stored in order to prevent recalculating + /// + [DataField("lastThreshold"), ViewVariables(VVAccess.ReadWrite)] + public HungerThreshold LastThreshold; + + /// + /// The current hunger threshold the entity is at + /// + [DataField("currentThreshold"), ViewVariables(VVAccess.ReadWrite)] + public HungerThreshold CurrentThreshold; + + /// + /// A dictionary relating HungerThreshold to the amount of needed for each one + /// + [DataField("thresholds", customTypeSerializer: typeof(DictionarySerializer))] + public Dictionary Thresholds = new() + { + { HungerThreshold.Overfed, 200.0f }, + { HungerThreshold.Okay, 150.0f }, + { HungerThreshold.Peckish, 100.0f }, + { HungerThreshold.Starving, 50.0f }, + { HungerThreshold.Dead, 0.0f } + }; + + /// + /// A dictionary relating hunger thresholds to corresponding alerts. + /// + [DataField("hungerThresholdAlerts", customTypeSerializer: typeof(DictionarySerializer))] + public Dictionary HungerThresholdAlerts = new() + { + { HungerThreshold.Peckish, AlertType.Peckish }, + { HungerThreshold.Starving, AlertType.Starving }, + { HungerThreshold.Dead, AlertType.Starving } + }; + + /// + /// A dictionary relating HungerThreshold to how much they modify . + /// + [DataField("hungerThresholdDecayModifiers", customTypeSerializer: typeof(DictionarySerializer))] + public Dictionary HungerThresholdDecayModifiers = new() + { + { HungerThreshold.Overfed, 1.2f }, + { HungerThreshold.Okay, 1f }, + { HungerThreshold.Peckish, 0.8f }, + { HungerThreshold.Starving, 0.6f }, + { HungerThreshold.Dead, 0.6f } + }; + + /// + /// The amount of slowdown applied when an entity is starving + /// + [DataField("starvingSlowdownModifier"), ViewVariables(VVAccess.ReadWrite)] + public float StarvingSlowdownModifier = 0.75f; + + /// + /// Damage dealt when your current threshold is at HungerThreshold.Dead + /// + [DataField("starvationDamage")] + public DamageSpecifier? StarvationDamage; + + /// + /// The time when the hunger will update next. + /// + [DataField("nextUpdateTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan NextUpdateTime; + + /// + /// The time between each update. + /// + [ViewVariables(VVAccess.ReadWrite)] + public TimeSpan UpdateRate = TimeSpan.FromSeconds(1); +} + +[Serializable, NetSerializable] +public sealed class HungerComponentState : ComponentState +{ + public float CurrentHunger; + + public float BaseDecayRate; + + public float ActualDecayRate; + + public HungerThreshold LastHungerThreshold; + + public HungerThreshold CurrentThreshold; + + public float StarvingSlowdownModifier; + + public TimeSpan NextUpdateTime; + + public HungerComponentState(float currentHunger, + float baseDecayRate, + float actualDecayRate, + HungerThreshold lastHungerThreshold, + HungerThreshold currentThreshold, + float starvingSlowdownModifier, + TimeSpan nextUpdateTime) + { + CurrentHunger = currentHunger; + BaseDecayRate = baseDecayRate; + ActualDecayRate = actualDecayRate; + LastHungerThreshold = lastHungerThreshold; + CurrentThreshold = currentThreshold; + StarvingSlowdownModifier = starvingSlowdownModifier; + NextUpdateTime = nextUpdateTime; + } +} + +[Serializable, NetSerializable] +public enum HungerThreshold : byte +{ + Overfed = 1 << 3, + Okay = 1 << 2, + Peckish = 1 << 1, + Starving = 1 << 0, + Dead = 0, +} diff --git a/Content.Shared/Nutrition/Components/SharedHungerComponent.cs b/Content.Shared/Nutrition/Components/SharedHungerComponent.cs deleted file mode 100644 index 88f1a00af5..0000000000 --- a/Content.Shared/Nutrition/Components/SharedHungerComponent.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Robust.Shared.GameStates; -using Robust.Shared.Serialization; - -namespace Content.Shared.Nutrition.Components -{ - [NetworkedComponent()] - public abstract class SharedHungerComponent : Component - { - [ViewVariables] - public abstract HungerThreshold CurrentHungerThreshold { get; } - - [Serializable, NetSerializable] - protected sealed class HungerComponentState : ComponentState - { - public HungerThreshold CurrentThreshold { get; } - - public HungerComponentState(HungerThreshold currentThreshold) - { - CurrentThreshold = currentThreshold; - } - } - } - - [Serializable, NetSerializable] - public enum HungerThreshold : byte - { - Overfed = 1 << 3, - Okay = 1 << 2, - Peckish = 1 << 1, - Starving = 1 << 0, - Dead = 0, - } -} diff --git a/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs b/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs new file mode 100644 index 0000000000..f7a6855abc --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs @@ -0,0 +1,226 @@ +using Content.Shared.Alert; +using Content.Shared.Damage; +using Content.Shared.Mobs.Systems; +using Content.Shared.Movement.Systems; +using Content.Shared.Nutrition.Components; +using Content.Shared.Rejuvenate; +using Robust.Shared.GameStates; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Shared.Nutrition.EntitySystems; + +public sealed class HungerSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; + [Dependency] private readonly SharedJetpackSystem _jetpack = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetState); + SubscribeLocalEvent(OnHandleState); + SubscribeLocalEvent(OnUnpaused); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnRefreshMovespeed); + SubscribeLocalEvent(OnRejuvenate); + } + + private void OnGetState(EntityUid uid, HungerComponent component, ref ComponentGetState args) + { + args.State = new HungerComponentState(component.CurrentHunger, + component.BaseDecayRate, + component.ActualDecayRate, + component.LastThreshold, + component.CurrentThreshold, + component.StarvingSlowdownModifier, + component.NextUpdateTime); + } + + private void OnHandleState(EntityUid uid, HungerComponent component, ref ComponentHandleState args) + { + if (args.Current is not HungerComponentState state) + return; + component.CurrentHunger = state.CurrentHunger; + component.BaseDecayRate = state.BaseDecayRate; + component.ActualDecayRate = state.ActualDecayRate; + component.LastThreshold = state.LastHungerThreshold; + component.CurrentThreshold = state.CurrentThreshold; + component.StarvingSlowdownModifier = state.StarvingSlowdownModifier; + component.NextUpdateTime = state.NextUpdateTime; + } + + private void OnUnpaused(EntityUid uid, HungerComponent component, ref EntityUnpausedEvent args) + { + component.NextUpdateTime += args.PausedTime; + } + + 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, AlertCategory.Hunger); + } + + 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); + } + + /// + /// Adds to the current hunger of an entity by the specified value + /// + /// + /// + /// + public void ModifyHunger(EntityUid uid, float amount, HungerComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + SetHunger(uid, component.CurrentHunger + amount, component); + } + + /// + /// Sets the current hunger of an entity to the specified value + /// + /// + /// + /// + public void SetHunger(EntityUid uid, float amount, HungerComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + component.CurrentHunger = Math.Clamp(amount, + component.Thresholds[HungerThreshold.Dead], + component.Thresholds[HungerThreshold.Overfed]); + UpdateCurrentThreshold(uid, component); + Dirty(component); + } + + 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; + DoHungerThresholdEffects(uid, component); + Dirty(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, AlertCategory.Hunger); + } + + if (component.StarvationDamage is { } damage && !_mobState.IsDead(uid)) + { + _damageable.TryChangeDamage(uid, damage, true, false); + } + + if (component.HungerThresholdDecayModifiers.TryGetValue(component.CurrentThreshold, out var modifier)) + { + component.ActualDecayRate = component.BaseDecayRate * modifier; + } + + component.LastThreshold = component.CurrentThreshold; + } + + /// + /// Gets the hunger threshold for an entity based on the amount of food specified. + /// If a specific amount isn't specified, just uses the current hunger of the entity + /// + /// + /// + /// + public HungerThreshold GetHungerThreshold(HungerComponent component, float? food = null) + { + food ??= component.CurrentHunger; + 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; + } + + 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 override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var hunger)) + { + if (_timing.CurTime < hunger.NextUpdateTime) + continue; + hunger.NextUpdateTime = _timing.CurTime + hunger.UpdateRate; + + ModifyHunger(uid, -hunger.ActualDecayRate, hunger); + } + } +} + diff --git a/Content.Shared/Nutrition/EntitySystems/SharedHungerSystem.cs b/Content.Shared/Nutrition/EntitySystems/SharedHungerSystem.cs deleted file mode 100644 index 36fefe8259..0000000000 --- a/Content.Shared/Nutrition/EntitySystems/SharedHungerSystem.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Content.Shared.Movement.Systems; -using Content.Shared.Nutrition.Components; - -namespace Content.Shared.Nutrition.EntitySystems -{ - public sealed class SharedHungerSystem : EntitySystem - { - [Dependency] private readonly SharedJetpackSystem _jetpack = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnRefreshMovespeed); - } - - private void OnRefreshMovespeed(EntityUid uid, SharedHungerComponent component, RefreshMovementSpeedModifiersEvent args) - { - if (_jetpack.IsUserFlying(component.Owner)) - return; - - float mod = component.CurrentHungerThreshold <= HungerThreshold.Starving ? 0.75f : 1.0f; - args.ModifySpeed(mod, mod); - } - } -}