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) );
}