| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- using System.Linq;
- using Content.Shared.Administration.Logs;
- using Content.Shared.Alert;
- using Content.Shared.CombatMode;
- using Content.Shared.Damage.Components;
- using Content.Shared.Damage.Events;
- using Content.Shared.Database;
- using Content.Shared.Effects;
- using Content.Shared.IdentityManagement;
- using Content.Shared.Popups;
- using Content.Shared.Projectiles;
- using Content.Shared.Rejuvenate;
- using Content.Shared.Rounding;
- using Content.Shared.Stunnable;
- using Content.Shared.Throwing;
- using Content.Shared.Weapons.Melee.Events;
- using JetBrains.Annotations;
- using Robust.Shared.Audio;
- using Robust.Shared.Audio.Systems;
- using Robust.Shared.Network;
- using Robust.Shared.Player;
- using Robust.Shared.Random;
- using Robust.Shared.Timing;
- namespace Content.Shared.Damage.Systems;
- public sealed partial class StaminaSystem : EntitySystem
- {
- [Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly INetManager _net = default!;
- [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
- [Dependency] private readonly AlertsSystem _alerts = default!;
- [Dependency] private readonly MetaDataSystem _metadata = default!;
- [Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
- [Dependency] private readonly SharedStunSystem _stunSystem = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
- /// <summary>
- /// How much of a buffer is there between the stun duration and when stuns can be re-applied.
- /// </summary>
- private static readonly TimeSpan StamCritBufferTime = TimeSpan.FromSeconds(3f);
- public override void Initialize()
- {
- base.Initialize();
- InitializeModifier();
- SubscribeLocalEvent<StaminaComponent, ComponentStartup>(OnStartup);
- SubscribeLocalEvent<StaminaComponent, ComponentShutdown>(OnShutdown);
- SubscribeLocalEvent<StaminaComponent, AfterAutoHandleStateEvent>(OnStamHandleState);
- SubscribeLocalEvent<StaminaComponent, DisarmedEvent>(OnDisarmed);
- SubscribeLocalEvent<StaminaComponent, RejuvenateEvent>(OnRejuvenate);
- SubscribeLocalEvent<StaminaDamageOnEmbedComponent, EmbedEvent>(OnProjectileEmbed);
- SubscribeLocalEvent<StaminaDamageOnCollideComponent, ProjectileHitEvent>(OnProjectileHit);
- SubscribeLocalEvent<StaminaDamageOnCollideComponent, ThrowDoHitEvent>(OnThrowHit);
- SubscribeLocalEvent<StaminaDamageOnHitComponent, MeleeHitEvent>(OnMeleeHit);
- }
- private void OnStamHandleState(EntityUid uid, StaminaComponent component, ref AfterAutoHandleStateEvent args)
- {
- if (component.Critical)
- EnterStamCrit(uid, component);
- else
- {
- if (component.StaminaDamage > 0f)
- EnsureComp<ActiveStaminaComponent>(uid);
- ExitStamCrit(uid, component);
- }
- }
- private void OnShutdown(EntityUid uid, StaminaComponent component, ComponentShutdown args)
- {
- if (MetaData(uid).EntityLifeStage < EntityLifeStage.Terminating)
- {
- RemCompDeferred<ActiveStaminaComponent>(uid);
- }
- _alerts.ClearAlert(uid, component.StaminaAlert);
- }
- private void OnStartup(EntityUid uid, StaminaComponent component, ComponentStartup args)
- {
- SetStaminaAlert(uid, component);
- }
- [PublicAPI]
- public float GetStaminaDamage(EntityUid uid, StaminaComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return 0f;
- var curTime = _timing.CurTime;
- var pauseTime = _metadata.GetPauseTime(uid);
- return MathF.Max(0f, component.StaminaDamage - MathF.Max(0f, (float)(curTime - (component.NextUpdate + pauseTime)).TotalSeconds * component.Decay));
- }
- private void OnRejuvenate(EntityUid uid, StaminaComponent component, RejuvenateEvent args)
- {
- if (component.StaminaDamage >= component.CritThreshold)
- {
- ExitStamCrit(uid, component);
- }
- component.StaminaDamage = 0;
- RemComp<ActiveStaminaComponent>(uid);
- SetStaminaAlert(uid, component);
- Dirty(uid, component);
- }
- private void OnDisarmed(EntityUid uid, StaminaComponent component, DisarmedEvent args)
- {
- if (args.Handled)
- return;
- if (component.Critical)
- return;
- var damage = args.PushProbability * component.CritThreshold;
- TakeStaminaDamage(uid, damage, component, source: args.Source);
- args.PopupPrefix = "disarm-action-shove-";
- args.IsStunned = component.Critical;
- args.Handled = true;
- }
- private void OnMeleeHit(EntityUid uid, StaminaDamageOnHitComponent component, MeleeHitEvent args)
- {
- if (!args.IsHit ||
- !args.HitEntities.Any() ||
- component.Damage <= 0f)
- {
- return;
- }
- var ev = new StaminaDamageOnHitAttemptEvent();
- RaiseLocalEvent(uid, ref ev);
- if (ev.Cancelled)
- return;
- var stamQuery = GetEntityQuery<StaminaComponent>();
- var toHit = new List<(EntityUid Entity, StaminaComponent Component)>();
- // Split stamina damage between all eligible targets.
- foreach (var ent in args.HitEntities)
- {
- if (!stamQuery.TryGetComponent(ent, out var stam))
- continue;
- toHit.Add((ent, stam));
- }
- var hitEvent = new StaminaMeleeHitEvent(toHit);
- RaiseLocalEvent(uid, hitEvent);
- if (hitEvent.Handled)
- return;
- var damage = component.Damage;
- damage *= hitEvent.Multiplier;
- damage += hitEvent.FlatModifier;
- foreach (var (ent, comp) in toHit)
- {
- TakeStaminaDamage(ent, damage / toHit.Count, comp, source: args.User, with: args.Weapon, sound: component.Sound);
- }
- }
- private void OnProjectileHit(EntityUid uid, StaminaDamageOnCollideComponent component, ref ProjectileHitEvent args)
- {
- OnCollide(uid, component, args.Target);
- }
- private void OnProjectileEmbed(EntityUid uid, StaminaDamageOnEmbedComponent component, ref EmbedEvent args)
- {
- if (!TryComp<StaminaComponent>(args.Embedded, out var stamina))
- return;
- TakeStaminaDamage(args.Embedded, component.Damage, stamina, source: uid);
- }
- private void OnThrowHit(EntityUid uid, StaminaDamageOnCollideComponent component, ThrowDoHitEvent args)
- {
- OnCollide(uid, component, args.Target);
- }
- private void OnCollide(EntityUid uid, StaminaDamageOnCollideComponent component, EntityUid target)
- {
- // you can't inflict stamina damage on things with no stamina component
- // this prevents stun batons from using up charges when throwing it at lockers or lights
- if (!HasComp<StaminaComponent>(target))
- return;
- var ev = new StaminaDamageOnHitAttemptEvent();
- RaiseLocalEvent(uid, ref ev);
- if (ev.Cancelled)
- return;
- TakeStaminaDamage(target, component.Damage, source: uid, sound: component.Sound);
- }
- private void SetStaminaAlert(EntityUid uid, StaminaComponent? component = null)
- {
- if (!Resolve(uid, ref component, false) || component.Deleted)
- return;
- var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, component.CritThreshold - component.StaminaDamage), component.CritThreshold, 7);
- _alerts.ShowAlert(uid, component.StaminaAlert, (short)severity);
- }
- /// <summary>
- /// Tries to take stamina damage without raising the entity over the crit threshold.
- /// </summary>
- public bool TryTakeStamina(EntityUid uid, float value, StaminaComponent? component = null, EntityUid? source = null, EntityUid? with = null)
- {
- // Something that has no Stamina component automatically passes stamina checks
- if (!Resolve(uid, ref component, false))
- return true;
- var oldStam = component.StaminaDamage;
- if (oldStam + value > component.CritThreshold || component.Critical)
- return false;
- TakeStaminaDamage(uid, value, component, source, with, visual: false);
- return true;
- }
- public void TakeStaminaDamage(EntityUid uid, float value, StaminaComponent? component = null,
- EntityUid? source = null, EntityUid? with = null, bool visual = true, SoundSpecifier? sound = null,
- bool shouldLog = true) // stalker-changes
- {
- if (!Resolve(uid, ref component, false))
- return;
- var ev = new BeforeStaminaDamageEvent(value);
- RaiseLocalEvent(uid, ref ev);
- if (ev.Cancelled)
- return;
- // Have we already reached the point of max stamina damage?
- if (component.Critical)
- return;
- var oldDamage = component.StaminaDamage;
- component.StaminaDamage = MathF.Max(0f, component.StaminaDamage + value);
- // Reset the decay cooldown upon taking damage.
- if (oldDamage < component.StaminaDamage)
- {
- var nextUpdate = _timing.CurTime + TimeSpan.FromSeconds(component.Cooldown);
- if (component.NextUpdate < nextUpdate)
- component.NextUpdate = nextUpdate;
- }
- var slowdownThreshold = component.SlowdownThreshold; // stalker-changes
- // If we go above n% then apply slowdown
- if (oldDamage < slowdownThreshold &&
- component.StaminaDamage > slowdownThreshold)
- {
- _stunSystem.TrySlowdown(uid, TimeSpan.FromSeconds(3), true, 0.8f, 0.8f);
- }
- SetStaminaAlert(uid, component);
- if (!component.Critical)
- {
- if (component.StaminaDamage >= component.CritThreshold)
- {
- EnterStamCrit(uid, component);
- }
- }
- else
- {
- if (component.StaminaDamage < component.CritThreshold)
- {
- ExitStamCrit(uid, component);
- }
- }
- EnsureComp<ActiveStaminaComponent>(uid);
- Dirty(uid, component);
- if (value <= 0)
- return;
- if (source != null && shouldLog) // stalker-changes
- {
- _adminLogger.Add(LogType.Stamina, $"{ToPrettyString(source.Value):user} caused {value} stamina damage to {ToPrettyString(uid):target}{(with != null ? $" using {ToPrettyString(with.Value):using}" : "")}");
- }
- else if (shouldLog) // stalker-changes
- {
- _adminLogger.Add(LogType.Stamina, $"{ToPrettyString(uid):target} took {value} stamina damage");
- }
- if (visual)
- {
- _color.RaiseEffect(Color.Aqua, new List<EntityUid>() { uid }, Filter.Pvs(uid, entityManager: EntityManager));
- }
- if (_net.IsServer)
- {
- _audio.PlayPvs(sound, uid);
- }
- }
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
- if (!_timing.IsFirstTimePredicted)
- return;
- var stamQuery = GetEntityQuery<StaminaComponent>();
- var query = EntityQueryEnumerator<ActiveStaminaComponent>();
- var curTime = _timing.CurTime;
- while (query.MoveNext(out var uid, out _))
- {
- // Just in case we have active but not stamina we'll check and account for it.
- if (!stamQuery.TryGetComponent(uid, out var comp) ||
- comp.StaminaDamage <= 0f && !comp.Critical)
- {
- RemComp<ActiveStaminaComponent>(uid);
- continue;
- }
- // Shouldn't need to consider paused time as we're only iterating non-paused stamina components.
- var nextUpdate = comp.NextUpdate;
- if (nextUpdate > curTime)
- continue;
- // We were in crit so come out of it and continue.
- if (comp.Critical)
- {
- ExitStamCrit(uid, comp);
- continue;
- }
- comp.NextUpdate += TimeSpan.FromSeconds(1f);
- TakeStaminaDamage(uid, -comp.Decay, comp);
- Dirty(uid, comp);
- }
- }
- private void EnterStamCrit(EntityUid uid, StaminaComponent? component = null)
- {
- if (!Resolve(uid, ref component) ||
- component.Critical)
- {
- return;
- }
- // To make the difference between a stun and a stamcrit clear
- // TODO: Mask?
- component.Critical = true;
- component.StaminaDamage = component.CritThreshold;
- _stunSystem.TryParalyze(uid, component.StunTime, true);
- // Give them buffer before being able to be re-stunned
- component.NextUpdate = _timing.CurTime + component.StunTime + StamCritBufferTime;
- EnsureComp<ActiveStaminaComponent>(uid);
- Dirty(uid, component);
- _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(uid):user} entered stamina crit");
- }
- private void ExitStamCrit(EntityUid uid, StaminaComponent? component = null)
- {
- if (!Resolve(uid, ref component) ||
- !component.Critical)
- {
- return;
- }
- component.Critical = false;
- component.StaminaDamage = 0f;
- component.NextUpdate = _timing.CurTime;
- SetStaminaAlert(uid, component);
- RemComp<ActiveStaminaComponent>(uid);
- Dirty(uid, component);
- _adminLogger.Add(LogType.Stamina, LogImpact.Low, $"{ToPrettyString(uid):user} recovered from stamina crit");
- }
- }
- /// <summary>
- /// Raised before stamina damage is dealt to allow other systems to cancel it.
- /// </summary>
- [ByRefEvent]
- public record struct BeforeStaminaDamageEvent(float Value, bool Cancelled = false);
|