diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs new file mode 100644 index 0000000000..1f38b05879 --- /dev/null +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs @@ -0,0 +1,110 @@ +// 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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Online.API.Requests.Responses; +using osuTK; + +namespace osu.Game.Tests.Visual.Beatmaps +{ + public class TestSceneDifficultySpectrumDisplay : OsuTestScene + { + private DifficultySpectrumDisplay display; + + private static APIBeatmapSet createBeatmapSetWith(params (int rulesetId, double stars)[] difficulties) => new APIBeatmapSet + { + Beatmaps = difficulties.Select(difficulty => new APIBeatmap + { + RulesetID = difficulty.rulesetId, + StarRating = difficulty.stars + }).ToList() + }; + + [Test] + public void TestSingleRuleset() + { + var beatmapSet = createBeatmapSetWith( + (rulesetId: 0, stars: 2.0), + (rulesetId: 0, stars: 3.2), + (rulesetId: 0, stars: 5.6)); + + createDisplay(beatmapSet); + } + + [Test] + public void TestMultipleRulesets() + { + var beatmapSet = createBeatmapSetWith( + (rulesetId: 0, stars: 2.0), + (rulesetId: 3, stars: 2.3), + (rulesetId: 0, stars: 3.2), + (rulesetId: 1, stars: 4.3), + (rulesetId: 0, stars: 5.6)); + + createDisplay(beatmapSet); + } + + [Test] + public void TestUnknownRuleset() + { + var beatmapSet = createBeatmapSetWith( + (rulesetId: 0, stars: 2.0), + (rulesetId: 3, stars: 2.3), + (rulesetId: 0, stars: 3.2), + (rulesetId: 1, stars: 4.3), + (rulesetId: 0, stars: 5.6), + (rulesetId: 15, stars: 7.8)); + + createDisplay(beatmapSet); + } + + [Test] + public void TestMaximumUncollapsed() + { + var beatmapSet = createBeatmapSetWith(Enumerable.Range(0, 12).Select(i => (rulesetId: i % 4, stars: 2.5 + i * 0.25)).ToArray()); + createDisplay(beatmapSet); + } + + [Test] + public void TestMinimumCollapsed() + { + var beatmapSet = createBeatmapSetWith(Enumerable.Range(0, 13).Select(i => (rulesetId: i % 4, stars: 2.5 + i * 0.25)).ToArray()); + createDisplay(beatmapSet); + } + + [Test] + public void TestAdjustableDotSize() + { + var beatmapSet = createBeatmapSetWith( + (rulesetId: 0, stars: 2.0), + (rulesetId: 3, stars: 2.3), + (rulesetId: 0, stars: 3.2), + (rulesetId: 1, stars: 4.3), + (rulesetId: 0, stars: 5.6)); + + createDisplay(beatmapSet); + + AddStep("change dot dimensions", () => + { + display.DotSize = new Vector2(8, 12); + display.DotSpacing = 2; + }); + AddStep("change dot dimensions back", () => + { + display.DotSize = new Vector2(4, 8); + display.DotSpacing = 1; + }); + } + + private void createDisplay(IBeatmapSetInfo beatmapSetInfo) => AddStep("create spectrum display", () => Child = display = new DifficultySpectrumDisplay(beatmapSetInfo) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(3) + }); + } +} diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs new file mode 100644 index 0000000000..1feaa88350 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -0,0 +1,169 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets; +using osuTK; + +namespace osu.Game.Beatmaps.Drawables +{ + public class DifficultySpectrumDisplay : CompositeDrawable + { + private Vector2 dotSize = new Vector2(4, 8); + + public Vector2 DotSize + { + get => dotSize; + set + { + dotSize = value; + + if (IsLoaded) + updateDotDimensions(); + } + } + + private float dotSpacing = 1; + + public float DotSpacing + { + get => dotSpacing; + set + { + dotSpacing = value; + + if (IsLoaded) + updateDotDimensions(); + } + } + + private readonly FillFlowContainer flow; + + public DifficultySpectrumDisplay(IBeatmapSetInfo beatmapSet) + { + AutoSizeAxes = Axes.Both; + + InternalChild = flow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(10, 0), + Direction = FillDirection.Horizontal, + }; + + // matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127 + bool collapsed = beatmapSet.Beatmaps.Count() > 12; + + foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID)) + { + flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key, rulesetGrouping, collapsed)); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateDotDimensions(); + } + + private void updateDotDimensions() + { + foreach (var group in flow) + { + group.DotSize = DotSize; + group.DotSpacing = DotSpacing; + } + } + + private class RulesetDifficultyGroup : FillFlowContainer + { + private readonly int rulesetId; + private readonly IEnumerable beatmapInfos; + private readonly bool collapsed; + + public RulesetDifficultyGroup(int rulesetId, IEnumerable beatmapInfos, bool collapsed) + { + this.rulesetId = rulesetId; + this.beatmapInfos = beatmapInfos; + this.collapsed = collapsed; + } + + public Vector2 DotSize + { + set + { + foreach (var dot in Children.OfType()) + dot.Size = value; + } + } + + public float DotSpacing + { + set => Spacing = new Vector2(value, 0); + } + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + AutoSizeAxes = Axes.Both; + Spacing = new Vector2(1, 0); + Direction = FillDirection.Horizontal; + + var icon = rulesets.GetRuleset(rulesetId)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }; + Add(icon.With(i => + { + i.Size = new Vector2(14); + i.Anchor = i.Origin = Anchor.Centre; + })); + + if (!collapsed) + { + foreach (var beatmapInfo in beatmapInfos.OrderBy(bi => bi.StarRating)) + Add(new DifficultyDot(beatmapInfo.StarRating)); + } + else + { + Add(new OsuSpriteText + { + Text = beatmapInfos.Count().ToLocalisableString(@"N0"), + Font = OsuFont.Default.With(size: 12), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding { Bottom = 1 } + }); + } + } + } + + private class DifficultyDot : CircularContainer + { + private readonly double starDifficulty; + + public DifficultyDot(double starDifficulty) + { + this.starDifficulty = starDifficulty; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Anchor = Origin = Anchor.Centre; + Masking = true; + + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.ForStarDifficulty(starDifficulty) + }; + } + } + } +}