A Blazor/Sandbox UI component for the game's message overlay. Renders different overlays for the Serving and GameOver states, shows tiles, stats, leaderboard, and handles tile click interactions and game control actions (launch, restart, return to menu).
@using System
@using Sandbox
@using Sandbox.UI
@using Sandbox.Services
@namespace Breakout
@inherits Panel
@if ( Game.IsValid() && Game.State == GameState.Serving )
{
<root>
<div class="overlay serve" onclick=@(() => Game.LaunchServingBall())>
<div class="serve-card">
<div class="serve-title">GET READY</div>
@if ( Game.BricksSpawnedIn )
{
<div class="serve-hint">Click to launch</div>
}
</div>
</div>
</root>
}
else if ( Game.IsValid() && Game.State == GameState.GameOver )
{
<root>
<div class="overlay over">
<div class="over-layout">
<div class="over-results">
<div class="card">
@if ( Game.IsNewHighScore )
{
<div class="badge">NEW HIGH SCORE!</div>
}
<div class="tile-row">
@for ( int i = 0; i < GameTiles.Length; i++ )
{
var t = GameTiles[i];
var gi = i;
<div class="tile" style="transform: rotate( @(RotOf( gi ))deg );">
<div class="tile-face @FaceClass( gi )" style="background-color: @ColorOf( gi );" onclick=@(() => TileClicked( gi ))>@t.Ch</div>
</div>
}
</div>
<div class="tile-row">
@for ( int i = 0; i < OverTiles.Length; i++ )
{
var t = OverTiles[i];
var gi = GameTiles.Length + i;
<div class="tile" style="transform: rotate( @(RotOf( gi ))deg );">
<div class="tile-face @FaceClass( gi )" style="background-color: @ColorOf( gi );" onclick=@(() => TileClicked( gi ))>@t.Ch</div>
</div>
}
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-label">FINAL SCORE</div>
<div class="stat-val">@Game.Score.ToString( "N0" )</div>
</div>
<div class="stat-card">
<div class="stat-label">LEVEL</div>
<div class="stat-val">@Game.Level</div>
</div>
<div class="stat-card">
<div class="stat-label">RANK</div>
<div class="stat-val">@(Me is { } m ? "#" + m.Rank : "—")</div>
</div>
</div>
<div class="buttons">
<button class="btn play-again" onclick=@(() => Game.StartGame( Game.Mode ))>↺ PLAY AGAIN</button>
<button class="btn menu-btn" onclick=@(() => Game.ReturnToMenu())>MENU</button>
</div>
</div>
</div>
<div class="over-board">
<LeaderboardPanel MaxRows=@(5)
Entries=@Entries
Me=@Me
Loading=@Loading
ActiveFilter=@ActiveFilter
FilterChanged=@FilterChanged
Title=@BoardTitle
Version=@Version />
</div>
</div>
</div>
</root>
}
@code
{
/// <summary>
/// The game this overlay reads state from and drives (launch, restart, menu).
/// </summary>
[Parameter] public BreakoutGame Game { get; set; }
/// <summary>
/// The local player's leaderboard row, used for the RANK stat and the board.
/// </summary>
[Parameter] public ScoreEntry? Me { get; set; }
/// <summary>
/// The ranked rows for the game-over leaderboard card.
/// </summary>
[Parameter] public ScoreEntry[] Entries { get; set; }
/// <summary>
/// True while the leaderboard is loading.
/// </summary>
[Parameter] public bool Loading { get; set; }
/// <summary>
/// Which leaderboard filter tab is selected.
/// </summary>
[Parameter] public ScoreFilter ActiveFilter { get; set; }
/// <summary>
/// Called when the player picks a different leaderboard filter.
/// </summary>
[Parameter] public Action<ScoreFilter> FilterChanged { get; set; }
/// <summary>
/// Heading text for the leaderboard card.
/// </summary>
[Parameter] public string BoardTitle { get; set; } = "LEADERBOARD";
/// <summary>
/// Bumped by the parent to force a re-render when the board data changes.
/// </summary>
[Parameter] public int Version { get; set; }
private static readonly (string Ch, string Color, int Rot)[] GameTiles = new (string, string, int)[]
{
("G", "#FF5D5D", -4),
("A", "#FFC93C", 3),
("M", "#46D07E", -3),
("E", "#4A9BFF", 4)
};
private static readonly (string Ch, string Color, int Rot)[] OverTiles = new (string, string, int)[]
{
("O", "#FF9E45", -8),
("V", "#FF7FB0", -3),
("E", "#9B7BFF", 3),
("R", "#FF7FB0", -2)
};
private static readonly string[] Palette = { "#FF5D5D", "#FF9E45", "#FFC93C", "#46D07E", "#4A9BFF", "#9B7BFF", "#FF7FB0" };
private readonly string[] tileColors = new string[GameTiles.Length + OverTiles.Length];
private readonly int?[] tileRots = new int?[GameTiles.Length + OverTiles.Length];
private readonly int[] tileHits = new int[GameTiles.Length + OverTiles.Length];
private int tileClicks;
private string DefaultColor( int idx ) => idx < GameTiles.Length ? GameTiles[idx].Color : OverTiles[idx - GameTiles.Length].Color;
private int DefaultRot( int idx ) => idx < GameTiles.Length ? GameTiles[idx].Rot : OverTiles[idx - GameTiles.Length].Rot;
private string ColorOf( int idx ) => tileColors[idx] ?? DefaultColor( idx );
private int RotOf( int idx ) => tileRots[idx] ?? DefaultRot( idx );
private string FaceClass( int idx ) => tileHits[idx] == 0 ? "" : (tileHits[idx] % 2 == 1 ? "hit-a" : "hit-b");
private void TileClicked( int idx )
{
// Pick a new random colour that isn't the current one (the guard just avoids repeats).
var current = ColorOf( idx );
var next = current;
for ( int guard = 0; guard < 12 && next == current; guard++ )
next = Palette[System.Random.Shared.Next( Palette.Length )];
tileColors[idx] = next;
// Pick a new tilt that's noticeably different from the current one.
var curRot = RotOf( idx );
var nextRot = curRot;
for ( int guard = 0; guard < 12 && System.Math.Abs( nextRot - curRot ) < 5; guard++ )
nextRot = System.Random.Shared.Next( -10, 11 );
tileRots[idx] = nextRot;
tileHits[idx]++;
tileClicks++;
}
protected override int BuildHash() => System.HashCode.Combine( Game?.State, Game?.Score, Game?.Level, Version, (int)ActiveFilter, Loading, Entries?.Length ?? -1, System.HashCode.Combine( Me?.Rank ?? -1, tileClicks, Game?.IsNewHighScore ?? false, Game?.BricksSpawnedIn ?? false ) );
}