]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Emotes Menu (#26702)
authorMorb <14136326+Morb0@users.noreply.github.com>
Mon, 29 Apr 2024 04:38:23 +0000 (07:38 +0300)
committerGitHub <noreply@github.com>
Mon, 29 Apr 2024 04:38:23 +0000 (21:38 -0700)
* Basic emote radial menu

* Move out from corvax

* Move to UI controller & add to top menu bar and key bind

* Make emote play

* Add name localization for emotes

* Localize chat messages

* Fix emote menu

* Add categories localization

* Fixes

* Fix

* Add emotes entity blacklist

* Fix entity whitelist required all logic

* Remove unused wagging emote

* Revert sprite

* Set default texture for emote icon

* Update Resources/keybinds.yml

---------

Co-authored-by: Kara <lunarautomaton6@gmail.com>
34 files changed:
Content.Client/Chat/UI/EmotesMenu.xaml [new file with mode: 0644]
Content.Client/Chat/UI/EmotesMenu.xaml.cs [new file with mode: 0644]
Content.Client/Input/ContentContexts.cs
Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs [new file with mode: 0644]
Content.Client/UserInterface/Systems/MenuBar/GameTopMenuBarUIController.cs
Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml
Content.Server/Chat/Systems/ChatSystem.Emote.cs
Content.Server/Speech/EmotesMenuSystem.cs [new file with mode: 0644]
Content.Server/Speech/EntitySystems/VocalSystem.cs
Content.Shared/Chat/EmotesEvents.cs [new file with mode: 0644]
Content.Shared/Chat/Prototypes/EmotePrototype.cs
Content.Shared/Chat/Prototypes/EmoteSoundsPrototype.cs
Content.Shared/Input/ContentKeyFunctions.cs
Content.Shared/Speech/Components/VocalComponent.cs [moved from Content.Server/Speech/Components/VocalComponent.cs with 83% similarity]
Content.Shared/Speech/SpeechComponent.cs
Content.Shared/Wagging/WaggingComponent.cs
Content.Shared/Whitelist/EntityWhitelist.cs
Resources/Locale/en-US/HUD/game-hud.ftl
Resources/Locale/en-US/chat/emotes.ftl [new file with mode: 0644]
Resources/Locale/en-US/chat/ui/emote-menu.ftl [new file with mode: 0644]
Resources/Locale/en-US/emotes/emotes.ftl [deleted file]
Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml
Resources/Prototypes/Entities/Mobs/NPCs/space.yml
Resources/Prototypes/Entities/Mobs/Species/arachnid.yml
Resources/Prototypes/Entities/Mobs/Species/moth.yml
Resources/Prototypes/Entities/Mobs/Species/slime.yml
Resources/Prototypes/Voice/disease_emotes.yml
Resources/Prototypes/Voice/speech_emotes.yml
Resources/Prototypes/Voice/tail_emotes.yml [deleted file]
Resources/Textures/Interface/emotes.svg [new file with mode: 0644]
Resources/Textures/Interface/emotes.svg.192dpi.png [new file with mode: 0644]
Resources/Textures/Interface/emotes.svg.192dpi.png.yml [new file with mode: 0644]
Resources/keybinds.yml

diff --git a/Content.Client/Chat/UI/EmotesMenu.xaml b/Content.Client/Chat/UI/EmotesMenu.xaml
new file mode 100644 (file)
index 0000000..819a654
--- /dev/null
@@ -0,0 +1,31 @@
+<ui:RadialMenu xmlns="https://spacestation14.io"
+                xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
+                BackButtonStyleClass="RadialMenuBackButton"
+                CloseButtonStyleClass="RadialMenuCloseButton"
+                VerticalExpand="True"
+                HorizontalExpand="True"
+                MinSize="450 450">
+
+    <!-- Main -->
+    <ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False">
+        <ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'emote-menu-category-general'}" TargetLayer="General" Visible="False">
+            <TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Head/Soft/mimesoft.rsi/icon.png"/>
+        </ui:RadialMenuTextureButton>
+        <ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'emote-menu-category-vocal'}" TargetLayer="Vocal" Visible="False">
+            <TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Actions/scream.png"/>
+        </ui:RadialMenuTextureButton>
+        <ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'emote-menu-category-hands'}" TargetLayer="Hands" Visible="False">
+            <TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Hands/Gloves/latex.rsi/icon.png"/>
+        </ui:RadialMenuTextureButton>
+    </ui:RadialContainer>
+
+    <!-- General -->
+    <ui:RadialContainer Name="General"  VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
+
+    <!-- Vocal -->
+    <ui:RadialContainer Name="Vocal"  VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
+
+    <!-- Hands -->
+    <ui:RadialContainer Name="Hands"  VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
+
+</ui:RadialMenu>
diff --git a/Content.Client/Chat/UI/EmotesMenu.xaml.cs b/Content.Client/Chat/UI/EmotesMenu.xaml.cs
new file mode 100644 (file)
index 0000000..a26d319
--- /dev/null
@@ -0,0 +1,112 @@
+using System.Numerics;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Chat.Prototypes;
+using Content.Shared.Speech;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Chat.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class EmotesMenu : RadialMenu
+{
+    [Dependency] private readonly EntityManager _entManager = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly ISharedPlayerManager _playerManager = default!;
+
+    private readonly SpriteSystem _spriteSystem;
+
+    public event Action<ProtoId<EmotePrototype>>? OnPlayEmote;
+
+    public EmotesMenu()
+    {
+        IoCManager.InjectDependencies(this);
+        RobustXamlLoader.Load(this);
+
+        _spriteSystem = _entManager.System<SpriteSystem>();
+
+        var main = FindControl<RadialContainer>("Main");
+
+        var emotes = _prototypeManager.EnumeratePrototypes<EmotePrototype>();
+        foreach (var emote in emotes)
+        {
+            var player = _playerManager.LocalSession?.AttachedEntity;
+            if (emote.Category == EmoteCategory.Invalid ||
+                emote.ChatTriggers.Count == 0 ||
+                !(player.HasValue && (emote.Whitelist?.IsValid(player.Value, _entManager) ?? true)) ||
+                (emote.Blacklist?.IsValid(player.Value, _entManager) ?? false))
+                continue;
+
+            if (!emote.Available &&
+                _entManager.TryGetComponent<SpeechComponent>(player.Value, out var speech) &&
+                !speech.AllowedEmotes.Contains(emote.ID))
+                continue;
+
+            var parent = FindControl<RadialContainer>(emote.Category.ToString());
+
+            var button = new EmoteMenuButton
+            {
+                StyleClasses = { "RadialMenuButton" },
+                SetSize = new Vector2(64f, 64f),
+                ToolTip = Loc.GetString(emote.Name),
+                ProtoId = emote.ID,
+            };
+
+            var tex = new TextureRect
+            {
+                VerticalAlignment = VAlignment.Center,
+                HorizontalAlignment = HAlignment.Center,
+                Texture = _spriteSystem.Frame0(emote.Icon),
+                TextureScale = new Vector2(2f, 2f),
+            };
+
+            button.AddChild(tex);
+            parent.AddChild(button);
+            foreach (var child in main.Children)
+            {
+                if (child is not RadialMenuTextureButton castChild)
+                    continue;
+
+                if (castChild.TargetLayer == emote.Category.ToString())
+                {
+                    castChild.Visible = true;
+                    break;
+                }
+            }
+        }
+
+
+        // Set up menu actions
+        foreach (var child in Children)
+        {
+            if (child is not RadialContainer container)
+                continue;
+            AddEmoteClickAction(container);
+        }
+    }
+
+    private void AddEmoteClickAction(RadialContainer container)
+    {
+        foreach (var child in container.Children)
+        {
+            if (child is not EmoteMenuButton castChild)
+                continue;
+
+            castChild.OnButtonUp += _ =>
+            {
+                OnPlayEmote?.Invoke(castChild.ProtoId);
+                Close();
+            };
+        }
+    }
+}
+
+
+public sealed class EmoteMenuButton : RadialMenuTextureButton
+{
+    public ProtoId<EmotePrototype> ProtoId { get; set; }
+}
index 8a7ca3b77352c1f98c2a344f2cf8fe877e82198e..7a8a99385458c691aa03919c5b9c0954dcd0f1c3 100644 (file)
@@ -60,6 +60,7 @@ namespace Content.Client.Input
             human.AddFunction(ContentKeyFunctions.UseItemInHand);
             human.AddFunction(ContentKeyFunctions.AltUseItemInHand);
             human.AddFunction(ContentKeyFunctions.OpenCharacterMenu);
