ScienceHud.razor

A Razor UI component for the Science game HUD. It renders canvas images, sidebar with tool palette and stats, toolbar controls, help and intro overlays, and binds UI actions to the ScienceGame component.

@using System
@using Sandbox
@using Sandbox.UI
@namespace Science
@inherits PanelComponent
@attribute [StyleSheet]

<root>
    @if ( Game != null )
    {
        <Image @ref="Canvas" class="canvas" />

        <div class="sidebar">
            <div class="brand">
                <div class="logo">SCIENCE</div>
                <div class="tagline">Falling-sand sandbox</div>
                <div class="rank">@Game.LabRank</div>
            </div>

            <div class="palette">
                @foreach ( var category in Categories )
                {
                    <div class="cat-head">@ScienceGame.CategoryName( category )</div>
                    @foreach ( var element in ElementsIn( category ) )
                    {
                        <button class="@ToolClass( element )" [email protected]( element ) onclick=@(() => Game.SelectTool( element ))>
                            <span class="@SwatchClass( element )"></span>
                            <b>@ScienceGame.ToolName( element )</b>
                        </button>
                    }
                }
            </div>
        </div>

        <div class="objective @(Game.ObjectiveComplete ? "complete" : "")">
            <div class="objective-top">
                <span>@Game.ActiveExperimentName: @Game.ObjectiveText | Tool @Game.CurrentToolName | Score @Game.LabScore.ToString( "N0" )</span>
                <b>@Game.ObjectiveProgress</b>
            </div>
            <div class="progress">
                <div class="progress-fill" style=@($"width: {Game.ObjectivePercent * 100f:0}%")></div>
            </div>
        </div>

        <div class="stats">
            <div class="stat-chip particles">
                <span class="stat-num">@Game.Particles.ToString( "N0" )</span>
                <span class="stat-lbl">Particles</span>
            </div>
            <div class="stat-chip reactions">
                <span class="stat-num">@Game.Reactions.ToString( "N0" )</span>
                <span class="stat-lbl">Reactions</span>
            </div>
            <div class="stat-chip discovered">
                <span class="stat-num">@Game.Discoveries / 24</span>
                <span class="stat-lbl">Found</span>
            </div>
        </div>

        <div class="notebook">
            <div class="nb-head">LAB NOTEBOOK</div>
            @if ( Game.RecentReactions.Count == 0 )
            {
                <div class="nb-empty">Mix materials to discover reactions...</div>
            }
            else
            {
                @foreach ( var entry in Game.RecentReactions )
                {
                    <div class="nb-entry">+ @entry</div>
                }
            }
        </div>

        <div class="milestones">
            <div class="nb-head">MILESTONES</div>
            @if ( Game.Milestones.Count == 0 )
            {
                <div class="nb-empty">Load an experiment or discover reactions.</div>
            }
            else
            {
                @foreach ( var entry in Game.Milestones )
                {
                    <div class="milestone-entry">@entry</div>
                }
            }
        </div>

        <div class="toolbar">
            <div class="group">
                <button class="icon-btn" tooltip="Smaller brush" onclick=@(() => Click( Game.DecreaseBrush ))>-</button>
                <div class="readout">@Game.BrushRadius</div>
                <button class="icon-btn" tooltip="Larger brush" onclick=@(() => Click( Game.IncreaseBrush ))>+</button>
            </div>

            <div class="group">
                <button class="mode-btn @(Game.Paused ? "active" : "")" onclick=@(() => Click( Game.TogglePause ))>@(Game.Paused ? "Run" : "Pause")</button>
                <button class="mode-btn" tooltip="Advance one frame" onclick=@(() => Click( Game.StepOnce ))>Step</button>
                <button class="mode-btn" tooltip="Cycle simulation speed" onclick=@(() => Click( Game.CycleSpeed ))>@Game.SpeedScale.ToString( "0.#" )x</button>
            </div>

            <div class="group experiments">
                <button class="@ExperimentClass( "Mixed Lab" )" tooltip="Starter reaction board" onclick=@(() => { Game.PlayClick(); Game.LoadScenario( 0 ); })>Lab</button>
                <button class="@ExperimentClass( "Volcano" )" tooltip="Heat and pressure chain" onclick=@(() => { Game.PlayClick(); Game.LoadScenario( 1 ); })>Volcano</button>
                <button class="@ExperimentClass( "Garden" )" tooltip="Growth and water system" onclick=@(() => { Game.PlayClick(); Game.LoadScenario( 2 ); })>Garden</button>
                <button class="@ExperimentClass( "Demolition" )" tooltip="Fuse and explosives test" onclick=@(() => { Game.PlayClick(); Game.LoadScenario( 3 ); })>Demo</button>
            </div>

            <div class="group">
                <button class="mode-btn" tooltip="Save sandbox" onclick=@(() => Click( Game.SaveState ))>Save</button>
                <button class="mode-btn" tooltip="Load sandbox" onclick=@(() => Click( Game.LoadState ))>Load</button>
                <button class="mode-btn" tooltip="Toggle grid" onclick=@(() => Click( Game.ToggleGrid ))>Grid</button>
                <button class="mode-btn @(Game.Muted ? "active" : "")" tooltip="Toggle sound" onclick=@(() => Game.ToggleMute())>@(Game.Muted ? "Muted" : "Sound")</button>
                <button class="mode-btn danger" tooltip="Clear canvas" onclick=@(() => Click( Game.ClearWorld ))>Clear</button>
                <button class="mode-btn" tooltip="Help" onclick=@(() => { ToggleHelp(); Game.PlayClick(); })>?</button>
            </div>
        </div>

        @if ( _showHelp )
        {
            <div class="overlay">
                <div class="backdrop" onclick=@(() => ToggleHelp())></div>
                <div class="panel">
                    <div class="panel-title">How to play</div>
                    <div class="help-cols">
                        <div class="help-col">
                            <div class="help-h">Controls</div>
                            <div class="help-row"><b>Left&nbsp;drag</b><span>Paint selected material</span></div>
                            <div class="help-row"><b>Right&nbsp;drag</b><span>Erase</span></div>
                            <div class="help-row"><b>- / +</b><span>Brush size</span></div>
                            <div class="help-row"><b>Pause / Step</b><span>Freeze and advance one frame</span></div>
                            <div class="help-row"><b>Speed</b><span>0.5x to 4x simulation</span></div>
                            <div class="help-row"><b>1-9</b><span>Quick-select common materials</span></div>
                        </div>
                        <div class="help-col">
                            <div class="help-h">Try these reactions</div>
                            <div class="help-row2">Drop <b>Water</b> onto <b>Lava</b> to make Stone &amp; Steam</div>
                            <div class="help-row2">Light <b>Oil</b> with <b>Fire</b> to make it burn</div>
                            <div class="help-row2"><b>Acid</b> eats almost anything it touches</div>
                            <div class="help-row2"><b>Seed</b> + <b>Water</b> grows <b>Plant</b></div>
                            <div class="help-row2"><b>Fuse</b> carries fire to <b>TNT</b></div>
                            <div class="help-row2"><b>Clone</b> copies any material beside it</div>
                        </div>
                    </div>
                    <button class="close-btn" onclick=@(() => ToggleHelp())>Got it</button>
                </div>
            </div>
        }

        @if ( _showIntro )
        {
            <div class="intro" onclick=@(() => DismissIntro())>
                <Image @ref="IntroCanvas" class="intro-canvas" />
                <div class="intro-scrim"></div>
                <div class="intro-inner">
                    <div class="intro-logo">SCIENCE</div>
                    <div class="intro-sub">A reactive falling-sand sandbox</div>
                    <div class="intro-hint">30 materials / live reactions / build, burn, dissolve, grow</div>
                    <button class="intro-btn" onclick=@(() => DismissIntro())>Enter the lab</button>
                    <div class="intro-foot">Click anywhere to begin</div>
                </div>
            </div>
        }
    }
</root>

@code {
    [Property] public ScienceGame Game { get; set; }

    Image Canvas { get; set; }
    Image IntroCanvas { get; set; }
    bool _showHelp;
    bool _showIntro = false;

    // Run the action, then a UI click so every control gives audible feedback.
    void Click( Action action )
    {
        action?.Invoke();
        Game?.PlayClick();
    }

    static readonly ElementCategory[] Categories =
    {
        ElementCategory.Powders, ElementCategory.Liquids, ElementCategory.Gases,
        ElementCategory.Energy, ElementCategory.Life, ElementCategory.Explosives,
        ElementCategory.Solids, ElementCategory.Tools
    };

    int TotalReactions => 24;

    protected override void OnStart()
    {
        base.OnStart();
        Game ??= Scene.GetAllComponents<ScienceGame>().FirstOrDefault();
        if ( !_showIntro ) Game?.SetAttractMode( false );
    }

    protected override void OnUpdate()
    {
        if ( Game == null ) return;

        if ( Game.GridTexture != null )
        {
            if ( Canvas.IsValid() ) Canvas.Texture = Game.GridTexture;
            if ( IntroCanvas.IsValid() ) IntroCanvas.Texture = Game.GridTexture;
        }

        if ( Canvas.IsValid() )
        {
            var rect = Canvas.Box.Rect;
            var mouse = Mouse.Position;
            bool inside = !_showHelp && !_showIntro
                && mouse.x >= rect.Left && mouse.x <= rect.Right
                && mouse.y >= rect.Top && mouse.y <= rect.Bottom;
            Game.SetCanvasRect( rect, inside );
        }
    }

    IEnumerable<Element> ElementsIn( ElementCategory category )
        => Game.ToolPalette.Where( e => ScienceGame.CategoryOf( e ) == category );

    void ToggleHelp() => _showHelp = !_showHelp;
    void DismissIntro()
    {
        if ( !_showIntro ) return;
        _showIntro = false;
        Game?.SetAttractMode( false );
        Game?.PlayClick();
    }

    string ToolClass( Element element ) => Game?.CurrentTool == element ? "tool-btn active" : "tool-btn";
    string SwatchClass( Element element ) => $"swatch {element.ToString().ToLowerInvariant()}";
    string ExperimentClass( string name ) => Game?.ActiveExperimentName == name ? "scenario-btn active" : "scenario-btn";

    protected override int BuildHash() => System.HashCode.Combine(
        System.HashCode.Combine( Game?.CurrentTool, Game?.BrushRadius, Game?.Paused, Game?.SpeedScale ),
        System.HashCode.Combine( Game?.Particles, Game?.Discoveries, Game?.LastReaction, Game?.RecentReactions.Count ),
        System.HashCode.Combine( Game?.ActiveExperimentName, Game?.ObjectiveProgress, Game?.ObjectiveComplete, Game?.LabScore ),
        System.HashCode.Combine( Game?.Milestones.Count, _showHelp, _showIntro, Game?.Muted ),
        System.HashCode.Combine( Game?.Particles, Game?.Reactions ) );
}