diff --git a/Content.Client/Trigger/TimerTriggerVisualizer.cs b/Content.Client/Trigger/TimerTriggerVisualizer.cs deleted file mode 100644 index ff3cd9593d..0000000000 --- a/Content.Client/Trigger/TimerTriggerVisualizer.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Content.Shared.Trigger; -using JetBrains.Annotations; -using Robust.Client.Animations; -using Robust.Client.GameObjects; -using Robust.Shared.Audio; -using Robust.Shared.Serialization; - -namespace Content.Client.Trigger -{ - [UsedImplicitly] - public sealed class TimerTriggerVisualizer : AppearanceVisualizer, ISerializationHooks - { - private const string AnimationKey = "priming_animation"; - - [DataField("countdown_sound")] - private SoundSpecifier? _countdownSound; - - private Animation PrimingAnimation = default!; - - void ISerializationHooks.AfterDeserialization() - { - PrimingAnimation = new Animation { Length = TimeSpan.MaxValue }; - { - var flick = new AnimationTrackSpriteFlick(); - PrimingAnimation.AnimationTracks.Add(flick); - flick.LayerKey = TriggerVisualLayers.Base; - flick.KeyFrames.Add(new AnimationTrackSpriteFlick.KeyFrame("primed", 0f)); - - if (_countdownSound != null) - { - var sound = new AnimationTrackPlaySound(); - PrimingAnimation.AnimationTracks.Add(sound); - sound.KeyFrames.Add(new AnimationTrackPlaySound.KeyFrame(_countdownSound.GetSound(), 0)); - } - } - } - - [Obsolete("Subscribe to your component being initialised instead.")] - public override void InitializeEntity(EntityUid entity) - { - IoCManager.Resolve().EnsureComponent(entity); - } - - [Obsolete("Subscribe to AppearanceChangeEvent instead.")] - public override void OnChangeData(AppearanceComponent component) - { - var entMan = IoCManager.Resolve(); - var sprite = entMan.GetComponent(component.Owner); - var animPlayer = entMan.GetComponent(component.Owner); - if (!component.TryGetData(TriggerVisuals.VisualState, out TriggerVisualState state)) - { - state = TriggerVisualState.Unprimed; - } - - switch (state) - { - case TriggerVisualState.Primed: - if (!animPlayer.HasRunningAnimation(AnimationKey)) - { - animPlayer.Play(PrimingAnimation, AnimationKey); - } - break; - case TriggerVisualState.Unprimed: - sprite.LayerSetState(0, "icon"); - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - } - public enum TriggerVisualLayers : byte - { - Base - } -} diff --git a/Content.Client/Trigger/TimerTriggerVisualizerComponent.cs b/Content.Client/Trigger/TimerTriggerVisualizerComponent.cs new file mode 100644 index 0000000000..ab8da38abf --- /dev/null +++ b/Content.Client/Trigger/TimerTriggerVisualizerComponent.cs @@ -0,0 +1,43 @@ +using Robust.Client.Animations; +using Robust.Shared.Audio; + +namespace Content.Client.Trigger; + +[RegisterComponent] +[Access(typeof(TimerTriggerVisualizerSystem))] +public sealed class TimerTriggerVisualsComponent : Component +{ + /// + /// The key used to index the priming animation. + /// + [ViewVariables] + public const string AnimationKey = "priming_animation"; + + /// + /// The RSI state used while the device has not been primed. + /// + [DataField("unprimedSprite")] + [ViewVariables(VVAccess.ReadWrite)] + public string UnprimedSprite = "icon"; + + /// + /// The RSI state used when the device is primed. + /// Not VVWrite-able because it's only used at component init to construct the priming animation. + /// + [DataField("primingSprite")] + public string PrimingSprite = "primed"; + + /// + /// The sound played when the device is primed. + /// Not VVWrite-able because it's only used at component init to construct the priming animation. + /// + [DataField("primingSound")] + public SoundSpecifier? PrimingSound; + + /// + /// The actual priming animation. + /// Constructed at component init from the sprite and sound. + /// + [ViewVariables(VVAccess.ReadWrite)] + public Animation PrimingAnimation = default!; +} diff --git a/Content.Client/Trigger/TimerTriggerVisualizerSystem.cs b/Content.Client/Trigger/TimerTriggerVisualizerSystem.cs new file mode 100644 index 0000000000..1c91565858 --- /dev/null +++ b/Content.Client/Trigger/TimerTriggerVisualizerSystem.cs @@ -0,0 +1,67 @@ +using Content.Shared.Trigger; +using Robust.Client.Animations; +using Robust.Client.GameObjects; +using Robust.Shared.GameObjects; + +namespace Content.Client.Trigger; + +public sealed class TimerTriggerVisualizerSystem : VisualizerSystem +{ + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnComponentInit); + } + + private void OnComponentInit(EntityUid uid, TimerTriggerVisualsComponent comp, ComponentInit args) + { + comp.PrimingAnimation = new Animation { + Length = TimeSpan.MaxValue, + AnimationTracks = { + new AnimationTrackSpriteFlick() { + LayerKey = TriggerVisualLayers.Base, + KeyFrames = { new AnimationTrackSpriteFlick.KeyFrame(comp.PrimingSprite, 0f) } + } + }, + }; + + if (comp.PrimingSound != null) + { + comp.PrimingAnimation.AnimationTracks.Add( + new AnimationTrackPlaySound() { + KeyFrames = { new AnimationTrackPlaySound.KeyFrame(_audioSystem.GetSound(comp.PrimingSound), 0) } + } + ); + } + } + + protected override void OnAppearanceChange(EntityUid uid, TimerTriggerVisualsComponent comp, ref AppearanceChangeEvent args) + { + if (args.Sprite == null + || !TryComp(uid, out var animPlayer)) + return; + + if (!AppearanceSystem.TryGetData(uid, TriggerVisuals.VisualState, out var state, args.Component)) + state = TriggerVisualState.Unprimed; + + switch (state) + { + case TriggerVisualState.Primed: + if (!AnimationSystem.HasRunningAnimation(uid, animPlayer, TimerTriggerVisualsComponent.AnimationKey)) + AnimationSystem.Play(uid, animPlayer, comp.PrimingAnimation, TimerTriggerVisualsComponent.AnimationKey); + break; + case TriggerVisualState.Unprimed: + args.Sprite.LayerSetState(TriggerVisualLayers.Base, comp.UnprimedSprite); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } +} + +public enum TriggerVisualLayers : byte +{ + Base +} diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml index 4d8235ccba..b1bf565bb2 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml @@ -29,8 +29,8 @@ - !type:DoActsBehavior acts: ["Destruction"] - type: Appearance - visuals: - - type: TimerTriggerVisualizer + - type: AnimationPlayer + - type: TimerTriggerVisuals - type: entity name: explosive grenade @@ -70,10 +70,9 @@ - type: SpawnOnTrigger proto: GrenadeFlashEffect - type: Appearance - visuals: - - type: TimerTriggerVisualizer - countdown_sound: - path: /Audio/Effects/countdown.ogg + - type: TimerTriggerVisuals + primingSound: + path: /Audio/Effects/countdown.ogg - type: entity id: GrenadeFlashEffect @@ -106,10 +105,9 @@ intensitySlope: 30 #Will destroy the tile under it reliably, space 1-2 more to rods. Only does any significant damage in a 5-tile cross. maxIntensity: 60 - type: Appearance - visuals: - - type: TimerTriggerVisualizer - countdown_sound: - path: /Audio/Effects/minibombcountdown.ogg + - type: TimerTriggerVisuals + primingSound: + path: /Audio/Effects/minibombcountdown.ogg - type: entity name: the nuclear option @@ -137,10 +135,9 @@ - !type:DoActsBehavior acts: ["Destruction"] - type: Appearance - visuals: - - type: TimerTriggerVisualizer - countdown_sound: - path: /Audio/Effects/countdown.ogg + - type: TimerTriggerVisuals + primingSound: + path: /Audio/Effects/countdown.ogg - type: entity name: modular grenade @@ -201,7 +198,6 @@ energyConsumption: 50000 - type: DeleteOnTrigger - type: Appearance - visuals: - - type: TimerTriggerVisualizer - countdown_sound: - path: /Audio/Effects/countdown.ogg + - type: TimerTriggerVisuals + primingSound: + path: /Audio/Effects/countdown.ogg