UI/MedalStandingsPanel.razor
@using Sandbox
@using Sandbox.UI
@using System
@using System.Linq
@inherits Panel
@namespace BlockParty
@attribute [StyleSheet( "MedalStandingsPanel.razor.scss" )]
<root class="@(Expanded ? "expanded" : "")">
<PixelTooltip @ref="TooltipLayer"></PixelTooltip>
@if ( Expanded )
{
<div class="shade" onclick=@Close></div>
}
@if ( !HasLetterbox && !Expanded )
{
<button class="compact-trigger" onclick=@Open>MEDALS</button>
<button class="compact-trigger completion-trigger" onclick=@OpenCompletions>LEVELS BEATEN</button>
}
else
{
<div class="@(Expanded ? "standings-content" : "standings-rail")" style=@CardStyle>
@if ( !Expanded || !CompletionsExpanded )
{
<div class="medal-card podium-card">
<div class="heading">
<label class="title">MEDALS</label>
@if ( Expanded )
{
<button class="square-btn i-close close" onclick=@Close><div class="img"></div></button>
}
</div>
@if ( Current is not null && (!Expanded || Current.Players.Count > 0) )
{
<div class="columns">
<div class="column-spacer"></div>
<div class="medal gold"><MedalIcon Place=@(1) OnHover=@Tip( AwardPoints.Tip( 1 ) ) Size=@(Expanded ? 44 : 32)></MedalIcon></div>
<div class="medal silver"><MedalIcon Place=@(2) OnHover=@Tip( AwardPoints.Tip( 2 ) ) Size=@(Expanded ? 44 : 32)></MedalIcon></div>
<div class="medal bronze"><MedalIcon Place=@(3) OnHover=@Tip( AwardPoints.Tip( 3 ) ) Size=@(Expanded ? 44 : 32)></MedalIcon></div>
@if ( Expanded ) { <div class="points"></div> }
</div>
}
@if ( Current is not MedalSnapshot snapshot )
{
<label class="notice">@Notice</label>
}
else if ( snapshot.Players.Count == 0 )
{
<label class="notice empty-medals">No medals held yet.</label>
}
else
{
<div class="medal-list">
@foreach ( var player in snapshot.Players.Take( Expanded ? snapshot.Players.Count : 5 ) )
{
<div class="medal-row @(player.AccountId == (long)Game.SteamId ? "you" : "")">
<label class="rank">@player.Rank</label>
<SteamAvatar [email protected] Size=@(Expanded ? 36 : 28)></SteamAvatar>
<LiteralLabel class="name" [email protected] />
<label class="medal gold @(player.Gold == 0 ? "zero" : "")">@player.Gold</label>
<label class="medal silver @(player.Silver == 0 ? "zero" : "")">@player.Silver</label>
<label class="medal bronze @(player.Bronze == 0 ? "zero" : "")">@player.Bronze</label>
@if ( Expanded ) { <label class="points" onmouseover=@Tip( "Total points" )>@player.Points</label> }
</div>
}
</div>
@if ( !Expanded && snapshot.Players.Skip( 5 ).FirstOrDefault( player => player.AccountId == (long)Game.SteamId ) is MedalPlayer me )
{
<div class="medal-row you personal">
<label class="rank">@me.Rank</label>
<label class="name">YOU</label>
<label class="medal gold @(me.Gold == 0 ? "zero" : "")">@me.Gold</label>
<label class="medal silver @(me.Silver == 0 ? "zero" : "")">@me.Silver</label>
<label class="medal bronze @(me.Bronze == 0 ? "zero" : "")">@me.Bronze</label>
</div>
}
}
@if ( Expanded )
{
@if ( Current is not null )
{
<label class="freshness">@Freshness</label>
}
}
else
{
<button class="view-all" onclick=@Open>VIEW MORE</button>
}
</div>
}
@if ( !Expanded || CompletionsExpanded )
{
<CompletionStandingsCard Expanded=@Expanded OnOpen=@OpenCompletions OnClose=@Close></CompletionStandingsCard>
}
@if ( Expanded && !CompletionsExpanded )
{
@* Keep the picker above the entire card, outside the heading's stacking layer. *@
<div class="medal-character-picker">
<CharacterPicker LeaderboardMode=@true CenterOverlayOnScreen=@true
Title="WHO DO YOU WANT TO SEE?" GetSelectedId=@(() => _characterId)
OnPick=@SelectCharacter OnClear=@(() => SelectCharacter( null ))
OnOpenChanged=@PickerOpenChanged @ref="_picker"></CharacterPicker>
</div>
}
</div>
}
</root>
@code
{
// Keep the tooltip outside the scrolling rows so it cannot be clipped.
private PixelTooltip TooltipLayer { get; set; }
private Panel _tipPanel;
private string _tipText;
private Action<PanelEvent> Tip( string text ) => e => { _tipPanel = e.This; _tipText = text; };
private void UpdateTooltip()
{
if ( !_pickerOpen && (!Expanded || !CompletionsExpanded) && _tipPanel.IsValid() && _tipPanel.HasHovered ) TooltipLayer?.Show( _tipText );
else TooltipLayer?.Hide();
}
public bool Expanded { get; private set; }
private bool CompletionsExpanded { get; set; }
private string _characterId;
private CharacterPicker _picker { get; set; }
private bool _pickerOpen;
private string ActiveCharacterId => Expanded && !CompletionsExpanded ? _characterId : null;
private MedalSnapshot Current => MedalStandings.GetSnapshot( ActiveCharacterId );
private string Notice => MedalStandings.GetNotice( ActiveCharacterId );
// Wheel over this overlay must never walk the level-select spine behind it. The engine sends the
// wheel to the hovered panel and bubbles it up the parents, so swallowing it here covers every
// pointer-events child (rail, cards, triggers, expanded shade) — the rail's own overflow scroll
// already consumed it before it got this far. Deliberately NOT a HasHovered test on the screen
// side: :hover latches on a panel's ancestors when the hovered child is deleted mid-hover (any
// razor rebuild), so it can read true forever and silently eat the map's wheel.
public override void OnMouseWheel( Vector2 value ) { }
private float ViewWidth => 1080f * Screen.Width / Math.Max( 1f, Screen.Height );
private float LetterboxWidth => (ViewWidth - 1080f) * 0.5f;
private bool HasLetterbox => LetterboxWidth >= 320f;
private double AgeMinutes => Current is null ? 0 : Math.Max( 0, (DateTimeOffset.UtcNow - Current.UpdatedAt).TotalMinutes );
private string Freshness => $"Updated {(AgeMinutes < 1 ? "just now" : AgeMinutes < 60 ? $"{(int)AgeMinutes}m ago" : $"{(int)(AgeMinutes / 60)}h ago")}";
private string CardStyle => Expanded
? $"width:{Px( Math.Min( CompletionsExpanded ? 720f : 820f, ViewWidth - 64f ) )};"
: $"width:{Px( Math.Min( 360f, LetterboxWidth - 40f ) )};right:{Px( (LetterboxWidth - Math.Min( 360f, LetterboxWidth - 40f )) * 0.5f )};";
private static string Px( float value ) => value.ToString( "0.###", System.Globalization.CultureInfo.InvariantCulture ) + "px";
private void Open() { CompletionsExpanded = false; Expanded = true; StateHasChanged(); }
private void OpenCompletions() { CompletionsExpanded = true; Expanded = true; StateHasChanged(); }
private void SelectCharacter( string id )
{
if ( id is not null && !Characters.TryGet( id, out _ ) ) return;
_characterId = id;
MedalStandings.Tick( id );
StateHasChanged();
}
private void PickerOpenChanged( bool open ) { _pickerOpen = open; StateHasChanged(); }
public void Close()
{
if ( _pickerOpen ) { _picker?.RequestClose(); _pickerOpen = false; }
else Expanded = false;
StateHasChanged();
}
public override void Tick()
{
UpdateTooltip();
MedalStandings.Tick();
if ( ActiveCharacterId is not null ) MedalStandings.Tick( ActiveCharacterId );
CompletionStandings.Tick();
}
protected override int BuildHash() => HashCode.Combine( Expanded, CompletionsExpanded, MedalStandings.Revision, (int)ViewWidth, (int)AgeMinutes, Notice, _characterId, _pickerOpen );
}