using Content.Client.Gameplay; using System.Numerics; using Content.Client.Message; using Content.Client.Paper; using Content.Shared.CCVar; using Content.Shared.Movement.Components; using Content.Shared.Tips; using Robust.Client.GameObjects; using Robust.Client.ResourceManagement; using Robust.Client.State; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controllers; using Robust.Client.UserInterface.Controls; using Robust.Client.Audio; using Robust.Shared.Configuration; using Robust.Shared.Console; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using static Content.Client.Tips.TippyUI; namespace Content.Client.Tips; public sealed class TippyUIController : UIController { [Dependency] private readonly IStateManager _state = default!; [Dependency] private readonly IConsoleHost _conHost = default!; [Dependency] private readonly IPrototypeManager _protoMan = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IResourceCache _resCache = default!; [UISystemDependency] private readonly AudioSystem _audio = default!; [UISystemDependency] private readonly EntityManager _entSys = default!; public const float Padding = 50; public static Angle WaddleRotation = Angle.FromDegrees(10); private EntityUid _entity; private float _secondsUntilNextState; private int _previousStep = 0; private TippyEvent? _currentMessage; private readonly Queue _queuedMessages = new(); public override void Initialize() { base.Initialize(); UIManager.OnScreenChanged += OnScreenChanged; } public void AddMessage(TippyEvent ev) { _queuedMessages.Enqueue(ev); } public override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); var screen = UIManager.ActiveScreen; if (screen == null) { _queuedMessages.Clear(); return; } var tippy = screen.GetOrAddWidget(); _secondsUntilNextState -= args.DeltaSeconds; if (_secondsUntilNextState <= 0) NextState(tippy); else { var pos = UpdatePosition(tippy, screen.Size, args); ; LayoutContainer.SetPosition(tippy, pos); } } private Vector2 UpdatePosition(TippyUI tippy, Vector2 screenSize, FrameEventArgs args) { if (_currentMessage == null) return default; var slideTime = _currentMessage.SlideTime; var offset = tippy.State switch { TippyState.Hidden => 0, TippyState.Revealing => Math.Clamp(1 - _secondsUntilNextState / slideTime, 0, 1), TippyState.Hiding => Math.Clamp(_secondsUntilNextState / slideTime, 0, 1), _ => 1, }; var waddle = _currentMessage.WaddleInterval; if (_currentMessage == null || waddle <= 0 || tippy.State == TippyState.Hidden || tippy.State == TippyState.Speaking || !EntityManager.TryGetComponent(_entity, out SpriteComponent? sprite)) { return new Vector2(screenSize.X - offset * (tippy.DesiredSize.X + Padding), (screenSize.Y - tippy.DesiredSize.Y) / 2); } var numSteps = (int) Math.Ceiling(slideTime / waddle); var curStep = (int) Math.Floor(numSteps * offset); var stepSize = (tippy.DesiredSize.X + Padding) / numSteps; if (curStep != _previousStep) { _previousStep = curStep; sprite.Rotation = sprite.Rotation > 0 ? -WaddleRotation : WaddleRotation; if (EntityManager.TryGetComponent(_entity, out FootstepModifierComponent? step)) { var audioParams = step.FootstepSoundCollection.Params .AddVolume(-7f) .WithVariation(0.1f); _audio.PlayGlobal(step.FootstepSoundCollection, EntityUid.Invalid, audioParams); } } return new Vector2(screenSize.X - stepSize * curStep, (screenSize.Y - tippy.DesiredSize.Y) / 2); } private void NextState(TippyUI tippy) { SpriteComponent? sprite; switch (tippy.State) { case TippyState.Hidden: if (!_queuedMessages.TryDequeue(out var next)) return; if (next.Proto != null) { _entity = EntityManager.SpawnEntity(next.Proto, MapCoordinates.Nullspace); tippy.ModifyLayers = false; } else { _entity = EntityManager.SpawnEntity(_cfg.GetCVar(CCVars.TippyEntity), MapCoordinates.Nullspace); tippy.ModifyLayers = true; } if (!EntityManager.TryGetComponent(_entity, out sprite)) return; if (!EntityManager.HasComponent(_entity)) { var paper = EntityManager.AddComponent(_entity); paper.BackgroundImagePath = "/Textures/Interface/Paper/paper_background_default.svg.96dpi.png"; paper.BackgroundPatchMargin = new(16f, 16f, 16f, 16f); paper.BackgroundModulate = new(255, 255, 204); paper.FontAccentColor = new(0, 0, 0); } tippy.InitLabel(EntityManager.GetComponentOrNull(_entity), _resCache); var scale = sprite.Scale; if (tippy.ModifyLayers) { sprite.Scale = Vector2.One; } else { sprite.Scale = new Vector2(3, 3); } tippy.Entity.SetEntity(_entity); tippy.Entity.Scale = scale; _currentMessage = next; _secondsUntilNextState = next.SlideTime; tippy.State = TippyState.Revealing; _previousStep = 0; if (tippy.ModifyLayers) { sprite.LayerSetAnimationTime("revealing", 0); sprite.LayerSetVisible("revealing", true); sprite.LayerSetVisible("speaking", false); sprite.LayerSetVisible("hiding", false); } sprite.Rotation = 0; tippy.Label.SetMarkup(_currentMessage.Msg); tippy.Label.Visible = false; tippy.LabelPanel.Visible = false; tippy.Visible = true; sprite.Visible = true; break; case TippyState.Revealing: tippy.State = TippyState.Speaking; if (!EntityManager.TryGetComponent(_entity, out sprite)) return; sprite.Rotation = 0; _previousStep = 0; if (tippy.ModifyLayers) { sprite.LayerSetAnimationTime("speaking", 0); sprite.LayerSetVisible("revealing", false); sprite.LayerSetVisible("speaking", true); sprite.LayerSetVisible("hiding", false); } tippy.Label.Visible = true; tippy.LabelPanel.Visible = true; tippy.InvalidateArrange(); tippy.InvalidateMeasure(); if (_currentMessage != null) _secondsUntilNextState = _currentMessage.SpeakTime; break; case TippyState.Speaking: tippy.State = TippyState.Hiding; if (!EntityManager.TryGetComponent(_entity, out sprite)) return; if (tippy.ModifyLayers) { sprite.LayerSetAnimationTime("hiding", 0); sprite.LayerSetVisible("revealing", false); sprite.LayerSetVisible("speaking", false); sprite.LayerSetVisible("hiding", true); } tippy.LabelPanel.Visible = false; if (_currentMessage != null) _secondsUntilNextState = _currentMessage.SlideTime; break; default: // finished hiding EntityManager.DeleteEntity(_entity); _entity = default; tippy.Visible = false; _currentMessage = null; _secondsUntilNextState = 0; tippy.State = TippyState.Hidden; break; } } private void OnScreenChanged((UIScreen? Old, UIScreen? New) ev) { ev.Old?.RemoveWidget(); _currentMessage = null; EntityManager.DeleteEntity(_entity); } }