+            human.AddFunction(ContentKeyFunctions.OpenEmotesMenu);
             human.AddFunction(ContentKeyFunctions.ActivateItemInWorld);
             human.AddFunction(ContentKeyFunctions.ThrowItemInHand);
             human.AddFunction(ContentKeyFunctions.AltActivateItemInWorld);
diff --git a/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs b/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs
new file mode 100644 (file)
index 0000000..7b86859
--- /dev/null
@@ -0,0 +1,125 @@
+using Content.Client.Chat.UI;
+using Content.Client.Gameplay;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Chat;
+using Content.Shared.Chat.Prototypes;
+using Content.Shared.Input;
+using JetBrains.Annotations;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.UserInterface.Controllers;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.UserInterface.Systems.Emotes;
+
+[UsedImplicitly]
+public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayState>
+{
+    [Dependency] private readonly IEntityManager _entityManager = default!;
+    [Dependency] private readonly IClyde _displayManager = default!;
+    [Dependency] private readonly IInputManager _inputManager = default!;
+
+    private MenuButton? EmotesButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.EmotesButton;
+    private EmotesMenu? _menu;
+
+    public void OnStateEntered(GameplayState state)
+    {
+        CommandBinds.Builder
+            .Bind(ContentKeyFunctions.OpenEmotesMenu,
+                InputCmdHandler.FromDelegate(_ => ToggleEmotesMenu(false)))
+            .Register<EmotesUIController>();
+    }
+
+    public void OnStateExited(GameplayState state)
+    {
+        CommandBinds.Unregister<EmotesUIController>();
+    }
+
+    private void ToggleEmotesMenu(bool centered)
+    {
+        if (_menu == null)
+        {
+            // setup window
+            _menu = UIManager.CreateWindow<EmotesMenu>();
+            _menu.OnClose += OnWindowClosed;
+            _menu.OnOpen += OnWindowOpen;
+            _menu.OnPlayEmote += OnPlayEmote;
+
+            if (EmotesButton != null)
+                EmotesButton.SetClickPressed(true);
+
+            if (centered)
+            {
+                _menu.OpenCentered();
+            }
+            else
+            {
+                // Open the menu, centered on the mouse
+                var vpSize = _displayManager.ScreenSize;
+                _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
+            }
+        }
+        else
+        {
+            _menu.OnClose -= OnWindowClosed;
+            _menu.OnOpen -= OnWindowOpen;
+            _menu.OnPlayEmote -= OnPlayEmote;
+
+            if (EmotesButton != null)
+                EmotesButton.SetClickPressed(false);
+
+            CloseMenu();
+        }
+    }
+
+    public void UnloadButton()
+    {
+        if (EmotesButton == null)
+            return;
+
+        EmotesButton.OnPressed -= ActionButtonPressed;
+    }
+
+    public void LoadButton()
+    {
+        if (EmotesButton == null)
+            return;
+
+        EmotesButton.OnPressed += ActionButtonPressed;
+    }
+
+    private void ActionButtonPressed(BaseButton.ButtonEventArgs args)
+    {
+        ToggleEmotesMenu(true);
+    }
+
+    private void OnWindowClosed()
+    {
+        if (EmotesButton != null)
+            EmotesButton.Pressed = false;
+
+        CloseMenu();
+    }
+
+    private void OnWindowOpen()
+    {
+        if (EmotesButton != null)
+            EmotesButton.Pressed = true;
+    }
+
+    private void CloseMenu()
+    {
+        if (_menu == null)
+            return;
+
+        _menu.Dispose();
+        _menu = null;
+    }
+
+    private void OnPlayEmote(ProtoId<EmotePrototype> protoId)
+    {
+        _entityManager.RaisePredictiveEvent(new PlayEmoteMessage(protoId));
+    }
+}
index 1505db48a796ec2ecaf36ce81bbcba34116ff712..e314310bc0c3d1f2d3347802aa52493894d7025a 100644 (file)
@@ -3,6 +3,7 @@ using Content.Client.UserInterface.Systems.Admin;
 using Content.Client.UserInterface.Systems.Bwoink;
 using Content.Client.UserInterface.Systems.Character;
 using Content.Client.UserInterface.Systems.Crafting;
