UI widget for a FPS scoreboard header. Defines a stateless ScoreboardView that constructs different header layouts (TDM team row, Domination with capture pips, FFA) from a ScoreboardModel and FpsTheme, and a ScoreboardWidget component that owns a model and theme, updates the clock each tick, and builds the UI using Parts helpers.
using Goo;
using Sandbox;
using Sandbox.UI;
namespace Goo.FpsUI;
// Stateless presenter: a compact top-center gamemode header. Returns the inner content; the caller
// wraps it in a backing (Parts.Panel) and anchors it. Three variants keyed off ScoreboardMode.
static class ScoreboardView
{
public static Container Build( ScoreboardModel m, FpsTheme t ) => m.Mode switch
{
ScoreboardMode.Domination => Domination( m, t ),
ScoreboardMode.Ffa => Ffa( m, t ),
_ => TeamRow( m, t ),
};
// TDM: friendly score | clock | enemy score.
static Container TeamRow( ScoreboardModel m, FpsTheme t ) => new()
{
Key = "teamRow", FlexDirection = FlexDirection.Row, AlignItems = Align.Center, Gap = 18f,
Children =
{
Score( "fs", m.FriendlyScore, t.TeamFriendly, t ),
Clock( m, t ),
Score( "es", m.EnemyScore, t.TeamEnemy, t ),
},
};
// Domination: the team row plus a capture-point pip strip.
static Container Domination( ScoreboardModel m, FpsTheme t ) => new()
{
Key = "dom", FlexDirection = FlexDirection.Column, AlignItems = Align.Center, Gap = 7f,
Children = { TeamRow( m, t ), Pips( m, t ) },
};
// FFA: placement | your score | clock | leader score.
static Container Ffa( ScoreboardModel m, FpsTheme t ) => new()
{
Key = "ffa", FlexDirection = FlexDirection.Row, AlignItems = Align.Center, Gap = 18f,
Children =
{
Small( "rank", $"#{m.PlayerRank}/{m.PlayerCount}", t.Ink, t, 16f ),
Big( "score", m.PlayerScore.ToString(), t.Xp, t ),
Clock( m, t ),
Small( "leader", $"LEADER {m.LeaderScore}", t.Dim, t, 14f ),
},
};
static Container Pips( ScoreboardModel m, FpsTheme t )
{
var row = new Container { Key = "pips", FlexDirection = FlexDirection.Row, AlignItems = Align.Center, Gap = 8f };
for ( int i = 0; i < m.Points.Length; i++ )
{
Color bg = m.Points[i] switch
{
CapOwner.Friendly => t.TeamFriendly,
CapOwner.Enemy => t.TeamEnemy,
_ => t.TrackBg,
};
string label = i < m.PointLabels.Length ? m.PointLabels[i] : ((char)('A' + i)).ToString();
row.Children.Add( new Container
{
Key = i.ToString(),
Width = 22f, Height = 22f, BackgroundColor = bg, BorderRadius = t.Radius,
AlignItems = Align.Center, JustifyContent = Justify.Center,
Children = { new Text( label )
{ FontFamily = t.FontFamily, FontSize = 13f, FontWeight = t.WeightBold, FontColor = Color.White } },
} );
}
return row;
}
// Fixed-width score so changing digits never shift the clock.
static Container Score( string key, int value, Color color, FpsTheme t ) => new()
{
Key = key, Width = 56f, AlignItems = Align.Center, JustifyContent = Justify.Center,
Children = { Big( "v", value.ToString(), color, t ) },
};
static Text Clock( ScoreboardModel m, FpsTheme t ) => new( m.Clock )
{
Key = "clk", FontFamily = t.FontFamily, FontSize = 20f, FontWeight = t.WeightBold,
FontColor = t.Ink, WhiteSpace = WhiteSpace.NoWrap,
};
static Text Big( string key, string s, Color c, FpsTheme t ) => new( s )
{
Key = key, FontFamily = t.FontFamily, FontSize = 26f, FontWeight = t.WeightBold,
FontColor = c, WhiteSpace = WhiteSpace.NoWrap,
};
static Text Small( string key, string s, Color c, FpsTheme t, float size ) => new( s )
{
Key = key, FontFamily = t.FontFamily, FontSize = size, FontColor = c, WhiteSpace = WhiteSpace.NoWrap,
};
}
// Standalone gamemode scoreboard header. Set Mode, mutate Scoreboard fields from your game.
public sealed partial class ScoreboardWidget : GooPanel<Container>
{
[Property] public ScoreboardMode Mode { get; set; } = ScoreboardMode.Tdm; // which header variant
readonly ScoreboardModel _m = new();
readonly FpsTheme _t = new();
bool _booted;
public ScoreboardModel Scoreboard => _m; // mutate fields directly (scores, timer, captures)
void Boot() { _m.Mode = Mode; _booted = true; }
// Demo-only seam: implemented in FpsDemo.cs, compiles out when that file is deleted.
partial void StepDemo( float dt, ref bool active );
protected override bool Tick( float dt )
{
if ( !_booted ) Boot();
bool demo = false;
StepDemo( dt, ref demo );
if ( !demo ) _m.Mode = Mode;
_m.Tick( dt ); // count the clock down
return true; // the clock changes every second, the header is cheap (text + rects)
}
protected override Container Build()
{
if ( !_booted ) Boot();
var root = Parts.Root( "fpsScoreboard" );
root.Children.Add( Parts.Anchor( "a", Parts.Corner.TopCenter, _t.Margin, Parts.Panel( "sbBg", _t, ScoreboardView.Build( _m, _t ) ) ) );
return root;
}
}