ThirstSystem.cs 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. using Content.Shared.Alert;
  2. using Content.Shared.Movement.Components;
  3. using Content.Shared.Movement.Systems;
  4. using Content.Shared.Nutrition.Components;
  5. using Content.Shared.Rejuvenate;
  6. using Content.Shared.StatusIcon;
  7. using JetBrains.Annotations;
  8. using Robust.Shared.Prototypes;
  9. using Robust.Shared.Random;
  10. using Robust.Shared.Timing;
  11. using Robust.Shared.Utility;
  12. using System.Diagnostics.CodeAnalysis;
  13. using Content.Shared.Damage;
  14. using Content.Shared.Mobs.Systems;
  15. namespace Content.Shared.Nutrition.EntitySystems;
  16. [UsedImplicitly]
  17. public sealed class ThirstSystem : EntitySystem
  18. {
  19. [Dependency] private readonly IGameTiming _timing = default!;
  20. [Dependency] private readonly IPrototypeManager _prototype = default!;
  21. [Dependency] private readonly IRobustRandom _random = default!;
  22. [Dependency] private readonly AlertsSystem _alerts = default!;
  23. [Dependency] private readonly MovementSpeedModifierSystem _movement = default!;
  24. [Dependency] private readonly SharedJetpackSystem _jetpack = default!;
  25. [Dependency] private readonly DamageableSystem _damageable = default!;
  26. [Dependency] private readonly MobStateSystem _mobState = default!;
  27. [ValidatePrototypeId<SatiationIconPrototype>]
  28. private const string ThirstIconOverhydratedId = "ThirstIconOverhydrated";
  29. [ValidatePrototypeId<SatiationIconPrototype>]
  30. private const string ThirstIconThirstyId = "ThirstIconThirsty";
  31. [ValidatePrototypeId<SatiationIconPrototype>]
  32. private const string ThirstIconParchedId = "ThirstIconParched";
  33. public override void Initialize()
  34. {
  35. base.Initialize();
  36. SubscribeLocalEvent<ThirstComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
  37. SubscribeLocalEvent<ThirstComponent, MapInitEvent>(OnMapInit);
  38. SubscribeLocalEvent<ThirstComponent, RejuvenateEvent>(OnRejuvenate);
  39. }
  40. private void OnMapInit(EntityUid uid, ThirstComponent component, MapInitEvent args)
  41. {
  42. // Do not change behavior unless starting value is explicitly defined
  43. if (component.CurrentThirst < 0)
  44. {
  45. component.CurrentThirst = _random.Next(
  46. (int)component.ThirstThresholds[ThirstThreshold.Thirsty] + 10,
  47. (int)component.ThirstThresholds[ThirstThreshold.Okay] - 1);
  48. }
  49. component.NextUpdateTime = _timing.CurTime;
  50. component.CurrentThirstThreshold = GetThirstThreshold(component, component.CurrentThirst);
  51. component.LastThirstThreshold = ThirstThreshold.Okay; // TODO: Potentially change this -> Used Okay because no effects.
  52. // TODO: Check all thresholds make sense and throw if they don't.
  53. UpdateEffects(uid, component);
  54. TryComp(uid, out MovementSpeedModifierComponent? moveMod);
  55. _movement.RefreshMovementSpeedModifiers(uid, moveMod);
  56. }
  57. private void OnRefreshMovespeed(EntityUid uid, ThirstComponent component, RefreshMovementSpeedModifiersEvent args)
  58. {
  59. // TODO: This should really be taken care of somewhere else
  60. if (_jetpack.IsUserFlying(uid))
  61. return;
  62. var mod = component.CurrentThirstThreshold <= ThirstThreshold.Parched ? 0.75f : 1.0f;
  63. args.ModifySpeed(mod, mod);
  64. }
  65. private void OnRejuvenate(EntityUid uid, ThirstComponent component, RejuvenateEvent args)
  66. {
  67. SetThirst(uid, component, component.ThirstThresholds[ThirstThreshold.Okay]);
  68. }
  69. private ThirstThreshold GetThirstThreshold(ThirstComponent component, float amount)
  70. {
  71. ThirstThreshold result = ThirstThreshold.Dead;
  72. var value = component.ThirstThresholds[ThirstThreshold.OverHydrated];
  73. foreach (var threshold in component.ThirstThresholds)
  74. {
  75. if (threshold.Value <= value && threshold.Value >= amount)
  76. {
  77. result = threshold.Key;
  78. value = threshold.Value;
  79. }
  80. }
  81. return result;
  82. }
  83. public void ModifyThirst(EntityUid uid, ThirstComponent component, float amount)
  84. {
  85. SetThirst(uid, component, component.CurrentThirst + amount);
  86. }
  87. public void SetThirst(EntityUid uid, ThirstComponent component, float amount)
  88. {
  89. component.CurrentThirst = Math.Clamp(amount,
  90. component.ThirstThresholds[ThirstThreshold.Dead],
  91. component.ThirstThresholds[ThirstThreshold.OverHydrated]
  92. );
  93. EntityManager.DirtyField(uid, component, nameof(ThirstComponent.CurrentThirst));
  94. }
  95. private bool IsMovementThreshold(ThirstThreshold threshold)
  96. {
  97. switch (threshold)
  98. {
  99. case ThirstThreshold.Dead:
  100. case ThirstThreshold.Parched:
  101. return true;
  102. case ThirstThreshold.Thirsty:
  103. case ThirstThreshold.Okay:
  104. case ThirstThreshold.OverHydrated:
  105. return false;
  106. default:
  107. throw new ArgumentOutOfRangeException(nameof(threshold), threshold, null);
  108. }
  109. }
  110. public bool TryGetStatusIconPrototype(ThirstComponent component, [NotNullWhen(true)] out SatiationIconPrototype? prototype)
  111. {
  112. switch (component.CurrentThirstThreshold)
  113. {
  114. case ThirstThreshold.OverHydrated:
  115. _prototype.TryIndex(ThirstIconOverhydratedId, out prototype);
  116. break;
  117. case ThirstThreshold.Thirsty:
  118. _prototype.TryIndex(ThirstIconThirstyId, out prototype);
  119. break;
  120. case ThirstThreshold.Parched:
  121. _prototype.TryIndex(ThirstIconParchedId, out prototype);
  122. break;
  123. default:
  124. prototype = null;
  125. break;
  126. }
  127. return prototype != null;
  128. }
  129. private void UpdateEffects(EntityUid uid, ThirstComponent component)
  130. {
  131. if (IsMovementThreshold(component.LastThirstThreshold) != IsMovementThreshold(component.CurrentThirstThreshold) &&
  132. TryComp(uid, out MovementSpeedModifierComponent? movementSlowdownComponent))
  133. {
  134. _movement.RefreshMovementSpeedModifiers(uid, movementSlowdownComponent);
  135. }
  136. // Update UI
  137. if (ThirstComponent.ThirstThresholdAlertTypes.TryGetValue(component.CurrentThirstThreshold, out var alertId))
  138. {
  139. _alerts.ShowAlert(uid, alertId);
  140. }
  141. else
  142. {
  143. _alerts.ClearAlertCategory(uid, component.ThirstyCategory);
  144. }
  145. switch (component.CurrentThirstThreshold)
  146. {
  147. case ThirstThreshold.OverHydrated:
  148. component.LastThirstThreshold = component.CurrentThirstThreshold;
  149. component.ActualDecayRate = component.BaseDecayRate * 1.2f;
  150. return;
  151. case ThirstThreshold.Okay:
  152. component.LastThirstThreshold = component.CurrentThirstThreshold;
  153. component.ActualDecayRate = component.BaseDecayRate;
  154. return;
  155. case ThirstThreshold.Thirsty:
  156. // Same as okay except with UI icon saying drink soon.
  157. component.LastThirstThreshold = component.CurrentThirstThreshold;
  158. component.ActualDecayRate = component.BaseDecayRate * 0.8f;
  159. return;
  160. case ThirstThreshold.Parched:
  161. _movement.RefreshMovementSpeedModifiers(uid);
  162. component.LastThirstThreshold = component.CurrentThirstThreshold;
  163. component.ActualDecayRate = component.BaseDecayRate * 0.6f;
  164. return;
  165. case ThirstThreshold.Dead:
  166. return;
  167. default:
  168. Log.Error($"No thirst threshold found for {component.CurrentThirstThreshold}");
  169. throw new ArgumentOutOfRangeException($"No thirst threshold found for {component.CurrentThirstThreshold}");
  170. }
  171. }
  172. private void DoContinuousThirstEffects(EntityUid uid, ThirstComponent? component = null)
  173. {
  174. if (!Resolve(uid, ref component))
  175. return;
  176. if (component.CurrentThirstThreshold <= ThirstThreshold.Parched &&
  177. component.ThirstDamage is { } damage &&
  178. !_mobState.IsDead(uid))
  179. {
  180. _damageable.TryChangeDamage(uid, damage, true, false);
  181. }
  182. }
  183. public override void Update(float frameTime)
  184. {
  185. base.Update(frameTime);
  186. var query = EntityQueryEnumerator<ThirstComponent>();
  187. while (query.MoveNext(out var uid, out var thirst))
  188. {
  189. if (_timing.CurTime < thirst.NextUpdateTime)
  190. continue;
  191. thirst.NextUpdateTime += thirst.UpdateRate;
  192. ModifyThirst(uid, thirst, -thirst.ActualDecayRate);
  193. var calculatedThirstThreshold = GetThirstThreshold(thirst, thirst.CurrentThirst);
  194. if (calculatedThirstThreshold == thirst.CurrentThirstThreshold)
  195. continue;
  196. thirst.CurrentThirstThreshold = calculatedThirstThreshold;
  197. UpdateEffects(uid, thirst);
  198. DoContinuousThirstEffects(uid, thirst);
  199. }
  200. }
  201. }