From 373c368b94de808ff8da1f9956b489daecee64d8 Mon Sep 17 00:00:00 2001 From: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> Date: Mon, 29 Apr 2024 06:38:16 +0200 Subject: [PATCH] Tippy, the helpful hint clown! (#26767) * Tippy is BACK * Clean up clippy from aprils fools * Changed names from clippy to tippy, added localization, removed local_clippy command, made it easier to target a specific player * Rename clippy.yml to tippy.yml --------- Co-authored-by: Kara --- Content.Client/Tips/TippyUI.xaml | 11 + Content.Client/Tips/TippyUI.xaml.cs | 54 ++++ Content.Client/Tips/TippyUIController.cs | 244 ++++++++++++++++++ Content.Client/Tips/TipsSystem.cs | 20 ++ Content.Server/Tips/TipsSystem.cs | 115 ++++++++- Content.Shared/CCVar/CCVars.cs | 10 + Content.Shared/Tips/TippyEvent.cs | 19 ++ .../Locale/en-US/commands/tippy-command.ftl | 12 + .../Prototypes/Entities/Debugging/tippy.yml | 29 +++ Resources/Textures/Tips/tippy.rsi/down.png | Bin 0 -> 5325 bytes Resources/Textures/Tips/tippy.rsi/left.png | Bin 0 -> 4000 bytes Resources/Textures/Tips/tippy.rsi/meta.json | 20 ++ Resources/Textures/Tips/tippy.rsi/right.png | Bin 0 -> 4341 bytes Resources/engineCommandPerms.yml | 2 + 14 files changed, 534 insertions(+), 2 deletions(-) create mode 100644 Content.Client/Tips/TippyUI.xaml create mode 100644 Content.Client/Tips/TippyUI.xaml.cs create mode 100644 Content.Client/Tips/TippyUIController.cs create mode 100644 Content.Client/Tips/TipsSystem.cs create mode 100644 Content.Shared/Tips/TippyEvent.cs create mode 100644 Resources/Locale/en-US/commands/tippy-command.ftl create mode 100644 Resources/Prototypes/Entities/Debugging/tippy.yml create mode 100644 Resources/Textures/Tips/tippy.rsi/down.png create mode 100644 Resources/Textures/Tips/tippy.rsi/left.png create mode 100644 Resources/Textures/Tips/tippy.rsi/meta.json create mode 100644 Resources/Textures/Tips/tippy.rsi/right.png diff --git a/Content.Client/Tips/TippyUI.xaml b/Content.Client/Tips/TippyUI.xaml new file mode 100644 index 0000000000..a86e05aadd --- /dev/null +++ b/Content.Client/Tips/TippyUI.xaml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/Content.Client/Tips/TippyUI.xaml.cs b/Content.Client/Tips/TippyUI.xaml.cs new file mode 100644 index 0000000000..de3eaf4f51 --- /dev/null +++ b/Content.Client/Tips/TippyUI.xaml.cs @@ -0,0 +1,54 @@ +using Content.Client.Paper; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Tips; + +[GenerateTypedNameReferences] +public sealed partial class TippyUI : UIWidget +{ + public TippyState State = TippyState.Hidden; + public bool ModifyLayers = true; + + public TippyUI() + { + RobustXamlLoader.Load(this); + } + + public void InitLabel(PaperVisualsComponent? visuals, IResourceCache resCache) + { + if (visuals == null) + return; + + Label.ModulateSelfOverride = visuals.FontAccentColor; + + if (visuals.BackgroundImagePath == null) + return; + + LabelPanel.ModulateSelfOverride = visuals.BackgroundModulate; + var backgroundImage = resCache.GetResource(visuals.BackgroundImagePath); + var backgroundImageMode = visuals.BackgroundImageTile ? StyleBoxTexture.StretchMode.Tile : StyleBoxTexture.StretchMode.Stretch; + var backgroundPatchMargin = visuals.BackgroundPatchMargin; + LabelPanel.PanelOverride = new StyleBoxTexture + { + Texture = backgroundImage, + TextureScale = visuals.BackgroundScale, + Mode = backgroundImageMode, + PatchMarginLeft = backgroundPatchMargin.Left, + PatchMarginBottom = backgroundPatchMargin.Bottom, + PatchMarginRight = backgroundPatchMargin.Right, + PatchMarginTop = backgroundPatchMargin.Top + }; + } + + public enum TippyState : byte + { + Hidden, + Revealing, + Speaking, + Hiding, + } +} diff --git a/Content.Client/Tips/TippyUIController.cs b/Content.Client/Tips/TippyUIController.cs new file mode 100644 index 0000000000..ad5a3fbcfb --- /dev/null +++ b/Content.Client/Tips/TippyUIController.cs @@ -0,0 +1,244 @@ +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); + } +} diff --git a/Content.Client/Tips/TipsSystem.cs b/Content.Client/Tips/TipsSystem.cs new file mode 100644 index 0000000000..f9376a7005 --- /dev/null +++ b/Content.Client/Tips/TipsSystem.cs @@ -0,0 +1,20 @@ +using Content.Shared.Tips; +using Robust.Client.UserInterface; + +namespace Content.Client.Tips; + +public sealed class TipsSystem : EntitySystem +{ + [Dependency] private readonly IUserInterfaceManager _uiMan = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeNetworkEvent(OnClippyEv); + } + + private void OnClippyEv(TippyEvent ev) + { + _uiMan.GetUIController().AddMessage(ev); + } +} diff --git a/Content.Server/Tips/TipsSystem.cs b/Content.Server/Tips/TipsSystem.cs index cc45a3a1d5..ccc732623b 100644 --- a/Content.Server/Tips/TipsSystem.cs +++ b/Content.Server/Tips/TipsSystem.cs @@ -1,9 +1,13 @@ -using Content.Server.Chat.Managers; +using Content.Server.Chat.Managers; using Content.Server.GameTicking; using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Dataset; +using Content.Shared.Tips; +using Robust.Server.GameObjects; +using Robust.Server.Player; using Robust.Shared.Configuration; +using Robust.Shared.Console; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -22,11 +26,14 @@ public sealed class TipsSystem : EntitySystem [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly GameTicker _ticker = default!; + [Dependency] private readonly IConsoleHost _conHost = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; private bool _tipsEnabled; private float _tipTimeOutOfRound; private float _tipTimeInRound; private string _tipsDataset = ""; + private float _tipTippyChance; [ViewVariables(VVAccess.ReadWrite)] private TimeSpan _nextTipTime = TimeSpan.Zero; @@ -40,10 +47,101 @@ public sealed class TipsSystem : EntitySystem Subs.CVar(_cfg, CCVars.TipFrequencyInRound, SetInRound, true); Subs.CVar(_cfg, CCVars.TipsEnabled, SetEnabled, true); Subs.CVar(_cfg, CCVars.TipsDataset, SetDataset, true); + Subs.CVar(_cfg, CCVars.TipsTippyChance, SetTippyChance, true); RecalculateNextTipTime(); + _conHost.RegisterCommand("tippy", Loc.GetString("cmd-tippy-desc"), Loc.GetString("cmd-tippy-help"), SendTippy, SendTippyHelper); + _conHost.RegisterCommand("tip", Loc.GetString("cmd-tip-desc"), "tip", SendTip); } + private CompletionResult SendTippyHelper(IConsoleShell shell, string[] args) + { + return args.Length switch + { + 1 => CompletionResult.FromHintOptions(CompletionHelper.SessionNames(), Loc.GetString("cmd-tippy-auto-1")), + 2 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-2")), + 3 => CompletionResult.FromHintOptions(CompletionHelper.PrototypeIDs(), Loc.GetString("cmd-tippy-auto-3")), + 4 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-4")), + 5 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-5")), + 6 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-6")), + _ => CompletionResult.Empty + }; + } + + private void SendTip(IConsoleShell shell, string argstr, string[] args) + { + AnnounceRandomTip(); + RecalculateNextTipTime(); + } + + private void SendTippy(IConsoleShell shell, string argstr, string[] args) + { + if (args.Length < 2) + { + shell.WriteLine(Loc.GetString("cmd-tippy-help")); + return; + } + + ActorComponent? actor = null; + if (args[0] != "all") + { + ICommonSession? session; + if (args.Length > 0) + { + // Get player entity + if (!_playerManager.TryGetSessionByUsername(args[0], out session)) + { + shell.WriteLine(Loc.GetString("cmd-tippy-error-no-user")); + return; + } + } + else + { + session = shell.Player; + } + + if (session?.AttachedEntity is not { } user) + { + shell.WriteLine(Loc.GetString("cmd-tippy-error-no-user")); + return; + } + + if (!TryComp(user, out actor)) + { + shell.WriteError(Loc.GetString("cmd-tippy-error-no-user")); + return; + } + } + + var ev = new TippyEvent(args[1]); + + string proto; + if (args.Length > 2) + { + ev.Proto = args[2]; + if (!_prototype.HasIndex(args[2])) + { + shell.WriteError(Loc.GetString("cmd-tippy-error-no-prototype", ("proto", args[2]))); + return; + } + } + + if (args.Length > 3) + ev.SpeakTime = float.Parse(args[3]); + + if (args.Length > 4) + ev.SlideTime = float.Parse(args[4]); + + if (args.Length > 5) + ev.WaddleInterval = float.Parse(args[5]); + + if (actor != null) + RaiseNetworkEvent(ev, actor.PlayerSession); + else + RaiseNetworkEvent(ev); + } + + public override void Update(float frameTime) { base.Update(frameTime); @@ -81,6 +179,11 @@ public sealed class TipsSystem : EntitySystem _tipsDataset = value; } + private void SetTippyChance(float value) + { + _tipTippyChance = value; + } + private void AnnounceRandomTip() { if (!_prototype.TryIndex(_tipsDataset, out var tips)) @@ -89,8 +192,16 @@ public sealed class TipsSystem : EntitySystem var tip = _random.Pick(tips.Values); var msg = Loc.GetString("tips-system-chat-message-wrap", ("tip", tip)); - _chat.ChatMessageToManyFiltered(Filter.Broadcast(), ChatChannel.OOC, tip, msg, + if (_random.Prob(_tipTippyChance)) + { + var ev = new TippyEvent(msg); + ev.SpeakTime = 1 + tip.Length * 0.05f; + RaiseNetworkEvent(ev); + } else + { + _chat.ChatMessageToManyFiltered(Filter.Broadcast(), ChatChannel.OOC, tip, msg, EntityUid.Invalid, false, false, Color.MediumPurple); + } } private void RecalculateNextTipTime() diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 6e20a7dc20..cbc035140f 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -435,6 +435,12 @@ namespace Content.Shared.CCVar public static readonly CVarDef LoginTipsDataset = CVarDef.Create("tips.login_dataset", "Tips"); + /// + /// The chance for Tippy to replace a normal tip message. + /// + public static readonly CVarDef TipsTippyChance = + CVarDef.Create("tips.tippy_chance", 0.01f); + /* * Console */ @@ -1985,6 +1991,10 @@ namespace Content.Shared.CCVar public static readonly CVarDef GatewayGeneratorEnabled = CVarDef.Create("gateway.generator_enabled", true); + // Clippy! + public static readonly CVarDef TippyEntity = + CVarDef.Create("tippy.entity", "Tippy", CVar.SERVER | CVar.REPLICATED); + /* * DEBUG */ diff --git a/Content.Shared/Tips/TippyEvent.cs b/Content.Shared/Tips/TippyEvent.cs new file mode 100644 index 0000000000..4370e9c822 --- /dev/null +++ b/Content.Shared/Tips/TippyEvent.cs @@ -0,0 +1,19 @@ + +using Robust.Shared.Serialization; + +namespace Content.Shared.Tips; + +[Serializable, NetSerializable] +public sealed class TippyEvent : EntityEventArgs +{ + public TippyEvent(string msg) + { + Msg = msg; + } + + public string Msg; + public string? Proto; + public float SpeakTime = 5; + public float SlideTime = 3; + public float WaddleInterval = 0.5f; +} diff --git a/Resources/Locale/en-US/commands/tippy-command.ftl b/Resources/Locale/en-US/commands/tippy-command.ftl new file mode 100644 index 0000000000..6b9a95a1dd --- /dev/null +++ b/Resources/Locale/en-US/commands/tippy-command.ftl @@ -0,0 +1,12 @@ +cmd-tippy-desc = Broadcast a message as Tippy the clown. +cmd-tippy-help = tippy [entity prototype] [speak time] [slide time] [waddle interval] +cmd-tippy-auto-1 = +cmd-tippy-auto-2 = message +cmd-tippy-auto-3 = entity prototype +cmd-tippy-auto-4 = speak time, in seconds +cmd-tippy-auto-5 = slide time, in seconds +cmd-tippy-auto-6 = waddle interval, in seconds +cmd-tippy-error-no-user = User not found. +cmd-tippy-error-no-prototype = Prototype not found: {$proto} + +cmd-tip-desc = Spawn a random game tip. diff --git a/Resources/Prototypes/Entities/Debugging/tippy.yml b/Resources/Prototypes/Entities/Debugging/tippy.yml new file mode 100644 index 0000000000..d8ba0fd51e --- /dev/null +++ b/Resources/Prototypes/Entities/Debugging/tippy.yml @@ -0,0 +1,29 @@ +- type: entity + id: Tippy + components: + - type: Sprite + netsync: false + noRot: false + scale: 4,4 + layers: + - sprite: Tips/tippy.rsi + state: left + map: [ "revealing" ] + - sprite: Tips/tippy.rsi + state: right + map: [ "hiding" ] + - sprite: Tips/tippy.rsi + state: down + visible: false + map: [ "speaking" ] + # footstep sounds wile waddling onto the screen. + - type: FootstepModifier + footstepSoundCollection: + collection: FootstepClown + # visuals for the speech bubble. + # only supports background image. + - type: PaperVisuals + backgroundImagePath: "/Textures/Interface/Paper/paper_background_default.svg.96dpi.png" + backgroundPatchMargin: 16.0, 16.0, 16.0, 16.0 + backgroundModulate: "#ffffcc" + fontAccentColor: "#000000" diff --git a/Resources/Textures/Tips/tippy.rsi/down.png b/Resources/Textures/Tips/tippy.rsi/down.png new file mode 100644 index 0000000000000000000000000000000000000000..bdfcf315b6c64df1adc683c088272eea038731f5 GIT binary patch literal 5325 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 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Tips/tippy.rsi/left.png b/Resources/Textures/Tips/tippy.rsi/left.png new file mode 100644 index 0000000000000000000000000000000000000000..f2293c6111dbc5a880f7ba53e7ca0070530ef12e GIT binary patch literal 4000 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