diff --git a/Content.Client/Jittering/JitteringSystem.cs b/Content.Client/Jittering/JitteringSystem.cs new file mode 100644 index 0000000000..dc9662a37b --- /dev/null +++ b/Content.Client/Jittering/JitteringSystem.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Immutable; +using Content.Shared.Jittering; +using Robust.Client.Animations; +using Robust.Client.GameObjects; +using Robust.Shared.Animations; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Random; + +namespace Content.Client.Jittering +{ + public class JitteringSystem : SharedJitteringSystem + { + [Dependency] private readonly IRobustRandom _random = default!; + + private readonly float[] _sign = { -1, 1 }; + private readonly string _jitterAnimationKey = "jittering"; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnAnimationCompleted); + } + + private void OnStartup(EntityUid uid, JitteringComponent jittering, ComponentStartup args) + { + if (!EntityManager.TryGetComponent(uid, out ISpriteComponent? sprite)) + return; + + var animationPlayer = EntityManager.EnsureComponent(uid); + + animationPlayer.Play(GetAnimation(jittering, sprite), _jitterAnimationKey); + } + + private void OnShutdown(EntityUid uid, JitteringComponent jittering, ComponentShutdown args) + { + if (EntityManager.TryGetComponent(uid, out AnimationPlayerComponent? animationPlayer)) + animationPlayer.Stop(_jitterAnimationKey); + + if (EntityManager.TryGetComponent(uid, out SpriteComponent? sprite)) + sprite.Offset = Vector2.Zero; + } + + private void OnAnimationCompleted(EntityUid uid, JitteringComponent jittering, AnimationCompletedEvent args) + { + if(args.Key != _jitterAnimationKey || jittering.EndTime <= GameTiming.CurTime) + return; + + if(EntityManager.TryGetComponent(uid, out AnimationPlayerComponent? animationPlayer) + && EntityManager.TryGetComponent(uid, out ISpriteComponent? sprite)) + animationPlayer.Play(GetAnimation(jittering, sprite), _jitterAnimationKey); + } + + private Animation GetAnimation(JitteringComponent jittering, ISpriteComponent sprite) + { + var amplitude = MathF.Min(4f, jittering.Amplitude / 100f + 1f) / 10f; + var offset = new Vector2(_random.NextFloat(amplitude/4f, amplitude), + _random.NextFloat(amplitude / 4f, amplitude / 3f)); + + offset.X *= _random.Pick(_sign); + offset.Y *= _random.Pick(_sign); + + if (Math.Sign(offset.X) == Math.Sign(jittering.LastJitter.X) + || Math.Sign(offset.Y) == Math.Sign(jittering.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; + } + + // Animation length shouldn't be too high so we will cap it at 2 seconds... + var length = Math.Min((1f/jittering.Frequency), 2f); + + jittering.LastJitter = offset; + + return new Animation() + { + Length = TimeSpan.FromSeconds(length), + AnimationTracks = + { + new AnimationTrackComponentProperty() + { + ComponentType = typeof(ISpriteComponent), + Property = nameof(ISpriteComponent.Offset), + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(sprite.Offset, 0f), + new AnimationTrackProperty.KeyFrame(offset, length), + } + } + } + }; + } + } +} diff --git a/Content.Server/Administration/Commands/RejuvenateCommand.cs b/Content.Server/Administration/Commands/RejuvenateCommand.cs index 7fbf25380e..b653065895 100644 --- a/Content.Server/Administration/Commands/RejuvenateCommand.cs +++ b/Content.Server/Administration/Commands/RejuvenateCommand.cs @@ -5,6 +5,7 @@ using Content.Server.Nutrition.EntitySystems; using Content.Server.Stunnable.Components; using Content.Shared.Administration; using Content.Shared.Damage; +using Content.Shared.Jittering; using Content.Shared.MobState; using Content.Shared.Nutrition.Components; using Robust.Server.Player; @@ -71,6 +72,11 @@ namespace Content.Server.Administration.Commands { EntitySystem.Get().SetCreamPied(target.Uid, creamPied, false); } + + if (target.HasComponent()) + { + target.RemoveComponent(); + } } } } diff --git a/Content.Server/Jittering/JitteringSystem.cs b/Content.Server/Jittering/JitteringSystem.cs new file mode 100644 index 0000000000..5e1ac722b4 --- /dev/null +++ b/Content.Server/Jittering/JitteringSystem.cs @@ -0,0 +1,9 @@ +using Content.Shared.Jittering; + +namespace Content.Server.Jittering +{ + public class JitteringSystem : SharedJitteringSystem + { + // This entity system only exists on the server so it will be registered, otherwise we can't use SharedJitteringSystem... + } +} diff --git a/Content.Server/Stunnable/StunbatonSystem.cs b/Content.Server/Stunnable/StunbatonSystem.cs index e1b29bee8b..b3df7146a2 100644 --- a/Content.Server/Stunnable/StunbatonSystem.cs +++ b/Content.Server/Stunnable/StunbatonSystem.cs @@ -1,5 +1,7 @@ +using System; using System.Linq; using Content.Server.Items; +using Content.Server.Jittering; using Content.Server.PowerCell.Components; using Content.Server.Stunnable.Components; using Content.Server.Weapon.Melee; @@ -70,6 +72,8 @@ namespace Content.Server.Stunnable if (!Get().CanUse(args.User)) return; + Get().DoJitter(args.User.Uid, TimeSpan.FromMinutes(1), 20f, 8f); + if (comp.Activated) { TurnOff(comp); diff --git a/Content.Shared/Jittering/JitteringComponent.cs b/Content.Shared/Jittering/JitteringComponent.cs new file mode 100644 index 0000000000..ebc87b818b --- /dev/null +++ b/Content.Shared/Jittering/JitteringComponent.cs @@ -0,0 +1,44 @@ +using System; +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.Maths; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Shared.Jittering +{ + [Friend(typeof(SharedJitteringSystem))] + [RegisterComponent, NetworkedComponent] + public class JitteringComponent : Component + { + public override string Name => "Jittering"; + + [ViewVariables(VVAccess.ReadWrite)] + public TimeSpan EndTime { get; set; } + + [ViewVariables(VVAccess.ReadWrite)] + public float Amplitude { get; set; } + + [ViewVariables(VVAccess.ReadWrite)] + public float Frequency { get; set; } + + [ViewVariables(VVAccess.ReadWrite)] + public Vector2 LastJitter { get; set; } + } + + [Serializable, NetSerializable] + public class JitteringComponentState : ComponentState + { + public TimeSpan EndTime { get; } + public float Amplitude { get; } + public float Frequency { get; } + + public JitteringComponentState(TimeSpan endTime, float amplitude, float frequency) + { + EndTime = endTime; + Amplitude = amplitude; + Frequency = frequency; + } + } +} diff --git a/Content.Shared/Jittering/SharedJitteringSystem.cs b/Content.Shared/Jittering/SharedJitteringSystem.cs new file mode 100644 index 0000000000..8e5295c610 --- /dev/null +++ b/Content.Shared/Jittering/SharedJitteringSystem.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.IoC; +using Robust.Shared.Timing; + +namespace Content.Shared.Jittering +{ + /// + /// A system for applying a jitter animation to any entity. + /// + public abstract class SharedJitteringSystem : EntitySystem + { + [Dependency] protected readonly IGameTiming GameTiming = default!; + + public float MaxAmplitude = 300f; + public float MinAmplitude = 1f; + + public float MaxFrequency = 10f; + public float MinFrequency = 1f; + + /// + /// List of jitter components to be removed, cached so we don't allocate it every tick. + /// + private readonly List _removeList = new(); + + public override void Initialize() + { + SubscribeLocalEvent(OnGetState); + SubscribeLocalEvent(OnHandleState); + } + + private void OnGetState(EntityUid uid, JitteringComponent component, ref ComponentGetState args) + { + args.State = new JitteringComponentState(component.EndTime, component.Amplitude, component.Frequency); + } + + private void OnHandleState(EntityUid uid, JitteringComponent component, ref ComponentHandleState args) + { + if (args.Current is not JitteringComponentState jitteringState) + return; + + component.EndTime = jitteringState.EndTime; + component.Amplitude = jitteringState.Amplitude; + component.Frequency = jitteringState.Frequency; + } + + /// + /// Applies a jitter effect to the specified entity. + /// You can apply this to any entity whatsoever, so be careful what you use it on! + /// + /// + /// If the entity is already jittering, the jitter values will be updated but only if they're greater + /// than the current ones and is false. + /// + /// Entity in question. + /// For how much time to apply the effect. + /// Jitteriness of the animation. See and . + /// Frequency for jittering. See and . + /// Whether to change any existing jitter value even if they're greater than the ones we're setting. + public void DoJitter(EntityUid uid, TimeSpan time, float amplitude = 10f, float frequency = 4f, bool forceValueChange = false) + { + var jittering = EntityManager.EnsureComponent(uid); + + var endTime = GameTiming.CurTime + time; + + amplitude = Math.Clamp(amplitude, MinAmplitude, MaxAmplitude); + frequency = Math.Clamp(frequency, MinFrequency, MaxFrequency); + + if (forceValueChange || jittering.EndTime < endTime) + jittering.EndTime = endTime; + + if(forceValueChange || jittering.Amplitude < amplitude) + jittering.Amplitude = amplitude; + + if (forceValueChange || jittering.Frequency < frequency) + jittering.Frequency = frequency; + + jittering.Dirty(); + } + + /// + /// Immediately stops any jitter animation from an entity. + /// + /// The entity in question. + public void StopJitter(EntityUid uid) + { + if (!EntityManager.HasComponent(uid)) + return; + + EntityManager.RemoveComponent(uid); + } + + public override void Update(float frameTime) + { + foreach (var jittering in EntityManager.EntityQuery()) + { + if(jittering.EndTime <= GameTiming.CurTime) + _removeList.Add(jittering); + } + + if (_removeList.Count == 0) + return; + + foreach (var jittering in _removeList) + { + jittering.Owner.RemoveComponent(); + } + + _removeList.Clear(); + } + } +} diff --git a/RobustToolbox b/RobustToolbox index 39f82694bf..22e0fbc6c1 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit 39f82694bfff778c00d80b17e8e4bac793d7b2f3 +Subproject commit 22e0fbc6c10207d43aa4fdb69cf03564548dadc2 diff --git a/SpaceStation14.sln.DotSettings b/SpaceStation14.sln.DotSettings index a175088713..11609fb2da 100644 --- a/SpaceStation14.sln.DotSettings +++ b/SpaceStation14.sln.DotSettings @@ -224,6 +224,8 @@ True True True + True + True True True True