]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Hunger ECS (#14939)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Mon, 3 Apr 2023 02:42:30 +0000 (22:42 -0400)
committerGitHub <noreply@github.com>
Mon, 3 Apr 2023 02:42:30 +0000 (12:42 +1000)
14 files changed:
Content.Client/Nutrition/Components/HungerComponent.cs [deleted file]
Content.Server/Animals/Systems/EggLayerSystem.cs
Content.Server/Animals/Systems/UdderSystem.cs
Content.Server/Chemistry/ReagentEffects/SatiateHunger.cs
Content.Server/Medical/VomitSystem.cs
Content.Server/Nutrition/Components/HungerComponent.cs [deleted file]
Content.Server/Nutrition/EntitySystems/HungerSystem.cs [deleted file]
Content.Server/Nutrition/Hungry.cs
Content.Server/RatKing/RatKingSystem.cs
Content.Server/Zombies/ZombifyOnDeathSystem.cs
Content.Shared/Nutrition/Components/HungerComponent.cs [new file with mode: 0644]
Content.Shared/Nutrition/Components/SharedHungerComponent.cs [deleted file]
Content.Shared/Nutrition/EntitySystems/HungerSystem.cs [new file with mode: 0644]
Content.Shared/Nutrition/EntitySystems/SharedHungerSystem.cs [deleted file]

diff --git a/Content.Client/Nutrition/Components/HungerComponent.cs b/Content.Client/Nutrition/Components/HungerComponent.cs
deleted file mode 100644 (file)
index db1b338..0000000
+++ /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;
-        }
-    }
-}
index d8fde67497dd61eceee9c7739045ed850a382fc5..a9ab3c5f29a9e7fe0c37e2fbc31a6cc24692e283 100644 (file)
@@ -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<EggLayerComponent>())
+        var query = EntityQueryEnumerator<EggLayerComponent>();
+        while (query.MoveNext(out var uid, out var eggLayer))
         {
             // Players should be using the action.
-            if (HasComp<ActorComponent>(eggLayer.Owner))
-                return;
+            if (HasComp<ActorComponent>(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);
 
index ad0e6be27f362e68a30f2ef6738524bed67bbc2b..dd774b8999ec4f841dfca17dba6db4a74f76701d 100644 (file)
@@ -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<HungerComponent?>(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;
                     }
 
index f00b4fe5cd80566edf3b514634d68e22fececfa9..7ef11578ad0eba6e2c0f0fa303eeda88397ea966 100644 (file)
@@ -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<HungerSystem>().ModifyHunger(args.SolutionEntity, NutritionFactor * (float) args.Quantity, hunger);
         }
     }
 }
index ed02bee666be47038fa01cf71e0e31f122b3a4da..af7989d7aedee3ec1598e387da56a168d8f0232f 100644 (file)
@@ -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!;
 
         /// <summary>
         /// 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<StomachComponent>(uid);
+            var stomachList = _body.GetBodyOrganComponents<StomachComponent>(uid);
             if (stomachList.Count == 0)
-            {
                 return;
-            }
+
             // Vomiting makes you hungrier and thirstier
             if (TryComp<HungerComponent>(uid, out var hunger))
-                hunger.UpdateFood(hungerAdded);
+                _hunger.ModifyHunger(uid, hungerAdded, hunger);
 
             if (TryComp<ThirstComponent>(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<StatusEffectsComponent>(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<PuddleComponent>(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<BloodstreamComponent>(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 (file)
index a02b620..0000000
+++ /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<HungerThreshold, float> HungerThresholds => _hungerThresholds;
-
-        [DataField("thresholds", customTypeSerializer: typeof(DictionarySerializer<HungerThreshold, float>))]
-        private Dictionary<HungerThreshold, float> _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<HungerThreshold, AlertType> 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<MovementSpeedModifierSystem>().RefreshMovementSpeedModifiers(Owner);
-                }
-
-                // Update UI
-                if (HungerThresholdAlertTypes.TryGetValue(_currentHungerThreshold, out var alertId))
-                {
-                    EntitySystem.Get<AlertsSystem>().ShowAlert(Owner, alertId);
-                }
-                else
-                {
-                    EntitySystem.Get<AlertsSystem>().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<MovementSpeedModifierSystem>().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 (file)
index b367cbe..0000000
+++ /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<HungerComponent, RejuvenateEvent>(OnRejuvenate);
-        }
-
-        public override void Update(float frameTime)
-        {
-            _accumulatedFrameTime += frameTime;
-
-            if (_accumulatedFrameTime > 1)
-            {
-                foreach (var comp in EntityManager.EntityQuery<HungerComponent>())
-                {
-                    comp.OnUpdate(_accumulatedFrameTime);
-                }
-
-                _accumulatedFrameTime -= 1;
-            }
-        }
-
-        private void OnRejuvenate(EntityUid uid, HungerComponent component, RejuvenateEvent args)
-        {
-            component.ResetFood();
-        }
-    }
-}
index 7c722be9bfbc0b47cd91f9684ae06509bb0f336d..c27f302a8d1f3a1bf1b5c4314385daafac2b3531 100644 (file)
@@ -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<HungerSystem>().SetHunger(playerEntity, hungryThreshold, hunger);
         }
     }
 }
