diff --git a/Content.Client/Clothing/Systems/WaddleClothingSystem.cs b/Content.Client/Clothing/Systems/WaddleClothingSystem.cs new file mode 100644 index 0000000000..b8ac3c207b --- /dev/null +++ b/Content.Client/Clothing/Systems/WaddleClothingSystem.cs @@ -0,0 +1,31 @@ +using Content.Shared.Clothing.Components; +using Content.Shared.Movement.Components; +using Content.Shared.Inventory.Events; + +namespace Content.Client.Clothing.Systems; + +public sealed class WaddleClothingSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGotEquipped); + SubscribeLocalEvent(OnGotUnequipped); + } + + private void OnGotEquipped(EntityUid entity, WaddleWhenWornComponent comp, GotEquippedEvent args) + { + var waddleAnimComp = EnsureComp(args.Equipee); + + waddleAnimComp.AnimationLength = comp.AnimationLength; + waddleAnimComp.HopIntensity = comp.HopIntensity; + waddleAnimComp.RunAnimationLengthMultiplier = comp.RunAnimationLengthMultiplier; + waddleAnimComp.TumbleIntensity = comp.TumbleIntensity; + } + + private void OnGotUnequipped(EntityUid entity, WaddleWhenWornComponent comp, GotUnequippedEvent args) + { + RemComp(args.Equipee); + } +} diff --git a/Content.Client/Movement/Systems/WaddleAnimationSystem.cs b/Content.Client/Movement/Systems/WaddleAnimationSystem.cs new file mode 100644 index 0000000000..83bb697b26 --- /dev/null +++ b/Content.Client/Movement/Systems/WaddleAnimationSystem.cs @@ -0,0 +1,135 @@ +using System.Numerics; +using Content.Client.Gravity; +using Content.Shared.Movement.Components; +using Content.Shared.Movement.Events; +using Robust.Client.Animations; +using Robust.Client.GameObjects; +using Robust.Shared.Animations; +using Robust.Shared.Timing; + +namespace Content.Client.Movement.Systems; + +public sealed class WaddleAnimationSystem : EntitySystem +{ + [Dependency] private readonly AnimationPlayerSystem _animation = default!; + [Dependency] private readonly GravitySystem _gravity = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnMovementInput); + SubscribeLocalEvent(OnStartedWalking); + SubscribeLocalEvent(OnStoppedWalking); + SubscribeLocalEvent(OnAnimationCompleted); + } + + private void OnMovementInput(EntityUid entity, WaddleAnimationComponent component, MoveInputEvent args) + { + // Prediction mitigation. Prediction means that MoveInputEvents are spammed repeatedly, even though you'd assume + // they're once-only for the user actually doing something. As such do nothing if we're just repeating this FoR. + if (!_timing.IsFirstTimePredicted) + { + return; + } + + if (!args.HasDirectionalMovement && component.IsCurrentlyWaddling) + { + component.IsCurrentlyWaddling = false; + + var stopped = new StoppedWaddlingEvent(entity); + + RaiseLocalEvent(entity, ref stopped); + + return; + } + + // Only start waddling if we're not currently AND we're actually moving. + if (component.IsCurrentlyWaddling || !args.HasDirectionalMovement) + return; + + component.IsCurrentlyWaddling = true; + + var started = new StartedWaddlingEvent(entity); + + RaiseLocalEvent(entity, ref started); + } + + private void OnStartedWalking(EntityUid uid, WaddleAnimationComponent component, StartedWaddlingEvent args) + { + if (_animation.HasRunningAnimation(uid, component.KeyName)) + { + return; + } + + if (!TryComp(uid, out var mover)) + { + return; + } + + if (_gravity.IsWeightless(uid)) + { + return; + } + + var tumbleIntensity = component.LastStep ? 360 - component.TumbleIntensity : component.TumbleIntensity; + var len = mover.Sprinting ? component.AnimationLength * component.RunAnimationLengthMultiplier : component.AnimationLength; + + component.LastStep = !component.LastStep; + component.IsCurrentlyWaddling = true; + + var anim = new Animation() + { + Length = TimeSpan.FromSeconds(len), + AnimationTracks = + { + new AnimationTrackComponentProperty() + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Rotation), + InterpolationMode = AnimationInterpolationMode.Linear, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(0), 0), + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(tumbleIntensity), len/2), + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(0), len/2), + } + }, + new AnimationTrackComponentProperty() + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Offset), + InterpolationMode = AnimationInterpolationMode.Linear, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(new Vector2(), 0), + new AnimationTrackProperty.KeyFrame(component.HopIntensity, len/2), + new AnimationTrackProperty.KeyFrame(new Vector2(), len/2), + } + } + } + }; + + _animation.Play(uid, anim, component.KeyName); + } + + private void OnStoppedWalking(EntityUid uid, WaddleAnimationComponent component, StoppedWaddlingEvent args) + { + _animation.Stop(uid, component.KeyName); + + if (!TryComp(uid, out var sprite)) + { + return; + } + + sprite.Offset = new Vector2(); + sprite.Rotation = Angle.FromDegrees(0); + component.IsCurrentlyWaddling = false; + } + + private void OnAnimationCompleted(EntityUid uid, WaddleAnimationComponent component, AnimationCompletedEvent args) + { + var started = new StartedWaddlingEvent(uid); + + RaiseLocalEvent(uid, ref started); + } +} diff --git a/Content.Shared/Clothing/Components/WaddleWhenWornComponent.cs b/Content.Shared/Clothing/Components/WaddleWhenWornComponent.cs new file mode 100644 index 0000000000..5cd7a72457 --- /dev/null +++ b/Content.Shared/Clothing/Components/WaddleWhenWornComponent.cs @@ -0,0 +1,35 @@ +using System.Numerics; + +namespace Content.Shared.Clothing.Components; + +/// +/// Defines something as causing waddling when worn. +/// +[RegisterComponent] +public sealed partial class WaddleWhenWornComponent : Component +{ + /// + /// How high should they hop during the waddle? Higher hop = more energy. + /// + [DataField] + public Vector2 HopIntensity = new(0, 0.25f); + + /// + /// How far should they rock backward and forward during the waddle? + /// Each step will alternate between this being a positive and negative rotation. More rock = more scary. + /// + [DataField] + public float TumbleIntensity = 20.0f; + + /// + /// How long should a complete step take? Less time = more chaos. + /// + [DataField] + public float AnimationLength = 0.66f; + + /// + /// How much shorter should the animation be when running? + /// + [DataField] + public float RunAnimationLengthMultiplier = 0.568f; +} diff --git a/Content.Shared/Movement/Components/WaddleAnimationComponent.cs b/Content.Shared/Movement/Components/WaddleAnimationComponent.cs new file mode 100644 index 0000000000..c43ef3042e --- /dev/null +++ b/Content.Shared/Movement/Components/WaddleAnimationComponent.cs @@ -0,0 +1,72 @@ +using System.Numerics; + +namespace Content.Shared.Movement.Components; + +/// +/// Declares that an entity has started to waddle like a duck/clown. +/// +/// The newly be-waddled. +[ByRefEvent] +public record struct StartedWaddlingEvent(EntityUid Entity) +{ + public EntityUid Entity = Entity; +} + +/// +/// Declares that an entity has stopped waddling like a duck/clown. +/// +/// The former waddle-er. +[ByRefEvent] +public record struct StoppedWaddlingEvent(EntityUid Entity) +{ + public EntityUid Entity = Entity; +} + +/// +/// Defines something as having a waddle animation when it moves. +/// +[RegisterComponent] +public sealed partial class WaddleAnimationComponent : Component +{ + /// + /// What's the name of this animation? Make sure it's unique so it can play along side other animations. + /// This prevents someone accidentally causing two identical waddling effects to play on someone at the same time. + /// + [DataField] + public string KeyName = "Waddle"; + + /// + /// How high should they hop during the waddle? Higher hop = more energy. + /// + [DataField] + public Vector2 HopIntensity = new(0, 0.25f); + + /// + /// How far should they rock backward and forward during the waddle? + /// Each step will alternate between this being a positive and negative rotation. More rock = more scary. + /// + [DataField] + public float TumbleIntensity = 20.0f; + + /// + /// How long should a complete step take? Less time = more chaos. + /// + [DataField] + public float AnimationLength = 0.66f; + + /// + /// How much shorter should the animation be when running? + /// + [DataField] + public float RunAnimationLengthMultiplier = 0.568f; + + /// + /// Stores which step we made last, so if someone cancels out of the animation mid-step then restarts it looks more natural. + /// + public bool LastStep; + + /// + /// Stores if we're currently waddling so we can start/stop as appropriate and can tell other systems our state. + /// + public bool IsCurrentlyWaddling; +} diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml b/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml index 3158e4de53..987eda582e 100644 --- a/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml @@ -15,6 +15,7 @@ parent: [ClothingShoesBaseButcherable, ClothingSlotBase] id: ClothingShoesClownBase components: + - type: WaddleWhenWorn - type: ItemSlots slots: item: