Predicted stamina (#12413)

* Predicted stamina

Needed to do some semblence of predicted melee.

* Okay now done.

* Pause support

* Comment
This commit is contained in:
metalgearsloth
2022-11-09 07:34:07 +11:00
committed by GitHub
parent 43b529f647
commit fb3f6fa80f
23 changed files with 148 additions and 95 deletions

View File

@@ -1,6 +1,6 @@
using Content.Server.Damage.Events;
using Content.Server.Weapons.Melee.Events;
using Content.Shared.Damage.Events;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Containers;
namespace Content.Server.Abilities.Boxer

View File

@@ -3,7 +3,6 @@ using Content.Server.Atmos.Components;
using Content.Server.Stunnable;
using Content.Server.Temperature.Components;
using Content.Server.Temperature.Systems;
using Content.Server.Weapons.Melee.Events;
using Content.Shared.ActionBlocker;
using Content.Shared.Alert;
using Content.Shared.Atmos;
@@ -14,6 +13,7 @@ using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Rejuvenate;
using Content.Shared.Temperature;
using Content.Shared.Weapons.Melee.Events;
using Robust.Server.GameObjects;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;

View File

@@ -1,6 +1,5 @@
using System.Linq;
using Content.Server.Chemistry.Components;
using Content.Server.Weapons.Melee.Events;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Weapons.Melee;

View File

@@ -1,20 +0,0 @@
namespace Content.Server.CombatMode
{
public sealed class DisarmedEvent : HandledEntityEventArgs
{
/// <summary>
/// The entity being disarmed.
/// </summary>
public EntityUid Target { get; init; }
/// <summary>
/// The entity performing the disarm.
/// </summary>
public EntityUid Source { get; init; }
/// <summary>
/// Probability for push/knockdown.
/// </summary>
public float PushProbability { get; init; }
}
}

View File

@@ -2,6 +2,7 @@ using Content.Shared.Damage;
using Content.Shared.MobState.EntitySystems;
using Content.Shared.MobState.Components;
using Content.Server.Damage.Components;
using Content.Shared.Damage.Components;
using Robust.Shared.Physics.Components;
namespace Content.Server.Contests

View File

@@ -1,10 +0,0 @@
namespace Content.Server.Damage.Components;
/// <summary>
/// Tracks whether an entity has ANY stamina damage for update purposes only.
/// </summary>
[RegisterComponent]
public sealed class ActiveStaminaComponent : Component
{
}

View File

@@ -1,47 +0,0 @@
using Content.Server.Damage.Systems;
using Robust.Shared.GameStates;
namespace Content.Server.Damage.Components;
/// <summary>
/// Add to an entity to paralyze it whenever it reaches critical amounts of Stamina DamageType.
/// </summary>
[RegisterComponent]
public sealed class StaminaComponent : Component
{
/// <summary>
/// Have we reached peak stamina damage and been paralyzed?
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("critical")]
public bool Critical;
/// <summary>
/// How much stamina reduces per second.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("decay")]
public float Decay = 3f;
/// <summary>
/// How much time after receiving damage until stamina starts decreasing.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("cooldown")]
public float DecayCooldown = 5f;
/// <summary>
/// How much stamina damage this entity has taken.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("staminaDamage")]
public float StaminaDamage;
/// <summary>
/// How much stamina damage is required to entire stam crit.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("excess")]
public float CritThreshold = 100f;
/// <summary>
/// Next time we're allowed to decrease stamina damage. Refreshes whenever the stam damage is changed.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("decayAccumulator")]
public float StaminaDecayAccumulator;
}

View File

@@ -1,13 +0,0 @@
using Robust.Shared.GameStates;
namespace Content.Server.Damage.Components;
/// <summary>
/// Applies stamina damage when colliding with an entity.
/// </summary>
[RegisterComponent]
public sealed class StaminaDamageOnCollideComponent : Component
{
[ViewVariables(VVAccess.ReadWrite), DataField("damage")]
public float Damage = 55f;
}

View File

@@ -1,10 +0,0 @@
using Robust.Shared.Audio;
namespace Content.Server.Damage.Components;
[RegisterComponent]
public sealed class StaminaDamageOnHitComponent : Component
{
[ViewVariables(VVAccess.ReadWrite), DataField("damage")]
public float Damage = 30f;
}