+using Content.Client.UserInterface.Systems.Emotes;
 using Content.Client.UserInterface.Systems.EscapeMenu;
 using Content.Client.UserInterface.Systems.Gameplay;
 using Content.Client.UserInterface.Systems.Guidebook;
@@ -22,6 +23,7 @@ public sealed class GameTopMenuBarUIController : UIController
     [Dependency] private readonly ActionUIController _action = default!;
     [Dependency] private readonly SandboxUIController _sandbox = default!;
     [Dependency] private readonly GuidebookUIController _guidebook = default!;
+    [Dependency] private readonly EmotesUIController _emotes = default!;
 
     private GameTopMenuBar? GameTopMenuBar => UIManager.GetActiveUIWidgetOrNull<GameTopMenuBar>();
 
@@ -44,6 +46,7 @@ public sealed class GameTopMenuBarUIController : UIController
         _ahelp.UnloadButton();
         _action.UnloadButton();
         _sandbox.UnloadButton();
+        _emotes.UnloadButton();
     }
 
     public void LoadButtons()
@@ -56,5 +59,6 @@ public sealed class GameTopMenuBarUIController : UIController
         _ahelp.LoadButton();
         _action.LoadButton();
         _sandbox.LoadButton();
+        _emotes.LoadButton();
     }
 }
index 3c8cd1d164f2dbf6284b6ef4345db136d8d38e1d..dc8972970ac869156c0a35d8b968910a499db46a 100644 (file)
         HorizontalExpand="True"
         AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
         />
+    <ui:MenuButton
+        Name="EmotesButton"
+        Access="Internal"
+        Icon="{xe:Tex '/Textures/Interface/emotes.svg.192dpi.png'}"
+        ToolTip="{Loc 'game-hud-open-emotes-menu-button-tooltip'}"
+        BoundKey = "{x:Static is:ContentKeyFunctions.OpenEmotesMenu}"
+        MinSize="42 64"
+        HorizontalExpand="True"
+        AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
+        />
     <ui:MenuButton
         Name="CraftingButton"
         Access="Internal"
index 8bba76dadda9c2b3b92e4d0808fa5d49618a9b2c..d120812b8801f405942410f1db0b0d33bbd47001 100644 (file)
@@ -1,5 +1,6 @@
 using System.Collections.Frozen;
 using Content.Shared.Chat.Prototypes;
+using Content.Shared.Speech;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
 
@@ -80,6 +81,16 @@ public partial class ChatSystem
         bool ignoreActionBlocker = false
         )
     {
+        if (!(emote.Whitelist?.IsValid(source, EntityManager) ?? true))
+            return;
+        if (emote.Blacklist?.IsValid(source, EntityManager) ?? false)
+            return;
+
+        if (!emote.Available &&
+            TryComp<SpeechComponent>(source, out var speech) &&
+            !speech.AllowedEmotes.Contains(emote.ID))
+            return;
+
         // check if proto has valid message for chat
         if (emote.ChatMessages.Count != 0)
         {
diff --git a/Content.Server/Speech/EmotesMenuSystem.cs b/Content.Server/Speech/EmotesMenuSystem.cs
new file mode 100644 (file)
index 0000000..a69b5a6
--- /dev/null
@@ -0,0 +1,30 @@
+using Content.Server.Chat.Systems;
+using Content.Shared.Chat;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Speech;
+
+public sealed partial class EmotesMenuSystem : EntitySystem
+{
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly ChatSystem _chat = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeAllEvent<PlayEmoteMessage>(OnPlayEmote);
+    }
+
+    private void OnPlayEmote(PlayEmoteMessage msg, EntitySessionEventArgs args)
+    {
+        var player = args.SenderSession.AttachedEntity;
+        if (!player.HasValue)
+            return;
+
+        if (!_prototypeManager.TryIndex(msg.ProtoId, out var proto) || proto.ChatTriggers.Count == 0)
+            return;
+
+        _chat.TryEmoteWithChat(player.Value, msg.ProtoId);
+    }
+}
index aedcbbd09960b4dc53a2d4bfd564443e649a5fbd..7c8ec21a94bb0c684ce89d5e646f2d7d211510db 100644 (file)
@@ -4,6 +4,7 @@ using Content.Server.Speech.Components;
 using Content.Shared.Chat.Prototypes;
 using Content.Shared.Humanoid;
 using Content.Shared.Speech;
+using Content.Shared.Speech.Components;
 using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.Prototypes;
diff --git a/Content.Shared/Chat/EmotesEvents.cs b/Content.Shared/Chat/EmotesEvents.cs
new file mode 100644 (file)
index 0000000..4479f8b
--- /dev/null
@@ -0,0 +1,11 @@
+using Content.Shared.Chat.Prototypes;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Chat;
+
+[Serializable, NetSerializable]
+public sealed class PlayEmoteMessage(ProtoId<EmotePrototype> protoId) : EntityEventArgs
+{
+    public readonly ProtoId<EmotePrototype> ProtoId = protoId;
+}
index 08f209d28d3601ecffe2ecff39a1f175fdf992b1..7ee958ee6a715e1ad11f246f0effe39c62d42f6e 100644 (file)
@@ -1,11 +1,13 @@
+using Content.Shared.Whitelist;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
 
 namespace Content.Shared.Chat.Prototypes;
 
 /// <summary>
 ///     IC emotes (scream, smile, clapping, etc).
-///     Entities can activate emotes by chat input or code.
+///     Entities can activate emotes by chat input, radial or code.
 /// </summary>
 [Prototype("emote")]
 public sealed partial class EmotePrototype : IPrototype
@@ -13,18 +15,50 @@ public sealed partial class EmotePrototype : IPrototype
     [IdDataField]
     public string ID { get; private set; } = default!;
 
+    /// <summary>
+    ///     Localization string for the emote name. Displayed in the radial UI.
+    /// </summary>
+    [DataField(required: true)]
+    public string Name = default!;
+
+    /// <summary>
+    ///     Determines if emote available to all by default
+    ///     <see cref="Whitelist"/> check comes after this setting
+    ///     <see cref="Content.Shared.Speech.SpeechComponent.AllowedEmotes"/> can ignore this setting
+    /// </summary>
+    [DataField]
+    public bool Available = true;
+
     /// <summary>
     ///     Different emote categories may be handled by different systems.
     ///     Also may be used for filtering.
     /// </summary>
-    [DataField("category")]
+    [DataField]
     public EmoteCategory Category = EmoteCategory.General;
 
+    /// <summary>
+    ///     An icon used to visually represent the emote in radial UI.
+    /// </summary>
+    [DataField]
+    public SpriteSpecifier Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Actions/scream.png"));
+
+    /// <summary>
+    ///     Determines conditions to this emote be available to use
+    /// </summary>
+    [DataField]
+    public EntityWhitelist? Whitelist;
+
+    /// <summary>
+    ///     Determines conditions to this emote be unavailable to use
+    /// </summary>
+    [DataField]
+    public EntityWhitelist? Blacklist;
+
     /// <summary>
     ///     Collection of words that will be sent to chat if emote activates.
     ///     Will be picked randomly from list.
     /// </summary>
-    [DataField("chatMessages")]
+    [DataField]
     public List<string> ChatMessages = new();
 
     /// <summary>
@@ -32,7 +66,7 @@ public sealed partial class EmotePrototype : IPrototype
     ///     When typed into players chat they will activate emote event.
     ///     All words should be unique across all emote prototypes.
     /// </summary>
-    [DataField("chatTriggers")]
+    [DataField]
     public HashSet<string> ChatTriggers = new();
 }
 
