using System.Diagnostics.CodeAnalysis; using Content.Shared.Disposal; using Content.Shared.Disposal.Components; using Content.Shared.DragDrop; using Content.Shared.Emag.Systems; using Robust.Client.GameObjects; using Robust.Client.Animations; using Robust.Client.Graphics; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.GameStates; using Robust.Shared.Physics.Events; using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent; namespace Content.Client.Disposal.Systems; public sealed class DisposalUnitSystem : SharedDisposalUnitSystem { [Dependency] private readonly AppearanceSystem _appearanceSystem = default!; [Dependency] private readonly AnimationPlayerSystem _animationSystem = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; private const string AnimationKey = "disposal_unit_animation"; private const string DefaultFlushState = "disposal-flush"; private const string DefaultChargeState = "disposal-charging"; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnHandleState); SubscribeLocalEvent(OnPreventCollide); SubscribeLocalEvent(OnCanDragDropOn); SubscribeLocalEvent(OnEmagged); SubscribeLocalEvent(OnComponentInit); SubscribeLocalEvent(OnAppearanceChange); } private void OnHandleState(EntityUid uid, DisposalUnitComponent component, ref ComponentHandleState args) { if (args.Current is not DisposalUnitComponentState state) return; component.FlushSound = state.FlushSound; component.State = state.State; component.NextPressurized = state.NextPressurized; component.AutomaticEngageTime = state.AutomaticEngageTime; component.NextFlush = state.NextFlush; component.Powered = state.Powered; component.Engaged = state.Engaged; component.RecentlyEjected.Clear(); component.RecentlyEjected.AddRange(EnsureEntityList(state.RecentlyEjected, uid)); } public override bool HasDisposals(EntityUid? uid) { return HasComp(uid); } public override bool ResolveDisposals(EntityUid uid, [NotNullWhen(true)] ref SharedDisposalUnitComponent? component) { if (component != null) return true; TryComp(uid, out var storage); component = storage; return component != null; } public override void DoInsertDisposalUnit(EntityUid uid, EntityUid toInsert, EntityUid user, SharedDisposalUnitComponent? disposal = null) { return; } private void OnComponentInit(EntityUid uid, SharedDisposalUnitComponent sharedDisposalUnit, ComponentInit args) { if (!TryComp(uid, out var sprite) || !TryComp(uid, out var appearance)) return; UpdateState(uid, sharedDisposalUnit, sprite, appearance); } private void OnAppearanceChange(EntityUid uid, SharedDisposalUnitComponent unit, ref AppearanceChangeEvent args) { if (args.Sprite == null) return; UpdateState(uid, unit, args.Sprite, args.Component); } /// /// Update visuals and tick animation /// private void UpdateState(EntityUid uid, SharedDisposalUnitComponent unit, SpriteComponent sprite, AppearanceComponent appearance) { if (!_appearanceSystem.TryGetData(uid, Visuals.VisualState, out var state, appearance)) return; sprite.LayerSetVisible(DisposalUnitVisualLayers.Unanchored, state == VisualState.UnAnchored); sprite.LayerSetVisible(DisposalUnitVisualLayers.Base, state == VisualState.Anchored); sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayFlush, state is VisualState.OverlayFlushing or VisualState.OverlayCharging); var chargingState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseCharging, out var chargingLayer) ? sprite.LayerGetState(chargingLayer) : new RSI.StateId(DefaultChargeState); // This is a transient state so not too worried about replaying in range. if (state == VisualState.OverlayFlushing) { if (!_animationSystem.HasRunningAnimation(uid, AnimationKey)) { var flushState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.OverlayFlush, out var flushLayer) ? sprite.LayerGetState(flushLayer) : new RSI.StateId(DefaultFlushState); // Setup the flush animation to play var anim = new Animation { Length = unit.FlushDelay, AnimationTracks = { new AnimationTrackSpriteFlick { LayerKey = DisposalUnitVisualLayers.OverlayFlush, KeyFrames = { // Play the flush animation new AnimationTrackSpriteFlick.KeyFrame(flushState, 0), // Return to base state (though, depending on how the unit is // configured we might get an appearance change event telling // us to go to charging state) new AnimationTrackSpriteFlick.KeyFrame(chargingState, (float) unit.FlushDelay.TotalSeconds) } }, } }; if (unit.FlushSound != null) { anim.AnimationTracks.Add( new AnimationTrackPlaySound { KeyFrames = { new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(unit.FlushSound), 0) } }); } _animationSystem.Play(uid, anim, AnimationKey); } } else if (state == VisualState.OverlayCharging) sprite.LayerSetState(DisposalUnitVisualLayers.OverlayFlush, chargingState); else _animationSystem.Stop(uid, AnimationKey); if (!_appearanceSystem.TryGetData(uid, Visuals.Handle, out var handleState, appearance)) handleState = HandleState.Normal; sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayEngaged, handleState != HandleState.Normal); if (!_appearanceSystem.TryGetData(uid, Visuals.Light, out var lightState, appearance)) lightState = LightStates.Off; sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayCharging, (lightState & LightStates.Charging) != 0); sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayReady, (lightState & LightStates.Ready) != 0); sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayFull, (lightState & LightStates.Full) != 0); } } public enum DisposalUnitVisualLayers : byte { Unanchored, Base, BaseCharging, OverlayFlush, OverlayCharging, OverlayReady, OverlayFull, OverlayEngaged }