From: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com> Date: Fri, 11 Jul 2025 12:13:11 +0000 (-0700) Subject: Stun and Stamina Visuals (#37196) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=ac895a0db4f9478999940353f5359b976fc3e3f8;p=space-station-14.git 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> --- diff --git a/Content.Client/Damage/Systems/StaminaSystem.cs b/Content.Client/Damage/Systems/StaminaSystem.cs index 69eb461a7d..046c72a8fa 100644 --- a/Content.Client/Damage/Systems/StaminaSystem.cs +++ b/Content.Client/Damage/Systems/StaminaSystem.cs @@ -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(OnAnimationCompleted); + SubscribeLocalEvent(OnActiveStaminaShutdown); + SubscribeLocalEvent(OnMobStateChanged); + } + + protected override void OnStamHandleState(Entity entity, ref AfterAutoHandleStateEvent args) + { + base.OnStamHandleState(entity, ref args); + + TryStartAnimation(entity); + } + + private void OnActiveStaminaShutdown(Entity 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(entity, out var stamina)) + return; + + StopAnimation((entity, stamina)); + } + + protected override void OnShutdown(Entity entity, ref ComponentShutdown args) + { + base.OnShutdown(entity, ref args); + + StopAnimation(entity); + } + + private void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) + { + if (args.NewMobState == MobState.Dead) + StopAnimation(ent); + } + + private void TryStartAnimation(Entity entity) + { + if (!TryComp(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 entity) + { + if(!Resolve(entity, ref entity.Comp2)) + return; + + _animation.Stop(entity.Owner, StaminaAnimationKey); + entity.Comp1.StartOffset = entity.Comp2.Offset; + } + + private void OnAnimationCompleted(Entity entity, ref AnimationCompletedEvent args) + { + if (args.Key != StaminaAnimationKey || !args.Finished || !TryComp(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(entity)) + return; + + PlayAnimation((entity, entity.Comp, sprite)); + } + + private void PlayAnimation(Entity 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); + } } diff --git a/Content.Client/Stunnable/StunSystem.cs b/Content.Client/Stunnable/StunSystem.cs index e519a0d5f0..cb59eda482 100644 --- a/Content.Client/Stunnable/StunSystem.cs +++ b/Content.Client/Stunnable/StunSystem.cs @@ -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 { - 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(OnComponentInit); + SubscribeLocalEvent(OnAppearanceChanged); } + + /// + /// Add stun visual layers + /// + private void OnComponentInit(Entity entity, ref ComponentInit args) + { + if (!TryComp(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 entity, ref AppearanceChangeEvent args) + { + if (args.Sprite != null) + UpdateAppearance((entity, args.Sprite), entity.Comp.State); + } + + private void UpdateAppearance(Entity 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(entity, StunVisuals.SeeingStars, out var stars) && stars; + + _spriteSystem.LayerSetVisible((entity, entity.Comp), index, visible); + _spriteSystem.LayerSetRsiState((entity, entity.Comp), index, state); + } + + /// + /// 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. + /// + /// The spriteComponent we're adjusting the offset of + /// How many times per second does the animation run? + /// How many times should we jitter during the animation? Also determines breathing frequency + /// Mininum jitter offset multiplier for X and Y directions + /// Maximum jitter offset multiplier for X and Y directions + /// Maximum breathing offset, this is in the Y direction + /// Starting offset because we don't have adjustment layers + /// Last jitter so we don't jitter to the same quadrant + /// + 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 { 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, } diff --git a/Content.Shared/Damage/Components/StaminaComponent.cs b/Content.Shared/Damage/Components/StaminaComponent.cs index ec086625ea..3a85f3f8dc 100644 --- a/Content.Shared/Damage/Components/StaminaComponent.cs +++ b/Content.Shared/Damage/Components/StaminaComponent.cs @@ -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 /// [DataField] public Dictionary StunModifierThresholds = new() { {0, 1f }, { 60, 0.7f }, { 80, 0.5f } }; + + #region Animation Data + + /// + /// 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). + /// + [DataField] + public float AnimationThreshold = 50; + + /// + /// Minimum y vector displacement for breathing at AnimationThreshold + /// + [DataField] + public float BreathingAmplitudeMin = 0.04f; + + /// + /// Maximum y vector amount we add to the BreathingAmplitudeMin + /// + [DataField] + public float BreathingAmplitudeMod = 0.04f; + + /// + /// Minimum vector displacement for jittering at AnimationThreshold + /// + [DataField] + public float JitterAmplitudeMin; + + /// + /// Maximum vector amount we add to the JitterAmplitudeMin + /// + [DataField] + public float JitterAmplitudeMod = 0.04f; + + /// + /// Min multipliers for JitterAmplitude in the X and Y directions, animation randomly chooses between these min and max multipliers + /// + [DataField] + public Vector2 JitterMin = Vector2.Create(0.5f, 0.125f); + + /// + /// Max multipliers for JitterAmplitude in the X and Y directions, animation randomly chooses between these min and max multipliers + /// + [DataField] + public Vector2 JitterMax = Vector2.Create(1f, 0.25f); + + /// + /// Minimum total animations per second + /// + [DataField] + public float FrequencyMin = 0.25f; + + /// + /// Maximum amount we add to the Frequency min just before crit + /// + [DataField] + public float FrequencyMod = 1.75f; + + /// + /// Jitter keyframes per animation + /// + [DataField] + public int Jitters = 4; + + /// + /// Vector of the last Jitter so we can make sure we don't jitter in the same quadrant twice in a row. + /// + [DataField] + public Vector2 LastJitter; + + /// + /// The offset that an entity had before jittering started, + /// so that we can reset it properly. + /// + [DataField] + public Vector2 StartOffset = Vector2.Zero; + + #endregion } diff --git a/Content.Shared/Damage/Systems/SharedStaminaSystem.cs b/Content.Shared/Damage/Systems/SharedStaminaSystem.cs index 0da09d0f6d..ae4562e690 100644 --- a/Content.Shared/Damage/Systems/SharedStaminaSystem.cs +++ b/Content.Shared/Damage/Systems/SharedStaminaSystem.cs @@ -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!; /// /// How much of a buffer is there between the stun duration and when stuns can be re-applied. /// - 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 entity, ref AfterAutoHandleStateEvent args) { - if (component.Critical) - EnterStamCrit(uid, component); + if (entity.Comp.Critical) + EnterStamCrit(entity); else { - if (component.StaminaDamage > 0f) - EnsureComp(uid); + if (entity.Comp.StaminaDamage > 0f) + EnsureComp(entity); - ExitStamCrit(uid, component); + ExitStamCrit(entity); } } - private void OnShutdown(EntityUid uid, StaminaComponent component, ComponentShutdown args) + protected virtual void OnShutdown(Entity entity, ref ComponentShutdown args) { - if (MetaData(uid).EntityLifeStage < EntityLifeStage.Terminating) + if (MetaData(entity).EntityLifeStage < EntityLifeStage.Terminating) { - RemCompDeferred(uid); + RemCompDeferred(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 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 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(uid); - SetStaminaAlert(uid, component); - Dirty(uid, component); + entity.Comp.StaminaDamage = 0; + AdjustSlowdown(entity.Owner); + RemComp(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 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 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(); var query = EntityQueryEnumerator(); - 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(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; } } diff --git a/Content.Shared/Stunnable/SharedStunSystem.Visualizer.cs b/Content.Shared/Stunnable/SharedStunSystem.Visualizer.cs new file mode 100644 index 0000000000..4d49621a8a --- /dev/null +++ b/Content.Shared/Stunnable/SharedStunSystem.Visualizer.cs @@ -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(OnStunMobStateChanged); + SubscribeLocalEvent(OnSleepStateChanged); + } + + private bool GetStarsData(Entity entity) + { + if (!Resolve(entity, ref entity.Comp2, false)) + return false; + + return Blocker.CanConsciouslyPerformAction(entity); + } + + private void OnStunMobStateChanged(Entity entity, ref MobStateChangedEvent args) + { + Appearance.SetData(entity, StunVisuals.SeeingStars, GetStarsData(entity)); + } + + private void OnSleepStateChanged(Entity entity, ref SleepStateChangedEvent args) + { + Appearance.SetData(entity, StunVisuals.SeeingStars, GetStarsData(entity)); + } + + public void TrySeeingStars(Entity 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(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, + } +} diff --git a/Content.Shared/Stunnable/SharedStunSystem.cs b/Content.Shared/Stunnable/SharedStunSystem.cs index e18a7f3439..c46dd10ed7 100644 --- a/Content.Shared/Stunnable/SharedStunSystem.cs +++ b/Content.Shared/Stunnable/SharedStunSystem.cs @@ -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(OnSlowRemove); SubscribeLocalEvent(UpdateCanMove); - SubscribeLocalEvent(UpdateCanMove); + SubscribeLocalEvent(OnStunShutdown); SubscribeLocalEvent(OnStunOnContactCollide); @@ -71,6 +73,9 @@ public abstract class SharedStunSystem : EntitySystem SubscribeLocalEvent(OnEquipAttempt); SubscribeLocalEvent(OnUnequipAttempt); SubscribeLocalEvent(OnMobStateChanged); + + // Stun Appearance Data + InitializeAppearance(); } private void OnAttemptInteract(Entity ent, ref InteractionAttemptEvent args) @@ -107,9 +112,16 @@ public abstract class SharedStunSystem : EntitySystem } + private void OnStunShutdown(Entity 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 ent, ref StartCollideEvent args) diff --git a/Content.Shared/Stunnable/StunVisualsComponent.cs b/Content.Shared/Stunnable/StunVisualsComponent.cs new file mode 100644 index 0000000000..6f1070a558 --- /dev/null +++ b/Content.Shared/Stunnable/StunVisualsComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.Utility; + +namespace Content.Shared.Stunnable; + +/// +/// This is used to listen to incoming events from the AppearanceSystem +/// +[RegisterComponent] +public sealed partial class StunVisualsComponent : Component +{ + [DataField] + public ResPath StarsPath = new ("Mobs/Effects/stunned.rsi"); + + [DataField] + public string State = "stunned"; +} diff --git a/Content.Shared/Stunnable/StunnedComponent.cs b/Content.Shared/Stunnable/StunnedComponent.cs index 51a27ac90a..037a3cd3ac 100644 --- a/Content.Shared/Stunnable/StunnedComponent.cs +++ b/Content.Shared/Stunnable/StunnedComponent.cs @@ -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; diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 5bf9a9ede0..f2ac678f52 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -278,6 +278,7 @@ Asphyxiation: -1.0 - type: FireVisuals alternateState: Standing + - type: StunVisuals - type: entity save: false diff --git a/Resources/Prototypes/Entities/Mobs/base.yml b/Resources/Prototypes/Entities/Mobs/base.yml index d7b82b5d95..cfd9c13631 100644 --- a/Resources/Prototypes/Entities/Mobs/base.yml +++ b/Resources/Prototypes/Entities/Mobs/base.yml @@ -44,6 +44,7 @@ - type: MovementSpeedModifier - type: RequireProjectileTarget active: False + - type: StunVisuals - type: entity save: false diff --git a/Resources/Textures/Mobs/Effects/stunned.rsi/meta.json b/Resources/Textures/Mobs/Effects/stunned.rsi/meta.json new file mode 100644 index 0000000000..ea721b943f --- /dev/null +++ b/Resources/Textures/Mobs/Effects/stunned.rsi/meta.json @@ -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 ] + ] + } + ] +} diff --git a/Resources/Textures/Mobs/Effects/stunned.rsi/stunned.png b/Resources/Textures/Mobs/Effects/stunned.rsi/stunned.png new file mode 100644 index 0000000000..7ad891b4f6 Binary files /dev/null and b/Resources/Textures/Mobs/Effects/stunned.rsi/stunned.png differ