diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs similarity index 87% rename from osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboard.cs rename to osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 186f27a8b2..fb27ec7654 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Users; @@ -14,19 +16,20 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelect { - [Description("PlaySongSelect leaderboard")] - public class TestSceneLeaderboard : OsuTestScene + public class TestSceneBeatmapLeaderboard : OsuTestScene { public override IReadOnlyList RequiredTypes => new[] { typeof(Placeholder), typeof(MessagePlaceholder), typeof(RetrievalFailurePlaceholder), + typeof(UserTopScoreContainer), + typeof(Leaderboard), }; private readonly FailableLeaderboard leaderboard; - public TestSceneLeaderboard() + public TestSceneBeatmapLeaderboard() { Add(leaderboard = new FailableLeaderboard { @@ -37,6 +40,7 @@ namespace osu.Game.Tests.Visual.SongSelect }); AddStep(@"New Scores", newScores); + AddStep(@"Show personal best", showPersonalBest); AddStep(@"Empty Scores", () => leaderboard.SetRetrievalState(PlaceholderState.NoScores)); AddStep(@"Network failure", () => leaderboard.SetRetrievalState(PlaceholderState.NetworkFailure)); AddStep(@"No supporter", () => leaderboard.SetRetrievalState(PlaceholderState.NotSupporter)); @@ -47,6 +51,32 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep($"{status} beatmap", () => showBeatmapWithStatus(status)); } + private void showPersonalBest() + { + leaderboard.TopScore = new APILegacyUserTopScoreInfo + { + Position = 999, + Score = new APILegacyScoreInfo + { + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + User = new User + { + Id = 6602580, + Username = @"waaiiru", + Country = new Country + { + FullName = @"Spain", + FlagName = @"ES", + }, + }, + } + }; + } + private void newScores() { var scores = new[] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs new file mode 100644 index 0000000000..7fac45e0f1 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs @@ -0,0 +1,119 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Scoring; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.SongSelect +{ + public class TestSceneUserTopScoreContainer : OsuTestScene + { + public TestSceneUserTopScoreContainer() + { + UserTopScoreContainer topScoreContainer; + + Add(new Container + { + Origin = Anchor.BottomCentre, + Anchor = Anchor.Centre, + AutoSizeAxes = Axes.Y, + Width = 500, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.DarkGreen, + }, + topScoreContainer = new UserTopScoreContainer + { + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + } + } + }); + + var scores = new[] + { + new APILegacyUserTopScoreInfo + { + Position = 999, + Score = new APILegacyScoreInfo + { + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + User = new User + { + Id = 6602580, + Username = @"waaiiru", + Country = new Country + { + FullName = @"Spain", + FlagName = @"ES", + }, + }, + } + }, + new APILegacyUserTopScoreInfo + { + Position = 110000, + Score = new APILegacyScoreInfo + { + Rank = ScoreRank.X, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + User = new User + { + Id = 4608074, + Username = @"Skycries", + Country = new Country + { + FullName = @"Brazil", + FlagName = @"BR", + }, + }, + } + }, + new APILegacyUserTopScoreInfo + { + Position = 22333, + Score = new APILegacyScoreInfo + { + Rank = ScoreRank.S, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + User = new User + { + Id = 1541390, + Username = @"Toukai", + Country = new Country + { + FullName = @"Canada", + FlagName = @"CA", + }, + }, + } + } + }; + + AddStep(@"Trigger visibility", topScoreContainer.ToggleVisibility); + AddStep(@"Add score(rank 999)", () => topScoreContainer.Score.Value = scores[0]); + AddStep(@"Add score(rank 110000)", () => topScoreContainer.Score.Value = scores[1]); + AddStep(@"Add score(rank 22333)", () => topScoreContainer.Score.Value = scores[2]); + AddStep(@"Add null score", () => topScoreContainer.Score.Value = null); + } + } +} diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 147556b78b..83de0635fb 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -35,6 +35,10 @@ namespace osu.Game.Online.Leaderboards private bool scoresLoadedOnce; + private readonly Container content; + + protected override Container Content => content; + private IEnumerable scores; public IEnumerable Scores @@ -60,13 +64,13 @@ namespace osu.Game.Online.Leaderboards // ensure placeholder is hidden when displaying scores PlaceholderState = PlaceholderState.Successful; - var sf = CreateScoreFlow(); - sf.ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)); + var scoreFlow = CreateScoreFlow(); + scoreFlow.ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)); // schedule because we may not be loaded yet (LoadComponentAsync complains). - showScoresDelegate = Schedule(() => LoadComponentAsync(sf, _ => + showScoresDelegate = Schedule(() => LoadComponentAsync(scoreFlow, _ => { - scrollContainer.Add(scrollFlow = sf); + scrollContainer.Add(scrollFlow = scoreFlow); int i = 0; @@ -116,9 +120,7 @@ namespace osu.Game.Online.Leaderboards { if (value != PlaceholderState.Successful) { - getScoresRequest?.Cancel(); - getScoresRequest = null; - Scores = null; + Reset(); } if (value == placeholderState) @@ -162,12 +164,35 @@ namespace osu.Game.Online.Leaderboards protected Leaderboard() { - Children = new Drawable[] + InternalChildren = new Drawable[] { - scrollContainer = new OsuScrollContainer + new GridContainer { RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + scrollContainer = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + } + }, + new Drawable[] + { + content = new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }, + } + }, }, loading = new LoadingAnimation(), placeholderContainer = new Container @@ -177,6 +202,13 @@ namespace osu.Game.Online.Leaderboards }; } + protected virtual void Reset() + { + getScoresRequest?.Cancel(); + getScoresRequest = null; + Scores = null; + } + private IAPIProvider api; private ScheduledDelegate pendingUpdateScores; diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 0b84cfc28a..9387482f14 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -20,23 +20,23 @@ using osu.Game.Scoring; using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; +using Humanizer; namespace osu.Game.Online.Leaderboards { public class LeaderboardScore : OsuClickableContainer { - public readonly int RankPosition; - public const float HEIGHT = 60; private const float corner_radius = 5; private const float edge_margin = 5; private const float background_alpha = 0.25f; - private const float rank_width = 30; + private const float rank_width = 35; protected Container RankContainer { get; private set; } private readonly ScoreInfo score; + private readonly int rank; private Box background; private Container content; @@ -52,7 +52,7 @@ namespace osu.Game.Online.Leaderboards public LeaderboardScore(ScoreInfo score, int rank) { this.score = score; - RankPosition = rank; + this.rank = rank; RelativeSizeAxes = Axes.X; Height = HEIGHT; @@ -79,8 +79,8 @@ namespace osu.Game.Online.Leaderboards { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.GetFont(size: 22, italics: true), - Text = RankPosition.ToString(), + Font = OsuFont.GetFont(size: 20, italics: true), + Text = rank.ToMetric(decimals: rank < 100000 ? 1 : 0), }, }, }, diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 33f040755e..337d46ecdd 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -37,8 +38,25 @@ namespace osu.Game.Screens.Select.Leaderboards } } + public APILegacyUserTopScoreInfo TopScore + { + get => topScoreContainer.Score.Value; + set + { + if (value == null) + topScoreContainer.Hide(); + else + { + topScoreContainer.Show(); + topScoreContainer.Score.Value = value; + } + } + } + private bool filterMods; + private UserTopScoreContainer topScoreContainer; + /// /// Whether to apply the game's currently selected mods as a filter when retrieving scores. /// @@ -77,6 +95,17 @@ namespace osu.Game.Screens.Select.Leaderboards if (filterMods) UpdateScores(); }; + + Content.Add(topScoreContainer = new UserTopScoreContainer + { + ScoreSelected = s => ScoreSelected?.Invoke(s) + }); + } + + protected override void Reset() + { + base.Reset(); + TopScore = null; } protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; @@ -141,7 +170,11 @@ namespace osu.Game.Screens.Select.Leaderboards var req = new GetScoresRequest(Beatmap, ruleset.Value ?? Beatmap.Ruleset, Scope, requestMods); - req.Success += r => scoresCallback?.Invoke(r.Scores); + req.Success += r => + { + scoresCallback?.Invoke(r.Scores); + TopScore = r.UserScore; + }; return req; } diff --git a/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs b/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs new file mode 100644 index 0000000000..da8f676cd0 --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs @@ -0,0 +1,94 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Threading; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Leaderboards; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Screens.Select.Leaderboards +{ + public class UserTopScoreContainer : VisibilityContainer + { + private const int duration = 500; + + private readonly Container scoreContainer; + + public Bindable Score = new Bindable(); + + public Action ScoreSelected; + + protected override bool StartHidden => true; + + public UserTopScoreContainer() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Margin = new MarginPadding { Vertical = 5 }; + + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = @"your personal best".ToUpper(), + Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold), + }, + scoreContainer = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } + } + } + }; + + Score.BindValueChanged(onScoreChanged); + } + + private CancellationTokenSource loadScoreCancellation; + + private void onScoreChanged(ValueChangedEvent score) + { + var newScore = score.NewValue; + + scoreContainer.Clear(); + loadScoreCancellation?.Cancel(); + + if (newScore == null) + return; + + LoadComponentAsync(new LeaderboardScore(newScore.Score, newScore.Position) + { + Action = () => ScoreSelected?.Invoke(newScore.Score) + }, drawableScore => + { + scoreContainer.Child = drawableScore; + drawableScore.FadeInFromZero(duration, Easing.OutQuint); + }, (loadScoreCancellation = new CancellationTokenSource()).Token); + } + + protected override void PopIn() => this.FadeIn(duration, Easing.OutQuint); + + protected override void PopOut() => this.FadeOut(duration, Easing.OutQuint); + } +} diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index d0cb5986a8..fca801ce78 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -223,7 +223,7 @@ namespace osu.Game.Screens.Select }); } - BeatmapDetails.Leaderboard.ScoreSelected += s => this.Push(new SoloResults(s)); + BeatmapDetails.Leaderboard.ScoreSelected += score => this.Push(new SoloResults(score)); } [BackgroundDependencyLoader(true)]