index c9a78e7d6d7ffd1d2d750672be1be9ad6ca5a2e2..2b7064c1e906952e27c89d09e91b779fcff7f8a7 100644 (file)
@@ -1,5 +1,6 @@
 using Robust.Shared.Audio;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
 
 namespace Content.Shared.Chat.Prototypes;
@@ -8,8 +9,8 @@ namespace Content.Shared.Chat.Prototypes;
 ///     Sounds collection for each <see cref="EmotePrototype"/>.
 ///     Different entities may use different sounds collections.
 /// </summary>
-[Prototype("emoteSounds")]
-public sealed partial class EmoteSoundsPrototype : IPrototype
+[Prototype("emoteSounds"), Serializable, NetSerializable]
+public sealed class EmoteSoundsPrototype : IPrototype
 {
     [IdDataField]
     public string ID { get; private set; } = default!;
index 886a0d5d3a1663e998c80a1385106b5dfe8395f2..2dd671816fd53e43532e5d83c155c3a76d587125 100644 (file)
@@ -25,6 +25,7 @@ namespace Content.Shared.Input
         public static readonly BoundKeyFunction CycleChatChannelBackward = "CycleChatChannelBackward";
         public static readonly BoundKeyFunction EscapeContext = "EscapeContext";
         public static readonly BoundKeyFunction OpenCharacterMenu = "OpenCharacterMenu";
+        public static readonly BoundKeyFunction OpenEmotesMenu = "OpenEmotesMenu";
         public static readonly BoundKeyFunction OpenCraftingMenu = "OpenCraftingMenu";
         public static readonly BoundKeyFunction OpenGuidebook = "OpenGuidebook";
         public static readonly BoundKeyFunction OpenInventoryMenu = "OpenInventoryMenu";
similarity index 83%
rename from Content.Server/Speech/Components/VocalComponent.cs
rename to Content.Shared/Speech/Components/VocalComponent.cs
index 029d638a66954c2b6ce48e7f60665efc14abea38..e5d2c9997fa4e32d74dced61f119315174d66a95 100644 (file)
@@ -1,18 +1,18 @@
-using Content.Server.Speech.EntitySystems;
 using Content.Shared.Chat.Prototypes;
 using Content.Shared.Humanoid;
 using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
 
-namespace Content.Server.Speech.Components;
+namespace Content.Shared.Speech.Components;
 
 /// <summary>
 ///     Component required for entities to be able to do vocal emotions.
 /// </summary>
-[RegisterComponent]
-[Access(typeof(VocalSystem))]
+[RegisterComponent, NetworkedComponent]
+[AutoGenerateComponentState]
 public sealed partial class VocalComponent : Component
 {
     /// <summary>
@@ -20,21 +20,27 @@ public sealed partial class VocalComponent : Component
     ///     Entities without <see cref="HumanoidComponent"/> considered to be <see cref="Sex.Unsexed"/>.
     /// </summary>
     [DataField("sounds", customTypeSerializer: typeof(PrototypeIdValueDictionarySerializer<Sex, EmoteSoundsPrototype>))]
+    [AutoNetworkedField]
     public Dictionary<Sex, string>? Sounds;
 
     [DataField("screamId", customTypeSerializer: typeof(PrototypeIdSerializer<EmotePrototype>))]
+    [AutoNetworkedField]
     public string ScreamId = "Scream";
 
     [DataField("wilhelm")]
+    [AutoNetworkedField]
     public SoundSpecifier Wilhelm = new SoundPathSpecifier("/Audio/Voice/Human/wilhelm_scream.ogg");
 
     [DataField("wilhelmProbability")]
+    [AutoNetworkedField]
     public float WilhelmProbability = 0.0002f;
 
     [DataField("screamAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
+    [AutoNetworkedField]
     public string ScreamAction = "ActionScream";
 
     [DataField("screamActionEntity")]
+    [AutoNetworkedField]
     public EntityUid? ScreamActionEntity;
 
     /// <summary>
@@ -42,5 +48,6 @@ public sealed partial class VocalComponent : Component
     ///     Null if no valid prototype for entity sex was found.
     /// </summary>
     [ViewVariables]
+    [AutoNetworkedField]
     public EmoteSoundsPrototype? EmoteSounds = null;
 }
index 272d9ef8cab45bb044ef7738448f0d1f5455537d..0882120718d7f3bd6c82bc988923c027d5390c31 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Shared.Chat.Prototypes;
 using Robust.Shared.Audio;
 using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
@@ -26,6 +27,13 @@ namespace Content.Shared.Speech
         [DataField]
         public ProtoId<SpeechVerbPrototype> SpeechVerb = "Default";
 
+        /// <summary>
+        ///     What emotes allowed to use event if emote <see cref="EmotePrototype.Available"/> is false
+        /// </summary>
+        [ViewVariables(VVAccess.ReadWrite)]
+        [DataField]
+        public List<ProtoId<EmotePrototype>> AllowedEmotes = new();
+
         /// <summary>
         ///     A mapping from chat suffixes loc strings to speech verb prototypes that should be conditionally used.
         ///     For things like '?' changing to 'asks' or '!!' making text bold and changing to 'yells'. Can be overridden if necessary.
index 76881827dd2b7ad3196ff1683124ac47199d3f4e..70e7f009c7d94fa4e443dadecd59dcd6c75fec31 100644 (file)
@@ -17,9 +17,6 @@ public sealed partial class WaggingComponent : Component
     [DataField]
     public EntityUid? ActionEntity;
 
-    [DataField]
-    public ProtoId<EmotePrototype> EmoteId = "WagTail";
-
     /// <summary>
     /// Suffix to add to get the animated marking.
     /// </summary>
index 942de2b0e8206e730b1bdf5221227a5c32a2d9d6..b412a09b989acc2532464c7d0a3341f92b813017 100644 (file)
@@ -94,6 +94,9 @@ namespace Content.Shared.Whitelist
                 return RequireAll ? tagSystem.HasAllTags(tags, Tags) : tagSystem.HasAnyTag(tags, Tags);
             }
 
+            if (RequireAll)
+                return true;
+
             return false;
         }
     }
