diff --git a/Content.Client/Chat/UI/EmotesMenu.xaml b/Content.Client/Chat/UI/EmotesMenu.xaml
new file mode 100644
index 0000000000..819a6543c4
--- /dev/null
+++ b/Content.Client/Chat/UI/EmotesMenu.xaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Chat/UI/EmotesMenu.xaml.cs b/Content.Client/Chat/UI/EmotesMenu.xaml.cs
new file mode 100644
index 0000000000..a26d319920
--- /dev/null
+++ b/Content.Client/Chat/UI/EmotesMenu.xaml.cs
@@ -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>? OnPlayEmote;
+
+ public EmotesMenu()
+ {
+ IoCManager.InjectDependencies(this);
+ RobustXamlLoader.Load(this);
+
+ _spriteSystem = _entManager.System();
+
+ var main = FindControl("Main");
+
+ var emotes = _prototypeManager.EnumeratePrototypes();
+ 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(player.Value, out var speech) &&
+ !speech.AllowedEmotes.Contains(emote.ID))
+ continue;
+
+ var parent = FindControl(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 ProtoId { get; set; }
+}
diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs
index 8a7ca3b773..7a8a993854 100644
--- a/Content.Client/Input/ContentContexts.cs
+++ b/Content.Client/Input/ContentContexts.cs
@@ -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
index 0000000000..7b86859a1a
--- /dev/null
+++ b/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs
@@ -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
+{
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IClyde _displayManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+
+ private MenuButton? EmotesButton => UIManager.GetActiveUIWidgetOrNull()?.EmotesButton;
+ private EmotesMenu? _menu;
+
+ public void OnStateEntered(GameplayState state)
+ {
+ CommandBinds.Builder
+ .Bind(ContentKeyFunctions.OpenEmotesMenu,
+ InputCmdHandler.FromDelegate(_ => ToggleEmotesMenu(false)))
+ .Register();
+ }
+
+ public void OnStateExited(GameplayState state)
+ {
+ CommandBinds.Unregister();
+ }
+
+ private void ToggleEmotesMenu(bool centered)
+ {
+ if (_menu == null)
+ {
+ // setup window
+ _menu = UIManager.CreateWindow();
+ _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 protoId)
+ {
+ _entityManager.RaisePredictiveEvent(new PlayEmoteMessage(protoId));
+ }
+}
diff --git a/Content.Client/UserInterface/Systems/MenuBar/GameTopMenuBarUIController.cs b/Content.Client/UserInterface/Systems/MenuBar/GameTopMenuBarUIController.cs
index 1505db48a7..e314310bc0 100644
--- a/Content.Client/UserInterface/Systems/MenuBar/GameTopMenuBarUIController.cs
+++ b/Content.Client/UserInterface/Systems/MenuBar/GameTopMenuBarUIController.cs
@@ -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();
@@ -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();
}
}
diff --git a/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml b/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml
index 3c8cd1d164..dc8972970a 100644
--- a/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml
+++ b/Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml
@@ -43,6 +43,16 @@
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
+
(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
index 0000000000..a69b5a65e4
--- /dev/null
+++ b/Content.Server/Speech/EmotesMenuSystem.cs
@@ -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(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);
+ }
+}
diff --git a/Content.Server/Speech/EntitySystems/VocalSystem.cs b/Content.Server/Speech/EntitySystems/VocalSystem.cs
index aedcbbd099..7c8ec21a94 100644
--- a/Content.Server/Speech/EntitySystems/VocalSystem.cs
+++ b/Content.Server/Speech/EntitySystems/VocalSystem.cs
@@ -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
index 0000000000..4479f8b2ab
--- /dev/null
+++ b/Content.Shared/Chat/EmotesEvents.cs
@@ -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 protoId) : EntityEventArgs
+{
+ public readonly ProtoId ProtoId = protoId;
+}
diff --git a/Content.Shared/Chat/Prototypes/EmotePrototype.cs b/Content.Shared/Chat/Prototypes/EmotePrototype.cs
index 08f209d28d..7ee958ee6a 100644
--- a/Content.Shared/Chat/Prototypes/EmotePrototype.cs
+++ b/Content.Shared/Chat/Prototypes/EmotePrototype.cs
@@ -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;
///
/// 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.
///
[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!;
+ ///
+ /// Localization string for the emote name. Displayed in the radial UI.
+ ///
+ [DataField(required: true)]
+ public string Name = default!;
+
+ ///
+ /// Determines if emote available to all by default
+ /// check comes after this setting
+ /// can ignore this setting
+ ///
+ [DataField]
+ public bool Available = true;
+
///
/// Different emote categories may be handled by different systems.
/// Also may be used for filtering.
///
- [DataField("category")]
+ [DataField]
public EmoteCategory Category = EmoteCategory.General;
+ ///
+ /// An icon used to visually represent the emote in radial UI.
+ ///
+ [DataField]
+ public SpriteSpecifier Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Actions/scream.png"));
+
+ ///
+ /// Determines conditions to this emote be available to use
+ ///
+ [DataField]
+ public EntityWhitelist? Whitelist;
+
+ ///
+ /// Determines conditions to this emote be unavailable to use
+ ///
+ [DataField]
+ public EntityWhitelist? Blacklist;
+
///
/// Collection of words that will be sent to chat if emote activates.
/// Will be picked randomly from list.
///
- [DataField("chatMessages")]
+ [DataField]
public List ChatMessages = new();
///
@@ -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.
///
- [DataField("chatTriggers")]
+ [DataField]
public HashSet ChatTriggers = new();
}
diff --git a/Content.Shared/Chat/Prototypes/EmoteSoundsPrototype.cs b/Content.Shared/Chat/Prototypes/EmoteSoundsPrototype.cs
index c9a78e7d6d..2b7064c1e9 100644
--- a/Content.Shared/Chat/Prototypes/EmoteSoundsPrototype.cs
+++ b/Content.Shared/Chat/Prototypes/EmoteSoundsPrototype.cs
@@ -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 .
/// Different entities may use different sounds collections.
///
-[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!;
diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs
index 886a0d5d3a..2dd671816f 100644
--- a/Content.Shared/Input/ContentKeyFunctions.cs
+++ b/Content.Shared/Input/ContentKeyFunctions.cs
@@ -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";
diff --git a/Content.Server/Speech/Components/VocalComponent.cs b/Content.Shared/Speech/Components/VocalComponent.cs
similarity index 83%
rename from Content.Server/Speech/Components/VocalComponent.cs
rename to Content.Shared/Speech/Components/VocalComponent.cs
index 029d638a66..e5d2c9997f 100644
--- a/Content.Server/Speech/Components/VocalComponent.cs
+++ b/Content.Shared/Speech/Components/VocalComponent.cs
@@ -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;
///
/// Component required for entities to be able to do vocal emotions.
///
-[RegisterComponent]
-[Access(typeof(VocalSystem))]
+[RegisterComponent, NetworkedComponent]
+[AutoGenerateComponentState]
public sealed partial class VocalComponent : Component
{
///
@@ -20,21 +20,27 @@ public sealed partial class VocalComponent : Component
/// Entities without considered to be .
///
[DataField("sounds", customTypeSerializer: typeof(PrototypeIdValueDictionarySerializer))]
+ [AutoNetworkedField]
public Dictionary? Sounds;
[DataField("screamId", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ [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))]
+ [AutoNetworkedField]
public string ScreamAction = "ActionScream";
[DataField("screamActionEntity")]
+ [AutoNetworkedField]
public EntityUid? ScreamActionEntity;
///
@@ -42,5 +48,6 @@ public sealed partial class VocalComponent : Component
/// Null if no valid prototype for entity sex was found.
///
[ViewVariables]
+ [AutoNetworkedField]
public EmoteSoundsPrototype? EmoteSounds = null;
}
diff --git a/Content.Shared/Speech/SpeechComponent.cs b/Content.Shared/Speech/SpeechComponent.cs
index 272d9ef8ca..0882120718 100644
--- a/Content.Shared/Speech/SpeechComponent.cs
+++ b/Content.Shared/Speech/SpeechComponent.cs
@@ -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 SpeechVerb = "Default";
+ ///
+ /// What emotes allowed to use event if emote is false
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
+ public List> AllowedEmotes = new();
+
///
/// 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.
diff --git a/Content.Shared/Wagging/WaggingComponent.cs b/Content.Shared/Wagging/WaggingComponent.cs
index 76881827dd..70e7f009c7 100644
--- a/Content.Shared/Wagging/WaggingComponent.cs
+++ b/Content.Shared/Wagging/WaggingComponent.cs
@@ -17,9 +17,6 @@ public sealed partial class WaggingComponent : Component
[DataField]
public EntityUid? ActionEntity;
- [DataField]
- public ProtoId EmoteId = "WagTail";
-
///
/// Suffix to add to get the animated marking.
///
diff --git a/Content.Shared/Whitelist/EntityWhitelist.cs b/Content.Shared/Whitelist/EntityWhitelist.cs
index 942de2b0e8..b412a09b98 100644
--- a/Content.Shared/Whitelist/EntityWhitelist.cs
+++ b/Content.Shared/Whitelist/EntityWhitelist.cs
@@ -94,6 +94,9 @@ namespace Content.Shared.Whitelist
return RequireAll ? tagSystem.HasAllTags(tags, Tags) : tagSystem.HasAnyTag(tags, Tags);
}
+ if (RequireAll)
+ return true;
+
return false;
}
}
diff --git a/Resources/Locale/en-US/HUD/game-hud.ftl b/Resources/Locale/en-US/HUD/game-hud.ftl
index 7f6573d2ad..ea423f080a 100644
--- a/Resources/Locale/en-US/HUD/game-hud.ftl
+++ b/Resources/Locale/en-US/HUD/game-hud.ftl
@@ -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
index 0000000000..86d79ffe4f
--- /dev/null
+++ b/Resources/Locale/en-US/chat/emotes.ftl
@@ -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
index 0000000000..1f92a93c63
--- /dev/null
+++ b/Resources/Locale/en-US/chat/ui/emote-menu.ftl
@@ -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
index 53c12312e5..0000000000
--- a/Resources/Locale/en-US/emotes/emotes.ftl
+++ /dev/null
@@ -1 +0,0 @@
-emote-deathgasp = seizes up and falls limp, {POSS-ADJ($entity)} eyes dead and lifeless...
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
index 1cf56c29c6..f8218023fb 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
@@ -16,6 +16,7 @@
- type: Speech
speechSounds: Squeak
speechVerb: SmallMob
+ allowedEmotes: ['Squeak']
- type: Fixtures
fixtures:
fix1:
@@ -432,6 +433,7 @@
- type: Speech
speechVerb: Moth
speechSounds: Squeak
+ allowedEmotes: ['Chitter']
- type: FaxableObject
insertingState: inserting_mothroach
- type: MothAccent
@@ -817,6 +819,7 @@
- type: Speech
speechVerb: Arachnid
speechSounds: Arachnid
+ allowedEmotes: ['Click']
- type: DamageStateVisuals
states:
Alive:
@@ -1509,6 +1512,7 @@
- type: Speech
speechSounds: Squeak
speechVerb: SmallMob
+ allowedEmotes: ['Squeak']
- type: Sprite
drawdepth: SmallMobs
sprite: Mobs/Animals/mouse.rsi
@@ -2232,6 +2236,7 @@
- type: Speech
speechVerb: Arachnid
speechSounds: Arachnid
+ allowedEmotes: ['Click']
- type: Vocal
sounds:
Male: UnisexArachnid
@@ -3037,6 +3042,7 @@
- type: Speech
speechVerb: SmallMob
speechSounds: Squeak
+ allowedEmotes: ['Squeak']
- type: Sprite
drawdepth: SmallMobs
sprite: Mobs/Animals/hamster.rsi
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml
index 95c30a174f..e667b931de 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml
@@ -125,6 +125,12 @@
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
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml
index 8cfd199dca..849cd83eba 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml
@@ -263,6 +263,7 @@
- type: Speech
speechVerb: Arachnid
speechSounds: Arachnid
+ allowedEmotes: ['Click']
- type: Vocal
sounds:
Male: UnisexArachnid
diff --git a/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml b/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml
index d59c7bfd02..ec742e59b5 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml
@@ -62,6 +62,7 @@
- type: Speech
speechVerb: Arachnid
speechSounds: Arachnid
+ allowedEmotes: ['Click']
- type: Vocal
sounds:
Male: UnisexArachnid
diff --git a/Resources/Prototypes/Entities/Mobs/Species/moth.yml b/Resources/Prototypes/Entities/Mobs/Species/moth.yml
index 199e99bef3..33bb46b172 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/moth.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/moth.yml
@@ -22,6 +22,7 @@
accent: zombieMoth
- type: Speech
speechVerb: Moth
+ allowedEmotes: ['Chitter']
- type: TypingIndicator
proto: moth
- type: Butcherable
diff --git a/Resources/Prototypes/Entities/Mobs/Species/slime.yml b/Resources/Prototypes/Entities/Mobs/Species/slime.yml
index 081973c3d2..2ab26ffcd6 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/slime.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/slime.yml
@@ -42,6 +42,7 @@
- type: Speech
speechVerb: Slime
speechSounds: Slime
+ allowedEmotes: ['Squish']
- type: TypingIndicator
proto: slime
- type: Vocal
diff --git a/Resources/Prototypes/Voice/disease_emotes.yml b/Resources/Prototypes/Voice/disease_emotes.yml
index af93025cae..c29d6dd017 100644
--- a/Resources/Prototypes/Voice/disease_emotes.yml
+++ b/Resources/Prototypes/Voice/disease_emotes.yml
@@ -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"]
diff --git a/Resources/Prototypes/Voice/speech_emotes.yml b/Resources/Prototypes/Voice/speech_emotes.yml
index 3b7ffc0107..a859a14c2b 100644
--- a/Resources/Prototypes/Voice/speech_emotes.yml
+++ b/Resources/Prototypes/Voice/speech_emotes.yml
@@ -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
@@ -31,8 +39,16 @@
- 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
@@ -64,8 +80,15 @@
- 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.
@@ -82,8 +105,16 @@
- 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
@@ -94,8 +125,16 @@
- 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.
@@ -109,8 +148,16 @@
- 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.
@@ -132,8 +179,17 @@
- 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.
@@ -147,8 +203,17 @@
- 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.
@@ -162,8 +227,16 @@
- 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.
@@ -177,8 +250,17 @@
- 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.
@@ -190,8 +272,16 @@
# 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
@@ -202,8 +292,16 @@
- 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
@@ -221,8 +319,16 @@
- 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.
@@ -233,14 +339,26 @@
- 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!
@@ -257,13 +375,20 @@
- 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!
@@ -281,8 +406,15 @@
# 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!
@@ -299,8 +431,15 @@
- 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.
@@ -317,8 +456,15 @@
- 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.
@@ -353,8 +499,15 @@
- 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
index 610a2ea801..0000000000
--- a/Resources/Prototypes/Voice/tail_emotes.yml
+++ /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
index 0000000000..352f7ed294
--- /dev/null
+++ b/Resources/Textures/Interface/emotes.svg
@@ -0,0 +1 @@
+
\ 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
index 0000000000..4e1f3c4f48
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
index 0000000000..dabd6601f7
--- /dev/null
+++ b/Resources/Textures/Interface/emotes.svg.192dpi.png.yml
@@ -0,0 +1,2 @@
+sample:
+ filter: true
diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml
index b08f4cb4ed..c11f59d17c 100644
--- a/Resources/keybinds.yml
+++ b/Resources/keybinds.yml
@@ -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.