diff --git a/Content.Shared/Damage/Components/StaminaComponent.cs b/Content.Shared/Damage/Components/StaminaComponent.cs index 14c3f6d9f5..ec086625ea 100644 --- a/Content.Shared/Damage/Components/StaminaComponent.cs +++ b/Content.Shared/Damage/Components/StaminaComponent.cs @@ -1,4 +1,5 @@ using Content.Shared.Alert; +using Content.Shared.FixedPoint; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; @@ -56,4 +57,22 @@ public sealed partial class StaminaComponent : Component [DataField] public ProtoId StaminaAlert = "Stamina"; + + /// + /// This flag indicates whether the value of decreases after the entity exits stamina crit. + /// + [DataField, AutoNetworkedField] + public bool AfterCritical; + + /// + /// This float determines how fast stamina will regenerate after exiting the stamina crit. + /// + [DataField, AutoNetworkedField] + public float AfterCritDecayMultiplier = 5f; + + /// + /// Thresholds that determine an entity's slowdown as a function of stamina damage. + /// + [DataField] + public Dictionary StunModifierThresholds = new() { {0, 1f }, { 60, 0.7f }, { 80, 0.5f } }; } diff --git a/Content.Shared/Damage/Systems/StaminaSystem.cs b/Content.Shared/Damage/Systems/StaminaSystem.cs index 96454d20dd..365b66b106 100644 --- a/Content.Shared/Damage/Systems/StaminaSystem.cs +++ b/Content.Shared/Damage/Systems/StaminaSystem.cs @@ -7,8 +7,7 @@ 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.FixedPoint; using Content.Shared.Projectiles; using Content.Shared.Rejuvenate; using Content.Shared.Rounding; @@ -21,7 +20,6 @@ using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; using Robust.Shared.Network; using Robust.Shared.Player; -using Robust.Shared.Random; using Robust.Shared.Timing; namespace Content.Shared.Damage.Systems; @@ -114,6 +112,7 @@ public sealed partial class StaminaSystem : EntitySystem } component.StaminaDamage = 0; + AdjustSlowdown(uid); RemComp(uid); SetStaminaAlert(uid, component); Dirty(uid, component); @@ -275,17 +274,16 @@ public sealed partial class StaminaSystem : EntitySystem 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); - } + AdjustSlowdown(uid); SetStaminaAlert(uid, component); + // Checking if the stamina damage has decreased to zero after exiting the stamcrit + if (component.AfterCritical && oldDamage > component.StaminaDamage && component.StaminaDamage <= 0f) + { + component.AfterCritical = false; // Since the recovery from the crit has been completed, we are no longer 'after crit' + } + if (!component.Critical) { if (component.StaminaDamage >= component.CritThreshold) @@ -330,9 +328,6 @@ public sealed partial class StaminaSystem : EntitySystem { base.Update(frameTime); - if (!_timing.IsFirstTimePredicted) - return; - var stamQuery = GetEntityQuery(); var query = EntityQueryEnumerator(); var curTime = _timing.CurTime; @@ -353,15 +348,17 @@ public sealed partial class StaminaSystem : EntitySystem if (nextUpdate > curTime) continue; - // We were in crit so come out of it and continue. + // Handle exiting critical condition and restoring stamina damage if (comp.Critical) - { ExitStamCrit(uid, comp); - continue; - } comp.NextUpdate += TimeSpan.FromSeconds(1f); - TakeStaminaDamage(uid, -comp.Decay, comp); + + TakeStaminaDamage( + uid, + comp.AfterCritical ? -comp.Decay * comp.AfterCritDecayMultiplier : -comp.Decay, // Recover faster after crit + comp); + Dirty(uid, comp); } } @@ -398,11 +395,38 @@ public sealed partial class StaminaSystem : EntitySystem } component.Critical = false; - component.StaminaDamage = 0f; + component.AfterCritical = true; // Set to true to indicate that stamina will be restored after exiting stamcrit component.NextUpdate = _timing.CurTime; + SetStaminaAlert(uid, component); - RemComp(uid); Dirty(uid, component); _adminLogger.Add(LogType.Stamina, LogImpact.Low, $"{ToPrettyString(uid):user} recovered from stamina crit"); } + + /// + /// Adjusts the movement speed of an entity based on its current value. + /// If the entity has a , its custom damage-to-speed thresholds are used, + /// otherwise, a default set of thresholds is applied. + /// The method determines the closest applicable damage threshold below the crit limit and applies the corresponding + /// speed modifier using the stun system. If no threshold is met then the entity's speed is restored to normal. + /// + /// Entity to update + private void AdjustSlowdown(Entity ent) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + var closest = FixedPoint2.Zero; + + // Iterate through the dictionary in the similar way as in Damage.SlowOnDamageSystem.OnRefreshMovespeed + foreach (var thres in ent.Comp.StunModifierThresholds) + { + var key = thres.Key.Float(); + + if (ent.Comp.StaminaDamage >= key && key > closest && closest < ent.Comp.CritThreshold) + closest = thres.Key; + } + + _stunSystem.UpdateStunModifiers(ent, ent.Comp.StunModifierThresholds[closest]); + } } diff --git a/Content.Shared/Stunnable/SharedStunSystem.cs b/Content.Shared/Stunnable/SharedStunSystem.cs index 2d6a05540a..17115d4aa6 100644 --- a/Content.Shared/Stunnable/SharedStunSystem.cs +++ b/Content.Shared/Stunnable/SharedStunSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Interaction.Events; using Content.Shared.Inventory.Events; using Content.Shared.Item; using Content.Shared.Bed.Sleep; +using Content.Shared.Damage.Components; using Content.Shared.Database; using Content.Shared.Hands; using Content.Shared.Mobs; @@ -248,13 +249,63 @@ public abstract class SharedStunSystem : EntitySystem slowed.SprintSpeedModifier *= runSpeedMultiplier; _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); - return true; } return false; } + /// + /// Updates the movement speed modifiers of an entity by applying or removing the . + /// If both walk and run modifiers are approximately 1 (i.e. normal speed) and is 0, + /// or if the both modifiers are 0, the slowdown component is removed to restore normal movement. + /// Otherwise, the slowdown component is created or updated with the provided modifiers, + /// and the movement speed is refreshed accordingly. + /// + /// Entity whose movement speed should be updated. + /// New walk speed modifier. Default is 1f (normal speed). + /// New run (sprint) speed modifier. Default is 1f (normal speed). + public void UpdateStunModifiers(Entity ent, + float walkSpeedModifier = 1f, + float runSpeedModifier = 1f) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + if ( + (MathHelper.CloseTo(walkSpeedModifier, 1f) && MathHelper.CloseTo(runSpeedModifier, 1f) && ent.Comp.StaminaDamage == 0f) || + (walkSpeedModifier == 0f && runSpeedModifier == 0f) + ) + { + RemComp(ent); + return; + } + + EnsureComp(ent, out var comp); + + comp.WalkSpeedModifier = walkSpeedModifier; + + comp.SprintSpeedModifier = runSpeedModifier; + + _movementSpeedModifier.RefreshMovementSpeedModifiers(ent); + + Dirty(ent); + } + + /// + /// A convenience overload of that sets both + /// walk and run speed modifiers to the same value. + /// + /// Entity whose movement speed should be updated. + /// New walk and run speed modifier. Default is 1f (normal speed). + /// + /// Optional of the entity. + /// + public void UpdateStunModifiers(Entity ent, float speedModifier = 1f) + { + UpdateStunModifiers(ent, speedModifier, speedModifier); + } + private void OnInteractHand(EntityUid uid, KnockedDownComponent knocked, InteractHandEvent args) { if (args.Handled || knocked.HelpTimer > 0f)