diff --git a/Content.Client/_ST/Interaction/StellarInteractionParticleSystem.cs b/Content.Client/_ST/Interaction/StellarInteractionParticleSystem.cs index 14c4ea73a98..30c43c0be18 100644 --- a/Content.Client/_ST/Interaction/StellarInteractionParticleSystem.cs +++ b/Content.Client/_ST/Interaction/StellarInteractionParticleSystem.cs @@ -20,7 +20,11 @@ public sealed class StellarInteractionParticleSystem : EntitySystem private const string AnimateKey = "particle-animation"; - private static readonly EntProtoId InteractionParticleId = "StellarInteractionParticle"; + private static readonly Dictionary InteractionParticleIds = new () + { + { StellarInteractionParticleType.Use, "StellarInteractionParticleUse" }, + { StellarInteractionParticleType.Pull, "StellarInteractionParticlePull" }, + }; public override void Initialize() { @@ -38,6 +42,11 @@ public sealed class StellarInteractionParticleSystem : EntitySystem if (!Exists(performer) || !Exists(target)) return; + if (ev.Type == StellarInteractionParticleType.Pull) + { + (performer, target) = (target, performer); + } + var performerXform = Transform(performer); var targetXform = Transform(target); if (performerXform.MapID == MapId.Nullspace || targetXform.MapID == MapId.Nullspace) @@ -47,7 +56,7 @@ public sealed class StellarInteractionParticleSystem : EntitySystem return; var performerTargetDelta = targetXform.LocalPosition - performerXform.LocalPosition; - var particle = Spawn(InteractionParticleId, performerXform.Coordinates); + var particle = Spawn(InteractionParticleIds[ev.Type], performerXform.Coordinates); if (used is { } usedEntity && Exists(usedEntity) && TryComp(usedEntity, out var usedSprite)) { @@ -58,10 +67,16 @@ public sealed class StellarInteractionParticleSystem : EntitySystem } var spriteColor = Comp(particle).Color; - _animation.Play(particle, GetAnimation(performerTargetDelta, spriteColor), AnimateKey); + var animation = ev.Type switch + { + StellarInteractionParticleType.Use => GetUseAnimation(performerTargetDelta, spriteColor), + StellarInteractionParticleType.Pull => GetPullAnimation(performerTargetDelta, spriteColor), + _ => throw new ArgumentOutOfRangeException(nameof(ev), $"Interaction particle event has unknown particle type {ev.Type}"), + }; + _animation.Play(particle, animation, AnimateKey); } - private Animation GetAnimation(Vector2 endOffset, Color color) + private Animation GetUseAnimation(Vector2 endOffset, Color color) { var startRotation = _random.NextAngle(Angle.FromDegrees(-40), Angle.FromDegrees(40)); var endRotation = Angle.Zero; @@ -126,4 +141,44 @@ public sealed class StellarInteractionParticleSystem : EntitySystem }, }; } + + private Animation GetPullAnimation(Vector2 endOffset, Color color) + { + var rotationLength = TimeSpan.FromMilliseconds(8f * (1000f / 12f)); + + var startOffset = new Vector2(); + var offsetLength = TimeSpan.FromMilliseconds(4f * (1000f / 12f)); + + var endColor = color.WithAlpha(0f); + + return new Animation + { + Length = rotationLength, + + AnimationTracks = + { + new AnimationTrackComponentProperty() + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Offset), + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(startOffset, 0f), + new AnimationTrackProperty.KeyFrame(endOffset, (float)rotationLength.TotalSeconds, Easings.InOutCirc), + }, + }, + new AnimationTrackComponentProperty() + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Color), + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(color, 0f), + new AnimationTrackProperty.KeyFrame(color, (float)offsetLength.TotalSeconds), + new AnimationTrackProperty.KeyFrame(endColor, (float)rotationLength.TotalSeconds, Easings.InOutCirc), + }, + }, + }, + }; + } } diff --git a/Content.Shared/Interaction/InteractUsing.cs b/Content.Shared/Interaction/InteractUsing.cs index 7d3d4e978ed..56fb9348e7d 100644 --- a/Content.Shared/Interaction/InteractUsing.cs +++ b/Content.Shared/Interaction/InteractUsing.cs @@ -30,6 +30,11 @@ public sealed class InteractUsingEvent : HandledEntityEventArgs /// public EntityCoordinates ClickLocation { get; } + /// + /// Stellar - set to false if this interaction shouldn't have an interaction particle + /// + public bool InteractionParticle = true; + public InteractUsingEvent(EntityUid user, EntityUid used, EntityUid target, EntityCoordinates clickLocation) { // Interact using should not have the same used and target. diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index a6472a6e8f7..6cf30638872 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -31,6 +31,7 @@ using Content.Shared.UserInterface; using Content.Shared.Verbs; using Content.Shared.Wall; using Content.Shared._Goobstation.DoAfter; // Goobstation +using Content.Shared.Inventory.VirtualItem; // Stellar - interaction particles using JetBrains.Annotations; using Robust.Shared.Containers; using Robust.Shared.Input; @@ -1073,8 +1074,8 @@ namespace Content.Shared.Interaction var userInteractUsingEvent = new UserInteractUsingEvent(user, used, target, clickLocation); RaiseLocalEvent(user, userInteractUsingEvent, true); - DoContactInteraction(user, used, null, true, interactUsingEvent); // Stellar - interaction particles - DoContactInteraction(user, target, used, true, interactUsingEvent); // Stellar - interaction particles + DoContactInteraction(user, used, null, true, interactUsingEvent, interactionParticles: interactUsingEvent.InteractionParticle); // Stellar - interaction particles + DoContactInteraction(user, target, used, true, interactUsingEvent, interactionParticles: interactUsingEvent.InteractionParticle); // Stellar - interaction particles // Contact interactions are currently only used for forensics, so we don't raise used -> target if (interactUsingEvent.Handled || userInteractUsingEvent.Handled) return true; @@ -1437,7 +1438,8 @@ namespace Content.Shared.Interaction /// Whether this interaction is predicted. is assumed to be the client entity. /// Optional handleable entity event to check. /// Whether to spawn interaction particles on this contact. - public void DoContactInteraction(EntityUid uidA, EntityUid? uidB, EntityUid? used, bool predicted, HandledEntityEventArgs? args = null, bool interactionParticles = true) // Stellar/ES - interaction particles + /// The type of interaction particle to spawn for this event. + public void DoContactInteraction(EntityUid uidA, EntityUid? uidB, EntityUid? used, bool predicted, HandledEntityEventArgs? args = null, bool interactionParticles = true, StellarInteractionParticleType interactionParticleType = StellarInteractionParticleType.Use) // Stellar/ES - interaction particles { if (uidB == null || args?.Handled == false) return; @@ -1459,7 +1461,7 @@ namespace Content.Shared.Interaction RaiseLocalEvent(uidB.Value, ev); // Begin Stellar/ES Additions - Interaction particles - if (!interactionParticles) + if (!interactionParticles || HasComp(uidB)) return; if (_net.IsServer) @@ -1468,11 +1470,11 @@ namespace Content.Shared.Interaction ? Filter.PvsExcept(uidA, entityManager: EntityManager) : Filter.Pvs(uidA, entityManager: EntityManager); - RaiseNetworkEvent(new StellarInteractionParticleEvent(GetNetEntity(uidA), GetNetEntity(used), GetNetEntity(uidB.Value), false), filter); + RaiseNetworkEvent(new StellarInteractionParticleEvent(GetNetEntity(uidA), GetNetEntity(used), GetNetEntity(uidB.Value), false, interactionParticleType), filter); } else if (_gameTiming.IsFirstTimePredicted) { - var evt = new StellarInteractionParticleEvent(GetNetEntity(uidA), GetNetEntity(used), GetNetEntity(uidB.Value), true); + var evt = new StellarInteractionParticleEvent(GetNetEntity(uidA), GetNetEntity(used), GetNetEntity(uidB.Value), true, interactionParticleType); RaiseLocalEvent(evt); } // End Stellar/ES Additions - Interaction particles diff --git a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs index c129ea19c71..490050553d3 100644 --- a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs +++ b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs @@ -1,3 +1,4 @@ +using Content.Shared._ST.Interaction; // Stellar - interaction particles using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.Alert; @@ -545,7 +546,7 @@ public sealed class PullingSystem : EntitySystem // Pulling confirmed - _interaction.DoContactInteraction(pullableUid, pullerUid, null, true); // Stellar - Interaction particles + _interaction.DoContactInteraction(pullerUid, pullableUid,null, true, interactionParticleType: StellarInteractionParticleType.Pull); // Stellar - Interaction particles // Use net entity so it's consistent across client and server. pullableComp.PullJointId = $"pull-joint-{GetNetEntity(pullableUid)}"; diff --git a/Content.Shared/Sound/SharedEmitSoundSystem.cs b/Content.Shared/Sound/SharedEmitSoundSystem.cs index ed1837e3f40..890bd05da54 100644 --- a/Content.Shared/Sound/SharedEmitSoundSystem.cs +++ b/Content.Shared/Sound/SharedEmitSoundSystem.cs @@ -62,6 +62,7 @@ public abstract class SharedEmitSoundSystem : EntitySystem if (_whitelistSystem.IsWhitelistFail(component.Blacklist, args.User)) { TryEmitSound(uid, component, args.User); + args.InteractionParticle = true; // Stellar - interaction particles } } diff --git a/Content.Shared/Stacks/SharedStackSystem.cs b/Content.Shared/Stacks/SharedStackSystem.cs index 990ddbd2f6e..3840638866f 100644 --- a/Content.Shared/Stacks/SharedStackSystem.cs +++ b/Content.Shared/Stacks/SharedStackSystem.cs @@ -106,6 +106,7 @@ public abstract partial class SharedStackSystem : EntitySystem var localRotation = Transform(args.Used).LocalRotation; _storage.PlayPickupAnimation(args.Used, popupPos, userCoords, localRotation, args.User); + args.InteractionParticle = false; // Stellar } /// diff --git a/Content.Shared/UserInterface/ActivatableUIEvents.cs b/Content.Shared/UserInterface/ActivatableUIEvents.cs index 81aac543343..e4da2e91329 100644 --- a/Content.Shared/UserInterface/ActivatableUIEvents.cs +++ b/Content.Shared/UserInterface/ActivatableUIEvents.cs @@ -52,6 +52,11 @@ public sealed class AfterActivatableUIOpenEvent(EntityUid user) : EntityEventArg /// The player that opened the UI. /// public readonly EntityUid User = user; + + /// + /// Stellar - if an interaction particle should be played for this event. + /// + public bool InteractionParticle; } /// diff --git a/Content.Shared/UserInterface/ActivatableUISystem.cs b/Content.Shared/UserInterface/ActivatableUISystem.cs index 8b2299ccfc4..0e844c33c7d 100644 --- a/Content.Shared/UserInterface/ActivatableUISystem.cs +++ b/Content.Shared/UserInterface/ActivatableUISystem.cs @@ -21,6 +21,7 @@ public sealed partial class ActivatableUISystem : EntitySystem [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; public override void Initialize() { @@ -72,7 +73,7 @@ public sealed partial class ActivatableUISystem : EntitySystem args.Verbs.Add(new ActivationVerb { - Act = () => InteractUI(args.User, uid, component), + Act = () => InteractUI(args.User, (uid, component)), // Stellar - interaction particles Text = Loc.GetString(component.VerbText), // TODO VERB ICON find a better icon Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")), @@ -86,7 +87,7 @@ public sealed partial class ActivatableUISystem : EntitySystem args.Verbs.Add(new Verb { - Act = () => InteractUI(args.User, uid, component), + Act = () => InteractUI(args.User, (uid, component)), // Stellar - interaction particles Text = Loc.GetString(component.VerbText), // TODO VERB ICON find a better icon Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")), @@ -133,7 +134,8 @@ public sealed partial class ActivatableUISystem : EntitySystem if (component.RequiredItems != null) return; - args.Handled = InteractUI(args.User, uid, component); + var interactionParticle = false; // Stellar - interaction particles + args.Handled = InteractUI(args.User, uid, component, ref interactionParticle); // Stellar - interaction particles } private void OnActivate(EntityUid uid, ActivatableUIComponent component, ActivateInWorldEvent args) @@ -147,7 +149,7 @@ public sealed partial class ActivatableUISystem : EntitySystem if (component.RequiredItems != null) return; - args.Handled = InteractUI(args.User, uid, component); + args.Handled = InteractUI(args.User, uid, component, ref args.InteractionParticle); // Stellar - interaction particles } private void OnInteractUsing(EntityUid uid, ActivatableUIComponent component, InteractUsingEvent args) @@ -164,7 +166,7 @@ public sealed partial class ActivatableUISystem : EntitySystem if (_whitelistSystem.IsWhitelistFail(component.RequiredItems, args.Used)) return; - args.Handled = InteractUI(args.User, uid, component); + args.Handled = InteractUI(args.User, uid, component, ref args.InteractionParticle); // Stellar - interaction particles } private void OnUIClose(EntityUid uid, ActivatableUIComponent component, BoundUIClosedEvent args) @@ -180,13 +182,23 @@ public sealed partial class ActivatableUISystem : EntitySystem SetCurrentSingleUser(uid, null, component); } - private bool InteractUI(EntityUid user, EntityUid uiEntity, ActivatableUIComponent aui) + // Begin Stellar - interaction particles + private void InteractUI(EntityUid user, Entity ui) + { + var interactionParticle = false; + InteractUI(user, ui, ui, ref interactionParticle); + _interaction.DoContactInteraction(user, ui, null, true, interactionParticles: interactionParticle); + } + // End Stellar - interaction particles + + private bool InteractUI(EntityUid user, EntityUid uiEntity, ActivatableUIComponent aui, ref bool interactionParticle) // Stellar - interaction particles { if (aui.Key == null || !_uiSystem.HasUi(uiEntity, aui.Key)) return false; if (_uiSystem.IsUiOpen(uiEntity, aui.Key, user)) { + interactionParticle = false; // Stellar - interaction particles _uiSystem.CloseUi(uiEntity, aui.Key, user); return true; } @@ -243,6 +255,8 @@ public sealed partial class ActivatableUISystem : EntitySystem var aae = new AfterActivatableUIOpenEvent(user); RaiseLocalEvent(uiEntity, aae); + interactionParticle = aae.InteractionParticle; // Stellar + return true; } diff --git a/Content.Shared/_ST/Interaction/InteractionParticleEvent.cs b/Content.Shared/_ST/Interaction/InteractionParticleEvent.cs index 48537057d8f..26277e8be48 100644 --- a/Content.Shared/_ST/Interaction/InteractionParticleEvent.cs +++ b/Content.Shared/_ST/Interaction/InteractionParticleEvent.cs @@ -10,7 +10,7 @@ namespace Content.Shared._ST.Interaction; /// Data for interaction particles /// [Serializable, NetSerializable] -public sealed class StellarInteractionParticleEvent(NetEntity performer, NetEntity? used, NetEntity target, bool isClientEvent) : EntityEventArgs +public sealed class StellarInteractionParticleEvent(NetEntity performer, NetEntity? used, NetEntity target, bool isClientEvent, StellarInteractionParticleType type) : EntityEventArgs { public NetEntity Performer = performer; @@ -22,4 +22,13 @@ public sealed class StellarInteractionParticleEvent(NetEntity performer, NetEnti /// Workaround for event subscription not working w/ the session overload /// public bool IsClientEvent = isClientEvent; + + public StellarInteractionParticleType Type = type; +} + +[Serializable, NetSerializable] +public enum StellarInteractionParticleType +{ + Use, + Pull, } diff --git a/Resources/Prototypes/_ST/Entities/Effects/interaction.yml b/Resources/Prototypes/_ST/Entities/Effects/interaction.yml index 655901e4134..05fff494dae 100644 --- a/Resources/Prototypes/_ST/Entities/Effects/interaction.yml +++ b/Resources/Prototypes/_ST/Entities/Effects/interaction.yml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT - type: entity - id: StellarInteractionParticle + id: StellarInteractionParticleUse categories: [ HideSpawnMenu ] components: - type: TimedDespawn @@ -16,3 +16,12 @@ - type: Tag tags: - HideContextMenu + +- type: entity + parent: StellarInteractionParticleUse + id: StellarInteractionParticlePull + categories: [ HideSpawnMenu ] + components: + - type: Sprite + state: pulling + loop: false diff --git a/Resources/Textures/_ST/Effects/interaction.rsi/meta.json b/Resources/Textures/_ST/Effects/interaction.rsi/meta.json index 4c0ece28ead..b982addb444 100644 --- a/Resources/Textures/_ST/Effects/interaction.rsi/meta.json +++ b/Resources/Textures/_ST/Effects/interaction.rsi/meta.json @@ -9,6 +9,21 @@ "states": [ { "name": "hand" + }, + { + "name": "pulling", + "delays": [ + [ + 0.083, + 0.083, + 0.083, + 0.083, + 0.083, + 0.083, + 0.083, + 0.083 + ] + ] } ] } diff --git a/Resources/Textures/_ST/Effects/interaction.rsi/pulling.png b/Resources/Textures/_ST/Effects/interaction.rsi/pulling.png new file mode 100644 index 00000000000..050e835dc34 Binary files /dev/null and b/Resources/Textures/_ST/Effects/interaction.rsi/pulling.png differ