Overhauled stamina slowdown behavior (#36336)

This commit is contained in:
nikitosych
2025-05-02 00:01:17 +01:00
committed by GitHub
parent 52062dcfad
commit 72d2d8ab0f
3 changed files with 116 additions and 22 deletions

View File

@@ -1,4 +1,5 @@
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.FixedPoint;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
@@ -56,4 +57,22 @@ public sealed partial class StaminaComponent : Component
[DataField] [DataField]
public ProtoId<AlertPrototype> StaminaAlert = "Stamina"; public ProtoId<AlertPrototype> StaminaAlert = "Stamina";
/// <summary>
/// This flag indicates whether the value of <see cref="StaminaDamage"/> decreases after the entity exits stamina crit.
/// </summary>
[DataField, AutoNetworkedField]
public bool AfterCritical;
/// <summary>
/// This float determines how fast stamina will regenerate after exiting the stamina crit.
/// </summary>
[DataField, AutoNetworkedField]
public float AfterCritDecayMultiplier = 5f;
/// <summary>
/// Thresholds that determine an entity's slowdown as a function of stamina damage.
/// </summary>
[DataField]
public Dictionary<FixedPoint2, float> StunModifierThresholds = new() { {0, 1f }, { 60, 0.7f }, { 80, 0.5f } };
} }

View File

@@ -7,8 +7,7 @@ using Content.Shared.Damage.Components;
using Content.Shared.Damage.Events; using Content.Shared.Damage.Events;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Effects; using Content.Shared.Effects;
using Content.Shared.IdentityManagement; using Content.Shared.FixedPoint;
using Content.Shared.Popups;
using Content.Shared.Projectiles; using Content.Shared.Projectiles;
using Content.Shared.Rejuvenate; using Content.Shared.Rejuvenate;
using Content.Shared.Rounding; using Content.Shared.Rounding;
@@ -21,7 +20,6 @@ using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Shared.Damage.Systems; namespace Content.Shared.Damage.Systems;
@@ -114,6 +112,7 @@ public sealed partial class StaminaSystem : EntitySystem
} }
component.StaminaDamage = 0; component.StaminaDamage = 0;
AdjustSlowdown(uid);
RemComp<ActiveStaminaComponent>(uid); RemComp<ActiveStaminaComponent>(uid);
SetStaminaAlert(uid, component); SetStaminaAlert(uid, component);
Dirty(uid, component); Dirty(uid, component);
@@ -275,17 +274,16 @@ public sealed partial class StaminaSystem : EntitySystem
component.NextUpdate = nextUpdate; component.NextUpdate = nextUpdate;
} }
var slowdownThreshold = component.CritThreshold / 2f; AdjustSlowdown(uid);
// 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); 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.Critical)
{ {
if (component.StaminaDamage >= component.CritThreshold) if (component.StaminaDamage >= component.CritThreshold)
@@ -330,9 +328,6 @@ public sealed partial class StaminaSystem : EntitySystem
{ {
base.Update(frameTime); base.Update(frameTime);
if (!_timing.IsFirstTimePredicted)
return;
var stamQuery = GetEntityQuery<StaminaComponent>(); var stamQuery = GetEntityQuery<StaminaComponent>();
var query = EntityQueryEnumerator<ActiveStaminaComponent>(); var query = EntityQueryEnumerator<ActiveStaminaComponent>();
var curTime = _timing.CurTime; var curTime = _timing.CurTime;
@@ -353,15 +348,17 @@ public sealed partial class StaminaSystem : EntitySystem
if (nextUpdate > curTime) if (nextUpdate > curTime)
continue; continue;
// We were in crit so come out of it and continue. // Handle exiting critical condition and restoring stamina damage
if (comp.Critical) if (comp.Critical)
{
ExitStamCrit(uid, comp); ExitStamCrit(uid, comp);
continue;
}
comp.NextUpdate += TimeSpan.FromSeconds(1f); 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); Dirty(uid, comp);
} }
} }
@@ -398,11 +395,38 @@ public sealed partial class StaminaSystem : EntitySystem
} }
component.Critical = false; 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; component.NextUpdate = _timing.CurTime;
SetStaminaAlert(uid, component); SetStaminaAlert(uid, component);
RemComp<ActiveStaminaComponent>(uid);
Dirty(uid, component); Dirty(uid, component);
_adminLogger.Add(LogType.Stamina, LogImpact.Low, $"{ToPrettyString(uid):user} recovered from stamina crit"); _adminLogger.Add(LogType.Stamina, LogImpact.Low, $"{ToPrettyString(uid):user} recovered from stamina crit");
} }
/// <summary>
/// Adjusts the movement speed of an entity based on its current <see cref="StaminaComponent.StaminaDamage"/> value.
/// If the entity has a <see cref="SlowOnDamageComponent"/>, 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.
/// </summary>
/// <param name="ent">Entity to update</param>
private void AdjustSlowdown(Entity<StaminaComponent?> 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]);
}
} }

