diff --git a/Content.Client/Chat/ChatManager.cs b/Content.Client/Chat/ChatManager.cs
index 8592028d11..61d1d4ae03 100644
--- a/Content.Client/Chat/ChatManager.cs
+++ b/Content.Client/Chat/ChatManager.cs
@@ -19,6 +19,12 @@ namespace Content.Client.Chat
{
internal sealed class ChatManager : IChatManager
{
+ private struct SpeechBubbleData
+ {
+ public string Message;
+ public SpeechBubble.SpeechType Type;
+ }
+
///
/// The max amount of chars allowed to fit in a single speech bubble.
///
@@ -117,7 +123,7 @@ namespace Content.Client.Chat
var msg = queueData.MessageQueue.Dequeue();
- queueData.TimeLeft += BubbleDelayBase + msg.Length * BubbleDelayFactor;
+ queueData.TimeLeft += BubbleDelayBase + msg.Message.Length * BubbleDelayFactor;
// We keep the queue around while it has 0 items. This allows us to keep the timer.
// When the timer hits 0 and there's no messages left, THEN we can clear it up.
@@ -291,13 +297,23 @@ namespace Content.Client.Chat
WriteChatMessage(storedMessage);
// Local messages that have an entity attached get a speech bubble.
- if ((msg.Channel == ChatChannel.Local || msg.Channel == ChatChannel.Dead) && msg.SenderEntity != default)
+ if (msg.SenderEntity == default)
+ return;
+
+ switch (msg.Channel)
{
- AddSpeechBubble(msg);
+ case ChatChannel.Local:
+ case ChatChannel.Dead:
+ AddSpeechBubble(msg, SpeechBubble.SpeechType.Say);
+ break;
+
+ case ChatChannel.Emotes:
+ AddSpeechBubble(msg, SpeechBubble.SpeechType.Emote);
+ break;
}
}
- private void AddSpeechBubble(MsgChatMessage msg)
+ private void AddSpeechBubble(MsgChatMessage msg, SpeechBubble.SpeechType speechType)
{
if (!_entityManager.TryGetEntity(msg.SenderEntity, out var entity))
{
@@ -305,8 +321,18 @@ namespace Content.Client.Chat
return;
}
+ var messages = SplitMessage(msg.Message);
+
+ foreach (var message in messages)
+ {
+ EnqueueSpeechBubble(entity, message, speechType);
+ }
+ }
+
+ private List SplitMessage(string msg)
+ {
// Split message into words separated by spaces.
- var words = msg.Message.Split(' ');
+ var words = msg.Split(' ');
var messages = new List();
var currentBuffer = new List();
@@ -346,13 +372,10 @@ namespace Content.Client.Chat
messages.Add(string.Join(" ", currentBuffer));
}
- foreach (var message in messages)
- {
- EnqueueSpeechBubble(entity, message);
- }
+ return messages;
}
- private void EnqueueSpeechBubble(IEntity entity, string contents)
+ private void EnqueueSpeechBubble(IEntity entity, string contents, SpeechBubble.SpeechType speechType)
{
if (!_queuedSpeechBubbles.TryGetValue(entity.Uid, out var queueData))
{
@@ -360,12 +383,16 @@ namespace Content.Client.Chat
_queuedSpeechBubbles.Add(entity.Uid, queueData);
}
- queueData.MessageQueue.Enqueue(contents);
+ queueData.MessageQueue.Enqueue(new SpeechBubbleData
+ {
+ Message = contents,
+ Type = speechType,
+ });
}
- private void CreateSpeechBubble(IEntity entity, string contents)
+ private void CreateSpeechBubble(IEntity entity, SpeechBubbleData speechData)
{
- var bubble = new SpeechBubble(contents, entity, _eyeManager, this);
+ var bubble = SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity, _eyeManager, this);
if (_activeSpeechBubbles.TryGetValue(entity.Uid, out var existing))
{
@@ -405,7 +432,7 @@ namespace Content.Client.Chat
///
public float TimeLeft { get; set; }
- public Queue MessageQueue { get; } = new Queue();
+ public Queue MessageQueue { get; } = new Queue();
}
}
}
diff --git a/Content.Client/Chat/SpeechBubble.cs b/Content.Client/Chat/SpeechBubble.cs
index 2766ab1b0b..147c3a8d5a 100644
--- a/Content.Client/Chat/SpeechBubble.cs
+++ b/Content.Client/Chat/SpeechBubble.cs
@@ -1,4 +1,5 @@
-using Content.Client.Interfaces.Chat;
+using System;
+using Content.Client.Interfaces.Chat;
using Robust.Client.Interfaces.Graphics.ClientEye;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -9,8 +10,14 @@ using Robust.Shared.Timing;
namespace Content.Client.Chat
{
- public class SpeechBubble : Control
+ public abstract class SpeechBubble : Control
{
+ public enum SpeechType
+ {
+ Emote,
+ Say
+ }
+
///
/// The total time a speech bubble stays on screen.
///
@@ -38,6 +45,21 @@ namespace Content.Client.Chat
public float ContentHeight { get; private set; }
+ public static SpeechBubble CreateSpeechBubble(SpeechType type, string text, IEntity senderEntity, IEyeManager eyeManager, IChatManager chatManager)
+ {
+ switch (type)
+ {
+ case SpeechType.Emote:
+ return new EmoteSpeechBubble(text, senderEntity, eyeManager, chatManager);
+
+ case SpeechType.Say:
+ return new SaySpeechBubble(text, senderEntity, eyeManager, chatManager);
+
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
public SpeechBubble(string text, IEntity senderEntity, IEyeManager eyeManager, IChatManager chatManager)
{
_chatManager = chatManager;
@@ -47,27 +69,18 @@ namespace Content.Client.Chat
// Use text clipping so new messages don't overlap old ones being pushed up.
RectClipContent = true;
- var label = new RichTextLabel
- {
- MaxWidth = 256,
- };
- label.SetMessage(text);
+ var bubble = BuildBubble(text);
- var panel = new PanelContainer
- {
- StyleClasses = { "tooltipBox" },
- Children = { label },
- ModulateSelfOverride = Color.White.WithAlpha(0.75f)
- };
-
- AddChild(panel);
+ AddChild(bubble);
ForceRunStyleUpdate();
- ContentHeight = panel.CombinedMinimumSize.Y;
+ ContentHeight = bubble.CombinedMinimumSize.Y;
_verticalOffsetAchieved = -ContentHeight;
}
+ protected abstract Control BuildBubble(string text);
+
protected override Vector2 CalculateMinimumSize()
{
return (base.CalculateMinimumSize().X, 0);
@@ -134,4 +147,57 @@ namespace Content.Client.Chat
}
}
}
+
+ public class EmoteSpeechBubble : SpeechBubble
+
+ {
+ public EmoteSpeechBubble(string text, IEntity senderEntity, IEyeManager eyeManager, IChatManager chatManager)
+ : base(text, senderEntity, eyeManager, chatManager)
+ {
+ }
+
+ protected override Control BuildBubble(string text)
+ {
+ var label = new RichTextLabel
+ {
+ MaxWidth = 256,
+ };
+ label.SetMessage(text);
+
+ var panel = new PanelContainer
+ {
+ StyleClasses = { "speechBox", "emoteBox" },
+ Children = { label },
+ ModulateSelfOverride = Color.White.WithAlpha(0.75f)
+ };
+
+ return panel;
+ }
+ }
+
+ public class SaySpeechBubble : SpeechBubble
+ {
+ public SaySpeechBubble(string text, IEntity senderEntity, IEyeManager eyeManager, IChatManager chatManager)
+ : base(text, senderEntity, eyeManager, chatManager)
+ {
+ }
+
+ protected override Control BuildBubble(string text)
+ {
+ var label = new RichTextLabel
+ {
+ MaxWidth = 256,
+ };
+ label.SetMessage(text);
+
+ var panel = new PanelContainer
+ {
+ StyleClasses = { "speechBox", "sayBox" },
+ Children = { label },
+ ModulateSelfOverride = Color.White.WithAlpha(0.75f)
+ };
+
+ return panel;
+ }
+ }
}
diff --git a/Content.Client/UserInterface/Stylesheets/StyleNano.cs b/Content.Client/UserInterface/Stylesheets/StyleNano.cs
index a96413b9d0..d2d9941eb8 100644
--- a/Content.Client/UserInterface/Stylesheets/StyleNano.cs
+++ b/Content.Client/UserInterface/Stylesheets/StyleNano.cs
@@ -44,6 +44,7 @@ namespace Content.Client.UserInterface.Stylesheets
var notoSans10 = resCache.GetFont("/Nano/NotoSans/NotoSans-Regular.ttf", 10);
var notoSansItalic10 = resCache.GetFont("/Nano/NotoSans/NotoSans-Italic.ttf", 10);
var notoSans12 = resCache.GetFont("/Nano/NotoSans/NotoSans-Regular.ttf", 12);
+ var notoSansItalic12 = resCache.GetFont("/Nano/NotoSans/NotoSans-Italic.ttf", 12);
var notoSansBold12 = resCache.GetFont("/Nano/NotoSans/NotoSans-Bold.ttf", 12);
var notoSansDisplayBold14 = resCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 14);
var notoSans16 = resCache.GetFont("/Nano/NotoSans/NotoSans-Regular.ttf", 16);
@@ -451,6 +452,19 @@ namespace Content.Client.UserInterface.Stylesheets
new StyleProperty(PanelContainer.StylePropertyPanel, tooltipBox)
}),
+ new StyleRule(new SelectorElement(typeof(PanelContainer), new[] {"speechBox", "sayBox"}, null, null), new[]
+ {
+ new StyleProperty(PanelContainer.StylePropertyPanel, tooltipBox)
+ }),
+
+ new StyleRule(new SelectorChild(
+ new SelectorElement(typeof(PanelContainer), new[] {"speechBox", "emoteBox"}, null, null),
+ new SelectorElement(typeof(RichTextLabel), null, null, null)),
+ new[]
+ {
+ new StyleProperty("font", notoSansItalic12),
+ }),
+
// Entity tooltip
new StyleRule(
new SelectorElement(typeof(PanelContainer), new[] {ExamineSystem.StyleClassEntityTooltip}, null,