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,