HealingSystem.cs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. using Content.Server.Administration.Logs;
  2. using Content.Server.Body.Components;
  3. using Content.Server.Body.Systems;
  4. using Content.Server.Medical.Components;
  5. using Content.Server.Popups;
  6. using Content.Server.Stack;
  7. using Content.Shared.Chemistry.EntitySystems;
  8. using Content.Shared.Audio;
  9. using Content.Shared.Damage;
  10. using Content.Shared.Database;
  11. using Content.Shared.DoAfter;
  12. using Content.Shared.FixedPoint;
  13. using Content.Shared.IdentityManagement;
  14. using Content.Shared.Interaction;
  15. using Content.Shared.Interaction.Events;
  16. using Content.Shared.Medical;
  17. using Content.Shared.Mobs;
  18. using Content.Shared.Mobs.Components;
  19. using Content.Shared.Mobs.Systems;
  20. using Content.Shared.Popups;
  21. using Content.Shared.Stacks;
  22. using Robust.Shared.Audio.Systems;
  23. using Robust.Shared.Random;
  24. namespace Content.Server.Medical;
  25. public sealed class HealingSystem : EntitySystem
  26. {
  27. [Dependency] private readonly SharedAudioSystem _audio = default!;
  28. [Dependency] private readonly IAdminLogManager _adminLogger = default!;
  29. [Dependency] private readonly DamageableSystem _damageable = default!;
  30. [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
  31. [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
  32. [Dependency] private readonly IRobustRandom _random = default!;
  33. [Dependency] private readonly StackSystem _stacks = default!;
  34. [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
  35. [Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
  36. [Dependency] private readonly PopupSystem _popupSystem = default!;
  37. [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
  38. public override void Initialize()
  39. {
  40. base.Initialize();
  41. SubscribeLocalEvent<HealingComponent, UseInHandEvent>(OnHealingUse);
  42. SubscribeLocalEvent<HealingComponent, AfterInteractEvent>(OnHealingAfterInteract);
  43. SubscribeLocalEvent<DamageableComponent, HealingDoAfterEvent>(OnDoAfter);
  44. }
  45. private void OnDoAfter(Entity<DamageableComponent> entity, ref HealingDoAfterEvent args)
  46. {
  47. var dontRepeat = false;
  48. if (!TryComp(args.Used, out HealingComponent? healing))
  49. return;
  50. if (args.Handled || args.Cancelled)
  51. return;
  52. if (healing.DamageContainers is not null &&
  53. entity.Comp.DamageContainerID is not null &&
  54. !healing.DamageContainers.Contains(entity.Comp.DamageContainerID))
  55. {
  56. return;
  57. }
  58. // Heal some bloodloss damage.
  59. if (healing.BloodlossModifier != 0)
  60. {
  61. if (!TryComp<BloodstreamComponent>(entity, out var bloodstream))
  62. return;
  63. var isBleeding = bloodstream.BleedAmount > 0;
  64. _bloodstreamSystem.TryModifyBleedAmount(entity.Owner, healing.BloodlossModifier);
  65. if (isBleeding != bloodstream.BleedAmount > 0)
  66. {
  67. var popup = (args.User == entity.Owner)
  68. ? Loc.GetString("medical-item-stop-bleeding-self")
  69. : Loc.GetString("medical-item-stop-bleeding", ("target", Identity.Entity(entity.Owner, EntityManager)));
  70. _popupSystem.PopupEntity(popup, entity, args.User);
  71. }
  72. }
  73. // Restores missing blood
  74. if (healing.ModifyBloodLevel != 0)
  75. _bloodstreamSystem.TryModifyBloodLevel(entity.Owner, healing.ModifyBloodLevel);
  76. var healed = _damageable.TryChangeDamage(entity.Owner, healing.Damage * _damageable.UniversalTopicalsHealModifier, true, origin: args.Args.User);
  77. if (healed == null && healing.BloodlossModifier != 0)
  78. return;
  79. var total = healed?.GetTotal() ?? FixedPoint2.Zero;
  80. // Re-verify that we can heal the damage.
  81. if (TryComp<StackComponent>(args.Used.Value, out var stackComp))
  82. {
  83. _stacks.Use(args.Used.Value, 1, stackComp);
  84. if (_stacks.GetCount(args.Used.Value, stackComp) <= 0)
  85. dontRepeat = true;
  86. }
  87. else
  88. {
  89. QueueDel(args.Used.Value);
  90. }
  91. if (entity.Owner != args.User)
  92. {
  93. _adminLogger.Add(LogType.Healed,
  94. $"{EntityManager.ToPrettyString(args.User):user} healed {EntityManager.ToPrettyString(entity.Owner):target} for {total:damage} damage");
  95. }
  96. else
  97. {
  98. _adminLogger.Add(LogType.Healed,
  99. $"{EntityManager.ToPrettyString(args.User):user} healed themselves for {total:damage} damage");
  100. }
  101. _audio.PlayPvs(healing.HealingEndSound, entity.Owner, AudioHelpers.WithVariation(0.125f, _random).WithVolume(1f));
  102. // Logic to determine the whether or not to repeat the healing action
  103. args.Repeat = (HasDamage(entity, healing) && !dontRepeat);
  104. if (!args.Repeat && !dontRepeat)
  105. _popupSystem.PopupEntity(Loc.GetString("medical-item-finished-using", ("item", args.Used)), entity.Owner, args.User);
  106. args.Handled = true;
  107. }
  108. private bool HasDamage(Entity<DamageableComponent> ent, HealingComponent healing)
  109. {
  110. var damageableDict = ent.Comp.Damage.DamageDict;
  111. var healingDict = healing.Damage.DamageDict;
  112. foreach (var type in healingDict)
  113. {
  114. if (damageableDict[type.Key].Value > 0)
  115. {
  116. return true;
  117. }
  118. }
  119. if (TryComp<BloodstreamComponent>(ent, out var bloodstream))
  120. {
  121. // Is ent missing blood that we can restore?
  122. if (healing.ModifyBloodLevel > 0
  123. && _solutionContainerSystem.ResolveSolution(ent.Owner, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution)
  124. && bloodSolution.Volume < bloodSolution.MaxVolume)
  125. {
  126. return true;
  127. }
  128. // Is ent bleeding and can we stop it?
  129. if (healing.BloodlossModifier < 0 && bloodstream.BleedAmount > 0)
  130. {
  131. return true;
  132. }
  133. }
  134. return false;
  135. }
  136. private void OnHealingUse(Entity<HealingComponent> entity, ref UseInHandEvent args)
  137. {
  138. if (args.Handled)
  139. return;
  140. if (TryHeal(entity, args.User, args.User, entity.Comp))
  141. args.Handled = true;
  142. }
  143. private void OnHealingAfterInteract(Entity<HealingComponent> entity, ref AfterInteractEvent args)
  144. {
  145. if (args.Handled || !args.CanReach || args.Target == null)
  146. return;
  147. if (TryHeal(entity, args.User, args.Target.Value, entity.Comp))
  148. args.Handled = true;
  149. }
  150. private bool TryHeal(EntityUid uid, EntityUid user, EntityUid target, HealingComponent component)
  151. {
  152. if (!TryComp<DamageableComponent>(target, out var targetDamage))
  153. return false;
  154. if (component.DamageContainers is not null &&
  155. targetDamage.DamageContainerID is not null &&
  156. !component.DamageContainers.Contains(targetDamage.DamageContainerID))
  157. {
  158. return false;
  159. }
  160. if (user != target && !_interactionSystem.InRangeUnobstructed(user, target, popup: true))
  161. return false;
  162. if (TryComp<StackComponent>(uid, out var stack) && stack.Count < 1)
  163. return false;
  164. if (!HasDamage((target, targetDamage), component))
  165. {
  166. _popupSystem.PopupEntity(Loc.GetString("medical-item-cant-use", ("item", uid)), uid, user);
  167. return false;
  168. }
  169. _audio.PlayPvs(component.HealingBeginSound, uid,
  170. AudioHelpers.WithVariation(0.125f, _random).WithVolume(1f));
  171. var isNotSelf = user != target;
  172. if (isNotSelf)
  173. {
  174. var msg = Loc.GetString("medical-item-popup-target", ("user", Identity.Entity(user, EntityManager)), ("item", uid));
  175. _popupSystem.PopupEntity(msg, target, target, PopupType.Medium);
  176. }
  177. var delay = isNotSelf
  178. ? component.Delay
  179. : component.Delay * GetScaledHealingPenalty(user, component);
  180. var doAfterEventArgs =
  181. new DoAfterArgs(EntityManager, user, delay, new HealingDoAfterEvent(), target, target: target, used: uid)
  182. {
  183. // Didn't break on damage as they may be trying to prevent it and
  184. // not being able to heal your own ticking damage would be frustrating.
  185. NeedHand = true,
  186. BreakOnMove = true,
  187. BreakOnWeightlessMove = false,
  188. };
  189. _doAfter.TryStartDoAfter(doAfterEventArgs);
  190. return true;
  191. }
  192. /// <summary>
  193. /// Scales the self-heal penalty based on the amount of damage taken
  194. /// </summary>
  195. /// <param name="uid"></param>
  196. /// <param name="component"></param>
  197. /// <returns></returns>
  198. public float GetScaledHealingPenalty(EntityUid uid, HealingComponent component)
  199. {
  200. var output = component.Delay;
  201. if (!TryComp<MobThresholdsComponent>(uid, out var mobThreshold) ||
  202. !TryComp<DamageableComponent>(uid, out var damageable))
  203. return output;
  204. if (!_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Critical, out var amount, mobThreshold))
  205. return 1;
  206. var percentDamage = (float) (damageable.TotalDamage / amount);
  207. //basically make it scale from 1 to the multiplier.
  208. var modifier = percentDamage * (component.SelfHealPenaltyMultiplier - 1) + 1;
  209. return Math.Max(modifier, 1);
  210. }
  211. }