index 7f6573d2adf9b6c948f5c9cae70a5dfb9ee30453..ea423f080abe867106c6ad8f5cb7373616f007f5 100644 (file)
@@ -1,6 +1,7 @@
 game-hud-open-escape-menu-button-tooltip = Open escape menu.
 game-hud-open-guide-menu-button-tooltip = Open guidebook menu.
 game-hud-open-character-menu-button-tooltip = Open character menu.
+game-hud-open-emotes-menu-button-tooltip= Open emotes menu.
 game-hud-open-inventory-menu-button-tooltip = Open inventory menu.
 game-hud-open-crafting-menu-button-tooltip = Open crafting menu.
 game-hud-open-actions-menu-button-tooltip = Open actions menu.
diff --git a/Resources/Locale/en-US/chat/emotes.ftl b/Resources/Locale/en-US/chat/emotes.ftl
new file mode 100644 (file)
index 0000000..86d79ff
--- /dev/null
@@ -0,0 +1,60 @@
+# Names
+chat-emote-name-scream = Scream
+chat-emote-name-laugh = Laugh
+chat-emote-name-honk = Honk
+chat-emote-name-sigh = Sigh
+chat-emote-name-whistle = Whistle
+chat-emote-name-crying = Crying
+chat-emote-name-squish = Squish
+chat-emote-name-chitter = Chitter
+chat-emote-name-squeak = Squeak
+chat-emote-name-click = Click
+chat-emote-name-clap = Clap
+chat-emote-name-snap = Snap
+chat-emote-name-salute = Salute
+chat-emote-name-deathgasp = Deathgasp
+chat-emote-name-buzz = Buzz
+chat-emote-name-weh = Weh
+chat-emote-name-chirp = Chirp
+chat-emote-name-beep = Beep
+chat-emote-name-chime = Chime
+chat-emote-name-buzztwo = Buzz Two
+chat-emote-name-ping = Ping
+chat-emote-name-sneeze = Sneeze
+chat-emote-name-cough = Cough
+chat-emote-name-catmeow = Cat Meow
+chat-emote-name-cathisses = Cat Hisses
+chat-emote-name-monkeyscreeches = Monkey Screeches
+chat-emote-name-robotbeep = Robot
+chat-emote-name-yawn = Yawn
+chat-emote-name-snore = Snore
+
+# Message
+chat-emote-msg-scream = screams!
+chat-emote-msg-laugh = laughs
+chat-emote-msg-honk = honks
+chat-emote-msg-sigh = sighs
+chat-emote-msg-whistle = whistle
+chat-emote-msg-crying = crying
+chat-emote-msg-squish = squishing
+chat-emote-msg-chitter = chitters.
+chat-emote-msg-squeak = squeaks.
+chat-emote-msg-click = click.
+chat-emote-msg-clap = claps!
+chat-emote-msg-snap = snaps fingers
+chat-emote-msg-salute = salute
+chat-emote-msg-deathgasp = seizes up and falls limp, {POSS-ADJ($entity)} eyes dead and lifeless...
+chat-emote-msg-buzz = buzz!
+chat-emote-msg-chirp = chirps!
+chat-emote-msg-beep = beeps.
+chat-emote-msg-chime = chimes.
+chat-emote-msg-buzzestwo = buzzes twice.
+chat-emote-msg-ping = pings.
+chat-emote-msg-sneeze = sneezes
+chat-emote-msg-cough = coughs
+chat-emote-msg-catmeow = meows
+chat-emote-msg-cathisses = hisses
+chat-emote-msg-monkeyscreeches = screeches
+chat-emote-msg-robotbeep = beeps
+chat-emote-msg-yawn = yawns
+chat-emote-msg-snore = snores
diff --git a/Resources/Locale/en-US/chat/ui/emote-menu.ftl b/Resources/Locale/en-US/chat/ui/emote-menu.ftl
new file mode 100644 (file)
index 0000000..1f92a93
--- /dev/null
@@ -0,0 +1,3 @@
+emote-menu-category-general = General
+emote-menu-category-vocal = Vocal
+emote-menu-category-hands = Hands
diff --git a/Resources/Locale/en-US/emotes/emotes.ftl b/Resources/Locale/en-US/emotes/emotes.ftl
deleted file mode 100644 (file)
index 53c1231..0000000
+++ /dev/null
@@ -1 +0,0 @@
-emote-deathgasp = seizes up and falls limp, {POSS-ADJ($entity)} eyes dead and lifeless...
index 1cf56c29c6aea67e3e77182e572befd8b3dedacc..f8218023fb4846d14bb871ec0575f34817d1aebd 100644 (file)
@@ -16,6 +16,7 @@
   - type: Speech
     speechSounds: Squeak
     speechVerb: SmallMob
