UI component (Razor) that renders a compact end-of-run block tally. It draws five block bodies and four side sprites each based on BlockTallyState, phase, type, and overall victory to pick asset image paths.
@using System
@using System.Collections.Generic
@using System.Linq
@using Sandbox
@using Sandbox.UI
@inherits Panel
@namespace BlockParty
@attribute [StyleSheet( "BlockTally.razor.scss" )]
@* Small graphic of a run's five blocks, reproducing the score-tally end state: a faded block
when untouched, each side lit for the phase it reached, and full colour + yellow (max) sides
when maxed. Blocks are shown in a fixed order (by type), independent of the order the player
pressed them. Pure display — the host row passes the decoded states (RunData.DecodeBlocks()).
Each block is one composited body layer (sprites/ui/blocks, level = phase + 1) with the four
side button sprites drawn on top; the edge buttons never overlap the central face features, so
this matches the live ScoreStage.TallyDisplay look with far fewer UI nodes. *@
<root class="block-tally">
@if ( Blocks is not null )
{
bool victory = IsVictory;
@foreach ( var b in Blocks )
{
<div class="block">
<div class="layer body" style="background-image: url( @BodyImage( b, victory ) )"></div>
@foreach ( var dir in Globals.GetAllDirections() )
{
<div class="layer side" style="background-image: url( @SideImage( b, dir, victory ) )"></div>
}
</div>
}
}
</root>
@code
{
[Parameter] public IReadOnlyList<BlockTallyState> Blocks { get; set; }
// A run is a victory iff every block reached max phase (the game ends the instant they all do),
// so we can detect it from the decoded states without threading a separate flag through. On a
// win the row shows every block with its death face and fully-lit (max) sides.
private bool IsVictory => Blocks is { Count: > 0 } && Blocks.All( b => b.Phase >= Block.NUM_PHASES - 1 );
// Body image for one block. Until a block advances past phase 0 it keeps the faded "asleep"
// tally look; once it reaches a phase it shows that phase's real face; on a full win every
// block shows its death face (mirrors ScoreStage end-of-tally + the live win sequence).
// Maps a block type to its UI-art base name. The map is the lowercased enum name.
private static string UiName( BlockType type ) => type.ToString().ToLowerInvariant();
private static string BodyImage( BlockTallyState b, bool victory )
{
string t = UiName( b.Type );
// A real Mimic result can only be phase 0; phase 1 replaces it with its disguise. Keep
// malformed/debug states renderable without carrying impossible Mimic phase/death composites.
if ( b.Type == BlockType.Mimic )
return b.Phase == 0 ? "sprites/ui/blocks/mimic_tally.png" : "sprites/ui/blocks/mimic_preview.png";
if ( victory )
return $"sprites/ui/blocks/{t}_dead.png";
if ( b.Phase >= 2 )
return $"sprites/ui/blocks/{t}_p2.png";
if ( b.Phase == 1 )
return $"sprites/ui/blocks/{t}_p1.png";
return $"sprites/ui/blocks/{t}_tally.png"; // phase 0: dim grey "asleep" body
}
// Side button state, matching the score-tally end state. Rest frame index: out buttons sit
// fully extended (3); pressed/max rest one notch in (2). When a block reached a new phase but
// pressed nothing in it, the four buttons that took it up stay depressed in the tier they were
// pressed at (phase-1 => phase-0 blue), so the progress reads instead of looking untouched.
private static string SideImage( BlockTallyState b, Direction dir, bool victory )
{
string d = Globals.GetStringForDirection( dir );
if ( victory || b.Phase >= 2 )
return $"sprites/ui/sides/max_2_{d}.png"; // maxed: every side yellow
int pressedCount = (b.Left ? 1 : 0) + (b.Right ? 1 : 0) + (b.Up ? 1 : 0) + (b.Down ? 1 : 0);
if ( b.Phase >= 1 && pressedCount == 0 )
return $"sprites/ui/sides/pressed{b.Phase - 1}_2_{d}.png"; // reached this phase, nothing pressed yet
bool pressed = IsPressed( b, dir );
if ( b.Phase == 1 )
return pressed ? $"sprites/ui/sides/pressed1_2_{d}.png" : $"sprites/ui/sides/out1dark_3_{d}.png";
return pressed ? $"sprites/ui/sides/pressed0_2_{d}.png" : $"sprites/ui/sides/out0dark_3_{d}.png";
}
private static bool IsPressed( BlockTallyState b, Direction dir ) => dir switch
{
Direction.Left => b.Left,
Direction.Right => b.Right,
Direction.Up => b.Up,
Direction.Down => b.Down,
_ => false,
};
protected override int BuildHash()
{
var hc = new HashCode();
hc.Add( Blocks?.Count ?? 0 );
if ( Blocks is not null )
foreach ( var b in Blocks )
hc.Add( b );
return hc.ToHashCode();
}
}