View File

@@ -1,13 +0,0 @@
using Robust.Shared.Audio;
namespace Content.Server.Damage.Events;
/// <summary>
/// Attempting to apply stamina damage on a melee hit to an entity.
/// </summary>
[ByRefEvent]
public struct StaminaDamageOnHitAttemptEvent
{
public bool Cancelled;
public SoundSpecifier? HitSoundOverride;
}

View File

@@ -1,30 +0,0 @@
using Robust.Shared.Collections;
using Content.Server.Damage.Components;
namespace Content.Server.Damage.Events;
/// <summary>
/// The components in the list are going to be hit,
/// give opportunities to change the damage or other stuff.
/// </summary>
public sealed class StaminaMeleeHitEvent : HandledEntityEventArgs
{
/// <summary>
/// List of hit stamina components.
public ValueList<StaminaComponent> HitList;
/// <summmary>
/// The multiplier. Generally, try to use *= or /= instead of overwriting.
/// </summary>
public float Multiplier = 1;
/// <summary>
/// The flat modifier. Generally, try to use += or -= instead of overwriting.
/// </summary>
public float FlatModifier = 0;
public StaminaMeleeHitEvent(ValueList<StaminaComponent> hitList)
{
HitList = hitList;
}
}

View File

@@ -1,275 +0,0 @@
using Content.Server.Damage.Components;
using Content.Server.Damage.Events;
using Content.Server.Popups;
using Content.Server.Administration.Logs;
using Content.Server.CombatMode;
using Content.Server.Weapons.Melee.Events;
using Content.Shared.Alert;
using Content.Shared.Rounding;
using Content.Shared.Stunnable;
using Content.Shared.Database;
using Content.Shared.IdentityManagement;
using Content.Shared.Popups;
using Robust.Shared.Collections;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Audio;
using Robust.Shared.Random;
using Robust.Shared.Physics.Events;
namespace Content.Server.Damage.Systems;
public sealed class StaminaSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly SharedStunSystem _stunSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
private const float UpdateCooldown = 2f;
private float _accumulator;
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>
private const float StamCritBufferTime = 3f;
private readonly List<EntityUid> _dirtyEntities = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StaminaComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<StaminaComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<StaminaComponent, DisarmedEvent>(OnDisarmed);
SubscribeLocalEvent<StaminaDamageOnCollideComponent, StartCollideEvent>(OnCollide);
SubscribeLocalEvent<StaminaDamageOnHitComponent, MeleeHitEvent>(OnHit);
}
private void OnShutdown(EntityUid uid, StaminaComponent component, ComponentShutdown args)
{
SetStaminaAlert(uid);
}
private void OnStartup(EntityUid uid, StaminaComponent component, ComponentStartup args)
{
SetStaminaAlert(uid, 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);
// 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), PopupType.LargeCaution);
_popup.PopupCursor(Loc.GetString("stunned-component-disarm-success", ("target", targetEnt)), Filter.Entities(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 OnHit(EntityUid uid, StaminaDamageOnHitComponent component, MeleeHitEvent args)
{
if (!args.IsHit)
return;
if (component.Damage <= 0f) return;
var ev = new StaminaDamageOnHitAttemptEvent();
RaiseLocalEvent(uid, ref ev);
if (ev.Cancelled) return;
args.HitSoundOverride = ev.HitSoundOverride;
var stamQuery = GetEntityQuery<StaminaComponent>();
var toHit = new ValueList<StaminaComponent>();
// Split stamina damage between all eligible targets.
foreach (var ent in args.HitEntities)
{
if (!stamQuery.TryGetComponent(ent, out var stam)) continue;
toHit.Add(stam);
}
var hitEvent = new StaminaMeleeHitEvent(toHit);
RaiseLocalEvent(uid, hitEvent, false);
if (hitEvent.Handled)
return;
var damage = component.Damage;
damage *= hitEvent.Multiplier;
damage += hitEvent.FlatModifier;
foreach (var comp in toHit)
{
var oldDamage = comp.StaminaDamage;
TakeStaminaDamage(comp.Owner, damage / toHit.Count, comp);
if (comp.StaminaDamage.Equals(oldDamage))
{
_popup.PopupEntity(Loc.GetString("stamina-resist"), comp.Owner, Filter.Entities(args.User));
}
}
}
private void OnCollide(EntityUid uid, StaminaDamageOnCollideComponent component, ref StartCollideEvent args)
{
if (!args.OurFixture.ID.Equals(CollideFixture)) return;
TakeStaminaDamage(args.OtherFixture.Body.Owner, component.Damage);
}
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);
}
public void TakeStaminaDamage(EntityUid uid, float value, StaminaComponent? component = null)
{
if (!Resolve(uid, ref component, false) || 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)
{
component.StaminaDecayAccumulator = component.DecayCooldown;
}
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);
// Can't do it here as resetting prediction gets cooked.
_dirtyEntities.Add(uid);
if (!component.Critical)
{
if (component.StaminaDamage >= component.CritThreshold)
{
EnterStamCrit(uid, component);
}
}
else
{
if (component.StaminaDamage < component.CritThreshold)
{
ExitStamCrit(uid, component);
}
}
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!_timing.IsFirstTimePredicted) return;
_accumulator -= frameTime;
if (_accumulator > 0f) return;
var stamQuery = GetEntityQuery<StaminaComponent>();
foreach (var uid in _dirtyEntities)
{
// Don't need to RemComp as they will get handled below.
if (!stamQuery.TryGetComponent(uid, out var comp) || comp.StaminaDamage <= 0f) continue;
EnsureComp<ActiveStaminaComponent>(uid);
}
_dirtyEntities.Clear();
_accumulator += UpdateCooldown;
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) ||
comp.StaminaDamage <= 0f)
{
RemComp<ActiveStaminaComponent>(active.Owner);
continue;
}
comp.StaminaDecayAccumulator -= UpdateCooldown;
if (comp.StaminaDecayAccumulator > 0f) continue;
// We were in crit so come out of it and continue.
if (comp.Critical)
{
ExitStamCrit(active.Owner, comp);
continue;
}
comp.StaminaDecayAccumulator = 0f;
TakeStaminaDamage(comp.Owner, -comp.Decay * UpdateCooldown, 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;
component.StaminaDecayAccumulator = 0f;
var stunTime = TimeSpan.FromSeconds(6);
_stunSystem.TryParalyze(uid, stunTime, true);
// Give them buffer before being able to be re-stunned
component.StaminaDecayAccumulator = (float) stunTime.TotalSeconds + StamCritBufferTime;
}
private void ExitStamCrit(EntityUid uid, StaminaComponent? component = null)
{
if (!Resolve(uid, ref component) ||
!component.Critical) return;
component.Critical = false;
component.StaminaDamage = 0f;
SetStaminaAlert(uid, component);
}
}

