diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileMatchmakingStatsDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileMatchmakingStatsDisplay.cs new file mode 100644 index 0000000000..78439d978e --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileMatchmakingStatsDisplay.cs @@ -0,0 +1,89 @@ +// 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.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.Profile; +using osu.Game.Overlays.Profile.Header.Components; +using osu.Game.Rulesets.Osu; +using osuTK; + +namespace osu.Game.Tests.Visual.Online +{ + public partial class TestSceneUserProfileMatchmakingStatsDisplay : OsuManualInputManagerTestScene + { + [Cached] + private readonly Bindable userProfileData = new Bindable(new UserProfileData(new APIUser(), new OsuRuleset().RulesetInfo)); + + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create", () => + { + Clear(); + Add(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background2, + }); + Add(new MatchmakingStatsDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1f), + User = { BindTarget = userProfileData }, + }); + }); + + AddStep("set stats", () => userProfileData.Value = new UserProfileData(new APIUser + { + MatchmakingStatistics = + [ + new APIUserMatchmakingStatistics + { + Plays = 10, + FirstPlacements = 8, + Rank = 1000, + Rating = 2000, + TotalPoints = 500, + Pool = + { + Name = "Active Pool" + } + }, + new APIUserMatchmakingStatistics + { + Plays = 5, + FirstPlacements = 4, + Rank = 500, + Rating = 1000, + TotalPoints = 250, + Pool = + { + Name = "Inactive Pool" + } + }, + new APIUserMatchmakingStatistics + { + Rating = 1500, + IsRatingProvisional = true, + Pool = + { + Name = "Provisional" + } + } + ] + }, new OsuRuleset().RulesetInfo)); + + AddStep("clear stats", () => userProfileData.Value = null); + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIMatchmakingPool.cs b/osu.Game/Online/API/Requests/Responses/APIMatchmakingPool.cs new file mode 100644 index 0000000000..a688ea9930 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIMatchmakingPool.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIMatchmakingPool + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("active")] + public bool Active { get; set; } + + [JsonProperty("ruleset_id")] + public int RulesetId { get; set; } + + [JsonProperty("variant_id")] + public int VariantId { get; set; } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index fa90e5cd50..4c72926048 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -297,6 +297,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("daily_challenge_user_stats")] public APIUserDailyChallengeStatistics DailyChallengeStatistics = new APIUserDailyChallengeStatistics(); + [JsonProperty("matchmaking_stats")] + public APIUserMatchmakingStatistics[] MatchmakingStatistics = []; + public override string ToString() => Username; /// diff --git a/osu.Game/Online/API/Requests/Responses/APIUserMatchmakingStatistics.cs b/osu.Game/Online/API/Requests/Responses/APIUserMatchmakingStatistics.cs new file mode 100644 index 0000000000..97f9a14ef6 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIUserMatchmakingStatistics.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIUserMatchmakingStatistics + { + [JsonProperty("user_id")] + public int UserId; + + [JsonProperty("pool_id")] + public int PoolId { get; set; } + + [JsonProperty("rating")] + public int Rating { get; set; } + + [JsonProperty("rank")] + public int Rank { get; set; } + + [JsonProperty("plays")] + public int Plays { get; set; } + + [JsonProperty("total_points")] + public int TotalPoints { get; set; } + + [JsonProperty("first_placements")] + public int FirstPlacements { get; set; } + + [JsonProperty("is_rating_provisional")] + public bool IsRatingProvisional { get; set; } + + [JsonProperty("pool")] + public APIMatchmakingPool Pool { get; set; } = new APIMatchmakingPool(); + } +} diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs index 029de96c41..e0bafa0255 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -70,11 +70,24 @@ namespace osu.Game.Overlays.Profile.Header.Components { Title = UsersStrings.ShowRankCountrySimple, }, - new DailyChallengeStatsDisplay + new FillFlowContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - User = { BindTarget = User }, + Spacing = new Vector2(20), + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new MatchmakingStatsDisplay + { + User = { BindTarget = User } + }, + new DailyChallengeStatsDisplay + { + User = { BindTarget = User }, + } + } } } } diff --git a/osu.Game/Overlays/Profile/Header/Components/MatchmakingStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/MatchmakingStatsDisplay.cs new file mode 100644 index 0000000000..e6bb42fedc --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/Components/MatchmakingStatsDisplay.cs @@ -0,0 +1,131 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Overlays.Profile.Header.Components +{ + public partial class MatchmakingStatsDisplay : CompositeDrawable, IHasCustomTooltip + { + public readonly Bindable User = new Bindable(); + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private OsuSpriteText rankText = null!; + + public MatchmakingStatsDisplay() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.Both, + CornerRadius = 6, + BorderThickness = 2, + BorderColour = colourProvider.Background4, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(3f), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Quick Play", + Margin = new MarginPadding { Horizontal = 5f, Vertical = 7f }, + Font = OsuFont.GetFont(size: 12) + }, + new Container + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + CornerRadius = 3, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + rankText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + UseFullGlyphHeight = false, + Colour = colourProvider.Content2, + Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f } + }, + } + }, + } + }, + } + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + User.BindValueChanged(_ => updateDisplay(), true); + } + + private void updateDisplay() + { + if (User.Value == null) + { + Hide(); + return; + } + + APIUserMatchmakingStatistics[] stats = User.Value.User.MatchmakingStatistics; + + if (stats.Length == 0) + { + Hide(); + return; + } + + APIUserMatchmakingStatistics[] mostRelevantStats = stats.OrderByDescending(s => s.Pool.Active).ThenByDescending(s => s.Pool.Id).ToArray(); + APIUserMatchmakingStatistics mostRelevantStat = mostRelevantStats.First(); + + rankText.Text = $"#{mostRelevantStat.Rank:N0}"; + + TooltipContent = new MatchmakingStatsTooltipData(colourProvider, mostRelevantStats); + + Show(); + } + + public ITooltip GetCustomTooltip() => new MatchmakingStatsTooltip(); + + public MatchmakingStatsTooltipData? TooltipContent { get; private set; } + } +} diff --git a/osu.Game/Overlays/Profile/Header/Components/MatchmakingStatsTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/MatchmakingStatsTooltip.cs new file mode 100644 index 0000000000..89d9192d57 --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/Components/MatchmakingStatsTooltip.cs @@ -0,0 +1,134 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Profile.Header.Components +{ + public partial class MatchmakingStatsTooltip : VisibilityContainer, ITooltip + { + private Box background = null!; + private Container tableContainer = null!; + + public MatchmakingStatsTooltip() + { + AutoSizeAxes = Axes.Both; + CornerRadius = 20f; + Masking = true; + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.25f), + Radius = 30f, + }; + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + }, + tableContainer = new Container + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding(15f), + } + }; + } + + public void SetContent(MatchmakingStatsTooltipData content) + { + var statistics = content.Statistics; + var colourProvider = content.ColourProvider; + + background.Colour = colourProvider.Background4; + + tableContainer.Child = new MatchmakingStatsTooltipTable(colourProvider) + { + AutoSizeAxes = Axes.Both, + Columns = + [ + new TableColumn(dimension: new Dimension(GridSizeMode.AutoSize)), + new TableColumn(dimension: new Dimension(GridSizeMode.AutoSize)), + new TableColumn("Wins", dimension: new Dimension(GridSizeMode.AutoSize)), + new TableColumn("Plays", dimension: new Dimension(GridSizeMode.AutoSize)), + new TableColumn("Points", dimension: new Dimension(GridSizeMode.AutoSize)), + new TableColumn("Rating", dimension: new Dimension(GridSizeMode.AutoSize)), + ], + RowSize = new Dimension(GridSizeMode.AutoSize), + Content = statistics.Select(s => createRow(colourProvider, s)).ToArray().ToRectangular() + }; + } + + private Drawable[] createRow(OverlayColourProvider colourProvider, APIUserMatchmakingStatistics stat) + { + return + [ + new StatisticText(colourProvider) + { + Text = stat.Pool.Name, + Colour = Color4.White + }, + new StatisticText(colourProvider) { Text = $"#{stat.Rank:N0}" }, + new StatisticText(colourProvider) { Text = stat.FirstPlacements.ToString("N0") }, + new StatisticText(colourProvider) { Text = stat.Plays.ToString("N0") }, + new StatisticText(colourProvider) { Text = stat.TotalPoints.ToString("N0") }, + new StatisticText(colourProvider) { Text = stat.Rating.ToString("N0") + (stat.IsRatingProvisional ? "*" : string.Empty) } + ]; + } + + protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); + + protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); + + public void Move(Vector2 pos) => Position = pos; + + private partial class MatchmakingStatsTooltipTable : TableContainer + { + private readonly OverlayColourProvider colourProvider; + + public MatchmakingStatsTooltipTable(OverlayColourProvider colourProvider) + { + this.colourProvider = colourProvider; + } + + protected override Drawable CreateHeader(int index, TableColumn? column) + { + return new StatisticText(colourProvider) + { + Text = column?.Header ?? string.Empty, + }; + } + } + + private partial class StatisticText : OsuSpriteText + { + public StatisticText(OverlayColourProvider colourProvider) + { + Font = OsuFont.GetFont(size: 12); + Padding = new MarginPadding { Horizontal = 5, Vertical = 2 }; + Colour = colourProvider.Content2; + } + } + } + + public record MatchmakingStatsTooltipData(OverlayColourProvider ColourProvider, APIUserMatchmakingStatistics[] Statistics); +}