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

<root style="opacity: @(Stage?.FadeAlpha ?? 0f)" onclick=@(() => Stage?.Confirm())>
	@{ if ( Stage is null ) return; }
	<PixelTooltip @ref="Tooltip"></PixelTooltip>

	@if ( Settings.Current.ShowTimer )
	{
		<RunTimer [email protected] ShowMilliseconds=@true />
	}

	@{ var sp = Project( Stage.ScoreWorldPos ); }
	@* The run's character floats just above the final score (glow behind it, like the UI badges);
	   a paired character (the twins) composes like the picker's twins icon. *@
	@{ var runCharacter = Stage.RunCharacter; }
	<div class="score-character @(runCharacter.Partner is null ? "" : "paired")" style="@ScoreCharacterStyle( sp.x, sp.y, runCharacter )">
		<div class="icon-glow"></div>
		@if ( runCharacter.Partner is CharacterDef runPartner )
		{
			<div class="sprite rear" style="background-image:url( @runPartner.PreviewImage );"></div>
		}
		<div class="sprite front" style="background-image:url( @runCharacter.PreviewImage );"></div>
	</div>
	<label class="score @(Stage.Victory ? "victory-score" : "non-victory")" style="left: @(sp.x)px; top: @(sp.y)px;">@Stage.Score</label>

	@{ var bannerPos = Project( new Vector2( 120f, 120f ) ); }

	@* Outcome banner one line above the TIME BONUS slot: VICTORY! on a win, YOU DIED on a loss. *@
	@if ( Stage.ShowVictory )
	{
		<label class="outcome victory" style="left: @(bannerPos.x)px; top: @(bannerPos.y - 55f)px;">VICTORY!</label>
	}
	else if ( Stage.ShowDeath )
	{
		<label class="outcome died" style="left: @(bannerPos.x)px; top: @(bannerPos.y - 55f)px;">YOU DIED</label>
	}

	@if ( !string.IsNullOrEmpty( Stage.Banner ) )
	{
		<label class="banner" style="left: @(bannerPos.x)px; top: @(bannerPos.y)px;">@Stage.Banner</label>
	}

	@* Coin count-out: on a win one line below the TIME BONUS slot (mirroring VICTORY!'s line above
	   it); on a loss there is no time bonus, so the coins take the empty centre slot under YOU DIED. *@
	@if ( !string.IsNullOrEmpty( Stage.CoinBanner ) )
	{
		<label class="banner coins" style="left: @(bannerPos.x)px; top: @(bannerPos.y + (Stage.Victory ? 55f : 0f))px;">@Stage.CoinBanner</label>
	}

	@foreach ( var f in Stage.Floaters )
	{
		var p = Project( f.World );
		var alpha = Math.Clamp( 1f - f.Age / 0.9f, 0f, 1f );
		<label class="floater" style="left: @(p.x)px; top: @(p.y)px; color: @f.Color.Hex; opacity: @alpha;">@f.Text</label>
	}

	@if ( Stage.Finished )
	{
		@* Daily runs always show the day's attempt state above the buttons (debug runs read the
		   display-only debug counter while daily_debug_nav is on). Null on non-daily runs. The line
		   hugs the top of the button list rather than sitting at a fixed height, so it can't drift up
		   into the tally's lower block row (which reaches down to 234px on a 4+ block layout). *@
		@if ( Stage.DailyAttemptsText is string attempts )
		{
			<label class="attempts" style="bottom: @(AttemptsBottom)px;">@attempts</label>
		}
		<div class="buttons">
			@* List wrapper is position:relative so the "smiling cube" selector (absolutely
			   positioned) can slide vertically between the rows — same selector the menus use. *@
			<div class="list">
				<div class="selector"
					 style="top: @(Stage.ButtonSelectorPos * ROW_PITCH + SELECTOR_INSET)px; background-position: @(-Stage.ButtonSelectorFrame * 100)% 0%;"></div>

				@for ( int i = 0; i < Stage.Buttons.Count; i++ )
				{
					var idx = i;
					var selected = Stage.SelectedButton == idx;
					<div class="button @(selected ? "selected" : "")"
						 onmouseover=@(() => Stage.SelectButton( idx ))
						 onclick=@((PanelEvent e) => { e.StopPropagation(); Stage.ActivateButton( idx ); })>
						<label class="text">@Label( Stage.Buttons[idx] )</label>
					</div>
				}
			</div>
		</div>

		@* Shortcuts out of the finished tally, in the play square's bottom corners so they stay clear
		   of the RESTART / CONTINUE lane. Right: replay / leaderboard — both remember this tally
		   (ScoreStage.Snapshot), so BACK from the board — or exiting the replay — drops us straight
		   back here with the count-out already played out. Left: the level's Steam Workshop page
		   (workshop levels only). Clicks are stopped: the root's own onclick would otherwise fire the
		   selected button. *@
		<div class="foot-buttons">
			@* Inner box is the 1080-wide play square, so the corners hug the arena's edges rather
			   than drifting out into the letterbox on wide screens. *@
			<div class="inner">
			<div class="left">
			@if ( Stage.WorkshopItemUrl is string workshopUrl )
			{
				<WorkshopLinkButton Url=@workshopUrl OnHover=@Tip( "Steam Workshop page" )></WorkshopLinkButton>
			}
			</div>
			<div class="right">
			@if ( Stage.CanWatchReplay )
			{
				<button class="square-btn i-replay" onclick=@((PanelEvent e) => { e.StopPropagation(); Stage.WatchReplay(); })
					onmouseover=@Tip( "Watch this run" )>
					<div class="img"></div>
				</button>
			}
			@if ( Stage.CanViewLeaderboard )
			{
				<button class="square-btn i-trophy" onclick=@((PanelEvent e) => { e.StopPropagation(); Stage.ViewLeaderboard(); })
					onmouseover=@Tip( "View full leaderboard" )>
					<div class="img"></div>
				</button>
			}
			</div>
			</div>
		</div>
	}