+    allowedEmotes: ['Squeak']
   - type: Fixtures
     fixtures:
       fix1:
   - type: Speech
     speechVerb: Moth
     speechSounds: Squeak
+    allowedEmotes: ['Chitter']
   - type: FaxableObject
     insertingState: inserting_mothroach
   - type: MothAccent
   - type: Speech
     speechVerb: Arachnid
     speechSounds: Arachnid
+    allowedEmotes: ['Click']
   - type: DamageStateVisuals
     states:
       Alive:
   - type: Speech
     speechSounds: Squeak
     speechVerb: SmallMob
+    allowedEmotes: ['Squeak']
   - type: Sprite
     drawdepth: SmallMobs
     sprite: Mobs/Animals/mouse.rsi
   - type: Speech
     speechVerb: Arachnid
     speechSounds: Arachnid
+    allowedEmotes: ['Click']
   - type: Vocal
     sounds:
       Male: UnisexArachnid
   - type: Speech
     speechVerb: SmallMob
     speechSounds: Squeak
+    allowedEmotes: ['Squeak']
   - type: Sprite
     drawdepth: SmallMobs
     sprite: Mobs/Animals/hamster.rsi
index 95c30a174f2dd0d9d447edf5872f99b837dd525f..e667b931dec2f05c21e706b1cd81894370920308 100644 (file)
     makeSentient: true
     name: ghost-role-information-slimes-name
     description: ghost-role-information-slimes-description
+  - type: Speech
+    speechVerb: Slime
+    speechSounds: Slime
+    allowedEmotes: ['Squish']
+  - type: TypingIndicator
+    proto: slime
   - type: NpcFactionMember
     factions:
     - SimpleNeutral
index 8cfd199dca77af421910d96b4dbb77d21005baeb..849cd83eba64db5c660bc4f4dfae71a8324ca57f 100644 (file)
   - type: Speech
     speechVerb: Arachnid
     speechSounds: Arachnid
+    allowedEmotes: ['Click']
   - type: Vocal
     sounds:
       Male: UnisexArachnid
index d59c7bfd02474f6570446ffc88bde07389ee1982..ec742e59b5e6fbc3e7d0b08dbc84d39f32aef8b2 100644 (file)
@@ -62,6 +62,7 @@
   - type: Speech
     speechVerb: Arachnid
     speechSounds: Arachnid
+    allowedEmotes: ['Click']
   - type: Vocal
     sounds:
       Male: UnisexArachnid
index 199e99bef39078c06ac429bce077f2209d0bb82f..33bb46b1725ba7564ec48f5464bcaec57a5940fc 100644 (file)
@@ -22,6 +22,7 @@
     accent: zombieMoth
   - type: Speech
     speechVerb: Moth
+    allowedEmotes: ['Chitter']
   - type: TypingIndicator
     proto: moth
   - type: Butcherable
index 081973c3d27c3fba73ff671b1812e45d88dea10e..2ab26ffcd61031d583b48c7ef7e3c35b555591fe 100644 (file)
@@ -42,6 +42,7 @@
   - type: Speech
     speechVerb: Slime
     speechSounds: Slime
+    allowedEmotes: ['Squish']
   - type: TypingIndicator
     proto: slime
   - type: Vocal
index af93025cae00f08439561789439ff5a072d78370..c29d6dd017462e63f168690fbc846179e3d290b5 100644 (file)
@@ -1,45 +1,65 @@
 - type: emote
   id: Sneeze
+  name: chat-emote-name-sneeze
   category: Vocal
-  chatMessages: [sneezes]
-    
+  chatMessages: ["chat-emote-msg-sneeze"]
+
 - type: emote
   id: Cough
+  name: chat-emote-name-cough
   category: Vocal
-  chatMessages: [coughs]
+  whitelist:
+    components:
+    - Vocal
+  blacklist:
+    components:
+    - BorgChassis
+  chatMessages: ["chat-emote-msg-cough"]
   chatTriggers:
     - cough
     - coughs
 
 - type: emote
   id: CatMeow
+  name: chat-emote-name-catmeow
   category: Vocal
-  chatMessages: [meows]
+  chatMessages: ["chat-emote-msg-catmeow"]
 
 - type: emote
   id: CatHisses
+  name: chat-emote-name-cathisses
   category: Vocal
-  chatMessages: [hisses]
+  chatMessages: ["chat-emote-msg-cathisses"]
 
 - type: emote
   id: MonkeyScreeches
+  name: chat-emote-name-monkeyscreeches
   category: Vocal
-  chatMessages: [screeches]
+  chatMessages: ["chat-emote-msg-monkeyscreeches"]
 
 - type: emote
   id: RobotBeep
+  name: chat-emote-name-robotbeep
   category: Vocal
-  chatMessages: [beeps]
+  chatMessages: ["chat-emote-msg-robotbeep"]
 
 - type: emote
   id: Yawn
+  name: chat-emote-name-yawn
   category: Vocal
-  chatMessages: [yawns]
+  whitelist:
+    components:
+    - Vocal
+  blacklist:
+    components:
+    - BorgChassis
+  chatMessages: ["chat-emote-msg-yawn"]
   chatTriggers:
     - yawn
     - yawns
 
 - type: emote
   id: Snore
+  name: chat-emote-name-snore
   category: Vocal
-  chatMessages: [snores]
+  chatMessages: ["chat-emote-msg-snore"]
index 3b7ffc010728cc9b972ee9abcb035892ae9b3aff..a859a14c2b03f1e932db8d85db169daca754dd19 100644 (file)
@@ -1,8 +1,16 @@
 # vocal emotes
 - type: emote
   id: Scream
