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 ;
using Content.Shared.IdentityManagement ;
2022-12-11 22:21:15 -06:00
using Content.Shared.Interaction ;
2022-09-05 22:19:33 -04:00
using Content.Shared.Popups ;
2022-11-09 07:34:07 +11:00
using Content.Shared.Rounding ;
using Content.Shared.Stunnable ;
using Content.Shared.Weapons.Melee.Events ;
using JetBrains.Annotations ;
using Robust.Shared.GameStates ;
using Robust.Shared.Physics.Events ;
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
public sealed class StaminaSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default ! ;
[Dependency] private readonly AlertsSystem _alerts = default ! ;
2022-11-09 07:34:07 +11:00
[Dependency] private readonly MetaDataSystem _metadata = default ! ;
[Dependency] private readonly SharedPopupSystem _popup = default ! ;
2022-07-06 18:06:12 +10:00
[Dependency] private readonly SharedStunSystem _stunSystem = default ! ;
2022-09-05 22:19:33 -04:00
[Dependency] private readonly IRobustRandom _random = default ! ;
2022-11-09 07:34:07 +11:00
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default ! ;
2022-07-06 18:06:12 +10:00
private const string CollideFixture = "projectile" ;
/// <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 ( ) ;
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 ) ;
SubscribeLocalEvent < StaminaDamageOnCollideComponent , StartCollideEvent > ( OnCollide ) ;
SubscribeLocalEvent < StaminaDamageOnHitComponent , MeleeHitEvent > ( OnHit ) ;
2022-07-06 18:06:12 +10:00
}
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 ) ) ;
}
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 ;
2022-12-11 22:21:15 -06: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 ;
}
2022-07-06 18:06:12 +10:00
private void OnHit ( EntityUid uid , StaminaDamageOnHitComponent component , MeleeHitEvent args )
{
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 ) ;
if ( ev . Cancelled ) return ;
2022-07-07 13:34:17 +10:00
args . HitSoundOverride = ev . HitSoundOverride ;
2022-07-06 18:06:12 +10:00
var stamQuery = GetEntityQuery < StaminaComponent > ( ) ;
2022-11-09 07:34:07 +11:00
var toHit = new List < StaminaComponent > ( ) ;
2022-07-06 18:06:12 +10:00
// Split stamina damage between all eligible targets.
foreach ( var ent in args . HitEntities )
{
if ( ! stamQuery . TryGetComponent ( ent , out var stam ) ) continue ;
toHit . Add ( stam ) ;
}
2022-07-26 21:34:19 -04:00
var hitEvent = new StaminaMeleeHitEvent ( toHit ) ;
RaiseLocalEvent ( uid , hitEvent , false ) ;
if ( hitEvent . Handled )
return ;
var damage = component . Damage ;
damage * = hitEvent . Multiplier ;
damage + = hitEvent . FlatModifier ;
2022-07-06 18:06:12 +10:00
foreach ( var comp in toHit )
{
var oldDamage = comp . StaminaDamage ;
2022-12-11 22:21:15 -06:00
TakeStaminaDamage ( comp . Owner , damage / toHit . Count , comp , source : args . User , with : component . Owner ) ;
2022-07-06 18:06:12 +10:00
if ( comp . StaminaDamage . Equals ( oldDamage ) )
{
2022-12-19 10:41:47 +13:00
_popup . PopupEntity ( Loc . GetString ( "stamina-resist" ) , comp . Owner , args . User ) ;
2022-07-06 18:06:12 +10:00
}
}
}
2022-09-14 17:26:26 +10:00
private void OnCollide ( EntityUid uid , StaminaDamageOnCollideComponent component , ref StartCollideEvent args )
2022-07-06 18:06:12 +10:00
{
if ( ! args . OurFixture . ID . Equals ( CollideFixture ) ) return ;
2022-12-11 22:21:15 -06:00
TakeStaminaDamage ( args . OtherFixture . Body . Owner , component . Damage , source : args . OurFixture . Body . Owner ) ;
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 ) ;
}
2022-12-11 22:21:15 -06:00
public void TakeStaminaDamage ( EntityUid uid , float value , StaminaComponent ? component = null , EntityUid ? source = null , EntityUid ? with = null )
2022-07-06 18:06:12 +10:00
{
2022-11-09 07:34:07 +11:00
if ( ! Resolve ( uid , ref component , false ) | | component . Critical )
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" ) ;
}
2022-07-06 18:06:12 +10:00
}
public override void Update ( float frameTime )
{
base . Update ( frameTime ) ;
if ( ! _timing . IsFirstTimePredicted ) return ;
2022-11-09 07:34:07 +11:00
var metaQuery = GetEntityQuery < MetaDataComponent > ( ) ;
2022-07-06 18:06:12 +10:00
var stamQuery = GetEntityQuery < StaminaComponent > ( ) ;
2022-11-09 07:34:07 +11:00
var curTime = _timing . CurTime ;
2022-07-06 18:06:12 +10:00
foreach ( var active in EntityQuery < ActiveStaminaComponent > ( ) )
{
// Just in case we have active but not stamina we'll check and account for it.
if ( ! stamQuery . TryGetComponent ( active . Owner , out var comp ) | |
2022-11-09 07:34:07 +11:00
comp . StaminaDamage < = 0f & & ! comp . Critical )
2022-07-06 18:06:12 +10:00
{
RemComp < ActiveStaminaComponent > ( active . Owner ) ;
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 )
{
ExitStamCrit ( active . Owner , comp ) ;
continue ;
}
2022-11-09 07:34:07 +11:00
comp . NextUpdate + = TimeSpan . FromSeconds ( 1f ) ;
TakeStaminaDamage ( comp . Owner , - comp . Decay , comp ) ;
Dirty ( comp ) ;
2022-07-06 18:06:12 +10:00
}
}
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 ;
var stunTime = TimeSpan . FromSeconds ( 6 ) ;
_stunSystem . TryParalyze ( uid , stunTime , true ) ;
// Give them buffer before being able to be re-stunned
2022-11-09 07:34:07 +11:00
component . NextUpdate = _timing . CurTime + stunTime + StamCritBufferTime ;
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 ) | |
! component . Critical ) return ;
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
}
}