Files
tbd-station-14/Content.Shared/Damage/Systems/StaminaSystem.cs

403 lines
14 KiB
C#

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.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 IRobustRandom _random = 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 SharedPopupSystem _popup = 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, EntityUnpausedEvent>(OnStamUnpaused);
SubscribeLocalEvent<StaminaComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<StaminaComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<StaminaComponent, AfterAutoHandleStateEvent>(OnStamHandleState);
SubscribeLocalEvent<StaminaComponent, DisarmedEvent>(OnDisarmed);
SubscribeLocalEvent<StaminaComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<StaminaDamageOnCollideComponent, ProjectileHitEvent>(OnProjectileHit);
SubscribeLocalEvent<StaminaDamageOnCollideComponent, ThrowDoHitEvent>(OnThrowHit);
SubscribeLocalEvent<StaminaDamageOnHitComponent, MeleeHitEvent>(OnMeleeHit);
}
private void OnStamUnpaused(EntityUid uid, StaminaComponent component, ref EntityUnpausedEvent args)
{
component.NextUpdate += args.PausedTime;
}
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);
}
SetStaminaAlert(uid);
}
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(component);
}
private void OnDisarmed(EntityUid uid, StaminaComponent component, DisarmedEvent args)
{
if (args.Handled || !_random.Prob(args.PushProbability))
return;
if (component.Critical)
return;
var damage = args.PushProbability * component.CritThreshold;
TakeStaminaDamage(uid, damage, component, source: args.Source);
// We need a better method of getting if the entity is going to resist stam damage, both this and the lines in the foreach at the end of OnHit() are awful
if (!component.Critical)
return;
var targetEnt = Identity.Entity(args.Target, EntityManager);
var sourceEnt = Identity.Entity(args.Source, EntityManager);
_popup.PopupEntity(Loc.GetString("stunned-component-disarm-success-others", ("source", sourceEnt), ("target", targetEnt)), targetEnt, Filter.PvsExcept(args.Source), true, PopupType.LargeCaution);
_popup.PopupCursor(Loc.GetString("stunned-component-disarm-success", ("target", targetEnt)), args.Source, PopupType.Large);
_adminLogger.Add(LogType.DisarmedKnockdown, LogImpact.Medium, $"{ToPrettyString(args.Source):user} knocked down {ToPrettyString(args.Target):target}");
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 OnThrowHit(EntityUid uid, StaminaDamageOnCollideComponent component, ThrowDoHitEvent args)
{
OnCollide(uid, component, args.Target);
}
private void OnCollide(EntityUid uid, StaminaDamageOnCollideComponent component, EntityUid target)
{
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)
{
_alerts.ClearAlert(uid, AlertType.Stamina);
return;
}
var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, component.CritThreshold - component.StaminaDamage), component.CritThreshold, 7);
_alerts.ShowAlert(uid, AlertType.Stamina, (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)
{
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.CritThreshold / 2f;
// 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(component);
if (value <= 0)
return;
if (source != null)
{
_adminLogger.Add(LogType.Stamina, $"{ToPrettyString(source.Value):user} caused {value} stamina damage to {ToPrettyString(uid):target}{(with != null ? $" using {ToPrettyString(with.Value):using}" : "")}");
}
else
{
_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(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(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(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);