A UI PanelComponent Razor file that displays a speech bubble above a Player showing their chat message character-by-character. It binds to a Player component in the parent, computes visibility based on chat state, and progressively reveals the Message over time in OnUpdate.
@using System
@using Sandbox
@using Sandbox.UI
@namespace BrickJam.UI
@inherits PanelComponent
<root>
@if ( Visible )
{
<div class="border">
<label class="label">@Shown</label>
</div>
}
</root>
@code {
private const float Delay = 0.05f;
private Player player;
private Player Player => player ??= GetComponentInParent<Player>();
private bool Visible => Player.IsValid() && Player.HasChatMessage && !(Player == Player.Local);
private string Message => Player?.ChatMessage ?? string.Empty;
private int textIndex;
private TimeUntil nextIndex = Delay;
private string lastMessage = "";
private string Shown => Message.Substring( 0, Math.Clamp( textIndex, 0, Message.Length ) );
protected override void OnUpdate()
{
if ( Message != lastMessage )
{
lastMessage = Message;
textIndex = 0;
nextIndex = Delay;
}
if ( textIndex < Message.Length && nextIndex )
{
nextIndex = Delay;
textIndex++;
}
}
protected override int BuildHash() => HashCode.Combine( Visible, textIndex, Message );
}
<style>
Speechbubble {
position: absolute;
background-color: rgba(white, 0);
justify-content: center;
flex-wrap: wrap-reverse;
transition: transform 1s ease-in-out;
transform-origin: bottom;
width: 100%;
height: 100%;
.border {
background-color: rgba(white, 0);
border-image: url(ui/speech.png) fill;
transform: scale(1);
padding: 5px;
padding-bottom: 0px;
flex-grow: 0;
color: white;
.label {
bottom: 15px;
right: 15px;
margin-right: -20px;
margin-bottom: -15px;
font-size: 32px;
font-family: "alagard";
font-weight: bold;
text-shadow: 2px 2px 0px black;
}
}
&:intro {
transform: scale(0);
}
&:outro {
transform: scale(0);
}
}
</style>