A Razor UI component for the Colour Break game, rendering menus, HUD, settings, bug reporting and controller navigation. It binds to a ColourBreakGame component, shows game state info, handles input, submits/manages bug reports and toggles settings.
@using Sandbox;
@using Sandbox.UI;
@using System;
@using System.Linq;
@inherits PanelComponent
<root class="cb-ui">
@if ( Game is null )
{
<div class="cb-loading">Loading...</div>
}
else if ( ShowSettings )
{
<div class="cb-screen cb-settings-screen">
<div class="cb-panel cb-settings-panel">
<h2 class="cb-panel-title">Settings</h2>
<div class="cb-readout">
<button class="@SettingRowClass( 0 )" onclick=@ToggleAudioClicked>
<span>Sound FX</span><b>@(Game.AudioEnabled ? "On" : "Off")</b>
</button>
<button class="@SettingRowClass( 1 )" onclick=@ToggleMusicClicked>
<span>Music</span><b>@(Game.MusicEnabled ? "On" : "Off")</b>
</button>
<button class="@SettingRowClass( 2 )" onclick=@ToggleEffectsClicked>
<span>Effects</span><b>@(Game.EffectsEnabled ? "On" : "Off")</b>
</button>
<button class="@SettingRowClass( 3 )" onclick=@ToggleShakeClicked>
<span>Camera Shake</span><b>@(Game.CameraShakeEnabled ? "On" : "Off")</b>
</button>
<button class="@SettingRowClass( 4 )" onclick=@ToggleColourBlindClicked>
<span>Colour Blind</span><b>@(Game.ColourBlindMode ? "On" : "Off")</b>
</button>
</div>
<div class="cb-buttons">
<button class="cb-btn cb-primary" onclick=@ToggleSettingsClicked>Done</button>
</div>
</div>
</div>
}
else if ( ShowBugManager && Game.IsDeveloper )
{
<div class="cb-screen cb-settings-screen">
<div class="cb-panel cb-manager-panel">
<h2 class="cb-panel-title">Bug Reports</h2>
@{ var reports = Game.GetBugReports(); }
<div class="cb-manager-list">
@if ( reports.Count == 0 )
{
<div class="cb-manager-empty">No reports yet.</div>
}
else
{
@foreach ( var r in reports )
{
<div class="@ManagerRowClass( r )">
<div class="cb-report-head">
<span class="cb-report-when">@r.WhenText</span>
<span class="cb-report-meta">@r.Reporter • @r.Context</span>
@if ( r.Resolved )
{
<span class="cb-report-tag">Resolved</span>
}
</div>
<div class="cb-report-msg">@r.Message</div>
<div class="cb-report-actions">
<button class="cb-mini-btn" onclick=@(() => ToggleResolve( r ))>@(r.Resolved ? "Reopen" : "Resolve")</button>
<button class="cb-mini-btn cb-mini-danger" onclick=@(() => DeleteReport( r.Id ))>Delete</button>
</div>
</div>
}
}
</div>
<div class="cb-buttons">
<button class="cb-btn cb-primary" onclick=@CloseManagerClicked>Done</button>
@if ( reports.Count > 0 )
{
<button class="cb-btn" onclick=@ClearAllClicked>Clear All</button>
}
</div>
</div>
</div>
}
else if ( ShowBugReport )
{
<div class="cb-screen cb-settings-screen">
<div class="cb-panel cb-report-panel">
<h2 class="cb-panel-title">Report a Bug</h2>
<p class="cb-report-intro">Found something broken or odd? Describe what happened - the more detail, the better.</p>
<TextEntry class="cb-report-input" Multiline=@true MaxLength=@(600) Placeholder="Describe the bug..." Value:bind="@ReportText"></TextEntry>
@if ( !string.IsNullOrEmpty( ReportFeedback ) )
{
<div class="cb-report-feedback">@ReportFeedback</div>
}
<div class="cb-buttons">
<button class="cb-btn cb-primary" onclick=@SubmitReportClicked>Send Report</button>
<button class="cb-btn" onclick=@CloseBugReportClicked>Back</button>
@if ( Game.IsDeveloper )
{
<button class="cb-btn cb-manager-btn" onclick=@OpenManagerClicked>Manager (@Game.OpenBugReportCount)</button>
}
</div>
</div>
</div>
}
else if ( Game.CurrentState == ColourBreakGame.GameState.Menu )
{
<div class="cb-screen cb-main">
<div class="cb-menu-top">
<div class="cb-title-wrap">
<h1 class="cb-title">Colour Break</h1>
<div class="cb-mode-tabs">
<button class="@ModeTabClass( false )" onclick=@SelectLevelsClicked>Levels</button>
<button class="@ModeTabClass( true )" onclick=@SelectEndlessClicked>Endless</button>
</div>
@if ( !Game.IsEndless )
{
<p class="cb-tagline">Level @Game.Level / @Game.LevelCount - @Game.LevelName</p>
<p class="cb-level-brief">@Game.LevelBrief</p>
<p class="cb-level-objective">@Game.ObjectiveText</p>
<div class="cb-level-rail">
@foreach ( var step in LevelSteps )
{
<span class="@LevelPipClass( step )">@LevelPipText( step )</span>
}
</div>
<div class="cb-level-select">
<button class="@LevelNavClass( Game.CanSelectPreviousLevel )" onclick=@PreviousLevelClicked><</button>
<div class="cb-level-select-copy">
<span>Level Select</span>
<b>@LevelSelectText</b>
</div>
<button class="@LevelNavClass( Game.CanSelectNextLevel )" onclick=@NextLevelClicked>></button>
</div>
}
else
{
<p class="cb-tagline">Endless - survive as long as the board lets you</p>
<p class="cb-level-brief">Six colours, no move limit, no target. The board keeps refilling and the run ends only when there is nothing left to match. Chase your best score.</p>
<p class="cb-level-objective">Best Score @Game.EndlessBest</p>
}
</div>
<div class="cb-menu-metrics">
@if ( !Game.IsEndless )
{
<div class="cb-menu-metric"><span>Goal</span><b>@Game.GoalScore</b></div>
<div class="cb-menu-metric"><span>Moves</span><b>@Game.MovesLeft</b></div>
<div class="cb-menu-metric"><span>Best</span><b>@BestText</b></div>
<div class="cb-menu-metric"><span>Stars</span><b>@TotalStarsText</b></div>
}
else
{
<div class="cb-menu-metric"><span>Colours</span><b>6</b></div>
<div class="cb-menu-metric"><span>Best</span><b>@Game.EndlessBest</b></div>
}
</div>
</div>
<div class="cb-menu-actions">
<button class="@PlayButtonClass" onclick=@PlayClicked>@PlayLabel</button>
<button class="cb-btn" onclick=@ToggleSettingsClicked>Settings</button>
<button class="cb-btn" onclick=@OpenBugReportClicked>Report a Bug</button>
<button class="cb-btn" onclick=@QuitClicked>Quit</button>
</div>
</div>
}
else
{
<div class="cb-hud">
@if ( !Game.IsEndless )
{
<div class="cb-stat">
<span class="cb-stat-label">Level</span>
<span class="cb-stat-value">@Game.Level</span>
<span class="cb-stat-subtitle">@Game.LevelName</span>
</div>
}
else
{
<div class="cb-stat">
<span class="cb-stat-label">Mode</span>
<span class="cb-stat-value">Endless</span>
<span class="cb-stat-subtitle">Best @Game.EndlessBest</span>
</div>
}
<div class="cb-stat">
<span class="cb-stat-label">Score</span>
<span class="cb-stat-value">@Game.Score</span>
</div>
@if ( !Game.IsEndless )
{
<div class="cb-stat">
<span class="cb-stat-label">Goal</span>
<span class="cb-stat-value">@Game.GoalScore</span>
</div>
<div class="cb-stat">
<span class="cb-stat-label">Moves Left</span>
<span class="cb-stat-value">@Game.MovesLeft</span>
</div>
}
else
{
<div class="cb-stat">
<span class="cb-stat-label">Moves Made</span>
<span class="cb-stat-value">@Game.Moves</span>
</div>
}
<div class="cb-status">
<span class="cb-status-label">Status</span>
<span class="cb-status-value">@Game.StatusText</span>
</div>
@if ( !Game.IsEndless )
{
<div class="cb-progress">
<div class="cb-progress-copy">
<span>Goal Progress</span>
<b>@ProgressText</b>
</div>
<div class="cb-progress-track">
<div class="cb-progress-fill" style=@ProgressStyle></div>
</div>
</div>
@if ( Game.HasSecondaryObjective )
{
<div class="cb-progress cb-objective">
<div class="cb-progress-copy">
<span>Objective</span>
<b>@Game.ObjectiveProgressText</b>
</div>
<div class="cb-objective-name">@Game.ObjectiveText</div>
<div class="cb-progress-track">
<div class="cb-progress-fill cb-objective-fill" style=@ObjectiveProgressStyle></div>
</div>
</div>
}
}
</div>
@if ( Game.ShowHowToPlay )
{
<div class="cb-hint">Drag a piece onto an adjacent one to swap • match 3+ of a colour to break them</div>
}
else if ( Game.HintActive )
{
<div class="cb-hint cb-hint-stuck">@StuckHintText</div>
}
<button class="cb-pause-btn" onclick=@PauseClicked>II</button>
@if ( Game.CurrentState == ColourBreakGame.GameState.Paused )
{
<div class="cb-screen cb-pause">
<div class="cb-panel">
<h2 class="cb-panel-title">Paused</h2>
<div class="cb-readout">
@if ( !Game.IsEndless )
{
<div class="cb-readout-row"><span>Level</span><b>@Game.LevelName</b></div>
<div class="cb-readout-row"><span>Score</span><b>@Game.Score</b></div>
<div class="cb-readout-row"><span>Goal</span><b>@Game.GoalScore</b></div>
@if ( Game.HasSecondaryObjective )
{
<div class="cb-readout-row"><span>Objective</span><b>@Game.ObjectiveProgressText</b></div>
}
<div class="cb-readout-row"><span>Moves Left</span><b>@Game.MovesLeft</b></div>
}
else
{
<div class="cb-readout-row"><span>Mode</span><b>Endless</b></div>
<div class="cb-readout-row"><span>Score</span><b>@Game.Score</b></div>
<div class="cb-readout-row"><span>Best</span><b>@Game.EndlessBest</b></div>
<div class="cb-readout-row"><span>Moves Made</span><b>@Game.Moves</b></div>
}
<div class="cb-readout-row"><span>State</span><b>@Game.StatusText</b></div>
</div>
<div class="cb-buttons">
<button class="cb-btn cb-primary" onclick=@ResumeClicked>Resume</button>
<button class="cb-btn" onclick=@RestartClicked>Restart</button>
<button class="cb-btn" onclick=@ToggleSettingsClicked>Settings</button>
<button class="cb-btn" onclick=@MenuClicked>Main Menu</button>
</div>
</div>
</div>
}
else if ( Game.CurrentState == ColourBreakGame.GameState.Won || Game.CurrentState == ColourBreakGame.GameState.Lost )
{
<div class="cb-screen cb-pause">
<div class="cb-panel">
<h2 class="cb-panel-title">@EndTitle</h2>
<div class="cb-readout">
@if ( Game.IsEndless )
{
@if ( Game.EndlessRecordSet )
{
<div class="cb-readout-row cb-record-row"><span>New Best!</span><b>@Game.Score</b></div>
}
<div class="cb-readout-row"><span>Score</span><b>@Game.Score</b></div>
<div class="cb-readout-row"><span>Best</span><b>@Game.EndlessBest</b></div>
<div class="cb-readout-row"><span>Moves Made</span><b>@Game.Moves</b></div>
}
else
{
<div class="cb-readout-row"><span>Level</span><b>@Game.LevelName</b></div>
@if ( Game.CurrentState == ColourBreakGame.GameState.Won )
{
<div class="cb-readout-row"><span>Rating</span><b>@Game.CompletionRank (@Game.CompletionStars / 3)</b></div>
}
<div class="cb-readout-row"><span>Score</span><b>@Game.Score</b></div>
<div class="cb-readout-row"><span>Goal</span><b>@Game.GoalScore</b></div>
@if ( Game.HasSecondaryObjective )
{
<div class="cb-readout-row"><span>Objective</span><b>@Game.ObjectiveProgressText</b></div>
}
<div class="cb-readout-row"><span>Moves Used</span><b>@Game.Moves</b></div>
}
</div>
<div class="cb-buttons">
<button class="cb-btn cb-primary" onclick=@EndPrimaryClicked>@EndPrimaryLabel</button>
<button class="cb-btn" onclick=@MenuClicked>Main Menu</button>
</div>
</div>
</div>
}
}
</root>
@code {
ColourBreakGame Game;
bool ShowSettings;
int SettingsIndex;
float ControllerUiCooldown;
bool ShowBugReport;
bool ShowBugManager;
string ReportText = "";
string ReportFeedback = "";
protected override void OnStart()
{
base.OnStart();
FindGame();
}
protected override void OnUpdate()
{
if ( Game is null || !Game.IsValid() )
FindGame();
ControllerUiCooldown = MathF.Max( 0f, ControllerUiCooldown - Time.Delta );
HandleControllerUi();
}
void FindGame()
{
Game = Scene?.GetAllComponents<ColourBreakGame>().FirstOrDefault();
}
protected override int BuildHash()
{
if ( Game is null ) return 0;
var hash = new HashCode();
hash.Add( Game.CurrentState );
hash.Add( Game.IsEndless );
hash.Add( Game.EndlessBest );
hash.Add( Game.EndlessRecordSet );
hash.Add( Game.Score );
hash.Add( Game.Moves );
hash.Add( Game.MovesLeft );
hash.Add( Game.GoalScore );
hash.Add( Game.Level );
hash.Add( Game.LevelName );
hash.Add( Game.LevelBrief );
hash.Add( Game.LevelCount );
hash.Add( Game.HasNextLevel );
hash.Add( Game.ColourCount );
hash.Add( Game.HighestUnlockedLevel );
hash.Add( Game.BestStars );
hash.Add( Game.TotalStars );
hash.Add( Game.SelectedLevelLocked );
hash.Add( Game.CanPlaySelectedLevel );
hash.Add( Game.CanSelectPreviousLevel );
hash.Add( Game.CanSelectNextLevel );
foreach ( var step in LevelSteps )
hash.Add( Game.GetBestStars( step ) );
hash.Add( Game.HasSecondaryObjective );
hash.Add( Game.ObjectiveText );
hash.Add( Game.ObjectiveProgressText );
hash.Add( Game.ObjectiveProgress );
hash.Add( Game.ObjectiveTarget );
hash.Add( Game.ObjectiveComplete );
hash.Add( Game.CompletionStars );
hash.Add( Game.CompletionRank );
hash.Add( Game.StatusText );
hash.Add( Game.ShowHowToPlay );
hash.Add( Game.HintActive );
hash.Add( Game.AudioEnabled );
hash.Add( Game.MusicEnabled );
hash.Add( Game.EffectsEnabled );
hash.Add( Game.CameraShakeEnabled );
hash.Add( Game.ColourBlindMode );
hash.Add( ShowSettings );
hash.Add( SettingsIndex );
hash.Add( ShowBugReport );
hash.Add( ShowBugManager );
hash.Add( Game.IsDeveloper );
hash.Add( Game.OpenBugReportCount );
hash.Add( ReportFeedback );
return hash.ToHashCode();
}
void PlayClicked()
{
ShowSettings = false;
if ( Game is null ) return;
if ( Game.IsEndless || Game.CanPlaySelectedLevel )
Game.StartNewGame();
}
void SelectLevelsClicked() { ShowSettings = false; Game?.SelectLevelsMode(); }
void SelectEndlessClicked() { ShowSettings = false; Game?.SelectEndlessMode(); }
string ModeTabClass( bool endless ) => Game != null && Game.IsEndless == endless ? "cb-mode-tab active" : "cb-mode-tab";
void PreviousLevelClicked() { ShowSettings = false; Game?.SelectPreviousLevel(); }
void NextLevelClicked() { ShowSettings = false; Game?.SelectNextLevel(); }
void PauseClicked() => Game?.PauseGame();
void ResumeClicked() { ShowSettings = false; Game?.ResumeGame(); }
void RestartClicked() { ShowSettings = false; Game?.RestartGame(); }
void MenuClicked() { ShowSettings = false; Game?.QuitToMenu(); }
void EndPrimaryClicked()
{
ShowSettings = false;
if ( Game is null )
return;
if ( Game.CurrentState == ColourBreakGame.GameState.Won && Game.HasNextLevel )
Game.StartNextLevel();
else
Game.RestartGame();
}
void ToggleSettingsClicked() { ShowSettings = !ShowSettings; StateHasChanged(); }
void ToggleAudioClicked() { Game?.ToggleAudio(); StateHasChanged(); }
void ToggleMusicClicked() { Game?.ToggleMusic(); StateHasChanged(); }
void ToggleEffectsClicked() { Game?.ToggleEffects(); StateHasChanged(); }
void ToggleShakeClicked() { Game?.ToggleCameraShake(); StateHasChanged(); }
void ToggleColourBlindClicked() { Game?.ToggleColourBlindMode(); StateHasChanged(); }
void OpenBugReportClicked()
{
ShowSettings = false;
ShowBugManager = false;
ReportFeedback = "";
ShowBugReport = true;
StateHasChanged();
}
void CloseBugReportClicked() { ShowBugReport = false; ShowBugManager = false; StateHasChanged(); }
void SubmitReportClicked()
{
if ( Game is null ) return;
if ( Game.SubmitBugReport( ReportText ) )
{
ReportText = "";
ReportFeedback = "Thanks - your report was saved.";
}
else
{
ReportFeedback = "Please type a description first.";
}
StateHasChanged();
}
void OpenManagerClicked() { ShowBugReport = false; ShowBugManager = true; StateHasChanged(); }
void CloseManagerClicked() { ShowBugManager = false; ShowBugReport = false; StateHasChanged(); }
void ToggleResolve( ColourBreakBugReport r )
{
if ( r is null || Game is null ) return;
if ( r.Resolved ) Game.ReopenBugReport( r.Id );
else Game.ResolveBugReport( r.Id );
StateHasChanged();
}
void DeleteReport( string id ) { Game?.DeleteBugReport( id ); StateHasChanged(); }
void ClearAllClicked() { Game?.ClearBugReports(); StateHasChanged(); }
string ManagerRowClass( ColourBreakBugReport r ) => r != null && r.Resolved ? "cb-report-row resolved" : "cb-report-row";
string ProgressText => Game is null ? "0%" : $"{Math.Clamp( (Game.Score / (float)Math.Max( 1, Game.GoalScore )) * 100f, 0f, 100f ):0}%";
string ProgressStyle => Game is null ? "width: 0%;" : $"width: {Math.Clamp( (Game.Score / (float)Math.Max( 1, Game.GoalScore )) * 100f, 0f, 100f ):0.##}%;";
string ObjectiveProgressStyle => Game is null || Game.ObjectiveTarget <= 0
? "width: 0%;"
: $"width: {Math.Clamp( (Game.ObjectiveProgress / (float)Math.Max( 1, Game.ObjectiveTarget )) * 100f, 0f, 100f ):0.##}%;";
IEnumerable<int> LevelSteps => Game is null ? Enumerable.Empty<int>() : Enumerable.Range( 1, Game.LevelCount );
string LevelPipClass( int step )
{
if ( Game is null )
return "cb-level-pip";
if ( step == Game.Level )
return "cb-level-pip active";
if ( Game.GetBestStars( step ) > 0 )
return "cb-level-pip complete";
return step > Game.HighestUnlockedLevel ? "cb-level-pip locked" : "cb-level-pip";
}
string LevelPipText( int step )
{
if ( Game is null )
return step.ToString();
int stars = Game.GetBestStars( step );
return stars > 0 ? $"{step}:{stars}" : step.ToString();
}
string BestText => Game is null || Game.BestStars <= 0 ? "-" : $"{Game.BestStars}/3";
string TotalStarsText => Game is null ? "-" : $"{Game.TotalStars}/{Game.MaxStars}";
string PlayLabel => Game is null ? "Play" : Game.IsEndless ? "Play Endless" : Game.SelectedLevelLocked ? "Locked" : "Play";
string PlayButtonClass => Game is null || Game.IsEndless || Game.CanPlaySelectedLevel ? "cb-btn cb-primary" : "cb-btn cb-disabled";
string StuckHintText => Game != null && Game.IsEndless
? "Stuck? Try the glowing pieces"
: "Stuck? Try the glowing pieces • press R to reshuffle";
string EndTitle => Game is null ? ""
: Game.IsEndless ? "No More Moves"
: Game.CurrentState == ColourBreakGame.GameState.Won ? "Level Complete" : "Out of Moves";
string LevelSelectText => Game is null
? ""
: Game.SelectedLevelLocked
? $"Locked - complete level {Game.HighestUnlockedLevel}"
: $"Unlocked through level {Game.HighestUnlockedLevel}";
string LevelNavClass( bool enabled ) => enabled ? "cb-level-nav" : "cb-level-nav disabled";
string SettingRowClass( int index ) => SettingsIndex == index ? "cb-setting-row focused" : "cb-setting-row";
void HandleControllerUi()
{
if ( Game is null || (!Input.UsingController && Input.ControllerCount <= 0) )
return;
if ( ShowSettings )
{
if ( ControllerMovePressed( out int dx, out int dy ) )
SettingsIndex = Math.Clamp( SettingsIndex - dy, 0, 4 );
if ( ActionPressed( "jump", "use", "attack1" ) )
ToggleFocusedSetting();
if ( ActionPressed( "attack2", "menu", "pause" ) )
{
ShowSettings = false;
StateHasChanged();
}
return;
}
if ( Game.CurrentState == ColourBreakGame.GameState.Menu )
{
if ( ControllerMovePressed( out int dx, out int dy ) )
{
if ( dy != 0 )
{
if ( Game.IsEndless ) Game.SelectLevelsMode();
else Game.SelectEndlessMode();
}
else if ( !Game.IsEndless && dx < 0 ) Game.SelectPreviousLevel();
else if ( !Game.IsEndless && dx > 0 ) Game.SelectNextLevel();
}
if ( ActionPressed( "jump", "use", "attack1" ) && (Game.IsEndless || Game.CanPlaySelectedLevel) )
Game.StartNewGame();
if ( ActionPressed( "attack2", "menu", "pause" ) )
{
ShowSettings = true;
SettingsIndex = 0;
StateHasChanged();
}
}
else if ( Game.CurrentState == ColourBreakGame.GameState.Paused )
{
if ( ActionPressed( "jump", "use", "attack1" ) )
Game.ResumeGame();
if ( ActionPressed( "attack2" ) )
Game.QuitToMenu();
}
else if ( Game.CurrentState == ColourBreakGame.GameState.Won || Game.CurrentState == ColourBreakGame.GameState.Lost )
{
if ( ActionPressed( "jump", "use", "attack1" ) )
EndPrimaryClicked();
if ( ActionPressed( "attack2" ) )
Game.QuitToMenu();
}
}
void ToggleFocusedSetting()
{
switch ( SettingsIndex )
{
case 0: Game?.ToggleAudio(); break;
case 1: Game?.ToggleMusic(); break;
case 2: Game?.ToggleEffects(); break;
case 3: Game?.ToggleCameraShake(); break;
case 4: Game?.ToggleColourBlindMode(); break;
}
StateHasChanged();
}
bool ControllerMovePressed( out int dx, out int dy )
{
dx = 0;
dy = 0;
if ( ControllerUiCooldown > 0f )
return false;
var move = Input.AnalogMove;
const float threshold = 0.45f;
if ( MathF.Abs( move.x ) > MathF.Abs( move.y ) && MathF.Abs( move.x ) > threshold )
dx = move.x > 0f ? 1 : -1;
else if ( MathF.Abs( move.y ) > threshold )
dy = move.y > 0f ? 1 : -1;
if ( dx == 0 && dy == 0 )
return false;
ControllerUiCooldown = 0.18f;
return true;
}
bool ActionPressed( params string[] actions )
{
foreach ( var action in actions )
if ( Input.Pressed( action ) )
return true;
return false;
}
string EndPrimaryLabel => Game is null ? "Retry"
: Game.IsEndless ? "Play Again"
: Game.CurrentState == ColourBreakGame.GameState.Won
? (Game.HasNextLevel ? "Next Level" : "Play Again")
: "Retry";
void QuitClicked()
{
try { global::Sandbox.Game.Close(); } catch { }
}
}