using System.Numerics; using Content.Client.Message; using Content.Client.Paper; using Content.Shared._Amour.Tips; using Content.Shared.CCVar; using Content.Shared.Movement.Components; using Robust.Client.Audio; 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.Shared.Configuration; using Robust.Shared.Console; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Client._Amour.Tips; public sealed class ClippyUIController : 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 ClippyEvent? _currentMessage; private readonly Queue _queuedMessages = new(); public override void Initialize() { base.Initialize(); _conHost.RegisterCommand("local_clippy", ClippyCommand); UIManager.OnScreenChanged += OnScreenChanged; } private void ClippyCommand(IConsoleShell shell, string argStr, string[] args) { if (args.Length == 0) { shell.WriteLine("usage: clippy [entity prototype] [speak time] [animate time] [waddle]"); return; } var ev = new ClippyEvent(args[0]); string proto; if (args.Length > 1) { ev.Proto = args[1]; if (!_protoMan.HasIndex(ev.Proto)) { shell.WriteError($"Unknown prototype: {ev.Proto}"); return; } } if (args.Length > 2) ev.SpeakTime = float.Parse(args[2]); if (args.Length > 3) ev.SlideTime = float.Parse(args[3]); if (args.Length > 4) ev.WaddleInterval = float.Parse(args[4]); AddMessage(ev); } public void AddMessage(ClippyEvent ev) { _queuedMessages.Enqueue(ev); } public override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); var screen = UIManager.ActiveScreen; if (screen == null) { _queuedMessages.Clear(); return; } var clippy = screen.GetOrAddWidget(); _secondsUntilNextState -= args.DeltaSeconds; if (_secondsUntilNextState <= 0) NextState(clippy); else { var pos = UpdatePosition(clippy, screen.Size, args); ; LayoutContainer.SetPosition(clippy, pos); } } private Vector2 UpdatePosition(ClippyUI clippy, Vector2 screenSize, FrameEventArgs args) { if (_currentMessage == null) return default; var slideTime = _currentMessage.SlideTime; var offset = clippy.State switch { ClippyUI.ClippyState.Hidden => 0, ClippyUI.ClippyState.Revealing => Math.Clamp(1 - _secondsUntilNextState / slideTime, 0, 1), ClippyUI.ClippyState.Hiding => Math.Clamp(_secondsUntilNextState / slideTime, 0, 1), _ => 1, }; var waddle = _currentMessage.WaddleInterval; if (_currentMessage == null || waddle <= 0 || clippy.State == ClippyUI.ClippyState.Hidden || clippy.State == ClippyUI.ClippyState.Speaking || !EntityManager.TryGetComponent(_entity, out SpriteComponent? sprite)) { return new Vector2(screenSize.X - offset * (clippy.DesiredSize.X + Padding), (screenSize.Y - clippy.DesiredSize.Y) / 2); } var numSteps = (int) Math.Ceiling(slideTime / waddle); var curStep = (int) Math.Floor(numSteps * offset); var stepSize = (clippy.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 - clippy.DesiredSize.Y) / 2); } private void NextState(ClippyUI clippy) { SpriteComponent? sprite; switch (clippy.State) { case ClippyUI.ClippyState.Hidden: if (!_queuedMessages.TryDequeue(out var next)) return; if (next.Proto != null) { _entity = EntityManager.SpawnEntity(next.Proto, MapCoordinates.Nullspace); clippy.ModifyLayers = false; } else { _entity = EntityManager.SpawnEntity(_cfg.GetCVar(CCVars.ClippyEntity), MapCoordinates.Nullspace); clippy.ModifyLayers = true; } if (!EntityManager.TryGetComponent(_entity, out sprite)) return; clippy.InitLabel(EntityManager.GetComponentOrNull(_entity), _resCache); 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); } var scale = sprite.Scale; if (clippy.ModifyLayers) { sprite.Scale = Vector2.One; } else { sprite.Scale = new Vector2(3, 3); } clippy.Entity.SetEntity(_entity); clippy.Entity.Scale = scale; _currentMessage = next; _secondsUntilNextState = next.SlideTime; clippy.State = ClippyUI.ClippyState.Revealing; _previousStep = 0; if (clippy.ModifyLayers) { sprite.LayerSetAnimationTime("revealing", 0); sprite.LayerSetVisible("revealing", true); sprite.LayerSetVisible("speaking", false); sprite.LayerSetVisible("hiding", false); } sprite.Rotation = 0; clippy.Label.SetMarkup(_currentMessage.Msg); clippy.LabelPanel.Visible = false; clippy.Label.Visible = false; clippy.Visible = true; sprite.Visible = true; break; case ClippyUI.ClippyState.Revealing: clippy.State = ClippyUI.ClippyState.Speaking; if (!EntityManager.TryGetComponent(_entity, out sprite)) return; sprite.Rotation = 0; _previousStep = 0; if (clippy.ModifyLayers) { sprite.LayerSetAnimationTime("speaking", 0); sprite.LayerSetVisible("revealing", false); sprite.LayerSetVisible("speaking", true); sprite.LayerSetVisible("hiding", false); } clippy.Label.Visible = true; clippy.LabelPanel.Visible = true; clippy.InvalidateArrange(); clippy.InvalidateMeasure(); if (_currentMessage != null) _secondsUntilNextState = _currentMessage.SpeakTime; break; case ClippyUI.ClippyState.Speaking: clippy.State = ClippyUI.ClippyState.Hiding; if (!EntityManager.TryGetComponent(_entity, out sprite)) return; if (clippy.ModifyLayers) { sprite.LayerSetAnimationTime("hiding", 0); sprite.LayerSetVisible("revealing", false); sprite.LayerSetVisible("speaking", false); sprite.LayerSetVisible("hiding", true); } clippy.LabelPanel.Visible = false; if (_currentMessage != null) _secondsUntilNextState = _currentMessage.SlideTime; break; default: // закончил прятаться EntityManager.DeleteEntity(_entity); _entity = default; clippy.Visible = false; _currentMessage = null; _secondsUntilNextState = 0; clippy.State = ClippyUI.ClippyState.Hidden; break; } } private void OnScreenChanged((UIScreen? Old, UIScreen? New) ev) { ev.Old?.RemoveWidget(); _currentMessage = null; EntityManager.DeleteEntity(_entity); } }