Stamina damage (#9230)

This commit is contained in:
metalgearsloth
2022-07-06 18:06:12 +10:00
committed by GitHub
parent 305cdc02cb
commit 40eecdd78a
129 changed files with 703 additions and 778 deletions

View File

@@ -0,0 +1,10 @@
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

@@ -0,0 +1,47 @@
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

@@ -0,0 +1,13 @@
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

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

View File

@@ -0,0 +1,10 @@
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;
}

View File

@@ -0,0 +1,221 @@
using Content.Server.Damage.Components;
using Content.Server.Damage.Events;
using Content.Server.Popups;
using Content.Server.Weapon.Melee;
using Content.Shared.Alert;
using Content.Shared.Rounding;
using Content.Shared.Stunnable;
using Robust.Shared.Collections;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Player;
using Robust.Shared.Timing;
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!;
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<StaminaDamageOnCollideComponent, StartCollideEvent>(OnCollide);
SubscribeLocalEvent<StaminaDamageOnHitComponent, MeleeHitEvent>(OnHit);
SubscribeLocalEvent<StaminaComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<StaminaComponent, ComponentShutdown>(OnShutdown);
}
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 OnHit(EntityUid uid, StaminaDamageOnHitComponent component, MeleeHitEvent args)
{
if (component.Damage <= 0f) return;
var ev = new StaminaDamageOnHitAttemptEvent();
RaiseLocalEvent(uid, ref ev);
if (ev.Cancelled) return;
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);
}
foreach (var comp in toHit)
{
var oldDamage = comp.StaminaDamage;
TakeStaminaDamage(comp.Owner, component.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, 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

@@ -30,7 +30,6 @@ namespace Content.Server.Flash
{
base.Initialize();
SubscribeLocalEvent<FlashComponent, MeleeHitEvent>(OnFlashMeleeHit);
SubscribeLocalEvent<FlashComponent, UseInHandEvent>(OnFlashUseInHand);
SubscribeLocalEvent<FlashComponent, ExaminedEvent>(OnFlashExamined);
SubscribeLocalEvent<InventoryComponent, FlashAttemptEvent>(OnInventoryFlashAttempt);
@@ -78,16 +77,6 @@ namespace Content.Server.Flash
}
}
private void OnFlashUseInHand(EntityUid uid, FlashComponent comp, UseInHandEvent args)
{
if (!UseFlash(comp, args.User))
{
return;
}
FlashArea(uid, args.User, comp.Range, comp.AoeFlashDuration, comp.SlowTo, true);
}
private bool UseFlash(FlashComponent comp, EntityUid user)
{
if (comp.HasUses)
@@ -121,39 +110,40 @@ namespace Content.Server.Flash
return false;
}
public void Flash(EntityUid target, EntityUid? user, EntityUid? used, float flashDuration, float slowTo, bool displayPopup = true)
public void Flash(EntityUid target, EntityUid? user, EntityUid? used, float flashDuration, float slowTo, bool displayPopup = true, FlashableComponent? flashable = null)
{
if (!Resolve(target, ref flashable, false)) return;
var attempt = new FlashAttemptEvent(target, user, used);
RaiseLocalEvent(target, attempt, true);
if (attempt.Cancelled)
return;
if (EntityManager.TryGetComponent<FlashableComponent>(target, out var flashable))
flashable.LastFlash = _gameTiming.CurTime;
flashable.Duration = flashDuration / 1000f; // TODO: Make this sane...
Dirty(flashable);
_stunSystem.TrySlowdown(target, TimeSpan.FromSeconds(flashDuration/1000f), true,
slowTo, slowTo);
if (displayPopup && user != null && target != user && EntityManager.EntityExists(user.Value))
{
flashable.LastFlash = _gameTiming.CurTime;
flashable.Duration = flashDuration / 1000f; // TODO: Make this sane...
Dirty(flashable);
_stunSystem.TrySlowdown(target, TimeSpan.FromSeconds(flashDuration/1000f), true,
slowTo, slowTo);
if (displayPopup && user != null && target != user && EntityManager.EntityExists(user.Value))
{
user.Value.PopupMessage(target, Loc.GetString("flash-component-user-blinds-you",
("user", user.Value)));
}
user.Value.PopupMessage(target, Loc.GetString("flash-component-user-blinds-you",
("user", user.Value)));
}
}
public void FlashArea(EntityUid source, EntityUid? user, float range, float duration, float slowTo = 0f, bool displayPopup = false, SoundSpecifier? sound = null)
public void FlashArea(EntityUid source, EntityUid? user, float range, float duration, float slowTo = 0.8f, bool displayPopup = false, SoundSpecifier? sound = null)
{
var transform = EntityManager.GetComponent<TransformComponent>(source);
var mapPosition = transform.MapPosition;
var flashableEntities = new List<EntityUid>();
var flashableQuery = GetEntityQuery<FlashableComponent>();
foreach (var entity in _entityLookup.GetEntitiesInRange(transform.Coordinates, range))
{
if (!EntityManager.HasComponent<FlashableComponent>(entity))
if (!flashableQuery.HasComponent(entity))
continue;
flashableEntities.Add(entity);
@@ -162,14 +152,15 @@ namespace Content.Server.Flash
foreach (var entity in flashableEntities)
{
// Check for unobstructed entities while ignoring the mobs with flashable components.
if (!_interactionSystem.InRangeUnobstructed(entity, transform.MapPosition, range, CollisionGroup.Opaque, (e) => flashableEntities.Contains(e)))
if (!_interactionSystem.InRangeUnobstructed(entity, mapPosition, range, CollisionGroup.Opaque, (e) => flashableEntities.Contains(e)))
continue;
Flash(entity, user, source, duration, slowTo, displayPopup);
// They shouldn't have flash removed in between right?
Flash(entity, user, source, duration, slowTo, displayPopup, flashableQuery.GetComponent(entity));
}
if (sound != null)
{
SoundSystem.Play(sound.GetSound(), Filter.Pvs(transform), transform.Coordinates);
SoundSystem.Play(sound.GetSound(), Filter.Pvs(transform), source);
}
}

View File

@@ -1,3 +1,4 @@
using Content.Server.Stunnable.Systems;
using Content.Shared.Sound;
using Content.Shared.Timing;
@@ -8,21 +9,6 @@ namespace Content.Server.Stunnable.Components
{
public bool Activated = false;
/// <summary>
/// What the <see cref="UseDelayComponent"/> is when the stun baton is active.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("activeCooldown")]
public TimeSpan ActiveDelay = TimeSpan.FromSeconds(4);
/// <summary>
/// Store what the <see cref="UseDelayComponent"/> was before being activated.
/// </summary>
public TimeSpan? OldDelay;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("paralyzeTime")]
public float ParalyzeTime { get; set; } = 5f;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("energyPerUse")]
public float EnergyPerUse { get; set; } = 350;

View File

@@ -1,10 +1,11 @@
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.Weapon.Melee;
using Content.Server.Weapon.Melee.Components;
using Content.Shared.Audio;
using Content.Shared.Examine;
using Content.Shared.Interaction.Events;
@@ -12,55 +13,48 @@ using Content.Shared.Item;
using Content.Shared.Jittering;
using Content.Shared.Popups;
using Content.Shared.StatusEffect;
using Content.Shared.Stunnable;
using Content.Shared.Throwing;
using Content.Shared.Timing;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Stunnable
namespace Content.Server.Stunnable.Systems
{
public sealed class StunbatonSystem : EntitySystem
{
[Dependency] private readonly MeleeWeaponSystem _melee = default!;
[Dependency] private readonly StunSystem _stunSystem = default!;
[Dependency] private readonly StutteringSystem _stutteringSystem = default!;
[Dependency] private readonly SharedJitteringSystem _jitterSystem = default!;
[Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StunbatonComponent, MeleeHitEvent>(OnMeleeHit);
SubscribeLocalEvent<StunbatonComponent, UseInHandEvent>(OnUseInHand);
SubscribeLocalEvent<StunbatonComponent, ThrowDoHitEvent>(OnThrowCollide);
SubscribeLocalEvent<StunbatonComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<StunbatonComponent, StaminaDamageOnHitAttemptEvent>(OnStaminaHitAttempt);
SubscribeLocalEvent<StunbatonComponent, MeleeHitEvent>(OnMeleeHit);
}
private void OnMeleeHit(EntityUid uid, StunbatonComponent comp, MeleeHitEvent args)
private void OnMeleeHit(EntityUid uid, StunbatonComponent component, MeleeHitEvent args)
{
if (!comp.Activated || !args.HitEntities.Any() || args.Handled || _useDelay.ActiveDelay(uid))
return;
if (!component.Activated) return;
if (!TryComp<BatteryComponent>(uid, out var battery) || !battery.TryUseCharge(comp.EnergyPerUse))
return;
// Don't apply damage if it's activated; just do stamina damage.
args.BonusDamage -= args.BaseDamage;
}
foreach (var entity in args.HitEntities)
private void OnStaminaHitAttempt(EntityUid uid, StunbatonComponent component, ref StaminaDamageOnHitAttemptEvent args)
{
if (!component.Activated ||
!TryComp<BatteryComponent>(uid, out var battery) || !battery.TryUseCharge(component.EnergyPerUse))
{
StunEntity(entity, comp);
SendPowerPulse(entity, args.User, uid);
args.Cancelled = true;
return;
}
_melee.SetAttackCooldown(uid, _timing.CurTime + comp.ActiveDelay);
_useDelay.BeginDelay(uid);
// No combat should occur if we successfully stunned.
args.Handled = true;
if (battery.CurrentCharge < component.EnergyPerUse)
{
SoundSystem.Play(component.SparksSound.GetSound(), Filter.Pvs(component.Owner, entityManager: EntityManager), uid, AudioHelpers.WithVariation(0.25f));
TurnOff(component);
}
}
private void OnUseInHand(EntityUid uid, StunbatonComponent comp, UseInHandEvent args)
@@ -75,21 +69,6 @@ namespace Content.Server.Stunnable
}
}
private void OnThrowCollide(EntityUid uid, StunbatonComponent comp, ThrowDoHitEvent args)
{
if (!comp.Activated)
return;
if (!TryComp<BatteryComponent>(uid, out var battery))
return;
if (_robustRandom.Prob(comp.OnThrowStunChance) && battery.TryUseCharge(comp.EnergyPerUse))
{
SendPowerPulse(args.Target, args.User, uid);
StunEntity(args.Target, comp);
}
}
private void OnExamined(EntityUid uid, StunbatonComponent comp, ExaminedEvent args)
{
var msg = comp.Activated
@@ -101,24 +80,6 @@ namespace Content.Server.Stunnable
("charge", (int)((battery.CurrentCharge/battery.MaxCharge) * 100))));
}
private void StunEntity(EntityUid entity, StunbatonComponent comp)
{
if (!EntityManager.TryGetComponent(entity, out StatusEffectsComponent? status) || !comp.Activated) return;
SoundSystem.Play(comp.StunSound.GetSound(), Filter.Pvs(comp.Owner), comp.Owner, AudioHelpers.WithVariation(0.25f));
_stunSystem.TryParalyze(entity, TimeSpan.FromSeconds(comp.ParalyzeTime), true, status);
var slowdownTime = TimeSpan.FromSeconds(comp.ParalyzeTime);
_jitterSystem.DoJitter(entity, slowdownTime, true, status:status);
_stutteringSystem.DoStutter(entity, slowdownTime, true, status);
if (!TryComp<BatteryComponent>(comp.Owner, out var battery) || !(battery.CurrentCharge < comp.EnergyPerUse))
return;
SoundSystem.Play(comp.SparksSound.GetSound(), Filter.Pvs(comp.Owner), comp.Owner, AudioHelpers.WithVariation(0.25f));
TurnOff(comp);
}
private void TurnOff(StunbatonComponent comp)
{
if (!comp.Activated)
@@ -135,11 +96,6 @@ namespace Content.Server.Stunnable
SoundSystem.Play(comp.SparksSound.GetSound(), Filter.Pvs(comp.Owner), comp.Owner, AudioHelpers.WithVariation(0.25f));
comp.Activated = false;
if (TryComp<UseDelayComponent>(comp.Owner, out var useDelay) && comp.OldDelay != null)
{
useDelay.Delay = comp.OldDelay.Value;
comp.OldDelay = null;
}
}
private void TurnOn(StunbatonComponent comp, EntityUid user)
@@ -147,13 +103,6 @@ namespace Content.Server.Stunnable
if (comp.Activated)
return;
if (EntityManager.TryGetComponent<SpriteComponent?>(comp.Owner, out var sprite) &&
EntityManager.TryGetComponent<SharedItemComponent?>(comp.Owner, out var item))
{
item.EquippedPrefix = "on";
sprite.LayerSetState(0, "stunbaton_on");
}
var playerFilter = Filter.Pvs(comp.Owner, entityManager: EntityManager);
if (!TryComp<BatteryComponent>(comp.Owner, out var battery) || battery.CurrentCharge < comp.EnergyPerUse)
{
@@ -162,14 +111,15 @@ namespace Content.Server.Stunnable
return;
}
SoundSystem.Play(comp.SparksSound.GetSound(), playerFilter, comp.Owner, AudioHelpers.WithVariation(0.25f));
comp.Activated = true;
if (TryComp<UseDelayComponent>(comp.Owner, out var useDelay))
if (EntityManager.TryGetComponent<SpriteComponent?>(comp.Owner, out var sprite) &&
EntityManager.TryGetComponent<SharedItemComponent?>(comp.Owner, out var item))
{
comp.OldDelay = useDelay.Delay;
useDelay.Delay = comp.ActiveDelay;
item.EquippedPrefix = "on";
sprite.LayerSetState(0, "stunbaton_on");
}
SoundSystem.Play(comp.SparksSound.GetSound(), playerFilter, comp.Owner, AudioHelpers.WithVariation(0.25f));
comp.Activated = true;
}
private void SendPowerPulse(EntityUid target, EntityUid? user, EntityUid used)

View File

@@ -1,4 +1,5 @@
using System.Linq;
using Content.Server.Damage.Systems;
using Content.Server.Projectiles.Components;
using Content.Server.Weapon.Melee;
using Content.Server.Weapon.Ranged.Components;
@@ -24,6 +25,7 @@ namespace Content.Server.Weapon.Ranged.Systems;
public sealed partial class GunSystem : SharedGunSystem
{
[Dependency] private readonly EffectSystem _effects = default!;
[Dependency] private readonly StaminaSystem _stamina = default!;
public const float DamagePitchVariation = MeleeWeaponSystem.DamagePitchVariation;
@@ -116,6 +118,9 @@ public sealed partial class GunSystem : SharedGunSystem
var distance = result.Distance;
FireEffects(fromCoordinates, distance, entityDirection.ToAngle(), hitscan, result.HitEntity);
if (hitscan.StaminaDamage > 0f)
_stamina.TakeStaminaDamage(result.HitEntity, hitscan.StaminaDamage);
var dmg = hitscan.Damage;
if (dmg != null)