HungerSystem.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. using System.Diagnostics.CodeAnalysis;
  2. using Content.Shared.Alert;
  3. using Content.Shared.Damage;
  4. using Content.Shared.Mobs.Systems;
  5. using Content.Shared.Movement.Systems;
  6. using Content.Shared.Nutrition.Components;
  7. using Content.Shared.Rejuvenate;
  8. using Content.Shared.StatusIcon;
  9. using Robust.Shared.Network;
  10. using Robust.Shared.Prototypes;
  11. using Robust.Shared.Random;
  12. using Robust.Shared.Timing;
  13. using Robust.Shared.Utility;
  14. namespace Content.Shared.Nutrition.EntitySystems;
  15. public sealed class HungerSystem : EntitySystem
  16. {
  17. [Dependency] private readonly IGameTiming _timing = default!;
  18. [Dependency] private readonly IPrototypeManager _prototype = default!;
  19. [Dependency] private readonly IRobustRandom _random = default!;
  20. [Dependency] private readonly AlertsSystem _alerts = default!;
  21. [Dependency] private readonly DamageableSystem _damageable = default!;
  22. [Dependency] private readonly MobStateSystem _mobState = default!;
  23. [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
  24. [Dependency] private readonly SharedJetpackSystem _jetpack = default!;
  25. [ValidatePrototypeId<SatiationIconPrototype>]
  26. private const string HungerIconOverfedId = "HungerIconOverfed";
  27. [ValidatePrototypeId<SatiationIconPrototype>]
  28. private const string HungerIconPeckishId = "HungerIconPeckish";
  29. [ValidatePrototypeId<SatiationIconPrototype>]
  30. private const string HungerIconStarvingId = "HungerIconStarving";
  31. public override void Initialize()
  32. {
  33. base.Initialize();
  34. SubscribeLocalEvent<HungerComponent, MapInitEvent>(OnMapInit);
  35. SubscribeLocalEvent<HungerComponent, ComponentShutdown>(OnShutdown);
  36. SubscribeLocalEvent<HungerComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
  37. SubscribeLocalEvent<HungerComponent, RejuvenateEvent>(OnRejuvenate);
  38. }
  39. private void OnMapInit(EntityUid uid, HungerComponent component, MapInitEvent args)
  40. {
  41. var amount = _random.Next(
  42. (int)component.Thresholds[HungerThreshold.Peckish] + 10,
  43. (int)component.Thresholds[HungerThreshold.Okay]);
  44. SetHunger(uid, amount, component);
  45. }
  46. private void OnShutdown(EntityUid uid, HungerComponent component, ComponentShutdown args)
  47. {
  48. _alerts.ClearAlertCategory(uid, component.HungerAlertCategory);
  49. }
  50. private void OnRefreshMovespeed(EntityUid uid, HungerComponent component, RefreshMovementSpeedModifiersEvent args)
  51. {
  52. if (component.CurrentThreshold > HungerThreshold.Starving)
  53. return;
  54. if (_jetpack.IsUserFlying(uid))
  55. return;
  56. args.ModifySpeed(component.StarvingSlowdownModifier, component.StarvingSlowdownModifier);
  57. }
  58. private void OnRejuvenate(EntityUid uid, HungerComponent component, RejuvenateEvent args)
  59. {
  60. SetHunger(uid, component.Thresholds[HungerThreshold.Okay], component);
  61. }
  62. /// <summary>
  63. /// Gets the current hunger value of the given <see cref="HungerComponent"/>.
  64. /// </summary>
  65. public float GetHunger(HungerComponent component)
  66. {
  67. var dt = _timing.CurTime - component.LastAuthoritativeHungerChangeTime;
  68. var value = component.LastAuthoritativeHungerValue - (float)dt.TotalSeconds * component.ActualDecayRate;
  69. return ClampHungerWithinThresholds(component, value);
  70. }
  71. /// <summary>
  72. /// Adds to the current hunger of an entity by the specified value
  73. /// </summary>
  74. /// <param name="uid"></param>
  75. /// <param name="amount"></param>
  76. /// <param name="component"></param>
  77. public void ModifyHunger(EntityUid uid, float amount, HungerComponent? component = null)
  78. {
  79. if (!Resolve(uid, ref component))
  80. return;
  81. SetHunger(uid, GetHunger(component) + amount, component);
  82. }
  83. /// <summary>
  84. /// Sets the current hunger of an entity to the specified value
  85. /// </summary>
  86. /// <param name="uid"></param>
  87. /// <param name="amount"></param>
  88. /// <param name="component"></param>
  89. public void SetHunger(EntityUid uid, float amount, HungerComponent? component = null)
  90. {
  91. if (!Resolve(uid, ref component))
  92. return;
  93. SetAuthoritativeHungerValue((uid, component), amount);
  94. UpdateCurrentThreshold(uid, component);
  95. }
  96. /// <summary>
  97. /// Sets <see cref="HungerComponent.LastAuthoritativeHungerValue"/> and
  98. /// <see cref="HungerComponent.LastAuthoritativeHungerChangeTime"/>, and dirties this entity. This "resets" the
  99. /// starting point for <see cref="GetHunger"/>'s calculation.
  100. /// </summary>
  101. /// <param name="entity">The entity whose hunger will be set.</param>
  102. /// <param name="value">The value to set the entity's hunger to.</param>
  103. private void SetAuthoritativeHungerValue(Entity<HungerComponent> entity, float value)
  104. {
  105. entity.Comp.LastAuthoritativeHungerChangeTime = _timing.CurTime;
  106. entity.Comp.LastAuthoritativeHungerValue = ClampHungerWithinThresholds(entity.Comp, value);
  107. DirtyField(entity.Owner, entity.Comp, nameof(HungerComponent.LastAuthoritativeHungerChangeTime));
  108. }
  109. private void UpdateCurrentThreshold(EntityUid uid, HungerComponent? component = null)
  110. {
  111. if (!Resolve(uid, ref component))
  112. return;
  113. var calculatedHungerThreshold = GetHungerThreshold(component);
  114. if (calculatedHungerThreshold == component.CurrentThreshold)
  115. return;
  116. component.CurrentThreshold = calculatedHungerThreshold;
  117. DoHungerThresholdEffects(uid, component);
  118. }
  119. private void DoHungerThresholdEffects(EntityUid uid, HungerComponent? component = null, bool force = false)
  120. {
  121. if (!Resolve(uid, ref component))
  122. return;
  123. if (component.CurrentThreshold == component.LastThreshold && !force)
  124. return;
  125. if (GetMovementThreshold(component.CurrentThreshold) != GetMovementThreshold(component.LastThreshold))
  126. {
  127. _movementSpeedModifier.RefreshMovementSpeedModifiers(uid);
  128. }
  129. if (component.HungerThresholdAlerts.TryGetValue(component.CurrentThreshold, out var alertId))
  130. {
  131. _alerts.ShowAlert(uid, alertId);
  132. }
  133. else
  134. {
  135. _alerts.ClearAlertCategory(uid, component.HungerAlertCategory);
  136. }
  137. if (component.HungerThresholdDecayModifiers.TryGetValue(component.CurrentThreshold, out var modifier))
  138. {
  139. component.ActualDecayRate = component.BaseDecayRate * modifier;
  140. SetAuthoritativeHungerValue((uid, component), GetHunger(component));
  141. }
  142. component.LastThreshold = component.CurrentThreshold;
  143. }
  144. private void DoContinuousHungerEffects(EntityUid uid, HungerComponent? component = null)
  145. {
  146. if (!Resolve(uid, ref component))
  147. return;
  148. if (component.CurrentThreshold <= HungerThreshold.Starving &&
  149. component.StarvationDamage is { } damage &&
  150. !_mobState.IsDead(uid))
  151. {
  152. _damageable.TryChangeDamage(uid, damage, true, false);
  153. }
  154. }
  155. /// <summary>
  156. /// Gets the hunger threshold for an entity based on the amount of food specified.
  157. /// If a specific amount isn't specified, just uses the current hunger of the entity
  158. /// </summary>
  159. /// <param name="component"></param>
  160. /// <param name="food"></param>
  161. /// <returns></returns>
  162. public HungerThreshold GetHungerThreshold(HungerComponent component, float? food = null)
  163. {
  164. food ??= GetHunger(component);
  165. var result = HungerThreshold.Dead;
  166. var value = component.Thresholds[HungerThreshold.Overfed];
  167. foreach (var threshold in component.Thresholds)
  168. {
  169. if (threshold.Value <= value && threshold.Value >= food)
  170. {
  171. result = threshold.Key;
  172. value = threshold.Value;
  173. }
  174. }
  175. return result;
  176. }
  177. /// <summary>
  178. /// A check that returns if the entity is below a hunger threshold.
  179. /// </summary>
  180. public bool IsHungerBelowState(EntityUid uid, HungerThreshold threshold, float? food = null, HungerComponent? comp = null)
  181. {
  182. if (!Resolve(uid, ref comp))
  183. return false; // It's never going to go hungry, so it's probably fine to assume that it's not... you know, hungry.
  184. return GetHungerThreshold(comp, food) < threshold;
  185. }
  186. private bool GetMovementThreshold(HungerThreshold threshold)
  187. {
  188. switch (threshold)
  189. {
  190. case HungerThreshold.Overfed:
  191. case HungerThreshold.Okay:
  192. return true;
  193. case HungerThreshold.Peckish:
  194. case HungerThreshold.Starving:
  195. case HungerThreshold.Dead:
  196. return false;
  197. default:
  198. throw new ArgumentOutOfRangeException(nameof(threshold), threshold, null);
  199. }
  200. }
  201. public bool TryGetStatusIconPrototype(HungerComponent component, [NotNullWhen(true)] out SatiationIconPrototype? prototype)
  202. {
  203. switch (component.CurrentThreshold)
  204. {
  205. case HungerThreshold.Overfed:
  206. _prototype.TryIndex(HungerIconOverfedId, out prototype);
  207. break;
  208. case HungerThreshold.Peckish:
  209. _prototype.TryIndex(HungerIconPeckishId, out prototype);
  210. break;
  211. case HungerThreshold.Starving:
  212. _prototype.TryIndex(HungerIconStarvingId, out prototype);
  213. break;
  214. default:
  215. prototype = null;
  216. break;
  217. }
  218. return prototype != null;
  219. }
  220. private static float ClampHungerWithinThresholds(HungerComponent component, float hungerValue)
  221. {
  222. return Math.Clamp(hungerValue,
  223. component.Thresholds[HungerThreshold.Dead],
  224. component.Thresholds[HungerThreshold.Overfed]);
  225. }
  226. public override void Update(float frameTime)
  227. {
  228. base.Update(frameTime);
  229. var query = EntityQueryEnumerator<HungerComponent>();
  230. while (query.MoveNext(out var uid, out var hunger))
  231. {
  232. if (_timing.CurTime < hunger.NextThresholdUpdateTime)
  233. continue;
  234. hunger.NextThresholdUpdateTime = _timing.CurTime + hunger.ThresholdUpdateRate;
  235. UpdateCurrentThreshold(uid, hunger);
  236. DoContinuousHungerEffects(uid, hunger);
  237. }
  238. }
  239. }