| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- using System.Numerics;
- using Content.Client.Chat.Managers;
- using Content.Shared.CCVar;
- using Content.Shared.Chat;
- using Content.Shared.Speech;
- 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!;
- private readonly SharedTransformSystem _transformSystem;
- 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;
- _transformSystem = _entityManager.System<SharedTransformSystem>();
- // 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 baseOffset = 0f;
- if (_entityManager.TryGetComponent<SpeechComponent>(_senderEntity, out var speech))
- baseOffset = speech.SpeechBubbleOffset;
- var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -(EntityVerticalOffset + baseOffset);
- var worldPos = _transformSystem.GetWorldPosition(xform) + 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.AddMarkupOrThrow(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(ConfigManager.GetCVar(CCVars.SpeechBubbleBackgroundOpacity))
- };
- 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(ConfigManager.GetCVar(CCVars.SpeechBubbleBackgroundOpacity)),
- };
- return unfanciedPanel;
- }
- var bubbleHeader = new RichTextLabel
- {
- ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.SpeechBubbleSpeakerOpacity)),
- Margin = new Thickness(1, 1, 1, 1),
- };
- var bubbleContent = new RichTextLabel
- {
- ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.SpeechBubbleTextOpacity)),
- MaxWidth = SpeechMaxWidth,
- Margin = new Thickness(2, 6, 2, 2),
- StyleClasses = { "bubbleContent" },
- };
- //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(ConfigManager.GetCVar(CCVars.SpeechBubbleBackgroundOpacity)),
- 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) ? ConfigManager.GetCVar(CCVars.SpeechBubbleBackgroundOpacity) : 0f),
- HorizontalAlignment = HAlignment.Center,
- VerticalAlignment = VAlignment.Top
- };
- var panel = new PanelContainer
- {
- Children = { mainPanel, headerPanel }
- };
- return panel;
- }
- }
- }
|