From 4ba629773bf010e03b0c85cc058a1868606ce686 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 15 Aug 2024 21:14:23 -0700 Subject: [PATCH] Implement song select v2 length and bpm statistic pill --- .../TestSceneLengthAndBPMStatisticPill.cs | 176 ++++++++++++++++++ .../Wedge/LengthAndBPMStatisticPill.cs | 143 ++++++++++++++ .../Wedge/LocalLengthAndBPMStatisticPill.cs | 104 +++++++++++ 3 files changed, 423 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneLengthAndBPMStatisticPill.cs create mode 100644 osu.Game/Screens/SelectV2/Wedge/LengthAndBPMStatisticPill.cs create mode 100644 osu.Game/Screens/SelectV2/Wedge/LocalLengthAndBPMStatisticPill.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLengthAndBPMStatisticPill.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLengthAndBPMStatisticPill.cs new file mode 100644 index 0000000000..63982ffd5f --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLengthAndBPMStatisticPill.cs @@ -0,0 +1,176 @@ +// 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.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Extensions; +using osu.Game.Graphics; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.SelectV2.Wedge; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneLengthAndBPMStatisticPill : SongSelectComponentsTestScene + { + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Test] + public void TestValueColour() + { + AddStep("set pill", () => Child = new LocalLengthAndBPMStatisticPill + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }); + + AddStep("set beatmap", () => + { + List objects = new List(); + for (double i = 0; i < 50000; i += 1000) + objects.Add(new HitCircle { StartTime = i }); + + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + Length = 83000, + OnlineID = 1, + }, + HitObjects = objects + }); + }); + + AddStep("set double time", () => SelectedMods.Value = new[] { new OsuModDoubleTime() }); + + AddAssert("length value is red", () => this.ChildrenOfType().ElementAt(0).ValueColour, + () => Is.EqualTo(colours.ForModType(ModType.DifficultyIncrease))); + AddAssert("bpm value is red", () => this.ChildrenOfType().ElementAt(1).ValueColour, + () => Is.EqualTo(colours.ForModType(ModType.DifficultyIncrease))); + + AddStep("set half time", () => SelectedMods.Value = new[] { new OsuModHalfTime() }); + + AddAssert("length value is green", () => this.ChildrenOfType().ElementAt(0).ValueColour, + () => Is.EqualTo(colours.ForModType(ModType.DifficultyReduction))); + AddAssert("bpm value is green", () => this.ChildrenOfType().ElementAt(1).ValueColour, + () => Is.EqualTo(colours.ForModType(ModType.DifficultyReduction))); + } + + [Test] + public void TestLengthUpdates() + { + OsuModDoubleTime? doubleTime = null; + + List objects = new List(); + for (double i = 0; i < 50000; i += 1000) + objects.Add(new HitCircle { StartTime = i }); + + Beatmap beatmap = new Beatmap + { + HitObjects = objects, + }; + + double drain = beatmap.CalculateDrainLength(); + beatmap.BeatmapInfo.Length = drain; + + AddStep("set pill", () => Child = new LocalLengthAndBPMStatisticPill + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }); + + AddStep("set beatmap", () => + { + Beatmap.Value = CreateWorkingBeatmap(beatmap); + }); + + checkDisplayedLength(drain); + + AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() }); + checkDisplayedLength(Math.Round(drain / 1.5f)); + + AddStep("change DT rate", () => doubleTime!.SpeedChange.Value = 2); + checkDisplayedLength(Math.Round(drain / 2)); + } + + private void checkDisplayedLength(double drain) + { + var displayedLength = drain.ToFormattedDuration(); + + AddAssert($"check map drain ({displayedLength})", () => this.ChildrenOfType().ElementAt(0).Value, () => Is.EqualTo(displayedLength)); + } + + [Test] + public void TestBPMUpdates() + { + const double bpm = 120; + OsuModDoubleTime? doubleTime = null; + + AddStep("set pill", () => Child = new LocalLengthAndBPMStatisticPill + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }); + + AddStep("set beatmap", () => + { + Beatmap beatmap = new Beatmap(); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm }); + + Beatmap.Value = CreateWorkingBeatmap(beatmap); + }); + + checkDisplayedBPM($"{bpm}"); + + AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() }); + checkDisplayedBPM($"{bpm * 1.5f}"); + + AddStep("change DT rate", () => doubleTime!.SpeedChange.Value = 2); + checkDisplayedBPM($"{bpm * 2}"); + } + + [TestCase(120, 125, null, "120-125 (120)")] + [TestCase(120, 120.6, null, "120-121 (120)")] + [TestCase(120, 120.4, null, "120")] + [TestCase(120, 120.6, "DT", "180-182 (180)")] + [TestCase(120, 120.4, "DT", "180")] + public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay) + { + AddStep("set pill", () => Child = new LocalLengthAndBPMStatisticPill + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }); + + if (mod != null) + AddStep($"select {mod}", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateModFromAcronym(mod) }); + + AddStep("set beatmap", () => + { + Beatmap beatmap = new Beatmap(); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm }); + beatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 60 * 1000 / otherBpm }); + beatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm }); + + Beatmap.Value = CreateWorkingBeatmap(beatmap); + }); + + checkDisplayedBPM(expectedDisplay); + } + + private void checkDisplayedBPM(string target) + { + AddAssert($"displayed bpm is {target}", () => this.ChildrenOfType().ElementAt(1).Value.ToString(), () => Is.EqualTo(target)); + } + } +} diff --git a/osu.Game/Screens/SelectV2/Wedge/LengthAndBPMStatisticPill.cs b/osu.Game/Screens/SelectV2/Wedge/LengthAndBPMStatisticPill.cs new file mode 100644 index 0000000000..78dd70f4ae --- /dev/null +++ b/osu.Game/Screens/SelectV2/Wedge/LengthAndBPMStatisticPill.cs @@ -0,0 +1,143 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2.Wedge +{ + public abstract partial class LengthAndBPMStatisticPill : CompositeDrawable + { + protected PillStatistic LengthStatistic = null!; + protected PillStatistic BPMStatistic = null!; + + protected LengthAndBPMStatisticPill() + { + AutoSizeAxes = Axes.X; + Height = 20; + Masking = true; + CornerRadius = 10; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4 + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10), + Padding = new MarginPadding { Horizontal = 20 }, + Children = new Drawable[] + { + LengthStatistic = new PillStatistic(new BeatmapStatistic { Name = "Length" }), + BPMStatistic = new PillStatistic(new BeatmapStatistic { Name = BeatmapsetsStrings.ShowStatsBpm }), + } + } + }; + } + + public partial class PillStatistic : CompositeDrawable, IHasTooltip + { + private readonly BeatmapStatistic statistic; + private OsuSpriteText valueSpriteText = null!; + + private LocalisableString valueText; + + public LocalisableString Value + { + get => valueText; + set + { + valueText = value; + + if (IsLoaded) + updateValueText(); + } + } + + private Color4 valueColour; + + public Color4 ValueColour + { + get => valueColour; + set + { + valueColour = value; + + if (IsLoaded) + updateValueText(); + } + } + + public PillStatistic(BeatmapStatistic statistic) + { + this.statistic = statistic; + + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + valueColour = colourProvider.Content2; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = statistic.Name, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 14), + }, + valueSpriteText = new OsuSpriteText + { + Text = statistic.Content, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 14), + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updateValueText(); + } + + private void updateValueText() + { + valueSpriteText.Text = LocalisableString.IsNullOrEmpty(valueText) ? "-" : valueText; + valueSpriteText.Colour = valueColour; + } + + public LocalisableString TooltipText { get; set; } + } + } +} diff --git a/osu.Game/Screens/SelectV2/Wedge/LocalLengthAndBPMStatisticPill.cs b/osu.Game/Screens/SelectV2/Wedge/LocalLengthAndBPMStatisticPill.cs new file mode 100644 index 0000000000..37ead80776 --- /dev/null +++ b/osu.Game/Screens/SelectV2/Wedge/LocalLengthAndBPMStatisticPill.cs @@ -0,0 +1,104 @@ +// 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.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Extensions; +using osu.Game.Graphics; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; + +namespace osu.Game.Screens.SelectV2.Wedge +{ + public partial class LocalLengthAndBPMStatisticPill : LengthAndBPMStatisticPill + { + private ModSettingChangeTracker? modSettingChangeTracker; + + [Resolved] + private IBindable> mods { get; set; } = null!; + + [Resolved] + private IBindable workingBeatmap { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + workingBeatmap.BindValueChanged(_ => updateStatistics()); + + mods.BindValueChanged(m => + { + modSettingChangeTracker?.Dispose(); + + updateStatistics(); + + modSettingChangeTracker = new ModSettingChangeTracker(m.NewValue); + modSettingChangeTracker.SettingChanged += _ => updateStatistics(); + }, true); + } + + private void updateStatistics() + { + // TODO: consider mods which apply variable rates. + double rate = 1; + foreach (var mod in mods.Value.OfType()) + rate = mod.ApplyToRate(0, rate); + + var beatmap = workingBeatmap.Value.Beatmap; + + int bpmMax = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMaximum, rate); + int bpmMin = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMinimum, rate); + int mostCommonBPM = FormatUtils.RoundBPM(60000 / beatmap.GetMostCommonBeatLength(), rate); + + string labelText = bpmMin == bpmMax + ? $"{bpmMin}" + : $"{bpmMin}-{bpmMax} ({mostCommonBPM})"; + + BPMStatistic.Value = labelText; + + double drainLength = Math.Round(beatmap.CalculateDrainLength() / rate); + double hitLength = Math.Round(beatmap.BeatmapInfo.Length / rate); + + LengthStatistic.Value = hitLength.ToFormattedDuration(); + LengthStatistic.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration()); + + BPMStatistic.ValueColour = getColourByRate(rate); + LengthStatistic.ValueColour = getColourByRate(rate); + } + + private Colour4 getColourByRate(double rate) + { + switch (rate) + { + case < 1: + return colours.ForModType(ModType.DifficultyReduction); + + case > 1: + return colours.ForModType(ModType.DifficultyIncrease); + + default: + return colourProvider.Content2; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + modSettingChangeTracker?.Dispose(); + } + } +}