View File

@@ -1,7 +1,6 @@
using Content.Server.Flash.Components;
using Content.Server.Light.EntitySystems;
using Content.Server.Stunnable;
using Content.Server.Weapons.Melee.Events;
using Content.Shared.Examine;
using Content.Shared.Flash;
using Content.Shared.IdentityManagement;
@@ -11,6 +10,7 @@ using Content.Shared.Inventory;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Tag;
using Content.Shared.Weapons.Melee.Events;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Player;

View File

@@ -10,6 +10,7 @@ using Content.Server.Storage.EntitySystems;
using Content.Server.Strip;
using Content.Server.Stunnable;
using Content.Shared.ActionBlocker;
using Content.Shared.CombatMode;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;

View File

@@ -1,24 +1,16 @@
using System.Linq;
using Content.Server.Damage.Components;
using Content.Server.Damage.Events;
using Content.Server.Power.Components;
using Content.Server.Power.Events;
using Content.Server.Speech.EntitySystems;
using Content.Server.Stunnable.Components;
using Content.Server.Weapons.Melee.Events;
using Content.Shared.Audio;
using Content.Shared.Damage.Events;
using Content.Shared.Examine;
using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Jittering;
using Content.Shared.Popups;
using Content.Shared.StatusEffect;
using Content.Shared.Throwing;
using Content.Shared.Toggleable;
using Robust.Server.GameObjects;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Server.Stunnable.Systems
{

View File

@@ -3,7 +3,6 @@ using Content.Server.Chemistry.Components;
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Tools.Components;
using Content.Server.Weapons.Melee.Events;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
@@ -11,6 +10,7 @@ using Content.Shared.Item;
using Content.Shared.Temperature;
using Content.Shared.Toggleable;
using Content.Shared.Tools.Components;
using Content.Shared.Weapons.Melee.Events;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;

View File

@@ -1,7 +1,6 @@
using Content.Server.CombatMode.Disarm;
using Content.Server.Kitchen.Components;
using Content.Server.Weapons.Melee.EnergySword.Components;
using Content.Server.Weapons.Melee.Events;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Item;
@@ -11,6 +10,7 @@ using Content.Shared.Temperature;
using Content.Shared.Toggleable;
using Content.Shared.Tools.Components;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Random;

View File

@@ -1,62 +0,0 @@
using Content.Shared.Damage;
using Robust.Shared.Audio;
namespace Content.Server.Weapons.Melee.Events;
/// <summary>
/// Raised directed on the melee weapon entity used to attack something in combat mode,
/// whether through a click attack or wide attack.
/// </summary>
public sealed class MeleeHitEvent : HandledEntityEventArgs
{
/// <summary>
/// The base amount of damage dealt by the melee hit.
/// </summary>
public readonly DamageSpecifier BaseDamage = new();
/// <summary>
/// Modifier sets to apply to the hit event when it's all said and done.
/// This should be modified by adding a new entry to the list.
/// </summary>
public List<DamageModifierSet> ModifiersList = new();
/// <summary>
/// Damage to add to the default melee weapon damage. Applied before modifiers.
/// </summary>
/// <remarks>
/// This might be required as damage modifier sets cannot add a new damage type to a DamageSpecifier.
/// </remarks>
public DamageSpecifier BonusDamage = new();
/// <summary>
/// A list containing every hit entity. Can be zero.
/// </summary>
public IEnumerable<EntityUid> HitEntities { get; }
/// <summary>
/// Used to define a new hit sound in case you want to override the default GenericHit.
/// Also gets a pitch modifier added to it.
/// </summary>
public SoundSpecifier? HitSoundOverride {get; set;}
/// <summary>
/// The user who attacked with the melee weapon.
/// </summary>
public EntityUid User { get; }
/// <summary>
/// Check if this is true before attempting to do something during a melee attack other than changing/adding bonus damage. <br/>
/// For example, do not spend charges unless <see cref="IsHit"/> equals true.
/// </summary>
/// <remarks>
/// Examining melee weapons calls this event, but with <see cref="IsHit"/> set to false.
/// </remarks>
public bool IsHit = true;
public MeleeHitEvent(List<EntityUid> hitEntities, EntityUid user, DamageSpecifier baseDamage)
{
HitEntities = hitEntities;
User = user;
BaseDamage = baseDamage;
}
}

View File

@@ -14,9 +14,9 @@ using Content.Server.Examine;
using Content.Server.Hands.Components;
using Content.Server.Movement.Systems;
using Content.Server.Weapons.Melee.Components;
using Content.Server.Weapons.Melee.Events;
using Content.Shared.CombatMode;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.IdentityManagement;

View File

@@ -9,6 +9,7 @@ using Content.Server.Stunnable;
using Content.Server.Weapons.Melee;
using Content.Server.Weapons.Ranged.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Weapons.Melee;

View File

@@ -11,7 +11,7 @@ using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Player;
using Content.Server.Actions.Events;
using Content.Server.Weapons.Melee.Events;
using Content.Shared.Weapons.Melee.Events;
namespace Content.Server.Wieldable

View File

@@ -12,9 +12,9 @@ using Content.Server.Inventory;
using Robust.Shared.Prototypes;
using Content.Server.Speech;
using Content.Server.Chat.Systems;
using Content.Server.Weapons.Melee.Events;
using Content.Shared.Movement.Systems;
using Content.Shared.Damage;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Zombies;
namespace Content.Server.Zombies