UI/BlockTally.razor

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.

File Access
@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();
	}
}