* Adds option to disable character names in chat/speechbubbles * Moved the coloring of names to clientside * Move string functions to SharedChatSystem to avoid duplicate code in SpeechBubble.cs * Changed to be put under Accessibility section * Cache CVar
291 lines
10 KiB
C#
291 lines
10 KiB
C#
using System.Numerics;
|
|
using Content.Client.Chat.Managers;
|
|
using Content.Shared.CCVar;
|
|
using Content.Shared.Chat;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Client.UserInterface;
|
|
using Robust.Client.UserInterface.Controls;
|
|
using Robust.Shared.Configuration;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Content.Client.Chat.UI
|
|
{
|
|
public abstract class SpeechBubble : Control
|
|
{
|
|
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
|
[Dependency] private readonly IEntityManager _entityManager = default!;
|
|
[Dependency] protected readonly IConfigurationManager ConfigManager = default!;
|
|
|
|
public enum SpeechType : byte
|
|
{
|
|
Emote,
|
|
Say,
|
|
Whisper,
|
|
Looc
|
|
}
|
|
|
|
/// <summary>
|
|
/// The total time a speech bubble stays on screen.
|
|
/// </summary>
|
|
private const float TotalTime = 4;
|
|
|
|
/// <summary>
|
|
/// The amount of time at the end of the bubble's life at which it starts fading.
|
|
/// </summary>
|
|
private const float FadeTime = 0.25f;
|
|
|
|
/// <summary>
|
|
/// The distance in world space to offset the speech bubble from the center of the entity.
|
|
/// i.e. greater -> higher above the mob's head.
|
|
/// </summary>
|
|
private const float EntityVerticalOffset = 0.5f;
|
|
|
|
/// <summary>
|
|
/// The default maximum width for speech bubbles.
|
|
/// </summary>
|
|
public const float SpeechMaxWidth = 256;
|
|
|
|
private readonly EntityUid _senderEntity;
|
|
|
|
private float _timeLeft = TotalTime;
|
|
|
|
public float VerticalOffset { get; set; }
|
|
private float _verticalOffsetAchieved;
|
|
|
|
public Vector2 ContentSize { get; private set; }
|
|
|
|
// man down
|
|
public event Action<EntityUid, SpeechBubble>? OnDied;
|
|
|
|
public static SpeechBubble CreateSpeechBubble(SpeechType type, ChatMessage message, EntityUid senderEntity)
|
|
{
|
|
switch (type)
|
|
{
|
|
case SpeechType.Emote:
|
|
return new TextSpeechBubble(message, senderEntity, "emoteBox");
|
|
|
|
case SpeechType.Say:
|
|
return new FancyTextSpeechBubble(message, senderEntity, "sayBox");
|
|
|
|
case SpeechType.Whisper:
|
|
return new FancyTextSpeechBubble(message, senderEntity, "whisperBox");
|
|
|
|
case SpeechType.Looc:
|
|
return new TextSpeechBubble(message, senderEntity, "emoteBox", Color.FromHex("#48d1cc"));
|
|
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
}
|
|
|
|
public SpeechBubble(ChatMessage message, EntityUid senderEntity, string speechStyleClass, Color? fontColor = null)
|
|
{
|
|
IoCManager.InjectDependencies(this);
|
|
_senderEntity = senderEntity;
|
|
|
|
// Use text clipping so new messages don't overlap old ones being pushed up.
|
|
RectClipContent = true;
|
|
|
|
var bubble = BuildBubble(message, speechStyleClass, fontColor);
|
|
|
|
AddChild(bubble);
|
|
|
|
ForceRunStyleUpdate();
|
|
|
|
bubble.Measure(Vector2Helpers.Infinity);
|
|
ContentSize = bubble.DesiredSize;
|
|
_verticalOffsetAchieved = -ContentSize.Y;
|
|
}
|
|
|
|
protected abstract Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null);
|
|
|
|
protected override void FrameUpdate(FrameEventArgs args)
|
|
{
|
|
base.FrameUpdate(args);
|
|
|
|
_timeLeft -= args.DeltaSeconds;
|
|
if (_entityManager.Deleted(_senderEntity) || _timeLeft <= 0)
|
|
{
|
|
// Timer spawn to prevent concurrent modification exception.
|
|
Timer.Spawn(0, Die);
|
|
return;
|
|
}
|
|
|
|
// Lerp to our new vertical offset if it's been modified.
|
|
if (MathHelper.CloseToPercent(_verticalOffsetAchieved - VerticalOffset, 0, 0.1))
|
|
{
|
|
_verticalOffsetAchieved = VerticalOffset;
|
|
}
|
|
else
|
|
{
|
|
_verticalOffsetAchieved = MathHelper.Lerp(_verticalOffsetAchieved, VerticalOffset, 10 * args.DeltaSeconds);
|
|
}
|
|
|
|
if (!_entityManager.TryGetComponent<TransformComponent>(_senderEntity, out var xform) || xform.MapID != _eyeManager.CurrentMap)
|
|
{
|
|
Modulate = Color.White.WithAlpha(0);
|
|
return;
|
|
}
|
|
|
|
if (_timeLeft <= FadeTime)
|
|
{
|
|
// Update alpha if we're fading.
|
|
Modulate = Color.White.WithAlpha(_timeLeft / FadeTime);
|
|
}
|
|
else
|
|
{
|
|
// Make opaque otherwise, because it might have been hidden before
|
|
Modulate = Color.White;
|
|
}
|
|
|
|
var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -EntityVerticalOffset;
|
|
var worldPos = xform.WorldPosition + offset;
|
|
|
|
var lowerCenter = _eyeManager.WorldToScreen(worldPos) / UIScale;
|
|
var screenPos = lowerCenter - new Vector2(ContentSize.X / 2, ContentSize.Y + _verticalOffsetAchieved);
|
|
// Round to nearest 0.5
|
|
screenPos = (screenPos * 2).Rounded() / 2;
|
|
LayoutContainer.SetPosition(this, screenPos);
|
|
|
|
var height = MathF.Ceiling(MathHelper.Clamp(lowerCenter.Y - screenPos.Y, 0, ContentSize.Y));
|
|
SetHeight = height;
|
|
}
|
|
|
|
private void Die()
|
|
{
|
|
if (Disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
OnDied?.Invoke(_senderEntity, this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Causes the speech bubble to start fading IMMEDIATELY.
|
|
/// </summary>
|
|
public void FadeNow()
|
|
{
|
|
if (_timeLeft > FadeTime)
|
|
{
|
|
_timeLeft = FadeTime;
|
|
}
|
|
}
|
|
|
|
protected FormattedMessage FormatSpeech(string message, Color? fontColor = null)
|
|
{
|
|
var msg = new FormattedMessage();
|
|
if (fontColor != null)
|
|
msg.PushColor(fontColor.Value);
|
|
msg.AddMarkup(message);
|
|
return msg;
|
|
}
|
|
|
|
protected FormattedMessage ExtractAndFormatSpeechSubstring(ChatMessage message, string tag, Color? fontColor = null)
|
|
{
|
|
return FormatSpeech(SharedChatSystem.GetStringInsideTag(message, tag), fontColor);
|
|
}
|
|
|
|
}
|
|
|
|
public sealed class TextSpeechBubble : SpeechBubble
|
|
{
|
|
public TextSpeechBubble(ChatMessage message, EntityUid senderEntity, string speechStyleClass, Color? fontColor = null)
|
|
: base(message, senderEntity, speechStyleClass, fontColor)
|
|
{
|
|
}
|
|
|
|
protected override Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null)
|
|
{
|
|
var label = new RichTextLabel
|
|
{
|
|
MaxWidth = SpeechMaxWidth,
|
|
};
|
|
|
|
label.SetMessage(FormatSpeech(message.WrappedMessage, fontColor));
|
|
|
|
var panel = new PanelContainer
|
|
{
|
|
StyleClasses = { "speechBox", speechStyleClass },
|
|
Children = { label },
|
|
ModulateSelfOverride = Color.White.WithAlpha(0.75f)
|
|
};
|
|
|
|
return panel;
|
|
}
|
|
}
|
|
|
|
public sealed class FancyTextSpeechBubble : SpeechBubble
|
|
{
|
|
|
|
public FancyTextSpeechBubble(ChatMessage message, EntityUid senderEntity, string speechStyleClass, Color? fontColor = null)
|
|
: base(message, senderEntity, speechStyleClass, fontColor)
|
|
{
|
|
}
|
|
|
|
protected override Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null)
|
|
{
|
|
if (!ConfigManager.GetCVar(CCVars.ChatEnableFancyBubbles))
|
|
{
|
|
var label = new RichTextLabel
|
|
{
|
|
MaxWidth = SpeechMaxWidth
|
|
};
|
|
|
|
label.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor));
|
|
|
|
var unfanciedPanel = new PanelContainer
|
|
{
|
|
StyleClasses = { "speechBox", speechStyleClass },
|
|
Children = { label },
|
|
ModulateSelfOverride = Color.White.WithAlpha(0.75f)
|
|
};
|
|
return unfanciedPanel;
|
|
}
|
|
|
|
var bubbleHeader = new RichTextLabel
|
|
{
|
|
Margin = new Thickness(1, 1, 1, 1)
|
|
};
|
|
|
|
var bubbleContent = new RichTextLabel
|
|
{
|
|
MaxWidth = SpeechMaxWidth,
|
|
Margin = new Thickness(2, 6, 2, 2)
|
|
};
|
|
|
|
//We'll be honest. *Yes* this is hacky. Doing this in a cleaner way would require a bottom-up refactor of how saycode handles sending chat messages. -Myr
|
|
bubbleHeader.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleHeader", fontColor));
|
|
bubbleContent.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor));
|
|
|
|
//As for below: Some day this could probably be converted to xaml. But that is not today. -Myr
|
|
var mainPanel = new PanelContainer
|
|
{
|
|
StyleClasses = { "speechBox", speechStyleClass },
|
|
Children = { bubbleContent },
|
|
ModulateSelfOverride = Color.White.WithAlpha(0.75f),
|
|
HorizontalAlignment = HAlignment.Center,
|
|
VerticalAlignment = VAlignment.Bottom,
|
|
Margin = new Thickness(4, 14, 4, 2)
|
|
};
|
|
|
|
var headerPanel = new PanelContainer
|
|
{
|
|
StyleClasses = { "speechBox", speechStyleClass },
|
|
Children = { bubbleHeader },
|
|
ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.ChatFancyNameBackground) ? 0.75f : 0f),
|
|
HorizontalAlignment = HAlignment.Center,
|
|
VerticalAlignment = VAlignment.Top
|
|
};
|
|
|
|
var panel = new PanelContainer
|
|
{
|
|
Children = { mainPanel, headerPanel }
|
|
};
|
|
|
|
return panel;
|
|
}
|
|
}
|
|
}
|