]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Tippy, the helpful hint clown! (#26767)
authorSlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Mon, 29 Apr 2024 04:38:16 +0000 (06:38 +0200)
committerGitHub <noreply@github.com>
Mon, 29 Apr 2024 04:38:16 +0000 (21:38 -0700)
* 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 <lunarautomaton6@gmail.com>
14 files changed:
Content.Client/Tips/TippyUI.xaml [new file with mode: 0644]
Content.Client/Tips/TippyUI.xaml.cs [new file with mode: 0644]
Content.Client/Tips/TippyUIController.cs [new file with mode: 0644]
Content.Client/Tips/TipsSystem.cs [new file with mode: 0644]
Content.Server/Tips/TipsSystem.cs
Content.Shared/CCVar/CCVars.cs
Content.Shared/Tips/TippyEvent.cs [new file with mode: 0644]
Resources/Locale/en-US/commands/tippy-command.ftl [new file with mode: 0644]
Resources/Prototypes/Entities/Debugging/tippy.yml [new file with mode: 0644]
Resources/Textures/Tips/tippy.rsi/down.png [new file with mode: 0644]
Resources/Textures/Tips/tippy.rsi/left.png [new file with mode: 0644]
Resources/Textures/Tips/tippy.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Tips/tippy.rsi/right.png [new file with mode: 0644]
Resources/engineCommandPerms.yml

diff --git a/Content.Client/Tips/TippyUI.xaml b/Content.Client/Tips/TippyUI.xaml
new file mode 100644 (file)
index 0000000..a86e05a
--- /dev/null
@@ -0,0 +1,11 @@
+<tips:TippyUI xmlns="https://spacestation14.io"
+               xmlns:tips="clr-namespace:Content.Client.Tips"
+               MinSize="64 64"
+               Visible="False">
+    <PanelContainer Name="LabelPanel" Access="Public" Visible="False" MaxWidth="300" MaxHeight="200">
+        <ScrollContainer Name="ScrollingContents" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" HorizontalExpand="True" VerticalExpand="True" HScrollEnabled="False" ReturnMeasure="True">
+            <RichTextLabel Name="Label" Access="Public"/>
+        </ScrollContainer>
+    </PanelContainer>
+    <SpriteView Name="Entity" Access="Public" MinSize="128 128"/>
+</tips:TippyUI>
diff --git a/Content.Client/Tips/TippyUI.xaml.cs b/Content.Client/Tips/TippyUI.xaml.cs
new file mode 100644 (file)
index 0000000..de3eaf4
--- /dev/null
@@ -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<TextureResource>(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 (file)
index 0000000..ad5a3fb
--- /dev/null
@@ -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<TippyEvent> _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<TippyUI>();
+        _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<PaperVisualsComponent>(_entity))
+                {
+                    var paper = EntityManager.AddComponent<PaperVisualsComponent>(_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<PaperVisualsComponent>(_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<TippyUI>();
+        _currentMessage = null;
+        EntityManager.DeleteEntity(_entity);
+    }
+}
diff --git a/Content.Client/Tips/TipsSystem.cs b/Content.Client/Tips/TipsSystem.cs
new file mode 100644 (file)
index 0000000..f9376a7
--- /dev/null
@@ -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<TippyEvent>(OnClippyEv);
+    }
+
+    private void OnClippyEv(TippyEvent ev)
+    {
+        _uiMan.GetUIController<TippyUIController>().AddMessage(ev);
+    }
+}
index cc45a3a1d5eb9771a72ddb822cafb65514e59b23..ccc732623bc708b8d66ad2ca23d2ef914fb699ce 100644 (file)
@@ -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<EntityPrototype>(), 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<EntityPrototype>(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<DatasetPrototype>(_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()
index 6e20a7dc2060396e3b19fb23b42bc96d4ea55c3c..cbc035140f535401d2e9d46d054db9348820fb16 100644 (file)
@@ -435,6 +435,12 @@ namespace Content.Shared.CCVar
         public static readonly CVarDef<string> LoginTipsDataset =
             CVarDef.Create("tips.login_dataset", "Tips");
 
+        /// <summary>
+        ///     The chance for Tippy to replace a normal tip message.
+        /// </summary>
+        public static readonly CVarDef<float> TipsTippyChance =
+            CVarDef.Create("tips.tippy_chance", 0.01f);
+
         /*
          * Console
          */
@@ -1985,6 +1991,10 @@ namespace Content.Shared.CCVar
         public static readonly CVarDef<bool> GatewayGeneratorEnabled =
             CVarDef.Create("gateway.generator_enabled", true);
 
+        // Clippy!
+        public static readonly CVarDef<string> 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 (file)
index 0000000..4370e9c
--- /dev/null
@@ -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 (file)
index 0000000..6b9a95a
--- /dev/null
@@ -0,0 +1,12 @@
+cmd-tippy-desc = Broadcast a message as Tippy the clown.
+cmd-tippy-help = tippy <user | all> <message> [entity prototype] [speak time] [slide time] [waddle interval]
+cmd-tippy-auto-1 = <user | all>
+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 (file)
index 0000000..d8ba0fd
--- /dev/null
@@ -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 (file)
index 0000000..bdfcf31
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 (file)
index 0000000..f2293c6
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 (file)
index 0000000..68942d7
--- /dev/null
@@ -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 (file)
index 0000000..9002629
Binary files /dev/null and b/Resources/Textures/Tips/tippy.rsi/right.png differ
index 51743c6e82256decce2dc114ac47cd55ab5dfdef..42cc4668a937e4cfba043869e067cb7e6bc181f0 100644 (file)
@@ -96,6 +96,8 @@
   - tp
   - tpto
   - respawn
+  - tippy
+  - tip
 
 - Flags: SERVER
   Commands: