2023-07-08 14:08:32 +10:00
using System.Numerics ;
2024-01-28 17:32:55 +07:00
using Content.Client._White.UserInterface.Controls ;
2023-12-04 18:10:49 -05:00
using Content.Shared.CCVar ;
using Content.Shared.Chat ;
2021-02-11 01:13:03 -08:00
using Robust.Client.Graphics ;
2019-07-30 23:13:05 +02:00
using Robust.Client.UserInterface ;
using Robust.Client.UserInterface.Controls ;
2023-12-04 18:10:49 -05:00
using Robust.Shared.Configuration ;
2019-08-04 01:08:55 +02:00
using Robust.Shared.Timing ;
2023-11-09 10:18:58 +01:00
using Robust.Shared.Utility ;
2019-07-30 23:13:05 +02:00
2021-06-09 22:19:39 +02:00
namespace Content.Client.Chat.UI
2019-07-30 23:13:05 +02:00
{
2020-04-30 18:09:09 +02:00
public abstract class SpeechBubble : Control
2019-07-30 23:13:05 +02:00
{
2023-12-04 18:10:49 -05:00
[Dependency] private readonly IEyeManager _eyeManager = default ! ;
[Dependency] private readonly IEntityManager _entityManager = default ! ;
[Dependency] protected readonly IConfigurationManager ConfigManager = default ! ;
2020-12-04 11:57:33 +01:00
public enum SpeechType : byte
2020-04-30 18:09:09 +02:00
{
Emote ,
2022-01-11 06:48:18 -08:00
Say ,
2023-11-09 10:18:58 +01:00
Whisper ,
Looc
2020-04-30 18:09:09 +02:00
}
2019-07-30 23:13:05 +02:00
/// <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 ;
2023-12-04 18:10:49 -05:00
/// <summary>
/// The default maximum width for speech bubbles.
/// </summary>
public const float SpeechMaxWidth = 256 ;
2021-12-05 18:09:01 +01:00
private readonly EntityUid _senderEntity ;
2019-07-30 23:13:05 +02:00
private float _timeLeft = TotalTime ;
public float VerticalOffset { get ; set ; }
private float _verticalOffsetAchieved ;
2022-05-27 05:34:25 +02:00
public Vector2 ContentSize { get ; private set ; }
2019-07-30 23:13:05 +02:00
2022-10-12 01:16:23 -07:00
// man down
public event Action < EntityUid , SpeechBubble > ? OnDied ;
2023-12-04 18:10:49 -05:00
public static SpeechBubble CreateSpeechBubble ( SpeechType type , ChatMessage message , EntityUid senderEntity )
2020-04-30 18:09:09 +02:00
{
switch ( type )
{
case SpeechType . Emote :
2023-12-04 18:10:49 -05:00
return new TextSpeechBubble ( message , senderEntity , "emoteBox" ) ;
2020-04-30 18:09:09 +02:00
case SpeechType . Say :
2023-12-04 18:10:49 -05:00
return new FancyTextSpeechBubble ( message , senderEntity , "sayBox" ) ;
2022-01-11 06:48:18 -08:00
case SpeechType . Whisper :
2023-12-04 18:10:49 -05:00
return new FancyTextSpeechBubble ( message , senderEntity , "whisperBox" ) ;
2020-04-30 18:09:09 +02:00
2023-11-09 10:18:58 +01:00
case SpeechType . Looc :
2023-12-04 18:10:49 -05:00
return new TextSpeechBubble ( message , senderEntity , "emoteBox" , Color . FromHex ( "#48d1cc" ) ) ;
2023-11-09 10:18:58 +01:00
2020-04-30 18:09:09 +02:00
default :
throw new ArgumentOutOfRangeException ( ) ;
}
}
2023-12-04 18:10:49 -05:00
public SpeechBubble ( ChatMessage message , EntityUid senderEntity , string speechStyleClass , Color ? fontColor = null )
2019-07-30 23:13:05 +02:00
{
2023-12-04 18:10:49 -05:00
IoCManager . InjectDependencies ( this ) ;
2019-07-30 23:13:05 +02:00
_senderEntity = senderEntity ;
// Use text clipping so new messages don't overlap old ones being pushed up.
RectClipContent = true ;
2023-12-04 18:10:49 -05:00
var bubble = BuildBubble ( message , speechStyleClass , fontColor ) ;
2019-07-30 23:13:05 +02:00
2020-04-30 18:09:09 +02:00
AddChild ( bubble ) ;
2019-07-30 23:13:05 +02:00
2019-08-14 22:04:35 +02:00
ForceRunStyleUpdate ( ) ;
2023-07-08 14:08:32 +10:00
bubble . Measure ( Vector2Helpers . Infinity ) ;
2022-05-27 05:34:25 +02:00
ContentSize = bubble . DesiredSize ;
_verticalOffsetAchieved = - ContentSize . Y ;
2019-07-30 23:13:05 +02:00
}
2023-12-04 18:10:49 -05:00
protected abstract Control BuildBubble ( ChatMessage message , string speechStyleClass , Color ? fontColor = null ) ;
2020-04-30 18:09:09 +02:00
2019-08-04 01:08:55 +02:00
protected override void FrameUpdate ( FrameEventArgs args )
2019-07-30 23:13:05 +02:00
{
base . FrameUpdate ( args ) ;
2019-08-04 01:08:55 +02:00
_timeLeft - = args . DeltaSeconds ;
2021-12-20 05:55:51 +00:00
if ( _entityManager . Deleted ( _senderEntity ) | | _timeLeft < = 0 )
{
// Timer spawn to prevent concurrent modification exception.
Timer . Spawn ( 0 , Die ) ;
return ;
}
2019-07-30 23:13:05 +02:00
2021-11-26 23:50:24 +01:00
// Lerp to our new vertical offset if it's been modified.
if ( MathHelper . CloseToPercent ( _verticalOffsetAchieved - VerticalOffset , 0 , 0.1 ) )
2019-07-30 23:13:05 +02:00
{
2021-11-26 23:50:24 +01:00
_verticalOffsetAchieved = VerticalOffset ;
}
else
{
_verticalOffsetAchieved = MathHelper . Lerp ( _verticalOffsetAchieved , VerticalOffset , 10 * args . DeltaSeconds ) ;
2019-07-30 23:13:05 +02:00
}
2022-05-19 10:12:09 +12:00
if ( ! _entityManager . TryGetComponent < TransformComponent > ( _senderEntity , out var xform ) | | xform . MapID ! = _eyeManager . CurrentMap )
2019-07-30 23:13:05 +02:00
{
2021-11-26 23:50:24 +01:00
Modulate = Color . White . WithAlpha ( 0 ) ;
2019-07-30 23:13:05 +02:00
return ;
}
2021-11-26 23:50:24 +01:00
if ( _timeLeft < = FadeTime )
2019-07-30 23:13:05 +02:00
{
2021-11-26 23:50:24 +01:00
// Update alpha if we're fading.
Modulate = Color . White . WithAlpha ( _timeLeft / FadeTime ) ;
2019-07-30 23:13:05 +02:00
}
else
{
2021-11-26 23:50:24 +01:00
// Make opaque otherwise, because it might have been hidden before
Modulate = Color . White ;
2019-07-30 23:13:05 +02:00
}
2022-06-23 13:23:23 +01:00
var offset = ( - _eyeManager . CurrentEye . Rotation ) . ToWorldVec ( ) * - EntityVerticalOffset ;
2023-09-01 12:30:29 +10:00
var worldPos = xform . WorldPosition + offset ;
2019-07-30 23:13:05 +02:00
2022-06-23 13:23:23 +01:00
var lowerCenter = _eyeManager . WorldToScreen ( worldPos ) / UIScale ;
2023-07-08 14:08:32 +10:00
var screenPos = lowerCenter - new Vector2 ( ContentSize . X / 2 , ContentSize . Y + _verticalOffsetAchieved ) ;
2021-10-26 18:31:22 -07:00
// Round to nearest 0.5
screenPos = ( screenPos * 2 ) . Rounded ( ) / 2 ;
2019-12-05 16:00:03 +01:00
LayoutContainer . SetPosition ( this , screenPos ) ;
2019-07-30 23:13:05 +02:00
2022-05-27 05:34:25 +02:00
var height = MathF . Ceiling ( MathHelper . Clamp ( lowerCenter . Y - screenPos . Y , 0 , ContentSize . Y ) ) ;
2021-02-21 12:38:56 +01:00
SetHeight = height ;
2019-07-30 23:13:05 +02:00
}
private void Die ( )
{
2019-07-31 13:17:06 +02:00
if ( Disposed )
{
return ;
}
2022-10-12 01:16:23 -07:00
OnDied ? . Invoke ( _senderEntity , this ) ;
2019-07-30 23:13:05 +02:00
}
/// <summary>
/// Causes the speech bubble to start fading IMMEDIATELY.
/// </summary>
public void FadeNow ( )
{
if ( _timeLeft > FadeTime )
{
_timeLeft = FadeTime ;
}
}
2023-12-04 18:10:49 -05:00
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 string ExtractSpeechSubstring ( ChatMessage message , string tag )
{
var rawmsg = message . WrappedMessage ;
var tagStart = rawmsg . IndexOf ( $"[{tag}]" ) ;
var tagEnd = rawmsg . IndexOf ( $"[/{tag}]" ) ;
2023-12-06 16:58:53 -05:00
if ( tagStart < 0 | | tagEnd < 0 ) //the above return -1 if the tag's not found, which in turn will cause the below to throw an exception. a blank speech bubble is far more noticeably broken than the bubble not appearing at all -bhijn
2023-12-05 16:40:03 -05:00
return "" ;
2023-12-04 18:10:49 -05:00
tagStart + = tag . Length + 2 ;
return rawmsg . Substring ( tagStart , tagEnd - tagStart ) ;
}
protected FormattedMessage ExtractAndFormatSpeechSubstring ( ChatMessage message , string tag , Color ? fontColor = null )
{
return FormatSpeech ( ExtractSpeechSubstring ( message , tag ) , fontColor ) ;
}
2019-07-30 23:13:05 +02:00
}
2020-04-30 18:09:09 +02:00
2022-02-16 00:23:23 -07:00
public sealed class TextSpeechBubble : SpeechBubble
2020-04-30 18:09:09 +02:00
{
2023-12-04 18:10:49 -05:00
public TextSpeechBubble ( ChatMessage message , EntityUid senderEntity , string speechStyleClass , Color ? fontColor = null )
: base ( message , senderEntity , speechStyleClass , fontColor )
2020-04-30 18:09:09 +02:00
{
}
2023-12-04 18:10:49 -05:00
protected override Control BuildBubble ( ChatMessage message , string speechStyleClass , Color ? fontColor = null )
2020-04-30 18:09:09 +02:00
{
2023-06-02 21:54:15 +03:00
var label = new ChatRichTextLabel
2020-04-30 18:09:09 +02:00
{
2023-12-04 18:10:49 -05:00
MaxWidth = SpeechMaxWidth ,
2020-04-30 18:09:09 +02:00
} ;
2023-11-09 10:18:58 +01:00
2023-12-04 18:10:49 -05:00
label . SetMessage ( FormatSpeech ( message . WrappedMessage , fontColor ) ) ;
2020-04-30 18:09:09 +02:00
var panel = new PanelContainer
{
2022-01-11 06:48:18 -08:00
StyleClasses = { "speechBox" , speechStyleClass } ,
2020-04-30 18:09:09 +02:00
Children = { label } ,
ModulateSelfOverride = Color . White . WithAlpha ( 0.75f )
} ;
return panel ;
}
}
2023-12-04 18:10:49 -05:00
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 ;
}
}
2019-07-30 23:13:05 +02:00
}