using Content.Client.Items; using Content.Client.Items.UI; using Content.Client.Message; using Content.Client.Power.Visualizers; using Content.Client.Stylesheets; using Content.Shared.Atmos.Components; using Content.Shared.Disposal.Components; using Content.Shared.Input; using Content.Shared.Inventory; using Content.Shared.SubFloor; using Robust.Client.Animations; using Robust.Client.GameObjects; using Robust.Client.Input; using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Timing; namespace Content.Client.SubFloor; public sealed partial class TrayScannerSystem : SharedTrayScannerSystem { [Dependency] private IGameTiming _timing = default!; [Dependency] private IPlayerManager _player = default!; [Dependency] private AnimationPlayerSystem _animation = default!; [Dependency] private EntityLookupSystem _lookup = default!; [Dependency] private InventorySystem _inventory = default!; [Dependency] private SharedAppearanceSystem _appearance = default!; [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private SpriteSystem _sprite = default!; [Dependency] private TrayScanRevealSystem _trayScanReveal = default!; [Dependency] private IInputManager _inputManager = default!; [Dependency] private EntityQuery _trayScannerQuery = default!; [Dependency] private EntityQuery _subFloorHideQuery = default!; private const string TRayAnimationKey = "trays"; private const double AnimationLength = 0.3; public const LookupFlags Flags = LookupFlags.Static | LookupFlags.Sundries | LookupFlags.Approximate; public override void Initialize() { base.Initialize(); Subs.ItemStatus(OnCollectItemStatus); } public override void Update(float frameTime) { base.Update(frameTime); if (!_timing.IsFirstTimePredicted) return; // TODO: Multiple viewports or w/e var player = _player.LocalEntity; if (!TryComp(player, out TransformComponent? playerXform)) return; var playerPos = _transform.GetWorldPosition(playerXform); var playerMap = playerXform.MapID; var range = 0f; var mode = TrayScannerMode.All; HashSet> inRange; // TODO: Should probably sub to player attached changes / inventory changes but inventory's // API is extremely skrungly. If this ever shows up on dottrace ping me and laugh. var canSee = false; foreach (var item in _inventory.GetHandOrInventoryEntities(player.Value, SlotFlags.POCKET)) { if (!_trayScannerQuery.TryGetComponent(item, out var scanner) || !scanner.Enabled) continue; range = MathF.Max(scanner.Range, range); mode = scanner.Mode; canSee = true; break; } inRange = new HashSet>(); if (canSee) { var entitiesInRange = new HashSet>(); _lookup.GetEntitiesInRange(playerMap, playerPos, range, entitiesInRange, flags: Flags); foreach (var (uid, comp) in entitiesInRange) { if (!MatchesMode(uid, mode)) continue; inRange.Add((uid, comp)); if (comp.IsUnderCover || _trayScanReveal.IsUnderRevealingEntity(uid)) EnsureComp(uid); } } var revealedQuery = AllEntityQuery(); while (revealedQuery.MoveNext(out var uid, out _, out var sprite)) { // Revealing // Add buffer range to avoid flickers. if (_subFloorHideQuery.TryGetComponent(uid, out var subfloor) && inRange.Contains((uid, subfloor))) { // Due to the fact client is predicting this server states will reset it constantly if ((!_appearance.TryGetData(uid, SubFloorVisuals.ScannerRevealed, out bool value) || !value) && sprite.Color.A > SubfloorRevealAlpha) { _sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(0f)); } SetRevealed(uid, true); if (sprite.Color.A >= SubfloorRevealAlpha || _animation.HasRunningAnimation(uid, TRayAnimationKey)) continue; _animation.Play(uid, new Animation() { Length = TimeSpan.FromSeconds(AnimationLength), AnimationTracks = { new AnimationTrackComponentProperty() { ComponentType = typeof(SpriteComponent), Property = nameof(SpriteComponent.Color), KeyFrames = { new AnimationTrackProperty.KeyFrame(sprite.Color.WithAlpha(0f), 0f), new AnimationTrackProperty.KeyFrame(sprite.Color.WithAlpha(SubfloorRevealAlpha), (float) AnimationLength) } } } }, TRayAnimationKey); } // Hiding else { // Hidden completely so unreveal and reset the alpha. if (sprite.Color.A <= 0f) { SetRevealed(uid, false); RemCompDeferred(uid); _sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(1f)); continue; } SetRevealed(uid, true); if (_animation.HasRunningAnimation(uid, TRayAnimationKey)) continue; _animation.Play(uid, new Animation() { Length = TimeSpan.FromSeconds(AnimationLength), AnimationTracks = { new AnimationTrackComponentProperty() { ComponentType = typeof(SpriteComponent), Property = nameof(SpriteComponent.Color), KeyFrames = { new AnimationTrackProperty.KeyFrame(sprite.Color, 0f), new AnimationTrackProperty.KeyFrame(sprite.Color.WithAlpha(0f), (float) AnimationLength) } } } }, TRayAnimationKey); } } } private void SetRevealed(EntityUid uid, bool value) { _appearance.SetData(uid, SubFloorVisuals.ScannerRevealed, value); } private bool MatchesMode(EntityUid uid, TrayScannerMode mode) { return mode switch { TrayScannerMode.All => true, TrayScannerMode.Wiring => HasComp(uid), // TODO: proper comp query after disposals refactor TrayScannerMode.Piping => HasComp(uid) || _appearance.TryGetData(uid, DisposalTubeVisuals.VisualState, out _), _ => false, }; } #region UI private Control OnCollectItemStatus(Entity entity) { _inputManager.TryGetKeyBinding((ContentKeyFunctions.AltUseItemInHand), out var binding); return new StatusControl(entity, binding?.GetKeyString() ?? ""); } private sealed class StatusControl : PollingItemStatusControl { private readonly RichTextLabel _label; private readonly TrayScannerComponent _scanner; private readonly string _keyBindingName; public StatusControl(TrayScannerComponent scanner, string keyBindingName) { _scanner = scanner; _keyBindingName = keyBindingName; _label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } }; AddChild(_label); } protected override TRayData PollData() { return new TRayData(_scanner.Enabled, _scanner.Mode); } protected override void Update(in TRayData data) { if (!data.Enabled) { _label.SetMarkup(string.Empty); return; } var modeLocString = data.Mode switch { TrayScannerMode.All => "tray-scanner-examine-mode-all", TrayScannerMode.Wiring => "tray-scanner-examine-mode-wiring", TrayScannerMode.Piping => "tray-scanner-examine-mode-piping", _ => "", }; _label.SetMarkup(Robust.Shared.Localization.Loc.GetString("tray-scanner-item-status-label", ("mode", Robust.Shared.Localization.Loc.GetString(modeLocString)), ("keybinding", _keyBindingName))); } public readonly record struct TRayData(bool Enabled, TrayScannerMode Mode); } #endregion }