index 55b64384ff36e3589e0595ed20f17a6021f7aabb..6dee181d5927585d99cbddf9fbdf2004b4e4dc31 100644 (file)
@@ -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
+    {
+
+    }
 };
index 1994b98a7a96debaefcdfda9dcbc3edcf3c32a7b..b040b0ae7717cbc0a85fc207b387710c98fcc104 100644 (file)
@@ -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 (file)
index 0000000..baca592
--- /dev/null
@@ -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
+{
+    /// <summary>
+    /// The current hunger amount of the entity
+    /// </summary>
+    [DataField("currentHunger"), ViewVariables(VVAccess.ReadWrite)]
+    public float CurrentHunger;
+
+    /// <summary>
+    /// The base amount at which <see cref="CurrentHunger"/> decays.
+    /// </summary>
+    [DataField("baseDecayRate"), ViewVariables(VVAccess.ReadWrite)]
+    public float BaseDecayRate = 0.01666666666f;
+
+    /// <summary>
+    /// The actual amount at which <see cref="CurrentHunger"/> decays.
+    /// Affected by <seealso cref="CurrentThreshold"/>
+    /// </summary>
+    [DataField("actualDecayRate"), ViewVariables(VVAccess.ReadWrite)]
+    public float ActualDecayRate;
+
+    /// <summary>
+    /// The last threshold this entity was at.
+    /// Stored in order to prevent recalculating
+    /// </summary>
+    [DataField("lastThreshold"), ViewVariables(VVAccess.ReadWrite)]
+    public HungerThreshold LastThreshold;
+
+    /// <summary>
+    /// The current hunger threshold the entity is at
+    /// </summary>
+    [DataField("currentThreshold"), ViewVariables(VVAccess.ReadWrite)]
+    public HungerThreshold CurrentThreshold;
+
+    /// <summary>
+    /// A dictionary relating HungerThreshold to the amount of <see cref="CurrentHunger"/> needed for each one
+    /// </summary>
+    [DataField("thresholds", customTypeSerializer: typeof(DictionarySerializer<HungerThreshold, float>))]
+    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", customTypeSerializer: typeof(DictionarySerializer<HungerThreshold, AlertType>))]
+    public Dictionary<HungerThreshold, AlertType> HungerThresholdAlerts = new()
+    {
+        { HungerThreshold.Peckish, AlertType.Peckish },
+        { HungerThreshold.Starving, AlertType.Starving },
+        { HungerThreshold.Dead, AlertType.Starving }
+    };
+
+    /// <summary>
+    /// A dictionary relating HungerThreshold to how much they modify <see cref="BaseDecayRate"/>.
+    /// </summary>
+    [DataField("hungerThresholdDecayModifiers", customTypeSerializer: typeof(DictionarySerializer<HungerThreshold, float>))]
+    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)]
+    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 will update next.
+    /// </summary>
+    [DataField("nextUpdateTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
+    public TimeSpan NextUpdateTime;
+
+    /// <summary>
+    /// The time between each update.
+    /// </summary>
+    [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 (file)
index 88f1a00..0000000
+++ /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 (file)
index 0000000..f7a6855
--- /dev/null
@@ -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<HungerComponent, ComponentGetState>(OnGetState);
+        SubscribeLocalEvent<HungerComponent, ComponentHandleState>(OnHandleState);
+        SubscribeLocalEvent<HungerComponent, EntityUnpausedEvent>(OnUnpaused);
+        SubscribeLocalEvent<HungerComponent, MapInitEvent>(OnMapInit);
+        SubscribeLocalEvent<HungerComponent, ComponentShutdown>(OnShutdown);
+        SubscribeLocalEvent<HungerComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
+        SubscribeLocalEvent<HungerComponent, RejuvenateEvent>(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);
+    }
+
+    /// <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, component.CurrentHunger + 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;
+        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;
+    }
+
+    /// <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 ??= 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<HungerComponent>();
+        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 (file)
index 36fefe8..0000000
+++ /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<SharedHungerComponent, RefreshMovementSpeedModifiersEvent>(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);
-        }
-    }
-}