Stun and Stamina Visuals (#37196)
* Stun animation * Commit 2 * Almost working commit * Best commit * Minor cleanup and value adjustments * Fix animation data getting wasted and cleaned up some stuff * Don't animate if dead * AppearanceSystem is for chumps * Cleanup * More cleanup * More cleanup * Half working commit * Documentation * Works * ComponentHandleState my beloved * AppearanceComp compatibility * Address review * Borgar * AND NOW THE END IS NEAR * AppearanceSystem compliance (Real) * Don't need to log missing there * I actually hate mob prototypes so much you don't even know --------- Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
a268a4aacc
commit
ac895a0db4
@@ -1,7 +1,126 @@
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Client.Stunnable;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Damage.Systems;
|
||||
|
||||
public sealed partial class StaminaSystem : SharedStaminaSystem
|
||||
{
|
||||
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
[Dependency] private readonly StunSystem _stun = default!; // Clientside Stun System
|
||||
|
||||
private const string StaminaAnimationKey = "stamina";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<StaminaComponent, AnimationCompletedEvent>(OnAnimationCompleted);
|
||||
SubscribeLocalEvent<ActiveStaminaComponent, ComponentShutdown>(OnActiveStaminaShutdown);
|
||||
SubscribeLocalEvent<StaminaComponent, MobStateChangedEvent>(OnMobStateChanged);
|
||||
}
|
||||
|
||||
protected override void OnStamHandleState(Entity<StaminaComponent> entity, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
base.OnStamHandleState(entity, ref args);
|
||||
|
||||
TryStartAnimation(entity);
|
||||
}
|
||||
|
||||
private void OnActiveStaminaShutdown(Entity<ActiveStaminaComponent> entity, ref ComponentShutdown args)
|
||||
{
|
||||
// If we don't have active stamina, we shouldn't have stamina damage. If the update loop can trust it we can trust it.
|
||||
if (!TryComp<StaminaComponent>(entity, out var stamina))
|
||||
return;
|
||||
|
||||
StopAnimation((entity, stamina));
|
||||
}
|
||||
|
||||
protected override void OnShutdown(Entity<StaminaComponent> entity, ref ComponentShutdown args)
|
||||
{
|
||||
base.OnShutdown(entity, ref args);
|
||||
|
||||
StopAnimation(entity);
|
||||
}
|
||||
|
||||
private void OnMobStateChanged(Entity<StaminaComponent> ent, ref MobStateChangedEvent args)
|
||||
{
|
||||
if (args.NewMobState == MobState.Dead)
|
||||
StopAnimation(ent);
|
||||
}
|
||||
|
||||
private void TryStartAnimation(Entity<StaminaComponent> entity)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(entity, out var sprite))
|
||||
return;
|
||||
|
||||
// If the animation is running, the system should update it accordingly
|
||||
// If we're below the threshold to animate, don't try to animate
|
||||
// If we're in stamcrit don't override it
|
||||
if (entity.Comp.AnimationThreshold > entity.Comp.StaminaDamage || _animation.HasRunningAnimation(entity, StaminaAnimationKey))
|
||||
return;
|
||||
|
||||
// Don't animate if we're dead
|
||||
if (_mobState.IsDead(entity))
|
||||
return;
|
||||
|
||||
entity.Comp.StartOffset = sprite.Offset;
|
||||
|
||||
PlayAnimation((entity, entity.Comp, sprite));
|
||||
}
|
||||
|
||||
private void StopAnimation(Entity<StaminaComponent, SpriteComponent?> entity)
|
||||
{
|
||||
if(!Resolve(entity, ref entity.Comp2))
|
||||
return;
|
||||
|
||||
_animation.Stop(entity.Owner, StaminaAnimationKey);
|
||||
entity.Comp1.StartOffset = entity.Comp2.Offset;
|
||||
}
|
||||
|
||||
private void OnAnimationCompleted(Entity<StaminaComponent> entity, ref AnimationCompletedEvent args)
|
||||
{
|
||||
if (args.Key != StaminaAnimationKey || !args.Finished || !TryComp<SpriteComponent>(entity, out var sprite))
|
||||
return;
|
||||
|
||||
// stop looping if we're below the threshold
|
||||
if (entity.Comp.AnimationThreshold > entity.Comp.StaminaDamage)
|
||||
{
|
||||
_animation.Stop(entity.Owner, StaminaAnimationKey);
|
||||
_sprite.SetOffset((entity, sprite), entity.Comp.StartOffset);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HasComp<AnimationPlayerComponent>(entity))
|
||||
return;
|
||||
|
||||
PlayAnimation((entity, entity.Comp, sprite));
|
||||
}
|
||||
|
||||
private void PlayAnimation(Entity<StaminaComponent, SpriteComponent> entity)
|
||||
{
|
||||
var step = Math.Clamp((entity.Comp1.StaminaDamage - entity.Comp1.AnimationThreshold) /
|
||||
(entity.Comp1.CritThreshold - entity.Comp1.AnimationThreshold),
|
||||
0f,
|
||||
1f); // The things I do for project 0 warnings
|
||||
var frequency = entity.Comp1.FrequencyMin + step * entity.Comp1.FrequencyMod;
|
||||
var jitter = entity.Comp1.JitterAmplitudeMin + step * entity.Comp1.JitterAmplitudeMod;
|
||||
var breathing = entity.Comp1.BreathingAmplitudeMin + step * entity.Comp1.BreathingAmplitudeMod;
|
||||
|
||||
_animation.Play(entity.Owner,
|
||||
_stun.GetFatigueAnimation(entity.Comp2,
|
||||
frequency,
|
||||
entity.Comp1.Jitters,
|
||||
jitter * entity.Comp1.JitterMin,
|
||||
jitter * entity.Comp1.JitterMax,
|
||||
breathing,
|
||||
entity.Comp1.StartOffset,
|
||||
ref entity.Comp1.LastJitter),
|
||||
StaminaAnimationKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,164 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Stunnable;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Stunnable;
|
||||
|
||||
namespace Content.Client.Stunnable
|
||||
{
|
||||
public sealed class StunSystem : SharedStunSystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SpriteSystem _spriteSystem = default!;
|
||||
|
||||
private readonly int[] _sign = [-1, 1];
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<StunVisualsComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<StunVisualsComponent, AppearanceChangeEvent>(OnAppearanceChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add stun visual layers
|
||||
/// </summary>
|
||||
private void OnComponentInit(Entity<StunVisualsComponent> entity, ref ComponentInit args)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(entity, out var sprite))
|
||||
return;
|
||||
|
||||
var spriteEntity = (entity.Owner, sprite);
|
||||
|
||||
_spriteSystem.LayerMapReserve(spriteEntity, StunVisualLayers.StamCrit);
|
||||
_spriteSystem.LayerSetVisible(spriteEntity, StunVisualLayers.StamCrit, false);
|
||||
_spriteSystem.LayerSetOffset(spriteEntity, StunVisualLayers.StamCrit, new Vector2(0, 0.3125f));
|
||||
|
||||
_spriteSystem.LayerSetRsi(spriteEntity, StunVisualLayers.StamCrit, entity.Comp.StarsPath);
|
||||
|
||||
UpdateAppearance((entity, sprite), entity.Comp.State);
|
||||
}
|
||||
|
||||
private void OnAppearanceChanged(Entity<StunVisualsComponent> entity, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite != null)
|
||||
UpdateAppearance((entity, args.Sprite), entity.Comp.State);
|
||||
}
|
||||
|
||||
private void UpdateAppearance(Entity<SpriteComponent?> entity, string state)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp))
|
||||
return;
|
||||
|
||||
if (!_spriteSystem.LayerMapTryGet((entity, entity.Comp), StunVisualLayers.StamCrit, out var index, false))
|
||||
return;
|
||||
|
||||
var visible = Appearance.TryGetData<bool>(entity, StunVisuals.SeeingStars, out var stars) && stars;
|
||||
|
||||
_spriteSystem.LayerSetVisible((entity, entity.Comp), index, visible);
|
||||
_spriteSystem.LayerSetRsiState((entity, entity.Comp), index, state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A simple fatigue animation, a mild modification of the jittering animation. The animation constructor is
|
||||
/// quite complex, but that's because the AnimationSystem doesn't have proper adjustment layers. In a potential
|
||||
/// future where proper adjustment layers are added feel free to clean this up to be an animation with two adjustment
|
||||
/// layers rather than one mega layer.
|
||||
/// </summary>
|
||||
/// <param name="sprite">The spriteComponent we're adjusting the offset of</param>
|
||||
/// <param name="frequency">How many times per second does the animation run?</param>
|
||||
/// <param name="jitters">How many times should we jitter during the animation? Also determines breathing frequency</param>
|
||||
/// <param name="minJitter">Mininum jitter offset multiplier for X and Y directions</param>
|
||||
/// <param name="maxJitter">Maximum jitter offset multiplier for X and Y directions</param>
|
||||
/// <param name="breathing">Maximum breathing offset, this is in the Y direction</param>
|
||||
/// <param name="startOffset">Starting offset because we don't have adjustment layers</param>
|
||||
/// <param name="lastJitter">Last jitter so we don't jitter to the same quadrant</param>
|
||||
/// <returns></returns>
|
||||
public Animation GetFatigueAnimation(SpriteComponent sprite,
|
||||
float frequency,
|
||||
int jitters,
|
||||
Vector2 minJitter,
|
||||
Vector2 maxJitter,
|
||||
float breathing,
|
||||
Vector2 startOffset,
|
||||
ref Vector2 lastJitter)
|
||||
{
|
||||
// avoid animations with negative length or infinite length
|
||||
if (frequency <= 0)
|
||||
return new Animation();
|
||||
|
||||
var breaths = new Vector2(0, breathing * 2) / jitters;
|
||||
|
||||
var length = 1 / frequency;
|
||||
var frames = length / jitters;
|
||||
|
||||
var keyFrames = new List<AnimationTrackProperty.KeyFrame> { new(sprite.Offset, 0f) };
|
||||
|
||||
// Spits out a list of keyframes to feed to the AnimationPlayer based on the variables we've inputted
|
||||
for (var i = 1; i <= jitters; i++)
|
||||
{
|
||||
var offset = new Vector2(_random.NextFloat(minJitter.X, maxJitter.X),
|
||||
_random.NextFloat(minJitter.Y, maxJitter.Y));
|
||||
offset.X *= _random.Pick(_sign);
|
||||
offset.Y *= _random.Pick(_sign);
|
||||
|
||||
if (i == 1 && Math.Sign(offset.X) == Math.Sign(lastJitter.X)
|
||||
&& Math.Sign(offset.Y) == Math.Sign(lastJitter.Y))
|
||||
{
|
||||
// If the sign is the same as last time on both axis we flip one randomly
|
||||
// to avoid jitter staying in one quadrant too much.
|
||||
if (_random.Prob(0.5f))
|
||||
offset.X *= -1;
|
||||
else
|
||||
offset.Y *= -1;
|
||||
}
|
||||
|
||||
lastJitter = offset;
|
||||
|
||||
// For the first half of the jitter, we vertically displace the sprite upwards to simulate breathing in
|
||||
if (i <= jitters / 2)
|
||||
{
|
||||
keyFrames.Add(new AnimationTrackProperty.KeyFrame(startOffset + breaths * i + offset, frames));
|
||||
}
|
||||
// For the next quarter we displace the sprite down, to about 12.5% breathing offset below our starting position
|
||||
// Simulates breathing out
|
||||
else if (i < jitters * 3 / 4)
|
||||
{
|
||||
keyFrames.Add(
|
||||
new AnimationTrackProperty.KeyFrame(startOffset + breaths * ( jitters - i * 1.5f ) + offset, frames));
|
||||
}
|
||||
// Return to our starting position for breathing, jitter reaches its final position
|
||||
else
|
||||
{
|
||||
keyFrames.Add(
|
||||
new AnimationTrackProperty.KeyFrame(startOffset + breaths * ( i - jitters ) + offset, frames));
|
||||
}
|
||||
}
|
||||
|
||||
return new Animation
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(length),
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackComponentProperty
|
||||
{
|
||||
// Heavy Breathing
|
||||
ComponentType = typeof(SpriteComponent),
|
||||
Property = nameof(SpriteComponent.Offset),
|
||||
InterpolationMode = AnimationInterpolationMode.Cubic,
|
||||
KeyFrames = keyFrames,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum StunVisualLayers : byte
|
||||
{
|
||||
StamCrit,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.GameStates;
|
||||
@@ -75,4 +76,82 @@ public sealed partial class StaminaComponent : Component
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<FixedPoint2, float> StunModifierThresholds = new() { {0, 1f }, { 60, 0.7f }, { 80, 0.5f } };
|
||||
|
||||
#region Animation Data
|
||||
|
||||
/// <summary>
|
||||
/// Threshold at which low stamina animations begin playing. This should be set to a value that means something.
|
||||
/// At 50, it is aligned so when you hit 60 stun the entity will be breathing once per second (well above hyperventilation).
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float AnimationThreshold = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum y vector displacement for breathing at AnimationThreshold
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float BreathingAmplitudeMin = 0.04f;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum y vector amount we add to the BreathingAmplitudeMin
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float BreathingAmplitudeMod = 0.04f;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum vector displacement for jittering at AnimationThreshold
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float JitterAmplitudeMin;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum vector amount we add to the JitterAmplitudeMin
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float JitterAmplitudeMod = 0.04f;
|
||||
|
||||
/// <summary>
|
||||
/// Min multipliers for JitterAmplitude in the X and Y directions, animation randomly chooses between these min and max multipliers
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Vector2 JitterMin = Vector2.Create(0.5f, 0.125f);
|
||||
|
||||
/// <summary>
|
||||
/// Max multipliers for JitterAmplitude in the X and Y directions, animation randomly chooses between these min and max multipliers
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Vector2 JitterMax = Vector2.Create(1f, 0.25f);
|
||||
|
||||
/// <summary>
|
||||
/// Minimum total animations per second
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float FrequencyMin = 0.25f;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum amount we add to the Frequency min just before crit
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float FrequencyMod = 1.75f;
|
||||
|
||||
/// <summary>
|
||||
/// Jitter keyframes per animation
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Jitters = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Vector of the last Jitter so we can make sure we don't jitter in the same quadrant twice in a row.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Vector2 LastJitter;
|
||||
|
||||
/// <summary>
|
||||
/// The offset that an entity had before jittering started,
|
||||
/// so that we can reset it properly.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Vector2 StartOffset = Vector2.Zero;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -20,26 +20,27 @@ using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Damage.Systems;
|
||||
|
||||
public abstract partial class SharedStaminaSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] protected readonly IGameTiming Timing = default!;
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly AlertsSystem _alerts = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
|
||||
[Dependency] private readonly SharedStunSystem _stunSystem = default!;
|
||||
[Dependency] protected readonly SharedStunSystem StunSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
|
||||
/// <summary>
|
||||
/// How much of a buffer is there between the stun duration and when stuns can be re-applied.
|
||||
/// </summary>
|
||||
private static readonly TimeSpan StamCritBufferTime = TimeSpan.FromSeconds(3f);
|
||||
protected static readonly TimeSpan StamCritBufferTime = TimeSpan.FromSeconds(3f);
|
||||
|
||||
public float UniversalStaminaDamageModifier { get; private set; } = 1f;
|
||||
|
||||
@@ -66,31 +67,31 @@ public abstract partial class SharedStaminaSystem : EntitySystem
|
||||
Subs.CVar(_config, CCVars.PlaytestStaminaDamageModifier, value => UniversalStaminaDamageModifier = value, true);
|
||||
}
|
||||
|
||||
private void OnStamHandleState(EntityUid uid, StaminaComponent component, ref AfterAutoHandleStateEvent args)
|
||||
protected virtual void OnStamHandleState(Entity<StaminaComponent> entity, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (component.Critical)
|
||||
EnterStamCrit(uid, component);
|
||||
if (entity.Comp.Critical)
|
||||
EnterStamCrit(entity);
|
||||
else
|
||||
{
|
||||
if (component.StaminaDamage > 0f)
|
||||
EnsureComp<ActiveStaminaComponent>(uid);
|
||||
if (entity.Comp.StaminaDamage > 0f)
|
||||
EnsureComp<ActiveStaminaComponent>(entity);
|
||||
|
||||
ExitStamCrit(uid, component);
|
||||
ExitStamCrit(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, StaminaComponent component, ComponentShutdown args)
|
||||
protected virtual void OnShutdown(Entity<StaminaComponent> entity, ref ComponentShutdown args)
|
||||
{
|
||||
if (MetaData(uid).EntityLifeStage < EntityLifeStage.Terminating)
|
||||
if (MetaData(entity).EntityLifeStage < EntityLifeStage.Terminating)
|
||||
{
|
||||
RemCompDeferred<ActiveStaminaComponent>(uid);
|
||||
RemCompDeferred<ActiveStaminaComponent>(entity);
|
||||
}
|
||||
_alerts.ClearAlert(uid, component.StaminaAlert);
|
||||
_alerts.ClearAlert(entity, entity.Comp.StaminaAlert);
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, StaminaComponent component, ComponentStartup args)
|
||||
private void OnStartup(Entity<StaminaComponent> entity, ref ComponentStartup args)
|
||||
{
|
||||
SetStaminaAlert(uid, component);
|
||||
UpdateStaminaVisuals(entity);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
@@ -99,23 +100,23 @@ public abstract partial class SharedStaminaSystem : EntitySystem
|
||||
if (!Resolve(uid, ref component))
|
||||
return 0f;
|
||||
|
||||
var curTime = _timing.CurTime;
|
||||
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));
|
||||
}
|
||||
|
||||
private void OnRejuvenate(EntityUid uid, StaminaComponent component, RejuvenateEvent args)
|
||||
private void OnRejuvenate(Entity<StaminaComponent> entity, ref RejuvenateEvent args)
|
||||
{
|
||||
if (component.StaminaDamage >= component.CritThreshold)
|
||||
if (entity.Comp.StaminaDamage >= entity.Comp.CritThreshold)
|
||||
{
|
||||
ExitStamCrit(uid, component);
|
||||
ExitStamCrit(entity, entity.Comp);
|
||||
}
|
||||
|
||||
component.StaminaDamage = 0;
|
||||
AdjustSlowdown(uid);
|
||||
RemComp<ActiveStaminaComponent>(uid);
|
||||
SetStaminaAlert(uid, component);
|
||||
Dirty(uid, component);
|
||||
entity.Comp.StaminaDamage = 0;
|
||||
AdjustSlowdown(entity.Owner);
|
||||
RemComp<ActiveStaminaComponent>(entity);
|
||||
UpdateStaminaVisuals(entity);
|
||||
Dirty(entity);
|
||||
}
|
||||
|
||||
private void OnDisarmed(EntityUid uid, StaminaComponent component, ref DisarmedEvent args)
|
||||
@@ -212,6 +213,15 @@ public abstract partial class SharedStaminaSystem : EntitySystem
|
||||
TakeStaminaDamage(target, component.Damage, source: uid, sound: component.Sound);
|
||||
}
|
||||
|
||||
private void UpdateStaminaVisuals(Entity<StaminaComponent> entity)
|
||||
{
|
||||
SetStaminaAlert(entity, entity.Comp);
|
||||
SetStaminaAnimation(entity);
|
||||
}
|
||||
|
||||
// Here so server can properly tell all clients in PVS range to start the animation
|
||||
protected virtual void SetStaminaAnimation(Entity<StaminaComponent> entity){}
|
||||
|
||||
private void SetStaminaAlert(EntityUid uid, StaminaComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component, false) || component.Deleted)
|
||||
@@ -268,7 +278,7 @@ public abstract partial class SharedStaminaSystem : EntitySystem
|
||||
// Reset the decay cooldown upon taking damage.
|
||||
if (oldDamage < component.StaminaDamage)
|
||||
{
|
||||
var nextUpdate = _timing.CurTime + TimeSpan.FromSeconds(component.Cooldown);
|
||||
var nextUpdate = Timing.CurTime + TimeSpan.FromSeconds(component.Cooldown);
|
||||
|
||||
if (component.NextUpdate < nextUpdate)
|
||||
component.NextUpdate = nextUpdate;
|
||||
@@ -276,7 +286,7 @@ public abstract partial class SharedStaminaSystem : EntitySystem
|
||||
|
||||
AdjustSlowdown(uid);
|
||||
|
||||
SetStaminaAlert(uid, component);
|
||||
UpdateStaminaVisuals((uid, component));
|
||||
|
||||
// Checking if the stamina damage has decreased to zero after exiting the stamcrit
|
||||
if (component.AfterCritical && oldDamage > component.StaminaDamage && component.StaminaDamage <= 0f)
|
||||
@@ -330,7 +340,7 @@ public abstract partial class SharedStaminaSystem : EntitySystem
|
||||
|
||||
var stamQuery = GetEntityQuery<StaminaComponent>();
|
||||
var query = EntityQueryEnumerator<ActiveStaminaComponent>();
|
||||
var curTime = _timing.CurTime;
|
||||
var curTime = Timing.CurTime;
|
||||
|
||||
while (query.MoveNext(out var uid, out _))
|
||||
{
|
||||
@@ -371,16 +381,14 @@ public abstract partial class SharedStaminaSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
// To make the difference between a stun and a stamcrit clear
|
||||
// TODO: Mask?
|
||||
|
||||
component.Critical = true;
|
||||
component.StaminaDamage = component.CritThreshold;
|
||||
|
||||
_stunSystem.TryParalyze(uid, component.StunTime, true);
|
||||
if (StunSystem.TryParalyze(uid, component.StunTime, true))
|
||||
StunSystem.TrySeeingStars(uid);
|
||||
|
||||
// Give them buffer before being able to be re-stunned
|
||||
component.NextUpdate = _timing.CurTime + component.StunTime + StamCritBufferTime;
|
||||
component.NextUpdate = Timing.CurTime + component.StunTime + StamCritBufferTime;
|
||||
EnsureComp<ActiveStaminaComponent>(uid);
|
||||
Dirty(uid, component);
|
||||
_adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(uid):user} entered stamina crit");
|
||||
@@ -396,9 +404,9 @@ public abstract partial class SharedStaminaSystem : EntitySystem
|
||||
|
||||
component.Critical = false;
|
||||
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);
|
||||
UpdateStaminaVisuals((uid, component));
|
||||
Dirty(uid, component);
|
||||
_adminLogger.Add(LogType.Stamina, LogImpact.Low, $"{ToPrettyString(uid):user} recovered from stamina crit");
|
||||
}
|
||||
@@ -427,6 +435,12 @@ public abstract partial class SharedStaminaSystem : EntitySystem
|
||||
closest = thres.Key;
|
||||
}
|
||||
|
||||
_stunSystem.UpdateStunModifiers(ent, ent.Comp.StunModifierThresholds[closest]);
|
||||
StunSystem.UpdateStunModifiers(ent, ent.Comp.StunModifierThresholds[closest]);
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class StaminaAnimationEvent(NetEntity entity) : EntityEventArgs
|
||||
{
|
||||
public NetEntity Entity = entity;
|
||||
}
|
||||
}
|
||||
|
||||
55
Content.Shared/Stunnable/SharedStunSystem.Visualizer.cs
Normal file
55
Content.Shared/Stunnable/SharedStunSystem.Visualizer.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Mobs;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Stunnable;
|
||||
|
||||
public abstract partial class SharedStunSystem
|
||||
{
|
||||
public void InitializeAppearance()
|
||||
{
|
||||
SubscribeLocalEvent<StunVisualsComponent, MobStateChangedEvent>(OnStunMobStateChanged);
|
||||
SubscribeLocalEvent<StunVisualsComponent, SleepStateChangedEvent>(OnSleepStateChanged);
|
||||
}
|
||||
|
||||
private bool GetStarsData(Entity<StunVisualsComponent, StunnedComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp2, false))
|
||||
return false;
|
||||
|
||||
return Blocker.CanConsciouslyPerformAction(entity);
|
||||
}
|
||||
|
||||
private void OnStunMobStateChanged(Entity<StunVisualsComponent> entity, ref MobStateChangedEvent args)
|
||||
{
|
||||
Appearance.SetData(entity, StunVisuals.SeeingStars, GetStarsData(entity));
|
||||
}
|
||||
|
||||
private void OnSleepStateChanged(Entity<StunVisualsComponent> entity, ref SleepStateChangedEvent args)
|
||||
{
|
||||
Appearance.SetData(entity, StunVisuals.SeeingStars, GetStarsData(entity));
|
||||
}
|
||||
|
||||
public void TrySeeingStars(Entity<AppearanceComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp))
|
||||
return;
|
||||
|
||||
// Here so server can tell the client to do things
|
||||
// Don't dirty the component if we don't need to
|
||||
if (!Appearance.TryGetData<bool>(entity, StunVisuals.SeeingStars, out var stars, entity.Comp) && stars)
|
||||
return;
|
||||
|
||||
if (!Blocker.CanConsciouslyPerformAction(entity))
|
||||
return;
|
||||
|
||||
Appearance.SetData(entity, StunVisuals.SeeingStars, true);
|
||||
Dirty(entity);
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable, Flags]
|
||||
public enum StunVisuals
|
||||
{
|
||||
SeeingStars,
|
||||
}
|
||||
}
|
||||
@@ -20,15 +20,17 @@ using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Stunnable;
|
||||
|
||||
public abstract class SharedStunSystem : EntitySystem
|
||||
public abstract partial class SharedStunSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
|
||||
[Dependency] protected readonly ActionBlockerSystem Blocker = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
|
||||
[Dependency] private readonly StandingStateSystem _standingState = default!;
|
||||
[Dependency] private readonly StatusEffectsSystem _statusEffect = default!;
|
||||
@@ -49,7 +51,7 @@ public abstract class SharedStunSystem : EntitySystem
|
||||
SubscribeLocalEvent<SlowedDownComponent, ComponentShutdown>(OnSlowRemove);
|
||||
|
||||
SubscribeLocalEvent<StunnedComponent, ComponentStartup>(UpdateCanMove);
|
||||
SubscribeLocalEvent<StunnedComponent, ComponentShutdown>(UpdateCanMove);
|
||||
SubscribeLocalEvent<StunnedComponent, ComponentShutdown>(OnStunShutdown);
|
||||
|
||||
SubscribeLocalEvent<StunOnContactComponent, StartCollideEvent>(OnStunOnContactCollide);
|
||||
|
||||
@@ -71,6 +73,9 @@ public abstract class SharedStunSystem : EntitySystem
|
||||
SubscribeLocalEvent<StunnedComponent, IsEquippingAttemptEvent>(OnEquipAttempt);
|
||||
SubscribeLocalEvent<StunnedComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
|
||||
SubscribeLocalEvent<MobStateComponent, MobStateChangedEvent>(OnMobStateChanged);
|
||||
|
||||
// Stun Appearance Data
|
||||
InitializeAppearance();
|
||||
}
|
||||
|
||||
private void OnAttemptInteract(Entity<StunnedComponent> ent, ref InteractionAttemptEvent args)
|
||||
@@ -107,9 +112,16 @@ public abstract class SharedStunSystem : EntitySystem
|
||||
|
||||
}
|
||||
|
||||
private void OnStunShutdown(Entity<StunnedComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
// This exists so the client can end their funny animation if they're playing one.
|
||||
UpdateCanMove(ent, ent.Comp, args);
|
||||
Appearance.RemoveData(ent, StunVisuals.SeeingStars);
|
||||
}
|
||||
|
||||
private void UpdateCanMove(EntityUid uid, StunnedComponent component, EntityEventArgs args)
|
||||
{
|
||||
_blocker.UpdateCanMove(uid);
|
||||
Blocker.UpdateCanMove(uid);
|
||||
}
|
||||
|
||||
private void OnStunOnContactCollide(Entity<StunOnContactComponent> ent, ref StartCollideEvent args)
|
||||
|
||||
16
Content.Shared/Stunnable/StunVisualsComponent.cs
Normal file
16
Content.Shared/Stunnable/StunVisualsComponent.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Stunnable;
|
||||
|
||||
/// <summary>
|
||||
/// This is used to listen to incoming events from the AppearanceSystem
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class StunVisualsComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public ResPath StarsPath = new ("Mobs/Effects/stunned.rsi");
|
||||
|
||||
[DataField]
|
||||
public string State = "stunned";
|
||||
}
|
||||
@@ -3,6 +3,4 @@ using Robust.Shared.GameStates;
|
||||
namespace Content.Shared.Stunnable;
|
||||
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SharedStunSystem))]
|
||||
public sealed partial class StunnedComponent : Component
|
||||
{
|
||||
}
|
||||
public sealed partial class StunnedComponent : Component;
|
||||
|
||||
@@ -278,6 +278,7 @@
|
||||
Asphyxiation: -1.0
|
||||
- type: FireVisuals
|
||||
alternateState: Standing
|
||||
- type: StunVisuals
|
||||
|
||||
- type: entity
|
||||
save: false
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
- type: MovementSpeedModifier
|
||||
- type: RequireProjectileTarget
|
||||
active: False
|
||||
- type: StunVisuals
|
||||
|
||||
- type: entity
|
||||
save: false
|
||||
|
||||
18
Resources/Textures/Mobs/Effects/stunned.rsi/meta.json
Normal file
18
Resources/Textures/Mobs/Effects/stunned.rsi/meta.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": 1,
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Created by Princess Cheeseballs, https://github.com/Princess-Cheeseballs",
|
||||
"states": [
|
||||
{
|
||||
"name": "stunned",
|
||||
"directions": 1,
|
||||
"delays": [
|
||||
[ 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1 ]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Resources/Textures/Mobs/Effects/stunned.rsi/stunned.png
Normal file
BIN
Resources/Textures/Mobs/Effects/stunned.rsi/stunned.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
Reference in New Issue
Block a user