</root>

@code
{
	public ScoreStage Stage { get; set; }

	// The run-character icon above the score: its sprite box (1080-ref px, scaled by the character's
	// preview proportion) and how far its centre sits above the score's centre.
	const float SCORE_CHAR_SIZE = 72f;
	const float SCORE_CHAR_LIFT = 75f;

	private static string ScoreCharacterStyle( float x, float y, CharacterDef character )
	{
		float size = SCORE_CHAR_SIZE * character.PreviewScale;
		return FormattableString.Invariant(
			$"left:{x - size / 2f:0.###}px;top:{y - SCORE_CHAR_LIFT - size / 2f:0.###}px;width:{size:0.###}px;height:{size:0.###}px;" );
	}

	// Vertical distance (1080-ref px) between button rows. MUST match the SCSS:
	// .button height + .list gap. The selector's top = ButtonSelectorPos * ROW_PITCH + inset.
	const float ROW_PITCH = 80f;
	// Vertically centre the (48px) cube against the (64px) row.
	const float SELECTOR_INSET = 8f;
	// Button block's bottom offset and the list's inter-row gap. MUST match the SCSS.
	const float BUTTONS_BOTTOM = 50f;
	const float LIST_GAP = 16f;
	// Gap between the top of the button list and the daily attempts line above it.
	const float ATTEMPTS_GAP = 6f;

	// The daily attempts line sits just above the button list, wherever that list's top lands (N
	// rows tall = N * ROW_PITCH minus the trailing gap), instead of at a fixed height: the clear
	// band above a two-button list is only ~40px, since the tally's lower block row reaches down to
	// 234px on a 4+ block layout.
	private float AttemptsBottom
		=> BUTTONS_BOTTOM + Stage.Buttons.Count * ROW_PITCH - LIST_GAP + ATTEMPTS_GAP;

	// Hover tooltip layer for the footer's icon buttons — same mechanism as the other screens: the
	// hovered panel is remembered on mouseover and re-validated each frame (see LevelEditorHud).
	private PixelTooltip Tooltip { get; set; }
	private Panel _tipPanel;
	private string _tipText;

	private Action<PanelEvent> Tip( string text )
		=> e => { _tipPanel = e.This; _tipText = text; };

	protected override void OnUpdate()
	{
		if ( _tipPanel.IsValid() && _tipPanel.HasHovered && !string.IsNullOrEmpty( _tipText ) ) Tooltip?.Show( _tipText );
		else Tooltip?.Hide();
	}

	private static string Label( ScoreStage.ScoreButton b ) => b switch
	{
		ScoreStage.ScoreButton.Restart => "RESTART",
		_ => "CONTINUE",
	};

	// Project a world position (240-unit play space, centred at 120,120) into 1080-reference UI px.
	private (float x, float y) Project( Vector2 world )
	{
		float px = 1080f / Arena.HEIGHT;
		float uiW = 1080f * (Screen.Width / (float)Screen.Height);
		float x = uiW / 2f + (world.x - Arena.WIDTH / 2f) * px;
		float y = (Arena.HEIGHT - world.y) * px;
		return (x, y);
	}

	protected override int BuildHash()
	{
		if ( Stage is null ) return 0;

		float ageSum = 0f;
		foreach ( var f in Stage.Floaters ) ageSum += f.Age;

		return System.HashCode.Combine(
			System.HashCode.Combine(
				Stage.Score,
				Settings.Current.ShowTimer,
				(int)Stage.GameTime,
				Stage.Finished,
				Stage.Banner,
				System.HashCode.Combine( Stage.ShowVictory, Stage.ShowDeath ),
				Stage.Floaters.Count,
				(int)(ageSum * 20f) ),
			System.HashCode.Combine(
				(int)(Stage.FadeAlpha * 20f),
				Stage.SelectedButton,
				(int)(Stage.ButtonSelectorPos * 100f),
				Stage.ButtonSelectorFrame,
				Stage.DailyAttemptsText,
				Stage.CoinBanner,
				Stage.CanWatchReplay,
				System.HashCode.Combine( Stage.CanViewLeaderboard, Stage.WorkshopItemUrl ) ) );
	}
}