Speech bubbles yo.

This commit is contained in:
Pieter-Jan Briers
2019-07-30 23:13:05 +02:00
parent 388cc8fdde
commit 0086e60b6a
5 changed files with 366 additions and 24 deletions

View File

@@ -1,23 +1,48 @@
using System.Collections.Generic;
using Content.Client.Interfaces.Chat;
using Content.Shared.Chat;
using Robust.Client;
using Robust.Client.Console;
using Robust.Client.Interfaces.Graphics.ClientEye;
using Robust.Client.Interfaces.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Chat
{
internal sealed class ChatManager : IChatManager
{
/// <summary>
/// The max amount of chars allowed to fit in a single speech bubble.
/// </summary>
private const int SingleBubbleCharLimit = 100;
/// <summary>
/// Base queue delay each speech bubble has.
/// </summary>
private const float BubbleDelayBase = 0.2f;
/// <summary>
/// Factor multiplied by speech bubble char length to add to delay.
/// </summary>
private const float BubbleDelayFactor = 0.8f / SingleBubbleCharLimit;
/// <summary>
/// The max amount of speech bubbles over a single entity at once.
/// </summary>
private const int SpeechBubbleCap = 4;
private const char ConCmdSlash = '/';
private const char OOCAlias = '[';
private const char MeAlias = '@';
public List<StoredChatMessage> filteredHistory = new List<StoredChatMessage>();
private readonly List<StoredChatMessage> filteredHistory = new List<StoredChatMessage>();
// Filter Button States
private bool _allState;
@@ -30,15 +55,69 @@ namespace Content.Client.Chat
#pragma warning disable 649
[Dependency] private readonly IClientNetManager _netManager;
[Dependency] private readonly IClientConsole _console;
[Dependency] private readonly IEntityManager _entityManager;
[Dependency] private readonly IEyeManager _eyeManager;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager;
#pragma warning restore 649
private ChatBox _currentChatBox;
/// <summary>
/// Speech bubbles that are currently visible on screen.
/// We track them to push them up when new ones get added.
/// </summary>
private readonly Dictionary<EntityUid, List<SpeechBubble>> _activeSpeechBubbles =
new Dictionary<EntityUid, List<SpeechBubble>>();
/// <summary>
/// Speech bubbles that are to-be-sent because of the "rate limit" they have.
/// </summary>
private readonly Dictionary<EntityUid, SpeechBubbleQueueData> _queuedSpeechBubbles
= new Dictionary<EntityUid, SpeechBubbleQueueData>();
public void Initialize()
{
_netManager.RegisterNetMessage<MsgChatMessage>(MsgChatMessage.NAME, _onChatMessage);
}
public void FrameUpdate(RenderFrameEventArgs delta)
{
// Update queued speech bubbles.
if (_queuedSpeechBubbles.Count == 0)
{
return;
}
foreach (var (entityUid, queueData) in _queuedSpeechBubbles.ShallowClone())
{
if (!_entityManager.TryGetEntity(entityUid, out var entity))
{
_queuedSpeechBubbles.Remove(entityUid);
continue;
}
queueData.TimeLeft -= delta.Elapsed;
if (queueData.TimeLeft > 0)
{
continue;
}
if (queueData.MessageQueue.Count == 0)
{
_queuedSpeechBubbles.Remove(entityUid);
continue;
}
var msg = queueData.MessageQueue.Dequeue();
queueData.TimeLeft += BubbleDelayBase + msg.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.
CreateSpeechBubble(entity, msg);
}
}
public void SetChatBox(ChatBox chatBox)
{
if (_currentChatBox != null)
@@ -60,6 +139,19 @@ namespace Content.Client.Chat
_currentChatBox.OOCButton.Pressed = !_oocState;
}
public void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble)
{
bubble.Dispose();
var list = _activeSpeechBubbles[entityUid];
list.Remove(bubble);
if (list.Count == 0)
{
_activeSpeechBubbles.Remove(entityUid);
}
}
private void WriteChatMessage(StoredChatMessage message)
{
Logger.Debug($"{message.Channel}: {message.Message}");
@@ -88,7 +180,6 @@ namespace Content.Client.Chat
}
_currentChatBox?.AddLine(messageText, message.Channel, color);
}
private void _onChatBoxTextSubmitted(ChatBox chatBox, string text)
@@ -185,15 +276,126 @@ namespace Content.Client.Chat
Logger.Debug($"{msg.Channel}: {msg.Message}");
// Log all incoming chat to repopulate when filter is un-toggled
StoredChatMessage storedMessage = new StoredChatMessage(msg);
var storedMessage = new StoredChatMessage(msg);
filteredHistory.Add(storedMessage);
WriteChatMessage(storedMessage);
// Local messages that have an entity attached get a speech bubble.
if (msg.Channel == ChatChannel.Local && msg.SenderEntity != default)
{
AddSpeechBubble(msg);
}
}
private void AddSpeechBubble(MsgChatMessage msg)
{
if (!_entityManager.TryGetEntity(msg.SenderEntity, out var entity))
{
Logger.WarningS("chat", "Got local chat message with invalid sender entity: {0}", msg.SenderEntity);
return;
}
// Split message into words separated by spaces.
var words = msg.Message.Split(' ');
var messages = new List<string>();
var currentBuffer = new List<string>();
// Really shoddy way to approximate word length.
// Yes, I am aware of all the crimes here.
// TODO: Improve this to use actual glyph width etc..
var currentWordLength = 0;
foreach (var word in words)
{
// +1 for the space.
currentWordLength += word.Length + 1;
if (currentWordLength > SingleBubbleCharLimit)
{
// Too long for the current speech bubble, flush it.
messages.Add(string.Join(" ", currentBuffer));
currentBuffer.Clear();
currentWordLength = word.Length;
if (currentWordLength > SingleBubbleCharLimit)
{
// Word is STILL too long.
// Truncate it with an ellipse.
messages.Add($"{word.Substring(0, SingleBubbleCharLimit-3)}...");
currentWordLength = 0;
continue;
}
}
currentBuffer.Add(word);
}
if (currentBuffer.Count != 0)
{
// Don't forget the last bubble.
messages.Add(string.Join(" ", currentBuffer));
}
foreach (var message in messages)
{
EnqueueSpeechBubble(entity, message);
}
}
private void EnqueueSpeechBubble(IEntity entity, string contents)
{
if (!_queuedSpeechBubbles.TryGetValue(entity.Uid, out var queueData))
{
queueData = new SpeechBubbleQueueData();
_queuedSpeechBubbles.Add(entity.Uid, queueData);
}
queueData.MessageQueue.Enqueue(contents);
}
private void CreateSpeechBubble(IEntity entity, string contents)
{
var bubble = new SpeechBubble(contents, entity, _eyeManager, this);
if (_activeSpeechBubbles.TryGetValue(entity.Uid, out var existing))
{
// Push up existing bubbles above the mob's head.
foreach (var existingBubble in existing)
{
existingBubble.VerticalOffset += bubble.ContentHeight;
}
}
else
{
existing = new List<SpeechBubble>();
_activeSpeechBubbles.Add(entity.Uid, existing);
}
existing.Add(bubble);
_userInterfaceManager.StateRoot.AddChild(bubble);
if (existing.Count > SpeechBubbleCap)
{
// Get the oldest to start fading fast.
var last = existing[0];
last.FadeNow();
}
}
private bool IsFiltered(ChatChannel channel)
{
// _ALLstate works as inverter.
// _allState works as inverter.
return _allState ^ _filteredChannels.HasFlag(channel);
}
private sealed class SpeechBubbleQueueData
{
/// <summary>
/// Time left until the next speech bubble can appear.
/// </summary>
public float TimeLeft { get; set; }
public Queue<string> MessageQueue { get; } = new Queue<string>();
}
}
}