UI/UnlockRevealOverlay.razor

A Razor UI component that displays a full-screen "YOU UNLOCKED" overlay when GameManager's UnlockReveal is active. It renders character artwork (including partner layering), sizing and positioning based on preview scales and provides click-to-dismiss behavior while animating opacity.

@using System
@using Sandbox
@using Sandbox.UI
@inherits PanelComponent
@namespace BlockParty
@attribute [StyleSheet( "UnlockRevealOverlay.razor.scss" )]

@* Persistent "YOU UNLOCKED" overlay, driven by GameManager's UnlockReveal. Renders nothing until a
   character is being revealed, then covers the whole screen (so whatever stage is underneath can't
   be clicked) until the player dismisses it and it fades away. *@
<root>
	@if ( Reveal is { IsActive: true, Character: CharacterDef character } )
	{
		var scale = ViewportScale( character );
		<div class="backdrop" style="opacity: @AlphaStyle();" onclick=@(() => Reveal.Dismiss())>
			<div class="panel">
				<label class="heading">YOU UNLOCKED</label>
				<div class="character @(character.Partner is null ? "single" : "paired")" style="@ViewportStyle( scale )">
					<div class="icon-glow"></div>
					@if ( character.Partner is CharacterDef partner )
					{
						<div class="sprite-layer rear" style="@LayerStyle( partner, scale )"></div>
					}
					<div class="sprite-layer front" style="@LayerStyle( character, scale )"></div>
				</div>
				<label class="name">@character.Name</label>
			</div>
		</div>
	}
</root>

@code
{
	/// <summary>The reveal state this overlay draws; set by GameManager when the overlay is created.</summary>
	public UnlockReveal Reveal { get; set; }

	// Same box as the character-picker's grid preview. The 110:120 ratio matters: the pre-baked glow
	// PNG is placed as a fixed percentage of it (see CharacterPicker.razor.scss .icon-glow), so if
	// this is resized keep both dimensions on the same multiplier — and re-check the panel padding,
	// which is sized to keep the glow's tail (60.5% of the height, per side) inside the border.
	const float ICON_WIDTH = 110f;
	const float ICON_HEIGHT = 120f;

	// The viewport holds the character AND (for a paired character) its partner, so it takes the
	// larger of the two preview scales; each layer is then sized by its own scale and centred in it.
	private static float ViewportScale( CharacterDef character )
		=> character.Partner is CharacterDef partner
			? MathF.Max( character.PreviewScale, partner.PreviewScale )
			: character.PreviewScale;

	private static string ViewportStyle( float viewportScale )
		=> $"width:{Px( ICON_WIDTH * viewportScale )};height:{Px( ICON_HEIGHT * viewportScale )};";

	private static string LayerStyle( CharacterDef character, float viewportScale )
	{
		float width = ICON_WIDTH * character.PreviewScale;
		float height = ICON_HEIGHT * character.PreviewScale;
		float left = (ICON_WIDTH * viewportScale - width) * 0.5f;
		float top = (ICON_HEIGHT * viewportScale - height) * 0.5f;
		return $"left:{Px( left )};top:{Px( top )};width:{Px( width )};height:{Px( height )};"
			+ $"background-image:url( {character.PreviewImage} );";
	}

	private static string Px( float value ) => $"{value:0.##}px";

	// Invariant so a comma-decimal locale can't emit "opacity: 0,5" — the same care TransitionOverlay
	// takes with its inline size.
	private string AlphaStyle()
		=> (Reveal?.Alpha ?? 0f).ToString( "0.###", System.Globalization.CultureInfo.InvariantCulture );

	protected override int BuildHash()
		=> HashCode.Combine( Reveal?.Character?.Id, (int)((Reveal?.Alpha ?? 0f) * 100f) );
}