Add emotes as borderless speech "bubbles"

This commit is contained in:
Hugo Laloge
2020-04-30 18:09:09 +02:00
parent 6ff691c338
commit a34983b095
3 changed files with 137 additions and 30 deletions

View File

@@ -19,6 +19,12 @@ namespace Content.Client.Chat
{ {
internal sealed class ChatManager : IChatManager internal sealed class ChatManager : IChatManager
{ {
private struct SpeechBubbleData
{
public string Message;
public SpeechBubble.SpeechType Type;
}
/// <summary> /// <summary>
/// The max amount of chars allowed to fit in a single speech bubble. /// The max amount of chars allowed to fit in a single speech bubble.
/// </summary> /// </summary>
@@ -117,7 +123,7 @@ namespace Content.Client.Chat
var msg = queueData.MessageQueue.Dequeue(); 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. // 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. // 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); WriteChatMessage(storedMessage);
// Local messages that have an entity attached get a speech bubble. // 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)) if (!_entityManager.TryGetEntity(msg.SenderEntity, out var entity))
{ {
@@ -305,8 +321,18 @@ namespace Content.Client.Chat
return; return;
} }
var messages = SplitMessage(msg.Message);
foreach (var message in messages)
{
EnqueueSpeechBubble(entity, message, speechType);
}
}
private List<string> SplitMessage(string msg)
{
// Split message into words separated by spaces. // Split message into words separated by spaces.
var words = msg.Message.Split(' '); var words = msg.Split(' ');
var messages = new List<string>(); var messages = new List<string>();
var currentBuffer = new List<string>(); var currentBuffer = new List<string>();
@@ -346,13 +372,10 @@ namespace Content.Client.Chat
messages.Add(string.Join(" ", currentBuffer)); messages.Add(string.Join(" ", currentBuffer));
} }
foreach (var message in messages) return messages;
{
EnqueueSpeechBubble(entity, message);
}
} }
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)) if (!_queuedSpeechBubbles.TryGetValue(entity.Uid, out var queueData))
{ {
@@ -360,12 +383,16 @@ namespace Content.Client.Chat
_queuedSpeechBubbles.Add(entity.Uid, queueData); _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)) if (_activeSpeechBubbles.TryGetValue(entity.Uid, out var existing))
{ {
@@ -405,7 +432,7 @@ namespace Content.Client.Chat
/// </summary> /// </summary>
public float TimeLeft { get; set; } public float TimeLeft { get; set; }
public Queue<string> MessageQueue { get; } = new Queue<string>(); public Queue<SpeechBubbleData> MessageQueue { get; } = new Queue<SpeechBubbleData>();
} }
} }
} }

View File

@@ -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.Interfaces.Graphics.ClientEye;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
@@ -9,8 +10,14 @@ using Robust.Shared.Timing;
namespace Content.Client.Chat namespace Content.Client.Chat
{ {
public class SpeechBubble : Control public abstract class SpeechBubble : Control
{ {
public enum SpeechType
{
Emote,
Say
}
/// <summary> /// <summary>
/// The total time a speech bubble stays on screen. /// The total time a speech bubble stays on screen.
/// </summary> /// </summary>
@@ -38,6 +45,21 @@ namespace Content.Client.Chat
public float ContentHeight { get; private set; } 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) public SpeechBubble(string text, IEntity senderEntity, IEyeManager eyeManager, IChatManager chatManager)
{ {
_chatManager = 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. // Use text clipping so new messages don't overlap old ones being pushed up.
RectClipContent = true; RectClipContent = true;
var label = new RichTextLabel var bubble = BuildBubble(text);
{
MaxWidth = 256,
};
label.SetMessage(text);
var panel = new PanelContainer AddChild(bubble);
{
StyleClasses = { "tooltipBox" },
Children = { label },
ModulateSelfOverride = Color.White.WithAlpha(0.75f)
};
AddChild(panel);
ForceRunStyleUpdate(); ForceRunStyleUpdate();
ContentHeight = panel.CombinedMinimumSize.Y; ContentHeight = bubble.CombinedMinimumSize.Y;
_verticalOffsetAchieved = -ContentHeight; _verticalOffsetAchieved = -ContentHeight;
} }
protected abstract Control BuildBubble(string text);
protected override Vector2 CalculateMinimumSize() protected override Vector2 CalculateMinimumSize()
{ {
return (base.CalculateMinimumSize().X, 0); 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;
}
}
} }

View File

@@ -44,6 +44,7 @@ namespace Content.Client.UserInterface.Stylesheets
var notoSans10 = resCache.GetFont("/Nano/NotoSans/NotoSans-Regular.ttf", 10); var notoSans10 = resCache.GetFont("/Nano/NotoSans/NotoSans-Regular.ttf", 10);
var notoSansItalic10 = resCache.GetFont("/Nano/NotoSans/NotoSans-Italic.ttf", 10); var notoSansItalic10 = resCache.GetFont("/Nano/NotoSans/NotoSans-Italic.ttf", 10);
var notoSans12 = resCache.GetFont("/Nano/NotoSans/NotoSans-Regular.ttf", 12); 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 notoSansBold12 = resCache.GetFont("/Nano/NotoSans/NotoSans-Bold.ttf", 12);
var notoSansDisplayBold14 = resCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 14); var notoSansDisplayBold14 = resCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 14);
var notoSans16 = resCache.GetFont("/Nano/NotoSans/NotoSans-Regular.ttf", 16); 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 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 // Entity tooltip
new StyleRule( new StyleRule(
new SelectorElement(typeof(PanelContainer), new[] {ExamineSystem.StyleClassEntityTooltip}, null, new SelectorElement(typeof(PanelContainer), new[] {ExamineSystem.StyleClassEntityTooltip}, null,