+  name: chat-emote-name-scream
   category: Vocal
-  chatMessages: [screams!]
+  icon: Interface/Actions/scream.png
+  whitelist:
+    components:
+    - Vocal
+  blacklist:
+    components:
+    - BorgChassis
+  chatMessages: ["chat-emote-msg-scream"]
   chatTriggers:
     - scream
     - screams
 
 - type: emote
   id: Laugh
+  name: chat-emote-name-laugh
   category: Vocal
-  chatMessages: [laughs]
+  icon: Interface/Actions/scream.png
+  whitelist:
+    components:
+    - Vocal
+  blacklist:
+    components:
+    - BorgChassis
+  chatMessages: ["chat-emote-msg-laugh"]
   chatTriggers:
     - laugh
     - laughs
 
 - type: emote
   id: Honk
+  name: chat-emote-name-honk
   category: Vocal
-  chatMessages: [honks]
+  icon: Interface/Actions/scream.png
+  whitelist:
+    requireAll: true
+    components:
+    - Vocal
+    - BorgChassis
+  chatMessages: ["chat-emote-msg-honk"]
   chatTriggers:
     - honk
     - honk.
 
 - type: emote
   id: Sigh
+  name: chat-emote-name-sigh
   category: Vocal
-  chatMessages: [sighs]
+  icon: Interface/Actions/scream.png
+  whitelist:
+    components:
+    - Vocal
+  blacklist:
+    components:
+    - BorgChassis
+  chatMessages: ["chat-emote-msg-sigh"]
   chatTriggers:
     - sigh
     - sighs
 
 - type: emote
   id: Whistle
+  name: chat-emote-name-whistle
   category: Vocal
-  chatMessages: [whistle]
+  icon: Interface/Actions/scream.png
+  whitelist:
+    components:
+    - Vocal
+  blacklist:
+    components:
+    - BorgChassis
+  chatMessages: ["chat-emote-msg-whistle"]
   chatTriggers:
     - whistle
     - whistle.
 
 - type: emote
   id: Crying
+  name: chat-emote-name-crying
   category: Vocal
-  chatMessages: [crying]
+  icon: Interface/Actions/scream.png
+  whitelist:
+    components:
+    - Vocal
+  blacklist:
+    components:
+    - BorgChassis
+  chatMessages: ["chat-emote-msg-crying"]
   chatTriggers:
     - cry
     - cry.
 
 - type: emote
   id: Squish
+  name: chat-emote-name-squish
   category: Vocal
-  chatMessages: [squishing]
+  available: false
+  icon: Interface/Actions/scream.png
+  whitelist:
+    components:
+    - Vocal
+  blacklist:
+    components:
+    - BorgChassis
+  chatMessages: ["chat-emote-msg-squish"]
   chatTriggers:
     - squish
     - squish.
 
 - type: emote
   id: Chitter
+  name: chat-emote-name-chitter
   category: Vocal
-  chatMessages: [chitters.]
+  available: false
+  icon: Interface/Actions/scream.png
+  whitelist:
+    components:
+    - Vocal
+  blacklist:
+    components:
+    - BorgChassis
+  chatMessages: ["chat-emote-msg-chitter"]
   chatTriggers:
    - chitter
    - chitter.
 
 - type: emote
   id: Squeak
+  name: chat-emote-name-squeak
   category: Vocal
-  chatMessages: [squeaks.]
+  available: false
+  whitelist:
+    components:
+      - Vocal
+  blacklist:
+    components:
+    - BorgChassis
+  chatMessages: ["chat-emote-msg-squeak"]
   chatTriggers:
    - squeak
    - squeak.
 
 - type: emote
   id: Click
+  name: chat-emote-name-click
   category: Vocal
-  chatMessages: [click.]
+  available: false
+  icon: Interface/Actions/scream.png
+  whitelist:
+    components:
+    - Vocal
+  blacklist:
+    components:
+    - BorgChassis
+  chatMessages: ["chat-emote-msg-click"]
   chatTriggers:
    - click
    - click.
 # hand emotes
 - type: emote
   id: Clap
+  name: chat-emote-name-clap
   category: Hands
-  chatMessages: [claps!]
+  icon: Interface/Actions/scream.png
+  whitelist:
+    components:
+    - Hands
+  blacklist:
+    components:
+    - BorgChassis
+  chatMessages: ["chat-emote-msg-clap"]
   chatTriggers:
     - clap
     - claps
 
 - type: emote
   id: Snap
+  name: chat-emote-name-snap
   category: Hands
-  chatMessages: [snaps fingers] # snaps <{THEIR($ent)}> fingers?
+  icon: Interface/Actions/scream.png
+  whitelist:
+    components:
+    - Hands
+  blacklist:
+    components:
+    - BorgChassis
+  chatMessages: ["chat-emote-msg-snap"] # snaps <{THEIR($ent)}> fingers?
   chatTriggers:
     - snap
     - snaps
 
 - type: emote
   id: Salute
+  name: chat-emote-name-salute
   category: Hands
-  chatMessages: [Salute]
+  icon: Interface/Actions/scream.png
+  whitelist:
+    components:
+    - Hands
+  blacklist:
+    components:
+    - BorgChassis
+  chatMessages: ["chat-emote-msg-salute"]
   chatTriggers:
     - salute
     - salute.
 
 - type: emote
   id: DefaultDeathgasp
-  chatMessages: ["emote-deathgasp"]
+  name: chat-emote-name-deathgasp
+  icon: Interface/Actions/scream.png
+  whitelist:
+    components:
+    - MobState
+  chatMessages: ["chat-emote-msg-deathgasp"]
   chatTriggers:
   - deathgasp
 
 - type: emote
   id: Buzz
+  name: chat-emote-name-buzz
   category: Vocal
-  chatMessages: [buzz!]
+  icon: Interface/Actions/scream.png
+  whitelist:
+    requireAll: true
+    components:
+    - BorgChassis
+    - Vocal
+  chatMessages: ["chat-emote-msg-buzz"]
   chatTriggers:
     - buzzing
     - buzzing!
 
 - type: emote
   id: Weh
