ColourBreakMenu.razor

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.

File Access
@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 &bull; @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>&lt;</button>
                            <div class="cb-level-select-copy">
                                <span>Level Select</span>
                                <b>@LevelSelectText</b>
                            </div>
                            <button class="@LevelNavClass( Game.CanSelectNextLevel )" onclick=@NextLevelClicked>&gt;</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 &bull; 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 { }
    }
}