-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);
+ }
}
+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<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,
}
+using System.Numerics;
using Content.Shared.Alert;
using Content.Shared.FixedPoint;
using Robust.Shared.GameStates;
/// </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
}
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;
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]
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)
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)
// 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;
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)
var stamQuery = GetEntityQuery<StaminaComponent>();
var query = EntityQueryEnumerator<ActiveStaminaComponent>();
- var curTime = _timing.CurTime;
+ var curTime = Timing.CurTime;
while (query.MoveNext(out var uid, out _))
{
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");
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");
}
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;
}
}
--- /dev/null
+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,
+ }
+}
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!;
SubscribeLocalEvent<SlowedDownComponent, ComponentShutdown>(OnSlowRemove);
SubscribeLocalEvent<StunnedComponent, ComponentStartup>(UpdateCanMove);
- SubscribeLocalEvent<StunnedComponent, ComponentShutdown>(UpdateCanMove);
+ SubscribeLocalEvent<StunnedComponent, ComponentShutdown>(OnStunShutdown);
SubscribeLocalEvent<StunOnContactComponent, StartCollideEvent>(OnStunOnContactCollide);
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)
}
+ 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)
--- /dev/null
+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";
+}
namespace Content.Shared.Stunnable;
[RegisterComponent, NetworkedComponent, Access(typeof(SharedStunSystem))]
-public sealed partial class StunnedComponent : Component
-{
-}
+public sealed partial class StunnedComponent : Component;
Asphyxiation: -1.0
- type: FireVisuals
alternateState: Standing
+ - type: StunVisuals
- type: entity
save: false
- type: MovementSpeedModifier
- type: RequireProjectileTarget
active: False
+ - type: StunVisuals
- type: entity
save: false
--- /dev/null
+{
+ "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 ]
+ ]
+ }
+ ]
+}