From: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> Date: Mon, 29 Apr 2024 04:38:16 +0000 (+0200) Subject: Tippy, the helpful hint clown! (#26767) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=373c368b94de808ff8da1f9956b489daecee64d8;p=space-station-14.git 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 --- 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 0000000000..bdfcf315b6 Binary files /dev/null and b/Resources/Textures/Tips/tippy.rsi/down.png differ diff --git a/Resources/Textures/Tips/tippy.rsi/left.png b/Resources/Textures/Tips/tippy.rsi/left.png new file mode 100644 index 0000000000..f2293c6111 Binary files /dev/null and b/Resources/Textures/Tips/tippy.rsi/left.png differ diff --git a/Resources/Textures/Tips/tippy.rsi/meta.json b/Resources/Textures/Tips/tippy.rsi/meta.json new file mode 100644 index 0000000000..68942d731c --- /dev/null +++ b/Resources/Textures/Tips/tippy.rsi/meta.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "down" + }, + { + "name": "left" + }, + { + "name": "right" + } + ] +} diff --git a/Resources/Textures/Tips/tippy.rsi/right.png b/Resources/Textures/Tips/tippy.rsi/right.png new file mode 100644 index 0000000000..900262932d Binary files /dev/null and b/Resources/Textures/Tips/tippy.rsi/right.png differ diff --git a/Resources/engineCommandPerms.yml b/Resources/engineCommandPerms.yml index 51743c6e82..42cc4668a9 100644 --- a/Resources/engineCommandPerms.yml +++ b/Resources/engineCommandPerms.yml @@ -96,6 +96,8 @@ - tp - tpto - respawn + - tippy + - tip - Flags: SERVER Commands: