1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-17 16:12:53 +08:00
osu-lazer/osu.Game/Screens/Ranking/ResultsScreen.cs

433 lines
16 KiB
C#
Raw Normal View History

2020-03-17 16:43:16 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2020-03-18 17:28:42 +08:00
using System;
2020-05-26 16:00:41 +08:00
using System.Collections.Generic;
2024-05-14 18:57:30 +08:00
using System.Diagnostics;
using System.Linq;
2020-03-17 16:43:16 +08:00
using osu.Framework.Allocation;
2022-07-22 18:06:31 +08:00
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
2020-03-17 16:43:16 +08:00
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
2021-09-16 17:26:12 +08:00
using osu.Framework.Input.Events;
2020-03-17 16:43:16 +08:00
using osu.Framework.Screens;
using osu.Game.Graphics;
2020-03-17 21:21:16 +08:00
using osu.Game.Graphics.Containers;
2020-03-17 16:43:16 +08:00
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Placeholders;
using osu.Game.Overlays;
2020-03-17 16:43:16 +08:00
using osu.Game.Scoring;
2020-03-17 16:45:25 +08:00
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking.Expanded.Accuracy;
using osu.Game.Screens.Ranking.Statistics;
2020-03-17 16:43:16 +08:00
using osuTK;
namespace osu.Game.Screens.Ranking
{
2022-11-24 13:32:20 +08:00
public abstract partial class ResultsScreen : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>
2020-03-17 16:43:16 +08:00
{
protected const float BACKGROUND_BLUR = 20;
private static readonly float screen_height = 768 - TwoLayerButton.SIZE_EXTENDED.Y;
2020-03-17 16:43:16 +08:00
public override bool DisallowExternalBeatmapRulesetChanges => true;
public override bool? AllowGlobalTrackControl => true;
protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered;
public readonly Bindable<ScoreInfo?> SelectedScore = new Bindable<ScoreInfo?>();
public readonly ScoreInfo? Score;
2020-07-31 18:57:05 +08:00
protected ScorePanelList ScorePanelList { get; private set; } = null!;
protected VerticalScrollContainer VerticalScrollContent { get; private set; } = null!;
[Resolved]
private Player? player { get; set; }
2020-03-17 16:45:25 +08:00
[Resolved]
private IAPIProvider api { get; set; } = null!;
protected StatisticsPanel StatisticsPanel { get; private set; } = null!;
private Drawable bottomPanel = null!;
private Container<ScorePanel> detachedPanelContainer = null!;
2020-03-17 16:43:16 +08:00
private bool lastFetchCompleted;
2020-07-28 19:58:13 +08:00
/// <summary>
/// Whether the user can retry the beatmap from the results screen.
/// </summary>
public bool AllowRetry { get; init; }
/// <summary>
/// Whether the user can watch the replay of the completed play from the results screen.
/// </summary>
public bool AllowWatchingReplay { get; init; } = true;
2020-07-31 18:57:05 +08:00
/// <summary>
/// Whether the user's personal statistics should be shown on the extended statistics panel
/// after clicking the score panel associated with the <see cref="ResultsScreen.Score"/> being presented.
/// Requires <see cref="Score"/> to be present.
/// </summary>
public bool ShowUserStatistics { get; init; }
private Sample? popInSample;
2022-07-22 18:06:31 +08:00
protected ResultsScreen(ScoreInfo? score)
2020-03-17 16:43:16 +08:00
{
2020-03-29 22:50:16 +08:00
Score = score;
SelectedScore.Value = score;
2020-03-17 16:43:16 +08:00
}
[BackgroundDependencyLoader]
2022-07-22 18:06:31 +08:00
private void load(AudioManager audio)
2020-03-17 16:43:16 +08:00
{
FillFlowContainer buttons;
2022-07-22 18:06:31 +08:00
popInSample = audio.Samples.Get(@"UI/overlay-pop-in");
InternalChild = new GridContainer
2020-03-17 16:43:16 +08:00
{
RelativeSizeAxes = Axes.Both,
Content = new[]
2020-03-17 16:43:16 +08:00
{
new Drawable[]
2020-03-17 21:21:16 +08:00
{
VerticalScrollContent = new VerticalScrollContainer
2020-03-17 16:43:16 +08:00
{
2020-06-17 21:29:00 +08:00
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
StatisticsPanel = createStatisticsPanel().With(panel =>
{
panel.RelativeSizeAxes = Axes.Both;
panel.Score.BindTarget = SelectedScore;
}),
2020-07-31 18:57:05 +08:00
ScorePanelList = new ScorePanelList
{
RelativeSizeAxes = Axes.Both,
SelectedScore = { BindTarget = SelectedScore },
PostExpandAction = () => StatisticsPanel.ToggleVisibility()
},
detachedPanelContainer = new Container<ScorePanel>
{
RelativeSizeAxes = Axes.Both
},
}
}
},
},
new[]
{
bottomPanel = new Container
2020-03-17 16:43:16 +08:00
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = TwoLayerButton.SIZE_EXTENDED.Y,
Alpha = 0,
2020-03-17 16:43:16 +08:00
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#333")
},
buttons = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5),
Direction = FillDirection.Horizontal
}
2020-03-17 16:43:16 +08:00
}
}
}
},
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
2020-03-17 16:43:16 +08:00
}
};
2020-03-17 16:45:25 +08:00
if (Score != null)
{
// only show flair / animation when arriving after watching a play that isn't autoplay.
bool shouldFlair = player != null && !Score.User.IsBot;
ScorePanelList.AddScore(Score, shouldFlair);
// this is mostly for medal display.
// we don't want the medal animation to trample on the results screen animation, so we (ab)use `OverlayActivationMode`
// to give the results screen enough time to play the animation out before the medals can be shown.
Scheduler.AddDelayed(() => OverlayActivationMode.Value = OverlayActivation.All, shouldFlair ? AccuracyCircle.TOTAL_DURATION + 1000 : 0);
}
if (SelectedScore.Value != null && AllowWatchingReplay)
{
buttons.Add(new ReplayDownloadButton(SelectedScore.Value)
{
Score = { BindTarget = SelectedScore! },
Width = 300
});
}
if (player != null && AllowRetry)
2020-03-17 16:45:25 +08:00
{
2020-03-30 17:56:35 +08:00
buttons.Add(new RetryButton { Width = 300 });
2020-03-30 17:56:35 +08:00
AddInternal(new HotkeyRetryOverlay
{
Action = () =>
2020-03-17 16:45:25 +08:00
{
2020-03-30 17:56:35 +08:00
if (!this.IsCurrentScreen()) return;
2020-03-17 16:45:25 +08:00
player?.Restart(true);
2020-03-30 17:56:35 +08:00
},
});
2020-03-17 16:45:25 +08:00
}
2020-03-17 16:43:16 +08:00
}
protected override void LoadComplete()
{
base.LoadComplete();
2020-07-22 19:24:55 +08:00
var req = FetchScores(fetchScoresCallback);
2020-05-26 16:00:41 +08:00
if (req != null)
api.Queue(req);
2020-06-18 21:27:27 +08:00
StatisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true);
}
2020-07-22 19:24:55 +08:00
protected override void Update()
{
base.Update();
if (lastFetchCompleted)
2020-07-22 19:24:55 +08:00
{
APIRequest? nextPageRequest = null;
2024-01-09 20:28:46 +08:00
if (ScorePanelList.IsScrolledToStart)
nextPageRequest = FetchNextPage(-1, fetchScoresCallback);
else if (ScorePanelList.IsScrolledToEnd)
nextPageRequest = FetchNextPage(1, fetchScoresCallback);
2020-07-22 19:24:55 +08:00
2024-01-09 20:28:46 +08:00
if (nextPageRequest != null)
{
lastFetchCompleted = false;
api.Queue(nextPageRequest);
2020-07-22 19:24:55 +08:00
}
}
}
2020-05-26 16:00:41 +08:00
/// <summary>
/// Performs a fetch/refresh of scores to be displayed.
/// </summary>
/// <param name="scoresCallback">A callback which should be called when fetching is completed. Scheduling is not required.</param>
/// <returns>An <see cref="APIRequest"/> responsible for the fetch operation. This will be queued and performed automatically.</returns>
protected virtual APIRequest? FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback) => null;
2020-05-26 16:00:41 +08:00
2020-07-28 19:58:13 +08:00
/// <summary>
/// Performs a fetch of the next page of scores. This is invoked every frame until a non-null <see cref="APIRequest"/> is returned.
/// </summary>
/// <param name="direction">The fetch direction. -1 to fetch scores greater than the current start of the list, and 1 to fetch scores lower than the current end of the list.</param>
/// <param name="scoresCallback">A callback which should be called when fetching is completed. Scheduling is not required.</param>
/// <returns>An <see cref="APIRequest"/> responsible for the fetch operation. This will be queued and performed automatically.</returns>
protected virtual APIRequest? FetchNextPage(int direction, Action<IEnumerable<ScoreInfo>> scoresCallback) => null;
2020-07-22 19:24:55 +08:00
/// <summary>
/// Creates the <see cref="Statistics.StatisticsPanel"/> to be used to display extended information about scores.
/// </summary>
private StatisticsPanel createStatisticsPanel()
{
return ShowUserStatistics && Score != null
? new UserStatisticsPanel(Score)
: new StatisticsPanel();
}
2020-07-22 19:24:55 +08:00
private void fetchScoresCallback(IEnumerable<ScoreInfo> scores) => Schedule(() =>
{
foreach (var s in scores)
addScore(s);
lastFetchCompleted = true;
2024-01-09 20:28:46 +08:00
if (ScorePanelList.IsEmpty)
2024-01-09 20:28:46 +08:00
{
// This can happen if for example a beatmap that is part of a playlist hasn't been played yet.
VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet));
}
2020-07-22 19:24:55 +08:00
});
public override void OnEntering(ScreenTransitionEvent e)
2020-03-17 16:43:16 +08:00
{
base.OnEntering(e);
2020-03-17 16:43:16 +08:00
ApplyToBackground(b =>
{
b.BlurAmount.Value = BACKGROUND_BLUR;
b.FadeColour(OsuColour.Gray(0.5f), 250);
});
2020-03-17 16:43:16 +08:00
bottomPanel.FadeTo(1, 250);
2022-07-22 18:06:31 +08:00
popInSample?.Play();
2020-03-17 16:43:16 +08:00
}
public override bool OnExiting(ScreenExitEvent e)
{
if (base.OnExiting(e))
return true;
// This is a stop-gap safety against components holding references to gameplay after exiting the gameplay flow.
// Right now, HitEvents are only used up to the results screen. If this changes in the future we need to remove
// HitObject references from HitEvent.
Score?.HitEvents.Clear();
this.FadeOut(100);
return false;
}
public override bool OnBackButton()
2020-03-17 16:43:16 +08:00
{
if (StatisticsPanel.State.Value == Visibility.Visible)
2020-06-18 21:27:27 +08:00
{
StatisticsPanel.Hide();
2020-06-18 21:27:27 +08:00
return true;
}
return false;
2020-03-17 16:43:16 +08:00
}
2020-03-17 21:21:16 +08:00
private void addScore(ScoreInfo score)
{
2020-07-31 18:57:05 +08:00
var panel = ScorePanelList.AddScore(score);
if (detachedPanel != null)
panel.Alpha = 0;
}
private ScorePanel? detachedPanel;
2020-06-18 21:27:27 +08:00
private void onStatisticsStateChanged(ValueChangedEvent<Visibility> state)
2020-03-17 21:21:16 +08:00
{
if (state.NewValue == Visibility.Visible)
2020-03-17 21:21:16 +08:00
{
2024-05-14 18:57:30 +08:00
Debug.Assert(SelectedScore.Value != null);
// Detach the panel in its original location, and move into the desired location in the local container.
2020-07-31 18:57:05 +08:00
var expandedPanel = ScorePanelList.GetPanelForScore(SelectedScore.Value);
var screenSpacePos = expandedPanel.ScreenSpaceDrawQuad.TopLeft;
2020-06-18 21:27:27 +08:00
// Detach and move into the local container.
2020-07-31 18:57:05 +08:00
ScorePanelList.Detach(expandedPanel);
detachedPanelContainer.Add(expandedPanel);
2020-06-22 14:42:55 +08:00
// Move into its original location in the local container first, then to the final location.
float origLocation = detachedPanelContainer.ToLocalSpace(screenSpacePos).X;
expandedPanel.MoveToX(origLocation)
2020-06-22 14:42:55 +08:00
.Then()
.MoveToX(StatisticsPanel.SIDE_PADDING, 400, Easing.OutElasticQuarter);
// Hide contracted panels.
2020-07-31 18:57:05 +08:00
foreach (var contracted in ScorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted))
contracted.FadeOut(150, Easing.OutQuint);
2020-07-31 18:57:05 +08:00
ScorePanelList.HandleInput = false;
// Dim background.
ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.4f), 400, Easing.OutQuint));
detachedPanel = expandedPanel;
}
else if (detachedPanel != null)
{
var screenSpacePos = detachedPanel.ScreenSpaceDrawQuad.TopLeft;
2020-06-18 21:27:27 +08:00
// Remove from the local container and re-attach.
detachedPanelContainer.Remove(detachedPanel, false);
2020-07-31 18:57:05 +08:00
ScorePanelList.Attach(detachedPanel);
2020-06-22 14:42:55 +08:00
// Move into its original location in the attached container first, then to the final location.
float origLocation = detachedPanel.Parent!.ToLocalSpace(screenSpacePos).X;
detachedPanel.MoveToX(origLocation)
2020-06-22 14:42:55 +08:00
.Then()
.MoveToX(0, 250, Easing.OutElasticQuarter);
// Show contracted panels.
2020-07-31 18:57:05 +08:00
foreach (var contracted in ScorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted))
contracted.FadeIn(150, Easing.OutQuint);
2020-07-31 18:57:05 +08:00
ScorePanelList.HandleInput = true;
// Un-dim background.
ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.5f), 250, Easing.OutQuint));
detachedPanel = null;
2020-03-17 21:21:16 +08:00
}
}
2021-09-16 17:26:12 +08:00
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)
return false;
2021-09-16 17:26:12 +08:00
switch (e.Action)
{
case GlobalAction.QuickExit:
if (this.IsCurrentScreen())
{
this.Exit();
return true;
}
break;
case GlobalAction.Select:
StatisticsPanel.ToggleVisibility();
return true;
}
return false;
}
2021-09-16 17:26:12 +08:00
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
2022-11-24 13:32:20 +08:00
protected partial class VerticalScrollContainer : OsuScrollContainer
{
protected override Container<Drawable> Content => content;
private readonly Container content;
public VerticalScrollContainer()
{
Masking = false;
base.Content.Add(content = new Container { RelativeSizeAxes = Axes.X });
}
protected override void Update()
{
base.Update();
content.Height = Math.Max(screen_height, DrawHeight);
}
}
2020-03-17 16:43:16 +08:00
}
}