PressurizedSolutionSystem.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. using Content.Shared.Chemistry.Reagent;
  2. using Content.Shared.Chemistry.EntitySystems;
  3. using Content.Shared.Hands.EntitySystems;
  4. using Content.Shared.Nutrition.Components;
  5. using Content.Shared.Throwing;
  6. using Content.Shared.IdentityManagement;
  7. using Robust.Shared.Audio.Systems;
  8. using Robust.Shared.Random;
  9. using Robust.Shared.Timing;
  10. using Robust.Shared.Prototypes;
  11. using Robust.Shared.Network;
  12. using Content.Shared.Fluids;
  13. using Content.Shared.Popups;
  14. namespace Content.Shared.Nutrition.EntitySystems;
  15. public sealed partial class PressurizedSolutionSystem : EntitySystem
  16. {
  17. [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
  18. [Dependency] private readonly OpenableSystem _openable = default!;
  19. [Dependency] private readonly SharedAudioSystem _audio = default!;
  20. [Dependency] private readonly SharedHandsSystem _hands = default!;
  21. [Dependency] private readonly SharedPopupSystem _popup = default!;
  22. [Dependency] private readonly SharedPuddleSystem _puddle = default!;
  23. [Dependency] private readonly INetManager _net = default!;
  24. [Dependency] private readonly IGameTiming _timing = default!;
  25. [Dependency] private readonly IRobustRandom _random = default!;
  26. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  27. public override void Initialize()
  28. {
  29. base.Initialize();
  30. SubscribeLocalEvent<PressurizedSolutionComponent, MapInitEvent>(OnMapInit);
  31. SubscribeLocalEvent<PressurizedSolutionComponent, ShakeEvent>(OnShake);
  32. SubscribeLocalEvent<PressurizedSolutionComponent, OpenableOpenedEvent>(OnOpened);
  33. SubscribeLocalEvent<PressurizedSolutionComponent, LandEvent>(OnLand);
  34. SubscribeLocalEvent<PressurizedSolutionComponent, SolutionContainerChangedEvent>(OnSolutionUpdate);
  35. }
  36. /// <summary>
  37. /// Helper method for checking if the solution's fizziness is high enough to spray.
  38. /// <paramref name="chanceMod"/> is added to the actual fizziness for the comparison.
  39. /// </summary>
  40. private bool SprayCheck(Entity<PressurizedSolutionComponent> entity, float chanceMod = 0)
  41. {
  42. return Fizziness((entity, entity.Comp)) + chanceMod > entity.Comp.SprayFizzinessThresholdRoll;
  43. }
  44. /// <summary>
  45. /// Calculates how readily the contained solution becomes fizzy.
  46. /// </summary>
  47. private float SolutionFizzability(Entity<PressurizedSolutionComponent> entity)
  48. {
  49. if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var _, out var solution))
  50. return 0;
  51. // An empty solution can't be fizzy
  52. if (solution.Volume <= 0)
  53. return 0;
  54. var totalFizzability = 0f;
  55. // Check each reagent in the solution
  56. foreach (var reagent in solution.Contents)
  57. {
  58. if (_prototypeManager.TryIndex(reagent.Reagent.Prototype, out ReagentPrototype? reagentProto) && reagentProto != null)
  59. {
  60. // What portion of the solution is this reagent?
  61. var proportion = (float) (reagent.Quantity / solution.Volume);
  62. totalFizzability += reagentProto.Fizziness * proportion;
  63. }
  64. }
  65. return totalFizzability;
  66. }
  67. /// <summary>
  68. /// Increases the fizziness level of the solution by the given amount,
  69. /// scaled by the solution's fizzability.
  70. /// 0 will result in no change, and 1 will maximize fizziness.
  71. /// Also rerolls the spray threshold.
  72. /// </summary>
  73. private void AddFizziness(Entity<PressurizedSolutionComponent> entity, float amount)
  74. {
  75. var fizzability = SolutionFizzability(entity);
  76. // Can't add fizziness if the solution isn't fizzy
  77. if (fizzability <= 0)
  78. return;
  79. // Make sure nothing is preventing fizziness from being added
  80. var attemptEv = new AttemptAddFizzinessEvent(entity, amount);
  81. RaiseLocalEvent(entity, ref attemptEv);
  82. if (attemptEv.Cancelled)
  83. return;
  84. // Scale added fizziness by the solution's fizzability
  85. amount *= fizzability;
  86. // Convert fizziness to time
  87. var duration = amount * entity.Comp.FizzinessMaxDuration;
  88. // Add to the existing settle time, if one exists. Otherwise, add to the current time
  89. var start = entity.Comp.FizzySettleTime > _timing.CurTime ? entity.Comp.FizzySettleTime : _timing.CurTime;
  90. var newTime = start + duration;
  91. // Cap the maximum fizziness
  92. var maxEnd = _timing.CurTime + entity.Comp.FizzinessMaxDuration;
  93. if (newTime > maxEnd)
  94. newTime = maxEnd;
  95. entity.Comp.FizzySettleTime = newTime;
  96. // Roll a new fizziness threshold
  97. RollSprayThreshold(entity);
  98. }
  99. /// <summary>
  100. /// Helper method. Performs a <see cref="SprayCheck"/>. If it passes, calls <see cref="TrySpray"/>. If it fails, <see cref="AddFizziness"/>.
  101. /// </summary>
  102. private void SprayOrAddFizziness(Entity<PressurizedSolutionComponent> entity, float chanceMod = 0, float fizzinessToAdd = 0, EntityUid? user = null)
  103. {
  104. if (SprayCheck(entity, chanceMod))
  105. TrySpray((entity, entity.Comp), user);
  106. else
  107. AddFizziness(entity, fizzinessToAdd);
  108. }
  109. /// <summary>
  110. /// Randomly generates a new spray threshold.
  111. /// This is the value used to compare fizziness against when doing <see cref="SprayCheck"/>.
  112. /// Since RNG will give different results between client and server, this is run on the server
  113. /// and synced to the client by marking the component dirty.
  114. /// We roll this in advance, rather than during <see cref="SprayCheck"/>, so that the value (hopefully)
  115. /// has time to get synced to the client, so we can try be accurate with prediction.
  116. /// </summary>
  117. private void RollSprayThreshold(Entity<PressurizedSolutionComponent> entity)
  118. {
  119. // Can't predict random, so we wait for the server to tell us
  120. if (!_net.IsServer)
  121. return;
  122. entity.Comp.SprayFizzinessThresholdRoll = _random.NextFloat();
  123. Dirty(entity, entity.Comp);
  124. }
  125. #region Public API
  126. /// <summary>
  127. /// Does the entity contain a solution capable of being fizzy?
  128. /// </summary>
  129. public bool CanSpray(Entity<PressurizedSolutionComponent?> entity)
  130. {
  131. if (!Resolve(entity, ref entity.Comp, false))
  132. return false;
  133. return SolutionFizzability((entity, entity.Comp)) > 0;
  134. }
  135. /// <summary>
  136. /// Attempts to spray the solution onto the given entity, or the ground if none is given.
  137. /// Fails if the solution isn't able to be sprayed.
  138. /// </summary>
  139. public bool TrySpray(Entity<PressurizedSolutionComponent?> entity, EntityUid? target = null)
  140. {
  141. if (!Resolve(entity, ref entity.Comp))
  142. return false;
  143. if (!CanSpray(entity))
  144. return false;
  145. if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var soln, out var interactions))
  146. return false;
  147. // If the container is openable, open it
  148. _openable.SetOpen(entity, true);
  149. // Get the spray solution from the container
  150. var solution = _solutionContainer.SplitSolution(soln.Value, interactions.Volume);
  151. // Spray the solution onto the ground and anyone nearby
  152. if (TryComp(entity, out TransformComponent? transform))
  153. _puddle.TrySplashSpillAt(entity, transform.Coordinates, solution, out _, sound: false);
  154. var drinkName = Identity.Entity(entity, EntityManager);
  155. if (target != null)
  156. {
  157. var victimName = Identity.Entity(target.Value, EntityManager);
  158. var selfMessage = Loc.GetString(entity.Comp.SprayHolderMessageSelf, ("victim", victimName), ("drink", drinkName));
  159. var othersMessage = Loc.GetString(entity.Comp.SprayHolderMessageOthers, ("victim", victimName), ("drink", drinkName));
  160. _popup.PopupPredicted(selfMessage, othersMessage, target.Value, target.Value);
  161. }
  162. else
  163. {
  164. // Show a popup to everyone in PVS range
  165. if (_timing.IsFirstTimePredicted)
  166. _popup.PopupEntity(Loc.GetString(entity.Comp.SprayGroundMessage, ("drink", drinkName)), entity);
  167. }
  168. _audio.PlayPredicted(entity.Comp.SpraySound, entity, target);
  169. // We just used all our fizziness, so clear it
  170. TryClearFizziness(entity);
  171. return true;
  172. }
  173. /// <summary>
  174. /// What is the current fizziness level of the solution, from 0 to 1?
  175. /// </summary>
  176. public double Fizziness(Entity<PressurizedSolutionComponent?> entity)
  177. {
  178. // No component means no fizz
  179. if (!Resolve(entity, ref entity.Comp, false))
  180. return 0;
  181. // No negative fizziness
  182. if (entity.Comp.FizzySettleTime <= _timing.CurTime)
  183. return 0;
  184. var currentDuration = entity.Comp.FizzySettleTime - _timing.CurTime;
  185. return Easings.InOutCubic((float) Math.Min(currentDuration / entity.Comp.FizzinessMaxDuration, 1));
  186. }
  187. /// <summary>
  188. /// Attempts to clear any fizziness in the solution.
  189. /// </summary>
  190. /// <remarks>Rolls a new spray threshold.</remarks>
  191. public void TryClearFizziness(Entity<PressurizedSolutionComponent?> entity)
  192. {
  193. if (!Resolve(entity, ref entity.Comp))
  194. return;
  195. entity.Comp.FizzySettleTime = TimeSpan.Zero;
  196. // Roll a new fizziness threshold
  197. RollSprayThreshold((entity, entity.Comp));
  198. }
  199. #endregion
  200. #region Event Handlers
  201. private void OnMapInit(Entity<PressurizedSolutionComponent> entity, ref MapInitEvent args)
  202. {
  203. RollSprayThreshold(entity);
  204. }
  205. private void OnOpened(Entity<PressurizedSolutionComponent> entity, ref OpenableOpenedEvent args)
  206. {
  207. // Make sure the opener is actually holding the drink
  208. var held = args.User != null && _hands.IsHolding(args.User.Value, entity, out _);
  209. SprayOrAddFizziness(entity, entity.Comp.SprayChanceModOnOpened, -1, held ? args.User : null);
  210. }
  211. private void OnShake(Entity<PressurizedSolutionComponent> entity, ref ShakeEvent args)
  212. {
  213. SprayOrAddFizziness(entity, entity.Comp.SprayChanceModOnShake, entity.Comp.FizzinessAddedOnShake, args.Shaker);
  214. }
  215. private void OnLand(Entity<PressurizedSolutionComponent> entity, ref LandEvent args)
  216. {
  217. SprayOrAddFizziness(entity, entity.Comp.SprayChanceModOnLand, entity.Comp.FizzinessAddedOnLand);
  218. }
  219. private void OnSolutionUpdate(Entity<PressurizedSolutionComponent> entity, ref SolutionContainerChangedEvent args)
  220. {
  221. if (args.SolutionId != entity.Comp.Solution)
  222. return;
  223. // If the solution is no longer capable of being fizzy, clear any built up fizziness
  224. if (SolutionFizzability(entity) <= 0)
  225. TryClearFizziness((entity, entity.Comp));
  226. }
  227. #endregion
  228. }
  229. [ByRefEvent]
  230. public record struct AttemptAddFizzinessEvent(Entity<PressurizedSolutionComponent> Entity, float Amount)
  231. {
  232. public bool Cancelled;
  233. }