2023-02-13 07:55:39 -05:00
using System.Linq ;
2022-11-09 07:34:07 +11:00
using Content.Shared.Administration.Logs ;
2022-07-06 18:06:12 +10:00
using Content.Shared.Alert ;
2022-11-09 07:34:07 +11:00
using Content.Shared.CombatMode ;
using Content.Shared.Damage.Components ;
using Content.Shared.Damage.Events ;
2022-09-05 22:19:33 -04:00
using Content.Shared.Database ;
2023-08-08 23:19:31 +03:00
using Content.Shared.Effects ;
2022-09-05 22:19:33 -04:00
using Content.Shared.IdentityManagement ;
using Content.Shared.Popups ;
2023-08-06 16:44:41 +03:00
using Content.Shared.Projectiles ;
2023-03-23 11:57:15 -07:00
using Content.Shared.Rejuvenate ;
2022-11-09 07:34:07 +11:00
using Content.Shared.Rounding ;
using Content.Shared.Stunnable ;
2023-08-08 23:19:31 +03:00
using Content.Shared.Throwing ;
2022-11-09 07:34:07 +11:00
using Content.Shared.Weapons.Melee.Events ;
using JetBrains.Annotations ;
2023-08-08 23:19:31 +03:00
using Robust.Shared.Audio ;
2022-11-09 07:34:07 +11:00
using Robust.Shared.GameStates ;
2023-08-08 23:19:31 +03:00
using Robust.Shared.Network ;
2022-07-06 18:06:12 +10:00
using Robust.Shared.Player ;
2022-09-05 22:19:33 -04:00
using Robust.Shared.Random ;
2022-11-09 07:34:07 +11:00
using Robust.Shared.Serialization ;
using Robust.Shared.Timing ;
2022-07-06 18:06:12 +10:00
2022-11-09 07:34:07 +11:00
namespace Content.Shared.Damage.Systems ;
2022-07-06 18:06:12 +10:00
2023-08-06 12:06:23 +01:00
public sealed partial class StaminaSystem : EntitySystem
2022-07-06 18:06:12 +10:00
{
[Dependency] private readonly IGameTiming _timing = default ! ;
2023-08-11 03:44:52 +10:00
[Dependency] private readonly INetManager _net = default ! ;
2023-03-13 00:19:05 +11:00
[Dependency] private readonly IRobustRandom _random = default ! ;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default ! ;
2022-07-06 18:06:12 +10:00
[Dependency] private readonly AlertsSystem _alerts = default ! ;
2022-11-09 07:34:07 +11:00
[Dependency] private readonly MetaDataSystem _metadata = default ! ;
2023-08-11 03:44:52 +10:00
[Dependency] private readonly SharedColorFlashEffectSystem _color = default ! ;
2022-11-09 07:34:07 +11:00
[Dependency] private readonly SharedPopupSystem _popup = default ! ;
2022-07-06 18:06:12 +10:00
[Dependency] private readonly SharedStunSystem _stunSystem = default ! ;
2023-08-08 23:19:31 +03:00
[Dependency] private readonly SharedAudioSystem _audio = default ! ;
2022-07-06 18:06:12 +10:00
/// <summary>
/// How much of a buffer is there between the stun duration and when stuns can be re-applied.
/// </summary>
2022-11-09 07:34:07 +11:00
private static readonly TimeSpan StamCritBufferTime = TimeSpan . FromSeconds ( 3f ) ;
2022-07-06 18:06:12 +10:00
public override void Initialize ( )
{
base . Initialize ( ) ;
2023-08-06 12:06:23 +01:00
InitializeModifier ( ) ;
2023-05-01 14:49:25 +10:00
SubscribeLocalEvent < StaminaComponent , EntityUnpausedEvent > ( OnStamUnpaused ) ;
2022-07-06 18:06:12 +10:00
SubscribeLocalEvent < StaminaComponent , ComponentStartup > ( OnStartup ) ;
SubscribeLocalEvent < StaminaComponent , ComponentShutdown > ( OnShutdown ) ;
2022-11-09 07:34:07 +11:00
SubscribeLocalEvent < StaminaComponent , ComponentGetState > ( OnStamGetState ) ;
SubscribeLocalEvent < StaminaComponent , ComponentHandleState > ( OnStamHandleState ) ;
2022-09-05 22:19:33 -04:00
SubscribeLocalEvent < StaminaComponent , DisarmedEvent > ( OnDisarmed ) ;
2023-03-23 11:57:15 -07:00
SubscribeLocalEvent < StaminaComponent , RejuvenateEvent > ( OnRejuvenate ) ;
2023-08-06 12:06:23 +01:00
2023-08-08 23:19:31 +03:00
SubscribeLocalEvent < StaminaDamageOnCollideComponent , ProjectileHitEvent > ( OnProjectileHit ) ;
SubscribeLocalEvent < StaminaDamageOnCollideComponent , ThrowDoHitEvent > ( OnThrowHit ) ;
SubscribeLocalEvent < StaminaDamageOnHitComponent , MeleeHitEvent > ( OnMeleeHit ) ;
2022-07-06 18:06:12 +10:00
}
2023-05-01 14:49:25 +10:00
private void OnStamUnpaused ( EntityUid uid , StaminaComponent component , ref EntityUnpausedEvent args )
{
component . NextUpdate + = args . PausedTime ;
}
2022-11-09 07:34:07 +11:00
private void OnStamGetState ( EntityUid uid , StaminaComponent component , ref ComponentGetState args )
{
args . State = new StaminaComponentState ( )
{
Critical = component . Critical ,
Decay = component . Decay ,
CritThreshold = component . CritThreshold ,
DecayCooldown = component . DecayCooldown ,
LastUpdate = component . NextUpdate ,
StaminaDamage = component . StaminaDamage ,
} ;
}
private void OnStamHandleState ( EntityUid uid , StaminaComponent component , ref ComponentHandleState args )
{
if ( args . Current is not StaminaComponentState state )
return ;
component . Critical = state . Critical ;
component . Decay = state . Decay ;
component . CritThreshold = state . CritThreshold ;
component . DecayCooldown = state . DecayCooldown ;
component . NextUpdate = state . LastUpdate ;
component . StaminaDamage = state . StaminaDamage ;
if ( component . Critical )
EnterStamCrit ( uid , component ) ;
else
{
if ( component . StaminaDamage > 0f )
EnsureComp < ActiveStaminaComponent > ( uid ) ;
ExitStamCrit ( uid , component ) ;
}
}
2022-07-06 18:06:12 +10:00
private void OnShutdown ( EntityUid uid , StaminaComponent component , ComponentShutdown args )
{
2022-11-09 07:34:07 +11:00
if ( MetaData ( uid ) . EntityLifeStage < EntityLifeStage . Terminating )
{
RemCompDeferred < ActiveStaminaComponent > ( uid ) ;
}
2022-07-06 18:06:12 +10:00
SetStaminaAlert ( uid ) ;
}
private void OnStartup ( EntityUid uid , StaminaComponent component , ComponentStartup args )
{
SetStaminaAlert ( uid , component ) ;
}
2022-11-09 07:34:07 +11:00
[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 ) ) ;
}
2023-03-23 11:57:15 -07:00
private void OnRejuvenate ( EntityUid uid , StaminaComponent component , RejuvenateEvent args )
{
if ( component . StaminaDamage > = component . CritThreshold )
{
ExitStamCrit ( uid , component ) ;
}
component . StaminaDamage = 0 ;
RemComp < ActiveStaminaComponent > ( uid ) ;
2023-08-13 10:26:45 +03:00
SetStaminaAlert ( uid , component ) ;
2023-03-23 11:57:15 -07:00
Dirty ( component ) ;
}
2022-09-05 22:19:33 -04:00
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 ;
2023-08-06 16:44:41 +03:00
TakeStaminaDamage ( uid , damage , component , source : args . Source ) ;
2022-09-05 22:19:33 -04:00
// 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 ) ;
2022-12-19 10:41:47 +13:00
_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 ) ;
2022-09-05 22:19:33 -04:00
_adminLogger . Add ( LogType . DisarmedKnockdown , LogImpact . Medium , $"{ToPrettyString(args.Source):user} knocked down {ToPrettyString(args.Target):target}" ) ;
args . Handled = true ;
}
2023-08-08 23:19:31 +03:00
private void OnMeleeHit ( EntityUid uid , StaminaDamageOnHitComponent component , MeleeHitEvent args )
2022-07-06 18:06:12 +10:00
{
2023-02-13 07:55:39 -05:00
if ( ! args . IsHit | |
! args . HitEntities . Any ( ) | |
component . Damage < = 0f )
{
2022-11-03 13:01:08 +01:00
return ;
2023-02-13 07:55:39 -05:00
}
2022-07-06 18:06:12 +10:00
var ev = new StaminaDamageOnHitAttemptEvent ( ) ;
RaiseLocalEvent ( uid , ref ev ) ;
2023-03-13 00:19:05 +11:00
if ( ev . Cancelled )
return ;
2022-07-06 18:06:12 +10:00
var stamQuery = GetEntityQuery < StaminaComponent > ( ) ;
2023-03-13 00:19:05 +11:00
var toHit = new List < ( EntityUid Entity , StaminaComponent Component ) > ( ) ;
2022-07-06 18:06:12 +10:00
// Split stamina damage between all eligible targets.
foreach ( var ent in args . HitEntities )
{
2023-03-13 00:19:05 +11:00
if ( ! stamQuery . TryGetComponent ( ent , out var stam ) )
continue ;
toHit . Add ( ( ent , stam ) ) ;
2022-07-06 18:06:12 +10:00
}
2022-07-26 21:34:19 -04:00
var hitEvent = new StaminaMeleeHitEvent ( toHit ) ;
2023-03-13 00:19:05 +11:00
RaiseLocalEvent ( uid , hitEvent ) ;
2022-07-26 21:34:19 -04:00
if ( hitEvent . Handled )
return ;
var damage = component . Damage ;
damage * = hitEvent . Multiplier ;
damage + = hitEvent . FlatModifier ;
2023-03-13 00:19:05 +11:00
foreach ( var ( ent , comp ) in toHit )
2022-07-06 18:06:12 +10:00
{
2023-08-08 23:19:31 +03:00
TakeStaminaDamage ( ent , damage / toHit . Count , comp , source : args . User , with : args . Weapon , sound : component . Sound ) ;
2022-07-06 18:06:12 +10:00
}
}
2023-08-08 23:19:31 +03:00
private void OnProjectileHit ( EntityUid uid , StaminaDamageOnCollideComponent component , ref ProjectileHitEvent args )
{
OnCollide ( uid , component , args . Target ) ;
}
private void OnThrowHit ( EntityUid uid , StaminaDamageOnCollideComponent component , ThrowDoHitEvent args )
2022-07-06 18:06:12 +10:00
{
2023-08-08 23:19:31 +03:00
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 ) ;
2022-07-06 18:06:12 +10:00
}
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 ) ;
}
2023-08-06 12:55:38 +10:00
/// <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 )
{
2023-08-14 16:10:15 +00:00
// Something that has no Stamina component automatically passes stamina checks
2023-08-06 12:55:38 +10:00
if ( ! Resolve ( uid , ref component , false ) )
2023-08-14 16:10:15 +00:00
return true ;
2023-08-06 12:55:38 +10:00
var oldStam = component . StaminaDamage ;
if ( oldStam + value > component . CritThreshold | | component . Critical )
return false ;
2023-08-08 23:19:31 +03:00
TakeStaminaDamage ( uid , value , component , source , with , visual : false ) ;
2023-08-06 12:55:38 +10:00
return true ;
}
2023-08-08 23:19:31 +03:00
public void TakeStaminaDamage ( EntityUid uid , float value , StaminaComponent ? component = null ,
EntityUid ? source = null , EntityUid ? with = null , bool visual = true , SoundSpecifier ? sound = null )
2022-07-06 18:06:12 +10:00
{
2023-03-23 11:57:15 -07:00
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 )
2022-11-09 07:34:07 +11:00
return ;
2022-07-06 18:06:12 +10:00
var oldDamage = component . StaminaDamage ;
component . StaminaDamage = MathF . Max ( 0f , component . StaminaDamage + value ) ;
// Reset the decay cooldown upon taking damage.
if ( oldDamage < component . StaminaDamage )
{
2022-11-09 07:34:07 +11:00
var nextUpdate = _timing . CurTime + TimeSpan . FromSeconds ( component . DecayCooldown ) ;
if ( component . NextUpdate < nextUpdate )
component . NextUpdate = nextUpdate ;
2022-07-06 18:06:12 +10:00
}
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 ) ;
}
}
2022-11-09 07:34:07 +11:00
EnsureComp < ActiveStaminaComponent > ( uid ) ;
Dirty ( component ) ;
2022-12-11 22:21:15 -06:00
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" ) ;
}
2023-08-08 23:19:31 +03:00
if ( visual )
{
2023-08-11 03:44:52 +10:00
_color . RaiseEffect ( Color . Aqua , new List < EntityUid > ( ) { uid } , Filter . Pvs ( uid , entityManager : EntityManager ) ) ;
2023-08-08 23:19:31 +03:00
}
if ( _net . IsServer )
{
_audio . PlayPvs ( sound , uid ) ;
}
2022-07-06 18:06:12 +10:00
}
public override void Update ( float frameTime )
{
base . Update ( frameTime ) ;
2023-03-13 00:19:05 +11:00
if ( ! _timing . IsFirstTimePredicted )
return ;
2022-07-06 18:06:12 +10:00
var stamQuery = GetEntityQuery < StaminaComponent > ( ) ;
2023-03-13 00:19:05 +11:00
var query = EntityQueryEnumerator < ActiveStaminaComponent > ( ) ;
2022-11-09 07:34:07 +11:00
var curTime = _timing . CurTime ;
2022-07-06 18:06:12 +10:00
2023-03-13 00:19:05 +11:00
while ( query . MoveNext ( out var uid , out _ ) )
2022-07-06 18:06:12 +10:00
{
// Just in case we have active but not stamina we'll check and account for it.
2023-03-13 00:19:05 +11:00
if ( ! stamQuery . TryGetComponent ( uid , out var comp ) | |
2022-11-09 07:34:07 +11:00
comp . StaminaDamage < = 0f & & ! comp . Critical )
2022-07-06 18:06:12 +10:00
{
2023-03-13 00:19:05 +11:00
RemComp < ActiveStaminaComponent > ( uid ) ;
2022-07-06 18:06:12 +10:00
continue ;
}
2022-11-09 07:34:07 +11:00
// Shouldn't need to consider paused time as we're only iterating non-paused stamina components.
var nextUpdate = comp . NextUpdate ;
2022-07-06 18:06:12 +10:00
2022-11-09 07:34:07 +11:00
if ( nextUpdate > curTime )
continue ;
2022-07-06 18:06:12 +10:00
// We were in crit so come out of it and continue.
if ( comp . Critical )
{
2023-03-13 00:19:05 +11:00
ExitStamCrit ( uid , comp ) ;
2022-07-06 18:06:12 +10:00
continue ;
}
2022-11-09 07:34:07 +11:00
comp . NextUpdate + = TimeSpan . FromSeconds ( 1f ) ;
2023-03-13 00:19:05 +11:00
TakeStaminaDamage ( uid , - comp . Decay , comp ) ;
2022-11-09 07:34:07 +11:00
Dirty ( comp ) ;
2022-07-06 18:06:12 +10:00
}
}
private void EnterStamCrit ( EntityUid uid , StaminaComponent ? component = null )
{
if ( ! Resolve ( uid , ref component ) | |
2023-03-13 00:19:05 +11:00
component . Critical )
{
return ;
}
2022-07-06 18:06:12 +10:00
// To make the difference between a stun and a stamcrit clear
// TODO: Mask?
component . Critical = true ;
component . StaminaDamage = component . CritThreshold ;
2023-08-21 19:18:57 +01:00
_stunSystem . TryParalyze ( uid , component . StunTime , true ) ;
2022-07-06 18:06:12 +10:00
// Give them buffer before being able to be re-stunned
2023-08-21 19:18:57 +01:00
component . NextUpdate = _timing . CurTime + component . StunTime + StamCritBufferTime ;
2022-11-09 07:34:07 +11:00
EnsureComp < ActiveStaminaComponent > ( uid ) ;
Dirty ( component ) ;
2022-12-11 22:21:15 -06:00
_adminLogger . Add ( LogType . Stamina , LogImpact . Medium , $"{ToPrettyString(uid):user} entered stamina crit" ) ;
2022-07-06 18:06:12 +10:00
}
private void ExitStamCrit ( EntityUid uid , StaminaComponent ? component = null )
{
if ( ! Resolve ( uid , ref component ) | |
2023-03-13 00:19:05 +11:00
! component . Critical )
{
return ;
}
2022-07-06 18:06:12 +10:00
component . Critical = false ;
component . StaminaDamage = 0f ;
2022-11-09 07:34:07 +11:00
component . NextUpdate = _timing . CurTime ;
2022-07-06 18:06:12 +10:00
SetStaminaAlert ( uid , component ) ;
2022-11-09 07:34:07 +11:00
RemComp < ActiveStaminaComponent > ( uid ) ;
Dirty ( component ) ;
2022-12-11 22:21:15 -06:00
_adminLogger . Add ( LogType . Stamina , LogImpact . Low , $"{ToPrettyString(uid):user} recovered from stamina crit" ) ;
2022-11-09 07:34:07 +11:00
}
[Serializable, NetSerializable]
private sealed class StaminaComponentState : ComponentState
{
public bool Critical ;
public float Decay ;
public float DecayCooldown ;
public float StaminaDamage ;
public float CritThreshold ;
public TimeSpan LastUpdate ;
2022-07-06 18:06:12 +10:00
}
2023-03-23 11:57:15 -07:00
2022-07-06 18:06:12 +10:00
}
2023-03-23 11:57:15 -07:00
/// <summary>
/// Raised before stamina damage is dealt to allow other systems to cancel it.
/// </summary>
[ByRefEvent]
2023-08-06 16:44:41 +03:00
public record struct BeforeStaminaDamageEvent ( float Value , bool Cancelled = false ) ;