+  name: chat-emote-name-weh
   category: Vocal
   chatMessages: [Wehs!]
 
 - type: emote
   id: Chirp
+  name: chat-emote-name-chirp
   category: Vocal
-  chatMessages: [chirps!]
+  icon: Interface/Actions/scream.png
+  whitelist:
+    requireAll: true
+    components:
+    - Nymph
+  chatMessages: ["chat-emote-msg-chirp"]
   chatTriggers:
     - chirp
     - chirp!
 # Machine Emotes
 - type: emote
   id: Beep
+  name: chat-emote-name-beep
   category: Vocal
-  chatMessages: [beeps.]
+  icon: Interface/Actions/scream.png
+  whitelist:
+    requireAll: true
+    components:
+    - BorgChassis
+    - Vocal
+  chatMessages: ["chat-emote-msg-beep"]
   chatTriggers:
     - beep
     - beep!
 
 - type: emote
   id: Chime
+  name: chat-emote-name-chime
   category: Vocal
-  chatMessages: [chimes.]
+  icon: Interface/Actions/scream.png
+  whitelist:
+    requireAll: true
+    components:
+    - BorgChassis
+    - Vocal
+  chatMessages: ["chat-emote-msg-chime"]
   chatTriggers:
     - chime
     - chime.
 
 - type: emote
   id: Buzz-Two
+  name: chat-emote-name-buzztwo
   category: Vocal
-  chatMessages: [buzzesTwice.]
+  icon: Interface/Actions/scream.png
+  whitelist:
+    requireAll: true
+    components:
+    - BorgChassis
+    - Vocal
+  chatMessages: ["chat-emote-msg-buzzestwo"]
   chatTriggers:
     - buzztwice
     - buzztwice.
 
 - type: emote
   id: Ping
+  name: chat-emote-name-ping
   category: Vocal
-  chatMessages: [pings.]
+  icon: Interface/Actions/scream.png
+  whitelist:
+    requireAll: true
+    components:
+    - BorgChassis
+    - Vocal
+  chatMessages: ["chat-emote-msg-ping"]
   chatTriggers:
     - ping
     - ping.
diff --git a/Resources/Prototypes/Voice/tail_emotes.yml b/Resources/Prototypes/Voice/tail_emotes.yml
deleted file mode 100644 (file)
index 610a2ea..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-- type: emote
-  id: WagTail
-  chatMessages: [wags tail]
-  chatTriggers:
-    - wag
-    - wag.
-    - wags
-    - wags.
-    - wagging
-    - wagging.
-    - wag tail
-    - wag tail.
-    - wags tail
-    - wags tail.
diff --git a/Resources/Textures/Interface/emotes.svg b/Resources/Textures/Interface/emotes.svg
new file mode 100644 (file)
index 0000000..352f7ed
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="height: 512px; width: 512px;"><g class="" transform="translate(0,0)" style=""><path d="M65.44 18.39l-2.327 9.717C53.95 66.384 49.07 107.884 49.07 151.293c0 93.415 23.097 178.085 61.047 240.014 17.218 28.096 37.652 51.6 60.447 68.92 26.69 21.257 56.353 32.962 87.377 32.962.364 0 1.147-.12 1.927-.25.623.008 1.247.02 1.87.02 60.13 0 113.67-39.724 151.62-101.653 37.95-61.93 61.047-146.598 61.047-240.014 0-41.557-4.858-81.203-13.256-118.012l-2.324-10.19-9.582 4.176c-50.92 22.196-113.98 35.705-182.086 35.713-2.014-.022-4.01-.06-6.002-.103V62.8c-1.296 0-2.586-.017-3.88-.03-69.783-2.053-125.493-18.078-182.545-40.698l-9.29-3.683zm380.816 28.747c6.792 32.774 10.824 67.647 10.824 104.156 0 90.547-22.596 172.38-58.494 230.963-35.9 58.582-84.36 93.38-136.848 93.38-.195 0-.39-.006-.584-.007v-63.987c-2.64.023-5.28-.03-7.914-.163-55.358-2.77-109.316-38.91-122.03-99.742l-2.355-11.256h94.895l37.404 14.207V80.206c1.946.042 3.896.078 5.862.098h.087c66.168 0 127.672-12.383 179.152-33.168zm-279.53 98.12c35.365 0 64.036 13.248 64.036 29.59 0 16.34-28.668 29.585-64.035 29.585-35.365 0-64.036-13.246-64.036-29.586 0-16.34 28.67-29.588 64.037-29.588zm186.282 0c-35.367 0-64.035 13.248-64.035 29.59 0 16.34 28.67 29.585 64.035 29.585 35.367 0 64.035-13.246 64.035-29.586 0-16.34-28.67-29.588-64.035-29.588zM152.572 319.17c14.72 45.053 57.247 71.428 101.602 73.646 44.8 2.24 90.238-19.45 110.416-73.646h-57.447l-44.204 16.187-42.62-16.187h-67.748z" fill="#fff" fill-opacity="1" data-darkreader-inline-fill="" style="--darkreader-inline-fill: #181a1b;"></path></g></svg>
\ No newline at end of file
diff --git a/Resources/Textures/Interface/emotes.svg.192dpi.png b/Resources/Textures/Interface/emotes.svg.192dpi.png
new file mode 100644 (file)
index 0000000..4e1f3c4
Binary files /dev/null and b/Resources/Textures/Interface/emotes.svg.192dpi.png differ
diff --git a/Resources/Textures/Interface/emotes.svg.192dpi.png.yml b/Resources/Textures/Interface/emotes.svg.192dpi.png.yml
new file mode 100644 (file)
index 0000000..dabd660
--- /dev/null
@@ -0,0 +1,2 @@
+sample:
+  filter: true
index b08f4cb4ed02f8feb07551e614619d0300a8b53d..c11f59d17c9370f643cba6da3bccfbe758d50356 100644 (file)
@@ -187,6 +187,9 @@ binds:
 - function: OpenCharacterMenu
   type: State
   key: C
+- function: OpenEmotesMenu
+  type: State
+  key: Y
 - function: TextCursorSelect
   # TextCursorSelect HAS to be above ExamineEntity
   # So that LineEdit receives it correctly.