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:
Princess Cheeseballs
2025-07-11 05:13:11 -07:00
committed by GitHub
parent a268a4aacc
commit ac895a0db4
12 changed files with 514 additions and 46 deletions

View File

@@ -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);
}
}

View File

@@ -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,
}

View File

@@ -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
}

View File

@@ -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;
}
}

View 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,
}
}

View File

@@ -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)

View 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";
}

View File

@@ -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;

View File

@@ -278,6 +278,7 @@
Asphyxiation: -1.0
- type: FireVisuals
alternateState: Standing
- type: StunVisuals
- type: entity
save: false

View File

@@ -44,6 +44,7 @@
- type: MovementSpeedModifier
- type: RequireProjectileTarget
active: False
- type: StunVisuals
- type: entity
save: false

View 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 ]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB