1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-13 20:33:35 +08:00

Add matchmaking profile badge (#37241)

Using the same styling as osu!web + daily challenge.

<img width="1920" height="1034" alt="Screenshot_20260409-164600"
src="https://github.com/user-attachments/assets/97e2270e-af9f-478d-b2d6-c9fb8be16720"
/>

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
This commit is contained in:
Dan Balasescu
2026-04-09 19:25:21 +09:00
committed by GitHub
Unverified
parent b838564039
commit 93b7c3324d
7 changed files with 434 additions and 2 deletions
@@ -0,0 +1,89 @@
// 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.
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?> userProfileData = new Bindable<UserProfileData?>(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);
}
}
}
@@ -0,0 +1,25 @@
// 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.
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; }
}
}
@@ -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;
/// <summary>
@@ -0,0 +1,37 @@
// 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.
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();
}
}
@@ -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 },
}
}
}
}
}
@@ -0,0 +1,131 @@
// 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.
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<MatchmakingStatsTooltipData>
{
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
[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<MatchmakingStatsTooltipData> GetCustomTooltip() => new MatchmakingStatsTooltip();
public MatchmakingStatsTooltipData? TooltipContent { get; private set; }
}
}
@@ -0,0 +1,134 @@
// 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.
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<MatchmakingStatsTooltipData>
{
private Box background = null!;
private Container<TableContainer> 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<TableContainer>
{
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);
}