From 162af7add53a0ee8b65374718cdb887815bf35b6 Mon Sep 17 00:00:00 2001 From: mirrorcult Date: Sat, 19 Feb 2022 12:16:44 -0700 Subject: [PATCH] Ghost orbiting (#6784) --- Content.Client/Orbit/OrbitVisualsSystem.cs | 151 ++++++++++++++++++ Content.Server/Entry/IgnoredComponents.cs | 2 +- .../Components/OrbitVisualsComponent.cs | 28 ++++ Content.Shared/Follower/FollowerSystem.cs | 86 ++++++++++ .../Entities/Mobs/Player/observer.yml | 1 + 5 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 Content.Client/Orbit/OrbitVisualsSystem.cs create mode 100644 Content.Shared/Follower/Components/OrbitVisualsComponent.cs diff --git a/Content.Client/Orbit/OrbitVisualsSystem.cs b/Content.Client/Orbit/OrbitVisualsSystem.cs new file mode 100644 index 0000000000..dad8cf2c96 --- /dev/null +++ b/Content.Client/Orbit/OrbitVisualsSystem.cs @@ -0,0 +1,151 @@ +using Content.Shared.Follower; +using Content.Shared.Follower.Components; +using Robust.Client.Animations; +using Robust.Client.GameObjects; +using Robust.Shared.Animations; +using Robust.Shared.Random; + +namespace Content.Client.Orbit; + +public sealed class OrbitVisualsSystem : VisualizerSystem +{ + [Dependency] private readonly IRobustRandom _robustRandom = default!; + + private readonly string _orbitAnimationKey = "orbiting"; + private readonly string _orbitStopKey = "orbiting_stop"; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnAnimationCompleted); + } + + private void OnComponentInit(EntityUid uid, OrbitVisualsComponent component, ComponentInit args) + { + component.OrbitDistance = + _robustRandom.NextFloat(0.75f * component.OrbitDistance, 1.25f * component.OrbitDistance); + + component.OrbitLength = _robustRandom.NextFloat(0.5f * component.OrbitLength, 1.5f * component.OrbitLength); + } + + public override void FrameUpdate(float frameTime) + { + base.FrameUpdate(frameTime); + + foreach (var (orbit, sprite) in EntityManager.EntityQuery()) + { + var angle = new Angle(Math.PI * 2 * orbit.Orbit); + var vec = angle.RotateVec(new Vector2(orbit.OrbitDistance, 0)); + + sprite.Rotation = angle; + sprite.Offset = vec; + } + } + + protected override void OnAppearanceChange(EntityUid uid, OrbitVisualsComponent component, ref AppearanceChangeEvent args) + { + if (!args.Component.TryGetData(OrbitingVisuals.IsOrbiting, out var orbiting)) + return; + + if (!TryComp(uid, out var sprite)) + return; + + var animationPlayer = EntityManager.EnsureComponent(uid); + + if (orbiting) + { + if (animationPlayer.HasRunningAnimation(_orbitAnimationKey)) + return; + + if (animationPlayer.HasRunningAnimation(_orbitStopKey)) + { + animationPlayer.Stop(_orbitStopKey); + } + + animationPlayer.Play(GetOrbitAnimation(component), _orbitAnimationKey); + } + else + { + RemComp(uid); + if (animationPlayer.HasRunningAnimation(_orbitAnimationKey)) + { + animationPlayer.Stop(_orbitAnimationKey); + } + + if (!animationPlayer.HasRunningAnimation(_orbitStopKey)) + { + animationPlayer.Play(GetStopAnimation(component, sprite), _orbitStopKey); + } + } + } + + private void OnAnimationCompleted(EntityUid uid, OrbitVisualsComponent component, AnimationCompletedEvent args) + { + if (args.Key == _orbitAnimationKey) + { + if(EntityManager.TryGetComponent(uid, out AnimationPlayerComponent? animationPlayer)) + animationPlayer.Play(GetOrbitAnimation(component), _orbitAnimationKey); + } + } + + private Animation GetOrbitAnimation(OrbitVisualsComponent component) + { + var length = component.OrbitLength; + + return new Animation() + { + Length = TimeSpan.FromSeconds(length), + AnimationTracks = + { + new AnimationTrackComponentProperty() + { + ComponentType = typeof(OrbitVisualsComponent), + Property = nameof(OrbitVisualsComponent.Orbit), + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(0.0f, 0f), + new AnimationTrackProperty.KeyFrame(1.0f, length), + }, + InterpolationMode = AnimationInterpolationMode.Linear + } + } + }; + } + + private Animation GetStopAnimation(OrbitVisualsComponent component, ISpriteComponent sprite) + { + var length = component.OrbitStopLength; + + 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(Vector2.Zero, length), + }, + InterpolationMode = AnimationInterpolationMode.Linear + }, + new AnimationTrackComponentProperty() + { + ComponentType = typeof(ISpriteComponent), + Property = nameof(ISpriteComponent.Rotation), + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(sprite.Rotation.Reduced(), 0f), + new AnimationTrackProperty.KeyFrame(Angle.Zero, length), + }, + InterpolationMode = AnimationInterpolationMode.Linear + } + } + }; + } +} diff --git a/Content.Server/Entry/IgnoredComponents.cs b/Content.Server/Entry/IgnoredComponents.cs index 9478f8565c..1b5280cc6c 100644 --- a/Content.Server/Entry/IgnoredComponents.cs +++ b/Content.Server/Entry/IgnoredComponents.cs @@ -16,7 +16,7 @@ namespace Content.Server.Entry "Icon", "ClientEntitySpawner", "CharacterInfo", - "ItemCabinetVisuals" + "ItemCabinetVisuals", }; } } diff --git a/Content.Shared/Follower/Components/OrbitVisualsComponent.cs b/Content.Shared/Follower/Components/OrbitVisualsComponent.cs new file mode 100644 index 0000000000..7611c93a56 --- /dev/null +++ b/Content.Shared/Follower/Components/OrbitVisualsComponent.cs @@ -0,0 +1,28 @@ +using Robust.Shared.Animations; + +namespace Content.Shared.Follower.Components; + +[RegisterComponent] +public sealed class OrbitVisualsComponent : Component +{ + /// + /// How long should the orbit animation last in seconds, before being randomized? + /// + public float OrbitLength = 2.0f; + + /// + /// How far away from the entity should the orbit be, before being randomized? + /// + public float OrbitDistance = 1.0f; + + /// + /// How long should the orbit stop animation last in seconds? + /// + public float OrbitStopLength = 1.0f; + + /// + /// How far along in the orbit, from 0 to 1, is this entity? + /// + [Animatable] + public float Orbit { get; set; } = 0.0f; +} diff --git a/Content.Shared/Follower/FollowerSystem.cs b/Content.Shared/Follower/FollowerSystem.cs index 9083a25e60..c7ae5d4e1d 100644 --- a/Content.Shared/Follower/FollowerSystem.cs +++ b/Content.Shared/Follower/FollowerSystem.cs @@ -6,6 +6,7 @@ using Robust.Shared.GameObjects; using Robust.Shared.Localization; using Content.Shared.Follower.Components; using Robust.Shared.Maths; +using Robust.Shared.Serialization; namespace Content.Shared.Follower; @@ -71,6 +72,18 @@ public sealed class FollowerSystem : EntitySystem var xform = Transform(follower); xform.AttachParent(entity); xform.LocalPosition = Vector2.Zero; + + if (TryComp(follower, out var appearance)) + { + EnsureComp(follower); + appearance.SetData(OrbitingVisuals.IsOrbiting, true); + } + + var followerEv = new StartedFollowingEntityEvent(entity, follower); + var entityEv = new EntityStartedFollowingEvent(entity, follower); + + RaiseLocalEvent(follower, followerEv); + RaiseLocalEvent(entity, entityEv, false); } /// @@ -89,7 +102,21 @@ public sealed class FollowerSystem : EntitySystem if (followed.Following.Count == 0) RemComp(target); RemComp(uid); + Transform(uid).AttachToGridOrMap(); + + if (TryComp(uid, out var appearance)) + { + // We don't remove OrbitVisuals here since the OrbitVisualsSystem will handle that itself + // during the OnChangeData, which is deferred.. + appearance.SetData(OrbitingVisuals.IsOrbiting, false); + } + + var uidEv = new StoppedFollowingEntityEvent(target, uid); + var targetEv = new EntityStoppedFollowingEvent(target, uid); + + RaiseLocalEvent(uid, uidEv); + RaiseLocalEvent(target, targetEv, false); } /// @@ -107,3 +134,62 @@ public sealed class FollowerSystem : EntitySystem } } } + +public abstract class FollowEvent : EntityEventArgs +{ + public EntityUid Following; + public EntityUid Follower; + + protected FollowEvent(EntityUid following, EntityUid follower) + { + Following = following; + Follower = follower; + } +} + +/// +/// Raised on an entity when it start following another entity. +/// +public sealed class StartedFollowingEntityEvent : FollowEvent +{ + public StartedFollowingEntityEvent(EntityUid following, EntityUid follower) : base(following, follower) + { + } +} + +/// +/// Raised on an entity when it stops following another entity. +/// +public sealed class StoppedFollowingEntityEvent : FollowEvent +{ + public StoppedFollowingEntityEvent(EntityUid following, EntityUid follower) : base(following, follower) + { + } +} + +/// +/// Raised on an entity when it start following another entity. +/// +public sealed class EntityStartedFollowingEvent : FollowEvent +{ + public EntityStartedFollowingEvent(EntityUid following, EntityUid follower) : base(following, follower) + { + } +} + +/// +/// Raised on an entity when it starts being followed by another entity. +/// +public sealed class EntityStoppedFollowingEvent : FollowEvent +{ + public EntityStoppedFollowingEvent(EntityUid following, EntityUid follower) : base(following, follower) + { + } +} + +[Serializable, NetSerializable] +public enum OrbitingVisuals : byte +{ + IsOrbiting +} + diff --git a/Resources/Prototypes/Entities/Mobs/Player/observer.yml b/Resources/Prototypes/Entities/Mobs/Player/observer.yml index 7a7023154b..c5a90eadd8 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/observer.yml @@ -21,6 +21,7 @@ mask: - GhostImpassable - type: PlayerInputMover + - type: Appearance - type: Eye drawFov: false - type: Input