View File

@@ -5,6 +5,7 @@ using Content.Shared.Interaction.Events;
using Content.Shared.Inventory.Events; using Content.Shared.Inventory.Events;
using Content.Shared.Item; using Content.Shared.Item;
using Content.Shared.Bed.Sleep; using Content.Shared.Bed.Sleep;
using Content.Shared.Damage.Components;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Hands; using Content.Shared.Hands;
using Content.Shared.Mobs; using Content.Shared.Mobs;
@@ -248,13 +249,63 @@ public abstract class SharedStunSystem : EntitySystem
slowed.SprintSpeedModifier *= runSpeedMultiplier; slowed.SprintSpeedModifier *= runSpeedMultiplier;
_movementSpeedModifier.RefreshMovementSpeedModifiers(uid); _movementSpeedModifier.RefreshMovementSpeedModifiers(uid);
return true; return true;
} }
return false; return false;
} }
/// <summary>
/// Updates the movement speed modifiers of an entity by applying or removing the <see cref="SlowedDownComponent"/>.
/// If both walk and run modifiers are approximately 1 (i.e. normal speed) and <see cref="StaminaComponent.StaminaDamage"/> 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.
/// </summary>
/// <param name="ent">Entity whose movement speed should be updated.</param>
/// <param name="walkSpeedModifier">New walk speed modifier. Default is 1f (normal speed).</param>
/// <param name="runSpeedModifier">New run (sprint) speed modifier. Default is 1f (normal speed).</param>
public void UpdateStunModifiers(Entity<StaminaComponent?> 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<SlowedDownComponent>(ent);
return;
}
EnsureComp<SlowedDownComponent>(ent, out var comp);
comp.WalkSpeedModifier = walkSpeedModifier;
comp.SprintSpeedModifier = runSpeedModifier;
_movementSpeedModifier.RefreshMovementSpeedModifiers(ent);
Dirty(ent);
}
/// <summary>
/// A convenience overload of <see cref="UpdateStunModifiers(EntityUid, float, float, StaminaComponent?)"/> that sets both
/// walk and run speed modifiers to the same value.
/// </summary>
/// <param name="ent">Entity whose movement speed should be updated.</param>
/// <param name="speedModifier">New walk and run speed modifier. Default is 1f (normal speed).</param>
/// <param name="component">
/// Optional <see cref="StaminaComponent"/> of the entity.
/// </param>
public void UpdateStunModifiers(Entity<StaminaComponent?> ent, float speedModifier = 1f)
{
UpdateStunModifiers(ent, speedModifier, speedModifier);
}
private void OnInteractHand(EntityUid uid, KnockedDownComponent knocked, InteractHandEvent args) private void OnInteractHand(EntityUid uid, KnockedDownComponent knocked, InteractHandEvent args)
{ {
if (args.Handled || knocked.HelpTimer > 0f) if (args.Handled || knocked.HelpTimer > 0f)