StaminaSystem.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. using System.Linq;
  2. using Content.Shared.Administration.Logs;
  3. using Content.Shared.Alert;
  4. using Content.Shared.CombatMode;
  5. using Content.Shared.Damage.Components;
  6. using Content.Shared.Damage.Events;
  7. using Content.Shared.Database;
  8. using Content.Shared.Effects;
  9. using Content.Shared.Jittering;
  10. using Content.Shared.Projectiles;
  11. using Content.Shared.Rejuvenate;
  12. using Content.Shared.Rounding;
  13. using Content.Shared.Speech.EntitySystems;
  14. using Content.Shared.StatusEffect;
  15. using Content.Shared.Stunnable;
  16. using Content.Shared.Throwing;
  17. using Content.Shared.Weapons.Melee.Events;
  18. using JetBrains.Annotations;
  19. using Robust.Shared.Audio;
  20. using Robust.Shared.Audio.Systems;
  21. using Robust.Shared.Network;
  22. using Robust.Shared.Player;
  23. using Robust.Shared.Random; // Goob - Shove
  24. using Robust.Shared.Timing;
  25. using Content.Shared.Common.Stunnable;
  26. namespace Content.Shared.Damage.Systems;
  27. public sealed partial class StaminaSystem : EntitySystem
  28. {
  29. [Dependency] private readonly IGameTiming _timing = default!;
  30. [Dependency] private readonly INetManager _net = default!;
  31. [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
  32. [Dependency] private readonly AlertsSystem _alerts = default!;
  33. [Dependency] private readonly MetaDataSystem _metadata = default!;
  34. [Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
  35. [Dependency] private readonly SharedStunSystem _stunSystem = default!;
  36. [Dependency] private readonly SharedAudioSystem _audio = default!;
  37. [Dependency] private readonly StatusEffectsSystem _statusEffect = default!; // goob edit
  38. [Dependency] private readonly SharedStutteringSystem _stutter = default!; // goob edit
  39. [Dependency] private readonly SharedJitteringSystem _jitter = default!; // goob edit
  40. [Dependency] private readonly IRobustRandom _random = default!; // Goob - Shove
  41. [Dependency] private readonly ILogManager _logManager = default!;
  42. private ISawmill _sawmill = default!;
  43. /// <summary>
  44. /// How much of a buffer is there between the stun duration and when stuns can be re-applied.
  45. /// </summary>
  46. private static readonly TimeSpan StamCritBufferTime = TimeSpan.FromSeconds(3f);
  47. /// <summary>
  48. /// Initializes the StaminaSystem, setting up event subscriptions for stamina-related components and configuring logging.
  49. /// </summary>
  50. public override void Initialize()
  51. {
  52. base.Initialize();
  53. InitializeModifier();
  54. SubscribeLocalEvent<StaminaComponent, ComponentStartup>(OnStartup);
  55. SubscribeLocalEvent<StaminaComponent, ComponentShutdown>(OnShutdown);
  56. SubscribeLocalEvent<StaminaComponent, AfterAutoHandleStateEvent>(OnStamHandleState);
  57. SubscribeLocalEvent<StaminaComponent, DisarmedEvent>(OnDisarmed);
  58. SubscribeLocalEvent<StaminaComponent, RejuvenateEvent>(OnRejuvenate);
  59. SubscribeLocalEvent<StaminaDamageOnEmbedComponent, EmbedEvent>(OnProjectileEmbed);
  60. SubscribeLocalEvent<StaminaDamageOnCollideComponent, ProjectileHitEvent>(OnProjectileHit);
  61. SubscribeLocalEvent<StaminaDamageOnCollideComponent, ThrowDoHitEvent>(OnThrowHit);
  62. SubscribeLocalEvent<StaminaDamageOnHitComponent, MeleeHitEvent>(OnMeleeHit);
  63. _sawmill = _logManager.GetSawmill("stamina");
  64. }
  65. /// <summary>
  66. /// Handles stamina state changes after state synchronisation, entering stamina critical state if necessary or updating active stamina components.
  67. /// </summary>
  68. private void OnStamHandleState(EntityUid uid, StaminaComponent component, ref AfterAutoHandleStateEvent args)
  69. {
  70. // goob edit - stunmeta
  71. if (component.Critical)
  72. EnterStamCrit(uid, component, duration: 3f);
  73. else
  74. {
  75. if (component.StaminaDamage > 0f)
  76. EnsureComp<ActiveStaminaComponent>(uid);
  77. ExitStamCrit(uid, component);
  78. }
  79. }
  80. private void OnShutdown(EntityUid uid, StaminaComponent component, ComponentShutdown args)
  81. {
  82. if (MetaData(uid).EntityLifeStage < EntityLifeStage.Terminating)
  83. {
  84. RemCompDeferred<ActiveStaminaComponent>(uid);
  85. }
  86. _alerts.ClearAlert(uid, component.StaminaAlert);
  87. }
  88. private void OnStartup(EntityUid uid, StaminaComponent component, ComponentStartup args)
  89. {
  90. SetStaminaAlert(uid, component);
  91. }
  92. [PublicAPI]
  93. public float GetStaminaDamage(EntityUid uid, StaminaComponent? component = null)
  94. {
  95. if (!Resolve(uid, ref component))
  96. return 0f;
  97. var curTime = _timing.CurTime;
  98. var pauseTime = _metadata.GetPauseTime(uid);
  99. return MathF.Max(0f, component.StaminaDamage - MathF.Max(0f, (float)(curTime - (component.NextUpdate + pauseTime)).TotalSeconds * component.Decay));
  100. }
  101. private void OnRejuvenate(EntityUid uid, StaminaComponent component, RejuvenateEvent args)
  102. {
  103. if (component.StaminaDamage >= component.CritThreshold)
  104. {
  105. ExitStamCrit(uid, component);
  106. }
  107. component.StaminaDamage = 0;
  108. RemComp<ActiveStaminaComponent>(uid);
  109. SetStaminaAlert(uid, component);
  110. Dirty(uid, component);
  111. }
  112. /// <summary>
  113. /// Applies immediate stamina damage with resistances to an entity when disarmed, unless already handled or in a critical state.
  114. /// </summary>
  115. private void OnDisarmed(EntityUid uid, StaminaComponent component, DisarmedEvent args)
  116. {
  117. // No random stamina damage
  118. if (args.Handled)
  119. return;
  120. if (component.Critical)
  121. return;
  122. TakeStaminaDamage(uid, args.StaminaDamage, component, source: args.Source, applyResistances: true, immediate: true);
  123. args.PopupPrefix = "disarm-action-shove-";
  124. args.IsStunned = component.Critical;
  125. // Shoving shouldnt handle it
  126. }
  127. /// <summary>
  128. /// Handles stamina damage application when an entity with a <see cref="StaminaDamageOnHitComponent"/> lands a melee hit,
  129. /// splitting immediate and overtime stamina damage among all valid hit entities and applying relevant multipliers and modifiers.
  130. /// </summary>
  131. private void OnMeleeHit(EntityUid uid, StaminaDamageOnHitComponent component, MeleeHitEvent args)
  132. {
  133. if (!args.IsHit ||
  134. !args.HitEntities.Any() ||
  135. component.Damage <= 0f)
  136. {
  137. return;
  138. }
  139. var ev = new StaminaDamageOnHitAttemptEvent(args.Direction == null, false); // Goob edit
  140. RaiseLocalEvent(uid, ref ev);
  141. if (ev.Cancelled)
  142. return;
  143. var stamQuery = GetEntityQuery<StaminaComponent>();
  144. var toHit = new List<(EntityUid Entity, StaminaComponent Component)>();
  145. // Split stamina damage between all eligible targets.
  146. foreach (var ent in args.HitEntities)
  147. {
  148. if (!stamQuery.TryGetComponent(ent, out var stam))
  149. continue;
  150. toHit.Add((ent, stam));
  151. }
  152. // Goobstation
  153. RaiseLocalEvent(uid, new StaminaDamageMeleeHitEvent(toHit, args.Direction));
  154. // goobstation
  155. foreach (var (ent, comp) in toHit)
  156. {
  157. var hitEvent = new TakeStaminaDamageEvent((ent, comp));
  158. // raise event for each entity hit
  159. RaiseLocalEvent(ent, hitEvent);
  160. if (hitEvent.Handled)
  161. return;
  162. var damageImmediate = component.Damage;
  163. var damageOvertime = component.Overtime;
  164. damageImmediate *= hitEvent.Multiplier;
  165. damageImmediate += hitEvent.FlatModifier;
  166. damageOvertime *= hitEvent.Multiplier;
  167. damageOvertime += hitEvent.FlatModifier;
  168. if (args.Direction == null)
  169. {
  170. damageImmediate *= component.LightAttackDamageMultiplier;
  171. damageOvertime *= component.LightAttackOvertimeDamageMultiplier;
  172. }
  173. TakeStaminaDamage(ent, damageImmediate / toHit.Count, comp, source: args.User, with: args.Weapon, sound: component.Sound, immediate: true);
  174. TakeOvertimeStaminaDamage(ent, damageOvertime);
  175. }
  176. }
  177. private void OnProjectileHit(EntityUid uid, StaminaDamageOnCollideComponent component, ref ProjectileHitEvent args)
  178. {
  179. OnCollide(uid, component, args.Target);
  180. }
  181. /// <summary>
  182. /// Applies immediate stamina damage with resistances to an entity when a projectile embeds into it.
  183. /// </summary>
  184. private void OnProjectileEmbed(EntityUid uid, StaminaDamageOnEmbedComponent component, ref EmbedEvent args)
  185. {
  186. if (!TryComp<StaminaComponent>(args.Embedded, out var stamina))
  187. return;
  188. TakeStaminaDamage(args.Embedded, component.Damage, stamina, source: uid, applyResistances: true, immediate: true);
  189. }
  190. /// <summary>
  191. /// Applies stamina damage to a target entity when struck by a thrown object.
  192. /// </summary>
  193. private void OnThrowHit(EntityUid uid, StaminaDamageOnCollideComponent component, ThrowDoHitEvent args)
  194. {
  195. OnCollide(uid, component, args.Target);
  196. }
  197. /// <summary>
  198. /// Applies stamina damage to a target entity upon collision if it has a stamina component, allowing for event-based modification or cancellation.
  199. /// </summary>
  200. /// <param name="uid">The entity causing the collision.</param>
  201. /// <param name="component">The stamina damage on collide component.</param>
  202. /// <param name="target">The entity being collided with.</param>
  203. private void OnCollide(EntityUid uid, StaminaDamageOnCollideComponent component, EntityUid target)
  204. {
  205. // you can't inflict stamina damage on things with no stamina component
  206. // this prevents stun batons from using up charges when throwing it at lockers or lights
  207. if (!TryComp<StaminaComponent>(target, out var stamComp))
  208. return;
  209. var ev = new StaminaDamageOnHitAttemptEvent();
  210. RaiseLocalEvent(uid, ref ev);
  211. if (ev.Cancelled)
  212. return;
  213. // goobstation
  214. var hitEvent = new TakeStaminaDamageEvent((target, stamComp));
  215. RaiseLocalEvent(target, hitEvent);
  216. if (hitEvent.Handled)
  217. return;
  218. var damage = component.Damage;
  219. var overtime = component.Damage;
  220. damage *= hitEvent.Multiplier;
  221. damage += hitEvent.FlatModifier;
  222. overtime *= hitEvent.Multiplier;
  223. overtime += hitEvent.FlatModifier;
  224. TakeStaminaDamage(target, damage, source: uid, sound: component.Sound, immediate: true);
  225. TakeOvertimeStaminaDamage(target, overtime); // Goobstation
  226. }
  227. /// <summary>
  228. /// Updates the stamina alert level for an entity based on its current stamina damage relative to the critical threshold.
  229. /// </summary>
  230. private void SetStaminaAlert(EntityUid uid, StaminaComponent? component = null)
  231. {
  232. if (!Resolve(uid, ref component, false) || component.Deleted)
  233. return;
  234. var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, component.CritThreshold - component.StaminaDamage), component.CritThreshold, 7);
  235. _alerts.ShowAlert(uid, component.StaminaAlert, (short)severity);
  236. }
  237. /// <summary>
  238. /// Tries to take stamina damage without raising the entity over the crit threshold.
  239. /// <summary>
  240. /// Attempts to apply stamina damage to an entity, returning whether the entity remains below the critical threshold.
  241. /// </summary>
  242. /// <param name="uid">The entity to apply stamina damage to.</param>
  243. /// <param name="value">The amount of stamina damage to attempt to apply.</param>
  244. /// <param name="component">Optional stamina component; resolved if not provided.</param>
  245. /// <param name="source">Optional source of the stamina damage.</param>
  246. /// <param name="with">Optional weapon or item used to inflict the damage.</param>
  247. /// <returns>True if the stamina damage was applied and the entity is not in a critical state; false if the entity would exceed or is already at the critical threshold.</returns>
  248. public bool TryTakeStamina(EntityUid uid, float value, StaminaComponent? component = null, EntityUid? source = null, EntityUid? with = null)
  249. {
  250. // Something that has no Stamina component automatically passes stamina checks
  251. if (!Resolve(uid, ref component, false))
  252. return true;
  253. var oldStam = component.StaminaDamage;
  254. if (oldStam + value > component.CritThreshold || component.Critical)
  255. return false;
  256. TakeStaminaDamage(uid, value, component, source, with, visual: false, immediate: true);
  257. return true;
  258. }
  259. /// <summary>
  260. /// Adds stamina damage over time to the specified entity, accumulating the value in its overtime stamina component.
  261. /// </summary>
  262. /// <param name="uid">The entity to receive overtime stamina damage.</param>
  263. /// <param name="value">The amount of stamina damage to add.</param>
  264. public void TakeOvertimeStaminaDamage(EntityUid uid, float value)
  265. {
  266. // do this only on server side because otherwise shit happens
  267. if (value == 0)
  268. return;
  269. var hasComp = TryComp<OvertimeStaminaDamageComponent>(uid, out var overtime);
  270. if (!hasComp)
  271. overtime = EnsureComp<OvertimeStaminaDamageComponent>(uid);
  272. overtime!.Amount = hasComp ? overtime.Amount + value : value;
  273. overtime!.Damage = hasComp ? overtime.Damage + value : value;
  274. }
  275. /// <summary>
  276. /// Applies stamina damage to an entity, optionally factoring in resistances, triggering visual and audio effects, and logging the event.
  277. /// </summary>
  278. /// <param name="uid">The entity receiving stamina damage.</param>
  279. /// <param name="value">The amount of stamina damage to apply.</param>
  280. /// <param name="component">Optional stamina component; resolved if not provided.</param>
  281. /// <param name="source">Optional source entity responsible for the damage.</param>
  282. /// <param name="with">Optional entity used to inflict the damage.</param>
  283. /// <param name="visual">Whether to trigger visual effects for the damage.</param>
  284. /// <param name="sound">Optional sound to play when damage is applied.</param>
  285. /// <param name="immediate">If true, applies a hard stun when entering stamina crit.</param>
  286. /// <param name="applyResistances">If true, applies resistance modifiers before applying damage.</param>
  287. /// <param name="shouldLog">Whether to log the stamina damage event.</param>
  288. /// <remarks>
  289. /// If the entity is already in stamina crit or the event is cancelled, no damage is applied. Exceeding the slowdown threshold applies jitter, stutter, and slowdown effects. Entering stamina crit may apply a hard stun depending on the <paramref name="immediate"/> flag.
  290. /// </remarks>
  291. public void TakeStaminaDamage(EntityUid uid, float value, StaminaComponent? component = null,
  292. EntityUid? source = null, EntityUid? with = null, bool visual = true, SoundSpecifier? sound = null, bool immediate = false, bool applyResistances = false, bool shouldLog = true)
  293. {
  294. if (!Resolve(uid, ref component, false)
  295. || value == 0) // no damage???
  296. return;
  297. var ev = new BeforeStaminaDamageEvent(value);
  298. RaiseLocalEvent(uid, ref ev);
  299. if (ev.Cancelled)
  300. return;
  301. // Have we already reached the point of max stamina damage?
  302. if (component.Critical)
  303. return;
  304. if (applyResistances)
  305. {
  306. var hitEvent = new TakeStaminaDamageEvent((uid, component));
  307. RaiseLocalEvent(uid, hitEvent);
  308. if (hitEvent.Handled)
  309. return;
  310. value *= hitEvent.Multiplier;
  311. value += hitEvent.FlatModifier;
  312. }
  313. var oldDamage = component.StaminaDamage;
  314. component.StaminaDamage = MathF.Max(0f, component.StaminaDamage + value);
  315. // Reset the decay cooldown upon taking damage.
  316. if (oldDamage < component.StaminaDamage)
  317. {
  318. var nextUpdate = _timing.CurTime + TimeSpan.FromSeconds(component.Cooldown);
  319. if (component.NextUpdate < nextUpdate)
  320. component.NextUpdate = nextUpdate;
  321. }
  322. var slowdownThreshold = component.SlowdownThreshold; // stalker-changes
  323. // If we go above n% then apply effects
  324. if (component.StaminaDamage > slowdownThreshold)
  325. {
  326. // goob edit - stunmeta
  327. _jitter.DoJitter(uid, TimeSpan.FromSeconds(2f), true);
  328. _stutter.DoStutter(uid, TimeSpan.FromSeconds(10f), true);
  329. _stunSystem.TrySlowdown(uid, TimeSpan.FromSeconds(8), true, 0.7f, 0.7f);
  330. }
  331. SetStaminaAlert(uid, component);
  332. if (!component.Critical && component.StaminaDamage >= component.CritThreshold && value > 0) // goob edit
  333. EnterStamCrit(uid, component, immediate, duration: 3f);
  334. else if (component.StaminaDamage < component.CritThreshold)
  335. ExitStamCrit(uid, component);
  336. EnsureComp<ActiveStaminaComponent>(uid);
  337. Dirty(uid, component);
  338. if (value <= 0)
  339. return;
  340. if (source != null && shouldLog) // stalker-changes
  341. {
  342. _adminLogger.Add(LogType.Stamina, $"{ToPrettyString(source.Value):user} caused {value} stamina damage to {ToPrettyString(uid):target}{(with != null ? $" using {ToPrettyString(with.Value):using}" : "")}");
  343. }
  344. else if (shouldLog) // stalker-changes
  345. {
  346. _adminLogger.Add(LogType.Stamina, $"{ToPrettyString(uid):target} took {value} stamina damage");
  347. }
  348. if (visual)
  349. {
  350. _color.RaiseEffect(Color.Aqua, new List<EntityUid>() { uid }, Filter.Pvs(uid, entityManager: EntityManager));
  351. }
  352. if (_net.IsServer)
  353. {
  354. _audio.PlayPvs(sound, uid);
  355. }
  356. }
  357. /// <summary>
  358. /// Enables or disables a stamina drain effect on an entity from a specified source, with an optional speed modification.
  359. /// </summary>
  360. /// <param name="target">The entity to apply or remove the stamina drain from.</param>
  361. /// <param name="drainRate">The rate at which stamina is drained per second.</param>
  362. /// <param name="enabled">Whether to enable or disable the stamina drain.</param>
  363. /// <param name="modifiesSpeed">Whether the drain should also affect the entity's movement speed.</param>
  364. /// <param name="source">The source entity responsible for the drain; if null, the target is used as the source.</param>
  365. public void ToggleStaminaDrain(EntityUid target, float drainRate, bool enabled, bool modifiesSpeed, EntityUid? source = null)
  366. {
  367. if (!TryComp<StaminaComponent>(target, out var stamina))
  368. return;
  369. // If theres no source, we assume its the target that caused the drain.
  370. var actualSource = source ?? target;
  371. if (enabled)
  372. {
  373. stamina.ActiveDrains[actualSource] = (drainRate, modifiesSpeed);
  374. EnsureComp<ActiveStaminaComponent>(target);
  375. }
  376. else
  377. stamina.ActiveDrains.Remove(actualSource);
  378. Dirty(target, stamina);
  379. }
  380. /// <summary>
  381. /// Processes stamina updates for all entities with active stamina components, applying stamina drains, handling recovery, and managing entry and exit from stamina critical states.
  382. /// </summary>
  383. public override void Update(float frameTime)
  384. {
  385. base.Update(frameTime);
  386. if (!_timing.IsFirstTimePredicted)
  387. return;
  388. var stamQuery = GetEntityQuery<StaminaComponent>();
  389. var query = EntityQueryEnumerator<ActiveStaminaComponent>();
  390. var curTime = _timing.CurTime;
  391. while (query.MoveNext(out var uid, out _))
  392. {
  393. // Just in case we have active but not stamina we'll check and account for it.
  394. if (!stamQuery.TryGetComponent(uid, out var comp) ||
  395. comp.StaminaDamage <= 0f && !comp.Critical && comp.ActiveDrains.Count == 0)
  396. {
  397. RemComp<ActiveStaminaComponent>(uid);
  398. continue;
  399. }
  400. if (comp.ActiveDrains.Count > 0)
  401. foreach (var (source, (drainRate, modifiesSpeed)) in comp.ActiveDrains)
  402. TakeStaminaDamage(uid,
  403. drainRate * frameTime,
  404. comp,
  405. source: source,
  406. visual: false);
  407. // Shouldn't need to consider paused time as we're only iterating non-paused stamina components.
  408. var nextUpdate = comp.NextUpdate;
  409. if (nextUpdate > curTime)
  410. continue;
  411. // We were in crit so come out of it and continue.
  412. if (comp.Critical)
  413. {
  414. ExitStamCrit(uid, comp);
  415. continue;
  416. }
  417. comp.NextUpdate += TimeSpan.FromSeconds(1f);
  418. // If theres no active drains, recover stamina.
  419. if (comp.ActiveDrains.Count == 0)
  420. TakeStaminaDamage(uid, -comp.Decay, comp);
  421. Dirty(uid, comp);
  422. }
  423. }
  424. /// <summary>
  425. /// Puts an entity into stamina critical state, optionally applying a hard stun (paralysis) for a specified duration.
  426. /// </summary>
  427. /// <param name="uid">The entity to enter stamina crit.</param>
  428. /// <param name="component">The stamina component, if already resolved.</param>
  429. /// <param name="hardStun">If true, applies a full paralysis; otherwise, does not apply a hard stun.</param>
  430. /// <param name="duration">Duration of the stun effect in seconds if hard stun is applied.</param>
  431. private void EnterStamCrit(EntityUid uid, StaminaComponent? component = null, bool hardStun = false, float duration = 6f)
  432. {
  433. if (!Resolve(uid, ref component) || component.Critical)
  434. {
  435. return;
  436. }
  437. _sawmill.Info("entering stamcrit");
  438. if (!hardStun)
  439. {
  440. _sawmill.Info("no hardcrit");
  441. //var parsedDuration = TimeSpan.FromSeconds(duration);
  442. //if (!_statusEffect.HasStatusEffect(uid, "KnockedDown"))
  443. // _stunSystem.TryKnockdown(uid, parsedDuration, true);
  444. //return;
  445. }
  446. else
  447. { // you got batonned hard.
  448. component.Critical = true;
  449. _stunSystem.TryParalyze(uid, component.StunTime, true);
  450. }
  451. component.NextUpdate = _timing.CurTime + component.StunTime + StamCritBufferTime; // Goobstation
  452. EnsureComp<ActiveStaminaComponent>(uid);
  453. Dirty(uid, component);
  454. _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(uid):user} entered stamina crit");
  455. }
  456. // goob edit - made it public.
  457. // in any case it requires a stamina component that can be freely modified.
  458. // so it doesn't really matter if it's public or private. besides, very convenient.
  459. /// <summary>
  460. /// Exits stamina critical state for the specified entity, resetting stamina damage and related effects.
  461. /// </summary>
  462. public void ExitStamCrit(EntityUid uid, StaminaComponent? component = null)
  463. {
  464. if (!Resolve(uid, ref component) ||
  465. !component.Critical)
  466. {
  467. return;
  468. }
  469. component.Critical = false;
  470. component.StaminaDamage = 0f;
  471. component.NextUpdate = _timing.CurTime;
  472. SetStaminaAlert(uid, component);
  473. RemComp<ActiveStaminaComponent>(uid);
  474. Dirty(uid, component);
  475. _adminLogger.Add(LogType.Stamina, LogImpact.Low, $"{ToPrettyString(uid):user} recovered from stamina crit");
  476. }
  477. }
  478. /// <summary>
  479. /// Raised before stamina damage is dealt to allow other systems to cancel it.
  480. /// </summary>
  481. [ByRefEvent]
  482. public record struct BeforeStaminaDamageEvent(float Value, bool Cancelled = false);