diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs index 1f05d66b86..d43290e661 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs @@ -1,6 +1,7 @@ // 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.Game.Beatmaps; @@ -16,26 +17,30 @@ namespace osu.Game.Rulesets.Catch.Beatmaps int fruits = HitObjects.Count(s => s is Fruit); int juiceStreams = HitObjects.Count(s => s is JuiceStream); int bananaShowers = HitObjects.Count(s => s is BananaShower); + int sum = Math.Max(1, fruits + juiceStreams); return new[] { new BeatmapStatistic { - Name = @"Fruit Count", + Name = @"Fruits", Content = fruits.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), + BarDisplayLength = fruits / (float)sum, }, new BeatmapStatistic { - Name = @"Juice Stream Count", + Name = @"Juice Streams", Content = juiceStreams.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), + BarDisplayLength = juiceStreams / (float)sum, }, new BeatmapStatistic { - Name = @"Banana Shower Count", + Name = @"Banana Showers", Content = bananaShowers.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), + BarDisplayLength = Math.Min(bananaShowers / 10f, 1), } }; } diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs index a83b61360b..64496d7628 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs @@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 5f, -19d, HitResult.Perfect }, new object[] { 5f, -19.2d, HitResult.Perfect }, new object[] { 5f, -19.38d, HitResult.Perfect }, + // new object[] { 5f, -19.4d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues) new object[] { 5f, -19.44d, HitResult.Great }, new object[] { 5f, -19.7d, HitResult.Great }, new object[] { 5f, -20d, HitResult.Great }, @@ -73,6 +74,7 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 9.3f, 14d, HitResult.Perfect }, new object[] { 9.3f, 14.2d, HitResult.Perfect }, new object[] { 9.3f, 14.6d, HitResult.Perfect }, + // new object[] { 9.3f, 14.67d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues) new object[] { 9.3f, 14.7d, HitResult.Great }, new object[] { 9.3f, 15d, HitResult.Great }, new object[] { 9.3f, 35d, HitResult.Great }, diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs index 8222e5477d..3ee1b63800 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs @@ -36,20 +36,23 @@ namespace osu.Game.Rulesets.Mania.Beatmaps { int notes = HitObjects.Count(s => s is Note); int holdNotes = HitObjects.Count(s => s is HoldNote); + int sum = Math.Max(1, notes + holdNotes); return new[] { new BeatmapStatistic { - Name = @"Note Count", + Name = @"Notes", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = notes.ToString(), + BarDisplayLength = notes / (float)sum, }, new BeatmapStatistic { - Name = @"Hold Note Count", + Name = @"Hold Notes", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = holdNotes.ToString(), + BarDisplayLength = holdNotes / (float)sum, }, }; } diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs index a5282877ee..d11b4aac3b 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs @@ -1,10 +1,10 @@ // 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.Game.Beatmaps; -using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Beatmaps @@ -16,26 +16,30 @@ namespace osu.Game.Rulesets.Osu.Beatmaps int circles = HitObjects.Count(c => c is HitCircle); int sliders = HitObjects.Count(s => s is Slider); int spinners = HitObjects.Count(s => s is Spinner); + int sum = Math.Max(1, circles + sliders); return new[] { new BeatmapStatistic { - Name = BeatmapsetsStrings.ShowStatsCountCircles, + Name = "Circles", Content = circles.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), + BarDisplayLength = circles / (float)sum, }, new BeatmapStatistic { - Name = BeatmapsetsStrings.ShowStatsCountSliders, + Name = "Sliders", Content = sliders.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), + BarDisplayLength = sliders / (float)sum, }, new BeatmapStatistic { - Name = @"Spinner Count", + Name = @"Spinners", Content = spinners.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), + BarDisplayLength = Math.Min(spinners / 10f, 1), } }; } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 992f4d5f03..222cf4242a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Mods { var spinner = (DrawableSpinner)drawable; - spinner.RotationTracker.Tracking = true; + spinner.RotationTracker.Tracking = spinner.RotationTracker.IsSpinnableTime; // early-return if we were paused to avoid division-by-zero in the subsequent calculations. if (Precision.AlmostEquals(spinner.Clock.Rate, 0)) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 8c21e6a6bc..64cedd216b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -277,13 +277,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.Update(); if (HandleUserInput) - { - bool isValidSpinningTime = Time.Current >= HitObject.StartTime && Time.Current <= HitObject.EndTime; - - RotationTracker.Tracking = !Result.HasResult - && correctButtonPressed() - && isValidSpinningTime; - } + RotationTracker.Tracking = RotationTracker.IsSpinnableTime && !Result.HasResult && correctButtonPressed(); if (spinningSample != null && spinnerFrequencyModulate) spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index 7e97f826f9..7cd1f39871 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default /// /// Whether currently in the correct time range to allow spinning. /// - private bool isSpinnableTime => drawableSpinner.HitObject.StartTime <= Time.Current && drawableSpinner.HitObject.EndTime > Time.Current; + public bool IsSpinnableTime => drawableSpinner.HitObject.StartTime <= Time.Current && drawableSpinner.HitObject.EndTime > Time.Current; protected override bool OnMouseMove(MouseMoveEvent e) { @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default lastAngle = thisAngle; } - IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f; + IsSpinning.Value = IsSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f; Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed)); } @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default /// The delta angle. public void AddRotation(float delta) { - if (!isSpinnableTime) + if (!IsSpinnableTime) return; if (!rotationTransferred) diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs index 41fe63a553..5b0582ab59 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs @@ -1,6 +1,7 @@ // 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.Game.Beatmaps; @@ -15,26 +16,30 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps int hits = HitObjects.Count(s => s is Hit); int drumRolls = HitObjects.Count(s => s is DrumRoll); int swells = HitObjects.Count(s => s is Swell); + int sum = Math.Max(1, hits + drumRolls); return new[] { new BeatmapStatistic { - Name = @"Hit Count", + Name = @"Hits", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = hits.ToString(), + BarDisplayLength = hits / (float)sum, }, new BeatmapStatistic { - Name = @"Drumroll Count", + Name = @"Drumrolls", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = drumRolls.ToString(), + BarDisplayLength = drumRolls / (float)sum, }, new BeatmapStatistic { - Name = @"Swell Count", + Name = @"Swells", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), Content = swells.ToString(), + BarDisplayLength = Math.Min(swells / 10f, 1), } }; } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs index cecb99c690..d94031380b 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio.Track; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Shapes; @@ -112,5 +113,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon .FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine); } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableHitObject.IsNotNull()) + drawableHitObject.ApplyCustomUpdateState -= updateStateTransforms; + } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs index b3833d372c..a7b1b9c4b6 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; @@ -202,5 +203,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default .Then() .FadeEdgeEffectTo(edge_alpha_kiai, duration, Easing.OutQuint); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableHitObject.IsNotNull()) + drawableHitObject.ApplyCustomUpdateState -= updateStateTransforms; + } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs index 9877efa127..58830f7492 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs @@ -17,12 +17,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { internal partial class LegacyKiaiGlow : BeatSyncedContainer { - private bool isKiaiActive; + [Resolved] + private HealthProcessor? healthProcessor { get; set; } + private bool isKiaiActive; private Sprite sprite = null!; - [BackgroundDependencyLoader(true)] - private void load(ISkinSource skin, HealthProcessor? healthProcessor) + [BackgroundDependencyLoader] + private void load(ISkinSource skin) { Child = sprite = new Sprite { @@ -33,6 +35,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Scale = new Vector2(TaikoLegacyHitTarget.SCALE), Colour = new Colour4(255, 228, 0, 255), }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); if (healthProcessor != null) healthProcessor.NewJudgement += onNewJudgement; @@ -61,5 +68,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy sprite.ScaleTo(TaikoLegacyHitTarget.SCALE + 0.15f).Then() .ScaleTo(TaikoLegacyHitTarget.SCALE, 80, Easing.OutQuad); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (healthProcessor != null) + healthProcessor.NewJudgement -= onNewJudgement; + } } } diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs index dcc4654437..82e02a9b6f 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs @@ -19,6 +19,8 @@ namespace osu.Game.Tests.Visual.Beatmaps { public partial class TestSceneBeatmapSetOnlineStatusPill : ThemeComparisonTestScene { + private bool showUnknownStatus; + protected override Drawable CreateContent() => new FillFlowContainer { AutoSizeAxes = Axes.Both, @@ -26,12 +28,20 @@ namespace osu.Game.Tests.Visual.Beatmaps Origin = Anchor.Centre, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 10), - ChildrenEnumerable = Enum.GetValues(typeof(BeatmapOnlineStatus)).Cast().Select(status => new BeatmapSetOnlineStatusPill + ChildrenEnumerable = Enum.GetValues(typeof(BeatmapOnlineStatus)).Cast().Select(status => new Container { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Status = status + RelativeSizeAxes = Axes.X, + Height = 20, + Children = new Drawable[] + { + new BeatmapSetOnlineStatusPill + { + ShowUnknownStatus = showUnknownStatus, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Status = status + } + } }) }; @@ -48,6 +58,12 @@ namespace osu.Game.Tests.Visual.Beatmaps pill.Width = 90; })); + AddStep("toggle show unknown", () => + { + showUnknownStatus = !showUnknownStatus; + CreateThemedContent(OverlayColourScheme.Red); + }); + AddStep("unset fixed width", () => statusPills.ForEach(pill => pill.AutoSizeAxes = Axes.Both)); } @@ -65,11 +81,6 @@ namespace osu.Game.Tests.Visual.Beatmaps pill.Status = BeatmapOnlineStatus.LocallyModified; break; - // skip none - case BeatmapOnlineStatus.LocallyModified: - pill.Status = BeatmapOnlineStatus.Graveyard; - break; - default: pill.Status = (pill.Status + 1); break; diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index 185ebc1d39..f1422b4654 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -15,7 +15,7 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.SelectV2.Leaderboards; +using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.Metadata; using osu.Game.Tests.Visual.OnlinePlay; @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddStep("force transforms to finish", () => FinishTransforms(true)); AddStep("right click second score", () => { - InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(1)); + InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(1)); InputManager.Click(MouseButton.Right); }); AddAssert("use these mods not present", diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 18cd720bf2..7e19f45a00 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -105,6 +105,19 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("no item selected", () => playlist.SelectedItem.Value == null); } + [Test] + public void TestMarkCompleted() + { + createPlaylist(); + AddStep("mark some items as complete", () => + { + playlist.Items[0].MarkCompleted(); + playlist.Items[2].MarkCompleted(); + playlist.Items[3].MarkCompleted(); + playlist.Items[5].MarkCompleted(); + }); + } + [Test] public void TestSelectable() { diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index ab9ee1d8cc..d0fc66252e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -215,6 +215,32 @@ namespace osu.Game.Tests.Visual.Online }); } + [Test] + public void TestChannelCloseViaMiddleClick() + { + var testPMChannel = new Channel(testUser); + + AddStep("Show overlay", () => chatOverlay.Show()); + joinTestChannel(0); + joinChannel(testPMChannel); + AddStep("Select PM channel", () => clickDrawable(getChannelListItem(testPMChannel))); + AddStep("Middle click", () => + { + var item = getChannelListItem(testPMChannel); + InputManager.MoveMouseTo(item); + InputManager.Click(MouseButton.Middle); + }); + AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(testPMChannel)); + AddStep("Select normal channel", () => clickDrawable(getChannelListItem(testChannel1))); + AddStep("Click close button", () => + { + var item = getChannelListItem(testChannel1); + InputManager.MoveMouseTo(item); + InputManager.Click(MouseButton.Middle); + }); + AddAssert("Normal channel closed", () => !channelManager.JoinedChannels.Contains(testChannel1)); + } + [Test] public void TestChannelCloseButton() { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index d8573b2d03..8132f8a841 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.SongSelect foreach (var rulesetInfo in rulesets.AvailableRulesets) { var instance = rulesetInfo.CreateInstance(); - var testBeatmap = createTestBeatmap(rulesetInfo); + var testBeatmap = CreateTestBeatmap(rulesetInfo); beatmaps.Add(testBeatmap); @@ -124,6 +124,12 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("reset mods", () => SelectedMods.SetDefault()); } + [Test] + public void TestTruncation() + { + selectBeatmap(CreateLongMetadata()); + } + [Test] public void TestNullBeatmap() { @@ -135,17 +141,11 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType().Any()); } - [Test] - public void TestTruncation() - { - selectBeatmap(createLongMetadata()); - } - [Test] public void TestBPMUpdates() { const double bpm = 120; - IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); + IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm }); OsuModDoubleTime doubleTime = null!; @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.SongSelect [TestCase(120, 120.4, "DT", "180")] public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay) { - IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); + IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo); 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 }); @@ -191,7 +191,7 @@ namespace osu.Game.Tests.Visual.SongSelect [TestCase] public void TestLengthUpdates() { - IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); + IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo); double drain = beatmap.CalculateDrainLength(); beatmap.BeatmapInfo.Length = drain; @@ -248,7 +248,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); } - private IBeatmap createTestBeatmap(RulesetInfo ruleset) + public static IBeatmap CreateTestBeatmap(RulesetInfo ruleset) { List objects = new List(); for (double i = 0; i < 50000; i += 1000) @@ -274,7 +274,7 @@ namespace osu.Game.Tests.Visual.SongSelect }; } - private IBeatmap createLongMetadata() + public static IBeatmap CreateLongMetadata() { return new Beatmap { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 474d2ee6e3..70f2fb1361 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -43,6 +43,8 @@ namespace osu.Game.Tests.Visual.SongSelect private BeatmapManager beatmapManager = null!; private PlaySongSelect songSelect = null!; + private LeaderboardManager leaderboardManager = null!; + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); @@ -51,6 +53,8 @@ namespace osu.Game.Tests.Visual.SongSelect dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API)); dependencies.CacheAs(songSelect = new PlaySongSelect()); + dependencies.Cache(leaderboardManager = new LeaderboardManager()); + Dependencies.Cache(Realm); return dependencies; @@ -60,6 +64,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void load() { LoadComponent(songSelect); + LoadComponent(leaderboardManager); } public TestSceneBeatmapLeaderboard() @@ -112,6 +117,27 @@ namespace osu.Game.Tests.Visual.SongSelect checkDisplayedCount(0); } + [Test] + public void TestLocalScoresDisplayWorksWhenStartingOffline() + { + BeatmapInfo beatmapInfo = null!; + + AddStep("Log out", () => API.Logout()); + AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local); + + AddStep(@"Set beatmap", () => + { + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + + leaderboard.BeatmapInfo = beatmapInfo; + }); + + clearScores(); + importMoreScores(() => beatmapInfo); + checkDisplayedCount(10); + } + [Test] public void TestLocalScoresDisplayOnBeatmapEdit() { @@ -180,8 +206,8 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestGlobalScoresDisplay() { AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global); - AddStep(@"New Scores", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo()))); - AddStep(@"New Scores with teams", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo()).Select(s => + AddStep(@"New Scores", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()))); + AddStep(@"New Scores with teams", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()).Select(s => { s.User.Team = new APITeam(); return s; @@ -286,7 +312,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep(@"Import new scores", () => { - foreach (var score in generateSampleScores(beatmapInfo())) + foreach (var score in GenerateSampleScores(beatmapInfo())) scoreManager.Import(score); }); } @@ -302,7 +328,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void checkStoredCount(int expected) => AddUntilStep($"Total scores stored is {expected}", () => Realm.Run(r => r.All().Count(s => !s.DeletePending)), () => Is.EqualTo(expected)); - private static ScoreInfo[] generateSampleScores(BeatmapInfo beatmapInfo) + public static ScoreInfo[] GenerateSampleScores(BeatmapInfo beatmapInfo) { return new[] { diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs similarity index 97% rename from osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs rename to osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 2c902a466f..28a0948696 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -16,6 +16,7 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osu.Game.Screens.Select; @@ -27,9 +28,9 @@ using osuTK.Graphics; using osuTK.Input; using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { - public abstract partial class BeatmapCarouselV2TestScene : OsuManualInputManagerTestScene + public abstract partial class BeatmapCarouselTestScene : OsuManualInputManagerTestScene { protected readonly BindableList BeatmapSets = new BindableList(); @@ -47,7 +48,7 @@ namespace osu.Game.Tests.Visual.SongSelect private int beatmapCount; - protected BeatmapCarouselV2TestScene() + protected BeatmapCarouselTestScene() { store = new TestBeatmapStore { @@ -96,6 +97,8 @@ namespace osu.Game.Tests.Visual.SongSelect { Carousel = new BeatmapCarousel { + BleedTop = 50, + BleedBottom = 50, Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 800, @@ -189,7 +192,7 @@ namespace osu.Game.Tests.Visual.SongSelect .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) .OrderBy(p => p.Y) .ElementAt(index) - .ChildrenOfType().Single() + .ChildrenOfType().Single() .TriggerClick(); }); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index 8694722acc..9e9cd3505a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Game.Graphics.Cursor; using osu.Game.Overlays; @@ -20,7 +19,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(10), }; private Container? resizeContainer; @@ -33,15 +31,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(10), Width = relativeWidth, Children = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background5, - }, Content } }; @@ -55,6 +47,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } + protected override void LoadComplete() + { + base.LoadComplete(); + ChangeBackgroundColour(ColourProvider.Background6); + } + [SetUpSteps] public virtual void SetUpSteps() { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs index 30ca26ce68..5fd921645b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs @@ -10,13 +10,13 @@ using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Tests.Resources; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { /// /// Covers common steps which can be used for manual testing. /// [TestFixture] - public partial class TestSceneBeatmapCarouselV2 : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarousel : BeatmapCarouselTestScene { [Test] [Explicit] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs similarity index 98% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index c378871eac..f0caa796b6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -9,10 +9,10 @@ using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { [TestFixture] - public partial class TestSceneBeatmapCarouselV2ArtistGrouping : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselArtistGrouping : BeatmapCarouselTestScene { [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs similarity index 97% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index 239c693ee1..a4cdf8abcb 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -5,15 +5,16 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { [TestFixture] - public partial class TestSceneBeatmapCarouselV2DifficultyGrouping : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselDifficultyGrouping : BeatmapCarouselTestScene { [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs similarity index 97% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs index b4048a5355..ac02d7a3a9 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs @@ -5,16 +5,17 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK; using osuTK.Input; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { [TestFixture] - public partial class TestSceneBeatmapCarouselV2NoGrouping : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselNoGrouping : BeatmapCarouselTestScene { [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs similarity index 94% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs index 890e1dd6e3..da3fc98c19 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs @@ -8,10 +8,10 @@ using osu.Framework.Testing; using osu.Game.Screens.Select; using osu.Game.Screens.SelectV2; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { [TestFixture] - public partial class TestSceneBeatmapCarouselV2Scrolling : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselScrolling : BeatmapCarouselTestScene { [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs deleted file mode 100644 index 9b07f01e52..0000000000 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs +++ /dev/null @@ -1,83 +0,0 @@ -// 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.Game.Screens.SelectV2; -using osu.Game.Tests.Visual.UserInterface; -using osuTK; - -namespace osu.Game.Tests.Visual.SongSelectV2 -{ - public partial class TestSceneBeatmapCarouselV2GroupPanel : ThemeComparisonTestScene - { - public TestSceneBeatmapCarouselV2GroupPanel() - : base(false) - { - } - - protected override Drawable CreateContent() - { - return new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 0.5f, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0f, 5f), - Children = new Drawable[] - { - new PanelGroup - { - Item = new CarouselItem(new GroupDefinition('A', "Group A")) - }, - new PanelGroup - { - Item = new CarouselItem(new GroupDefinition('A', "Group A")), - KeyboardSelected = { Value = true } - }, - new PanelGroup - { - Item = new CarouselItem(new GroupDefinition('A', "Group A")), - Expanded = { Value = true } - }, - new PanelGroup - { - Item = new CarouselItem(new GroupDefinition('A', "Group A")), - KeyboardSelected = { Value = true }, - Expanded = { Value = true } - }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(1, "1")) - }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(3, "3")), - Expanded = { Value = true } - }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(5, "5")), - }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(7, "7")), - Expanded = { Value = true } - }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(8, "8")), - }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(9, "9")), - Expanded = { Value = true } - }, - } - }; - } - } -} diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs deleted file mode 100644 index 5b717887e2..0000000000 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs +++ /dev/null @@ -1,213 +0,0 @@ -// 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 NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Legacy; -using osu.Game.Screens.Select; -using osu.Game.Screens.SelectV2; - -namespace osu.Game.Tests.Visual.SongSelectV2 -{ - public partial class TestSceneBeatmapInfoWedge : SongSelectComponentsTestScene - { - private RulesetStore rulesets = null!; - private TestBeatmapInfoWedgeV2 infoWedge = null!; - private readonly List beatmaps = new List(); - - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) - { - this.rulesets = rulesets; - } - - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("reset mods", () => SelectedMods.SetDefault()); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - AddRange(new Drawable[] - { - // This exists only to make the wedge more visible in the test scene - new Box - { - Y = -20, - Colour = Colour4.Cornsilk.Darken(0.2f), - Height = BeatmapInfoWedgeV2.WEDGE_HEIGHT + 40, - Width = 0.65f, - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = 20, Left = -10 } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 20 }, - Child = infoWedge = new TestBeatmapInfoWedgeV2 - { - Width = 0.6f, - RelativeSizeAxes = Axes.X, - }, - } - }); - - AddSliderStep("change star difficulty", 0, 11.9, 5.55, v => - { - foreach (var hasCurrentValue in infoWedge.ChildrenOfType>()) - hasCurrentValue.Current.Value = new StarDifficulty(v, 0); - }); - } - - [Test] - public void TestRulesetChange() - { - selectBeatmap(Beatmap.Value.Beatmap); - - AddWaitStep("wait for select", 3); - - foreach (var rulesetInfo in rulesets.AvailableRulesets) - { - var instance = rulesetInfo.CreateInstance(); - var testBeatmap = createTestBeatmap(rulesetInfo); - - beatmaps.Add(testBeatmap); - - setRuleset(rulesetInfo); - - selectBeatmap(testBeatmap); - - testBeatmapLabels(instance); - } - } - - [Test] - public void TestWedgeVisibility() - { - AddStep("hide", () => { infoWedge.Hide(); }); - AddWaitStep("wait for hide", 3); - AddAssert("check visibility", () => infoWedge.Alpha == 0); - AddStep("show", () => { infoWedge.Show(); }); - AddWaitStep("wait for show", 1); - AddAssert("check visibility", () => infoWedge.Alpha > 0); - } - - private void testBeatmapLabels(Ruleset ruleset) - { - AddAssert("check title", () => infoWedge.Info!.TitleLabel.Current.Value == $"{ruleset.ShortName}Title"); - AddAssert("check artist", () => infoWedge.Info!.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist"); - } - - [Test] - public void TestTruncation() - { - selectBeatmap(createLongMetadata()); - } - - [Test] - public void TestNullBeatmapWithBackground() - { - selectBeatmap(null); - AddAssert("check default title", () => infoWedge.Info!.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title); - AddAssert("check default artist", () => infoWedge.Info!.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist); - AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType().Any()); - } - - private void setRuleset(RulesetInfo rulesetInfo) - { - Container? containerBefore = null; - - AddStep("set ruleset", () => - { - // wedge content is only refreshed if the ruleset changes, so only wait for load in that case. - if (!rulesetInfo.Equals(Ruleset.Value)) - containerBefore = infoWedge.DisplayedContent; - - Ruleset.Value = rulesetInfo; - }); - - AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); - } - - private void selectBeatmap(IBeatmap? b) - { - Container? containerBefore = null; - - AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () => - { - containerBefore = infoWedge.DisplayedContent; - infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b); - infoWedge.Show(); - }); - - AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); - } - - private IBeatmap createTestBeatmap(RulesetInfo ruleset) - { - List objects = new List(); - for (double i = 0; i < 50000; i += 1000) - objects.Add(new TestHitObject { StartTime = i }); - - return new Beatmap - { - BeatmapInfo = new BeatmapInfo - { - Metadata = new BeatmapMetadata - { - Author = { Username = $"{ruleset.ShortName}Author" }, - Artist = $"{ruleset.ShortName}Artist", - Source = $"{ruleset.ShortName}Source", - Title = $"{ruleset.ShortName}Title" - }, - Ruleset = ruleset, - StarRating = 6, - DifficultyName = $"{ruleset.ShortName}Version", - Difficulty = new BeatmapDifficulty() - }, - HitObjects = objects - }; - } - - private IBeatmap createLongMetadata() - { - return new Beatmap - { - BeatmapInfo = new BeatmapInfo - { - Metadata = new BeatmapMetadata - { - Author = { Username = "WWWWWWWWWWWWWWW" }, - Artist = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Artist", - Source = "Verrrrry long Source", - Title = "Verrrrry long Title" - }, - DifficultyName = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version", - Status = BeatmapOnlineStatus.Graveyard, - }, - }; - } - - private partial class TestBeatmapInfoWedgeV2 : BeatmapInfoWedgeV2 - { - public new Container? DisplayedContent => base.DisplayedContent; - public new WedgeInfoText? Info => base.Info; - } - - private class TestHitObject : ConvertHitObject; - } -} diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs deleted file mode 100644 index 49e7e2bc1a..0000000000 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs +++ /dev/null @@ -1,44 +0,0 @@ -// 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.Localisation; -using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Screens.SelectV2.Wedge; - -namespace osu.Game.Tests.Visual.SongSelectV2 -{ - public partial class TestSceneDifficultyNameContent : SongSelectComponentsTestScene - { - private DifficultyNameContent? difficultyNameContent; - - [Test] - public void TestLocalBeatmap() - { - AddStep("set component", () => Child = difficultyNameContent = new LocalDifficultyNameContent()); - - AddAssert("difficulty name is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().Text)); - AddAssert("author is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().ChildrenOfType().Single().Text)); - - AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new Beatmap - { - BeatmapInfo = new BeatmapInfo - { - DifficultyName = "really long difficulty name that gets truncated", - Metadata = new BeatmapMetadata - { - Author = { Username = "really long username that is autosized" }, - }, - OnlineID = 1, - } - })); - - AddAssert("difficulty name is set", () => !LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().Text)); - AddAssert("author is set", () => !LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().ChildrenOfType().Single().Text)); - } - } -} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneFooterButtonMods.cs similarity index 92% rename from osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneFooterButtonMods.cs index e86f83ee15..5c2c6eaf1d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneFooterButtonMods.cs @@ -13,19 +13,19 @@ using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.SelectV2.Footer; +using osu.Game.Screens.SelectV2; using osu.Game.Utils; -namespace osu.Game.Tests.Visual.UserInterface +namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneScreenFooterButtonMods : OsuTestScene + public partial class TestSceneFooterButtonMods : OsuTestScene { private readonly TestScreenFooterButtonMods footerButtonMods; [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); - public TestSceneScreenFooterButtonMods() + public TestSceneFooterButtonMods() { Add(footerButtonMods = new TestScreenFooterButtonMods(new TestModSelectOverlay()) { @@ -98,9 +98,9 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestUnrankedBadge() { AddStep(@"Add unranked mod", () => changeMods(new[] { new OsuModDeflate() })); - AddUntilStep("Unranked badge shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 1); + AddUntilStep("Unranked badge shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 1); AddStep(@"Clear selected mod", () => changeMods(Array.Empty())); - AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 0); + AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 0); } private void changeMods(IReadOnlyList mods) => footerButtonMods.Current.Value = mods; @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.UserInterface } } - private partial class TestScreenFooterButtonMods : ScreenFooterButtonMods + private partial class TestScreenFooterButtonMods : FooterButtonMods { public new OsuSpriteText MultiplierText => base.MultiplierText; diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs index 26d39c9203..b59a31c173 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs @@ -20,7 +20,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Screens.SelectV2.Leaderboards; +using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; @@ -53,14 +53,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(0f, 2f), - Shear = new Vector2(OsuGame.SHEAR, 0) + Shear = OsuGame.SHEAR, }, drawWidthText = new OsuSpriteText(), }; foreach (var scoreInfo in getTestScores()) { - fillFlow.Add(new LeaderboardScoreV2(scoreInfo) + fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo) { Rank = scoreInfo.Position, IsPersonalBest = scoreInfo.User.Id == 2, @@ -93,7 +93,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 foreach (var scoreInfo in getTestScores()) { - fillFlow.Add(new LeaderboardScoreV2(scoreInfo) + fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo) { Rank = scoreInfo.Position, IsPersonalBest = scoreInfo.User.Id == 2, @@ -108,7 +108,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestUseTheseModsDoesNotCopySystemMods() { - LeaderboardScoreV2 score = null!; + BeatmapLeaderboardScore score = null!; AddStep("create content", () => { @@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(0f, 2f), - Shear = new Vector2(OsuGame.SHEAR, 0) + Shear = OsuGame.SHEAR, }, drawWidthText = new OsuSpriteText(), }; @@ -146,7 +146,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Date = DateTimeOffset.Now.AddYears(-2), }; - fillFlow.Add(score = new LeaderboardScoreV2(scoreInfo) + fillFlow.Add(score = new BeatmapLeaderboardScore(scoreInfo) { Rank = scoreInfo.Position, Shear = Vector2.Zero, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs index 1947721d5d..53a1355fc2 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Overlays; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; @@ -18,14 +19,14 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselV2DifficultyPanel : ThemeComparisonTestScene + public partial class TestScenePanelBeatmap : ThemeComparisonTestScene { [Resolved] private BeatmapManager beatmaps { get; set; } = null!; private BeatmapInfo beatmap = null!; - public TestSceneBeatmapCarouselV2DifficultyPanel() + public TestScenePanelBeatmap() : base(false) { } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs index 2dbe9e6cd1..4adee17868 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Overlays; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; @@ -18,14 +19,14 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselV2StandalonePanel : ThemeComparisonTestScene + public partial class TestScenePanelBeatmapStandalone : ThemeComparisonTestScene { [Resolved] private BeatmapManager beatmaps { get; set; } = null!; private BeatmapInfo beatmap = null!; - public TestSceneBeatmapCarouselV2StandalonePanel() + public TestScenePanelBeatmapStandalone() : base(false) { } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs new file mode 100644 index 0000000000..54c6cb1c0e --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs @@ -0,0 +1,120 @@ +// 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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Graphics.Carousel; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestScenePanelGroup : ThemeComparisonTestScene + { + public TestScenePanelGroup() + : base(false) + { + } + + [Test] + public void TestGeneral() + { + AddStep("general", () => CreateThemedContent(OverlayColourScheme.Aquamarine)); + } + + [Test] + public void TestStars() + { + for (int i = 0; i <= 10; i++) + { + int star = i; + + AddStep($"display {i} star(s)", () => + { + ContentContainer.Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] + { + (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine)) + }, + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new[] + { + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(star, star.ToString())) + }, + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(star, star.ToString())), + KeyboardSelected = { Value = true }, + }, + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(star, star.ToString())), + Expanded = { Value = true }, + }, + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(star, star.ToString())), + Expanded = { Value = true }, + KeyboardSelected = { Value = true }, + }, + }, + } + }; + }); + } + } + + protected override Drawable CreateContent() + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + new PanelGroup + { + Item = new CarouselItem(new GroupDefinition('A', "Group A")) + }, + new PanelGroup + { + Item = new CarouselItem(new GroupDefinition('A', "Group A")), + KeyboardSelected = { Value = true } + }, + new PanelGroup + { + Item = new CarouselItem(new GroupDefinition('A', "Group A")), + Expanded = { Value = true } + }, + new PanelGroup + { + Item = new CarouselItem(new GroupDefinition('A', "Group A")), + KeyboardSelected = { Value = true }, + Expanded = { Value = true } + }, + } + }; + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs index ef34394e12..16f6b2cc9c 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Overlays; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; @@ -16,14 +17,14 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselV2SetPanel : ThemeComparisonTestScene + public partial class TestScenePanelSet : ThemeComparisonTestScene { [Resolved] private BeatmapManager beatmaps { get; set; } = null!; private BeatmapSetInfo beatmapSet = null!; - public TestSceneBeatmapCarouselV2SetPanel() + public TestScenePanelSet() : base(false) { } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelUpdateBeatmapButton.cs similarity index 90% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestScenePanelUpdateBeatmapButton.cs index ba3f2635b0..781691d3db 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelUpdateBeatmapButton.cs @@ -9,14 +9,14 @@ using osu.Game.Screens.SelectV2; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneUpdateBeatmapSetButtonV2 : OsuTestScene + public partial class TestScenePanelUpdateBeatmapButton : OsuTestScene { - private UpdateBeatmapSetButton button = null!; + private PanelUpdateBeatmapButton button = null!; [SetUp] public void SetUp() => Schedule(() => { - Child = button = new UpdateBeatmapSetButton + Child = button = new PanelUpdateBeatmapButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneScreenFooter.cs similarity index 98% rename from osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneScreenFooter.cs index 054bbb39d1..bdecebd64f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneScreenFooter.cs @@ -15,9 +15,9 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Screens.Footer; -using osu.Game.Screens.SelectV2.Footer; +using osu.Game.Screens.SelectV2; -namespace osu.Game.Tests.Visual.UserInterface +namespace osu.Game.Tests.Visual.SongSelectV2 { public partial class TestSceneScreenFooter : OsuManualInputManagerTestScene { @@ -51,9 +51,9 @@ namespace osu.Game.Tests.Visual.UserInterface screenFooter.SetButtons(new ScreenFooterButton[] { - new ScreenFooterButtonMods(modOverlay) { Current = SelectedMods }, - new ScreenFooterButtonRandom(), - new ScreenFooterButtonOptions(), + new FooterButtonMods(modOverlay) { Current = SelectedMods }, + new FooterButtonRandom(), + new FooterButtonOptions(), }); }); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 630f3c95ee..986ad6fc46 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -22,7 +22,7 @@ using osu.Game.Rulesets.Taiko; using osu.Game.Screens; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; -using osu.Game.Screens.SelectV2.Footer; +using osu.Game.Screens.SelectV2; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelectV2 @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { base.SetUpSteps(); - AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SoloSongSelect())); + AddStep("load screen", () => Stack.Push(new SoloSongSelect())); AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect songSelect && songSelect.IsLoaded); } @@ -199,7 +199,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("Press F1", () => { - InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); AddAssert("Overlay visible", () => this.ChildrenOfType().Single().State.Value == Visibility.Visible); diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index eecd097a97..2be7c4aff3 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -53,6 +53,14 @@ namespace osu.Game.Tournament return new ProductionEndpointConfiguration(); } + public override void SetHost(GameHost host) + { + base.SetHost(host); + + if (host.Window != null) + host.Window.Title = $"{Name} [tournament client]"; + } + private TournamentSpriteText initialisationText = null!; [BackgroundDependencyLoader] diff --git a/osu.Game/Beatmaps/BeatmapOnlineStatus.cs b/osu.Game/Beatmaps/BeatmapOnlineStatus.cs index 41393a8a39..d489aeda3f 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineStatus.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineStatus.cs @@ -14,10 +14,10 @@ namespace osu.Game.Beatmaps /// This is a special status given when local changes are made via the editor. /// Once in this state, online status changes should be ignored unless the beatmap is reverted or submitted. /// - [Description("Local")] [LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.LocallyModified))] LocallyModified = -4, + [Description("Unknown")] None = -3, [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusGraveyard))] diff --git a/osu.Game/Beatmaps/BeatmapStatistic.cs b/osu.Game/Beatmaps/BeatmapStatistic.cs index 13e0e4ad5e..64e42f3f02 100644 --- a/osu.Game/Beatmaps/BeatmapStatistic.cs +++ b/osu.Game/Beatmaps/BeatmapStatistic.cs @@ -16,7 +16,19 @@ namespace osu.Game.Beatmaps /// public Func CreateIcon; - public string Content; + /// + /// The name of this statistic. + /// public LocalisableString Name; + + /// + /// The text representing the value of this statistic. + /// + public string Content; + + /// + /// The length of a bar which visually represents this statistic's relevance in the beatmap. + /// + public float? BarDisplayLength; } } diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index 7b99ad40de..c6a3c7db3c 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -19,9 +19,10 @@ namespace osu.Game.Beatmaps.Drawables { public partial class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip { - private const double animation_duration = 400; - - private BeatmapOnlineStatus status; + /// + /// Whether to show as "unknown" instead of fading out. + /// + public bool ShowUnknownStatus { get; init; } public BeatmapOnlineStatus Status { @@ -34,30 +35,27 @@ namespace osu.Game.Beatmaps.Drawables status = value; if (IsLoaded) - { - AutoSizeDuration = (float)animation_duration; - AutoSizeEasing = Easing.OutQuint; - updateState(); - } } } + private BeatmapOnlineStatus status; + public float TextSize { - get => statusText.Font.Size; - set => statusText.Font = statusText.Font.With(size: value); + init => statusText.Font = statusText.Font.With(size: value); } public MarginPadding TextPadding { - get => statusText.Padding; - set => statusText.Padding = value; + init => statusText.Padding = value; } private readonly OsuSpriteText statusText; private readonly Box background; + private const double animation_duration = 400; + [Resolved] private OsuColour colours { get; set; } = null!; @@ -66,6 +64,7 @@ namespace osu.Game.Beatmaps.Drawables public BeatmapSetOnlineStatusPill() { + AutoSizeAxes = Axes.Both; Masking = true; Alpha = 0; @@ -99,14 +98,27 @@ namespace osu.Game.Beatmaps.Drawables private void updateState() { - if (Status == BeatmapOnlineStatus.None) + if (Status == BeatmapOnlineStatus.None && !ShowUnknownStatus) { - Hide(); + this.FadeOut(animation_duration, Easing.OutQuint); return; } + // The autosize animation on this component is intended to animate horizontal sizing only. + // To avoid vertical autosize animating from zero to non-zero, only apply the duration + // after we have a valid size. + if (Height > 0) + { + AutoSizeDuration = (float)animation_duration; + AutoSizeEasing = Easing.OutQuint; + } + this.FadeIn(animation_duration, Easing.OutQuint); + // Handle the case where transition from hidden to non-hidden may cause + // a fade from a colour that doesn't make sense (due to not being able to see the previous colour). + double duration = Alpha > 0 ? animation_duration : 0; + Color4 statusTextColour; if (colourProvider != null) @@ -114,8 +126,8 @@ namespace osu.Game.Beatmaps.Drawables else statusTextColour = status == BeatmapOnlineStatus.Graveyard ? colours.GreySeaFoamLight : Color4.Black; - statusText.FadeColour(statusTextColour, animation_duration, Easing.OutQuint); - background.FadeColour(OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter, animation_duration, Easing.OutQuint); + statusText.FadeColour(statusTextColour, duration, Easing.OutQuint); + background.FadeColour(OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter, duration, Easing.OutQuint); statusText.Text = Status.GetLocalisableDescription().ToUpper(); } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs index 41513ec7a2..ee2f682708 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs @@ -30,7 +30,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards { new BeatmapSetOnlineStatusPill { - AutoSizeAxes = Axes.Both, Status = beatmapSet.Status, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs index 4119ffb636..050a78a6b4 100644 --- a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs @@ -1,7 +1,6 @@ // 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -39,8 +38,6 @@ namespace osu.Game.Beatmaps.Drawables private readonly Bindable displayedStars = new BindableDouble(); - private readonly Container textContainer; - /// /// The currently displayed stars of this display wrapped in a bindable. /// This bindable gets transformed on change rather than instantaneous, if animation is enabled. @@ -119,19 +116,14 @@ namespace osu.Game.Beatmaps.Drawables Size = new Vector2(8f), }, Empty(), - textContainer = new Container + starsText = new OsuSpriteText { - AutoSizeAxes = Axes.Y, - Child = starsText = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Margin = new MarginPadding { Bottom = 1.5f }, - // todo: this should be size: 12f, but to match up with the design, it needs to be 14.4f - // see https://github.com/ppy/osu-framework/issues/3271. - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), - Shadow = false, - }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Margin = new MarginPadding { Bottom = 1.5f }, + Spacing = new Vector2(-1.4f), + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold, fixedWidth: true), + Shadow = false, }, } } @@ -162,11 +154,6 @@ namespace osu.Game.Beatmaps.Drawables starIcon.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4Extensions.FromHex("303d47"); starsText.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4.Black.Opacity(0.75f); - - // In order to avoid autosize throwing the width of these displays all over the place, - // let's lock in some sane defaults for the text width based on how many digits we're - // displaying. - textContainer.Width = 24 + Math.Max(starsText.Text.ToString().Length - 4, 0) * 6; }, true); } } diff --git a/osu.Game/Database/RealmDetachedBeatmapStore.cs b/osu.Game/Database/RealmDetachedBeatmapStore.cs index b05e07ef31..6954bb320a 100644 --- a/osu.Game/Database/RealmDetachedBeatmapStore.cs +++ b/osu.Game/Database/RealmDetachedBeatmapStore.cs @@ -30,7 +30,8 @@ namespace osu.Game.Database public override IBindableList GetBeatmapSets(CancellationToken? cancellationToken) { loaded.Wait(cancellationToken ?? CancellationToken.None); - return detachedBeatmapSets.GetBoundCopy(); + lock (detachedBeatmapSets) + return detachedBeatmapSets.GetBoundCopy(); } [BackgroundDependencyLoader] @@ -65,8 +66,11 @@ namespace osu.Game.Database { var detached = frozenSets.Detach(); - detachedBeatmapSets.Clear(); - detachedBeatmapSets.AddRange(detached); + lock (detachedBeatmapSets) + { + detachedBeatmapSets.Clear(); + detachedBeatmapSets.AddRange(detached); + } }); } finally @@ -116,22 +120,28 @@ namespace osu.Game.Database if (!loaded.IsSet) return; - // If this ever leads to performance issues, we could dequeue a limited number of operations per update frame. - while (pendingOperations.TryDequeue(out var op)) + if (pendingOperations.Count == 0) + return; + + lock (detachedBeatmapSets) { - switch (op.Type) + // If this ever leads to performance issues, we could dequeue a limited number of operations per update frame. + while (pendingOperations.TryDequeue(out var op)) { - case OperationType.Insert: - detachedBeatmapSets.Insert(op.Index, op.BeatmapSet!); - break; + switch (op.Type) + { + case OperationType.Insert: + detachedBeatmapSets.Insert(op.Index, op.BeatmapSet!); + break; - case OperationType.Update: - detachedBeatmapSets.ReplaceRange(op.Index, 1, new[] { op.BeatmapSet! }); - break; + case OperationType.Update: + detachedBeatmapSets.ReplaceRange(op.Index, 1, new[] { op.BeatmapSet! }); + break; - case OperationType.Remove: - detachedBeatmapSets.RemoveAt(op.Index); - break; + case OperationType.Remove: + detachedBeatmapSets.RemoveAt(op.Index); + break; + } } } } diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs similarity index 98% rename from osu.Game/Screens/SelectV2/Carousel.cs rename to osu.Game/Graphics/Carousel/Carousel.cs index 5339b5358b..3a02eb7119 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -24,7 +24,7 @@ using osu.Game.Input.Bindings; using osuTK; using osuTK.Input; -namespace osu.Game.Screens.SelectV2 +namespace osu.Game.Graphics.Carousel { /// /// A highly efficient vertical list display that is used primarily for the song select screen, @@ -38,12 +38,12 @@ namespace osu.Game.Screens.SelectV2 /// /// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it. /// - public float BleedTop { get; set; } = 0; + public float BleedTop { get; set; } /// /// Height of the area below the carousel that should be treated as visible due to transparency of elements in front of it. /// - public float BleedBottom { get; set; } = 0; + public float BleedBottom { get; set; } /// /// The number of pixels outside the carousel's vertical bounds to manifest drawables. @@ -228,6 +228,7 @@ namespace osu.Game.Screens.SelectV2 { InternalChild = Scroll = new CarouselScrollContainer { + Masking = false, RelativeSizeAxes = Axes.Both, }; @@ -505,7 +506,7 @@ namespace osu.Game.Screens.SelectV2 private void scrollToSelection() { if (currentKeyboardSelection.CarouselItem != null) - Scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight); + Scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight + BleedTop); } #endregion @@ -519,17 +520,17 @@ namespace osu.Game.Screens.SelectV2 /// /// The position of the lower visible bound with respect to the current scroll position. /// - private float visibleBottomBound => (float)(Scroll.Current + DrawHeight + BleedBottom); + private float visibleBottomBound; /// /// The position of the upper visible bound with respect to the current scroll position. /// - private float visibleUpperBound => (float)(Scroll.Current - BleedTop); + private float visibleUpperBound; /// /// Half the height of the visible content. /// - private float visibleHalfHeight => (DrawHeight + BleedBottom + BleedTop) / 2; + private float visibleHalfHeight; protected override void Update() { @@ -538,6 +539,10 @@ namespace osu.Game.Screens.SelectV2 if (carouselItems == null) return; + visibleBottomBound = (float)(Scroll.Current + DrawHeight + BleedBottom); + visibleUpperBound = (float)(Scroll.Current - BleedTop); + visibleHalfHeight = (DrawHeight + BleedBottom + BleedTop) / 2; + if (!selectionValid.IsValid) { refreshAfterSelection(); @@ -582,7 +587,7 @@ namespace osu.Game.Screens.SelectV2 protected virtual float GetPanelXOffset(Drawable panel) { Vector2 posInScroll = Scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); - float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight); + float dist = Math.Abs(1f - (posInScroll.Y + BleedTop) / visibleHalfHeight); return offsetX(dist, visibleHalfHeight); } diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Graphics/Carousel/CarouselItem.cs similarity index 98% rename from osu.Game/Screens/SelectV2/CarouselItem.cs rename to osu.Game/Graphics/Carousel/CarouselItem.cs index 36dc48a497..223c8d9869 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Graphics/Carousel/CarouselItem.cs @@ -3,7 +3,7 @@ using System; -namespace osu.Game.Screens.SelectV2 +namespace osu.Game.Graphics.Carousel { /// /// Represents a single display item for display in a . diff --git a/osu.Game/Screens/SelectV2/ICarouselFilter.cs b/osu.Game/Graphics/Carousel/ICarouselFilter.cs similarity index 95% rename from osu.Game/Screens/SelectV2/ICarouselFilter.cs rename to osu.Game/Graphics/Carousel/ICarouselFilter.cs index f510a7cd4b..570f480aab 100644 --- a/osu.Game/Screens/SelectV2/ICarouselFilter.cs +++ b/osu.Game/Graphics/Carousel/ICarouselFilter.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace osu.Game.Screens.SelectV2 +namespace osu.Game.Graphics.Carousel { /// /// An interface representing a filter operation which can be run on a . diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Graphics/Carousel/ICarouselPanel.cs similarity index 97% rename from osu.Game/Screens/SelectV2/ICarouselPanel.cs rename to osu.Game/Graphics/Carousel/ICarouselPanel.cs index 4fba0d2827..5f0ebc263c 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Graphics/Carousel/ICarouselPanel.cs @@ -5,7 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; -namespace osu.Game.Screens.SelectV2 +namespace osu.Game.Graphics.Carousel { /// /// An interface to be attached to any s which are used for display inside a . diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 2c43876fb2..dd5e19e167 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -20,10 +20,7 @@ namespace osu.Game.Graphics public static Color4 Gray(float amt) => new Color4(amt, amt, amt, 1f); public static Color4 Gray(byte amt) => new Color4(amt, amt, amt, 255); - /// - /// Retrieves the colour for a given point in the star range. - /// - public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(new[] + public static readonly (float, Color4)[] STAR_DIFFICULTY_SPECTRUM = { (0.1f, Color4Extensions.FromHex("aaaaaa")), (0.1f, Color4Extensions.FromHex("4290fb")), @@ -37,7 +34,13 @@ namespace osu.Game.Graphics (6.7f, Color4Extensions.FromHex("6563de")), (7.7f, Color4Extensions.FromHex("18158e")), (9.0f, Color4.Black), - }, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero)); + (10.0f, Color4.Black), + }; + + /// + /// Retrieves the colour for a given point in the star range. + /// + public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(STAR_DIFFICULTY_SPECTRUM, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero)); /// /// Retrieves the colour for a . @@ -120,6 +123,9 @@ namespace osu.Game.Graphics { switch (status) { + case BeatmapOnlineStatus.None: + return Color4.RosyBrown; + case BeatmapOnlineStatus.LocallyModified: return Color4.OrangeRed; @@ -403,6 +409,12 @@ namespace osu.Game.Graphics public readonly Color4 Orange3 = Color4Extensions.FromHex(@"cca633"); public readonly Color4 Orange4 = Color4Extensions.FromHex(@"6b5c2e"); + public readonly Color4 DarkOrange0 = Color4Extensions.FromHex(@"ffbb99"); + public readonly Color4 DarkOrange1 = Color4Extensions.FromHex(@"ff9966"); + public readonly Color4 DarkOrange2 = Color4Extensions.FromHex(@"eb7e47"); + public readonly Color4 DarkOrange3 = Color4Extensions.FromHex(@"cc6633"); + public readonly Color4 DarkOrange4 = Color4Extensions.FromHex(@"6b422e"); + public readonly Color4 Red0 = Color4Extensions.FromHex(@"ff9b9b"); public readonly Color4 Red1 = Color4Extensions.FromHex(@"ff6666"); public readonly Color4 Red2 = Color4Extensions.FromHex(@"eb4747"); diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 7aa98ece95..b314c602f5 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -15,15 +15,65 @@ namespace osu.Game.Graphics /// public const float DEFAULT_FONT_SIZE = 16; + /// + /// Template font styles which should be preferred whenever possible for UI elements. + /// + public static class Style + { + /// + /// Equivalent to Torus with 32px size and semi-bold weight. + /// + public static FontUsage Title => GetFont(Typeface.TorusAlternate, size: 32, weight: FontWeight.Regular); + + /// + /// Torus with 28px size and semi-bold weight. + /// + public static FontUsage Subtitle => GetFont(size: 28, weight: FontWeight.Regular); + + /// + /// Torus with 22px size and bold weight. + /// + public static FontUsage Heading1 => GetFont(size: 22, weight: FontWeight.Bold); + + /// + /// Torus with 18px size and semi-bold weight. + /// + public static FontUsage Heading2 => GetFont(size: 18, weight: FontWeight.SemiBold); + + /// + /// Torus with 16px size and regular weight. + /// + public static FontUsage Body => GetFont(size: DEFAULT_FONT_SIZE, weight: FontWeight.Regular); + + /// + /// Torus with 14px size and regular weight. + /// + public static FontUsage Caption1 => GetFont(size: 14, weight: FontWeight.Regular); + + /// + /// Torus with 12px size and regular weight. + /// + public static FontUsage Caption2 => GetFont(size: 12, weight: FontWeight.Regular); + } + /// /// The default font. /// - public static FontUsage Default => GetFont(); + public static FontUsage Default => GetFont(weight: FontWeight.Medium); + /// + /// Font face for numeric display. + /// public static FontUsage Numeric => GetFont(Typeface.Venera, weight: FontWeight.Bold); + /// + /// Default font face for UI and game elements. + /// public static FontUsage Torus => GetFont(Typeface.Torus, weight: FontWeight.Regular); + /// + /// Default font face with alternate character set for headings and flair text. + /// public static FontUsage TorusAlternate => GetFont(Typeface.TorusAlternate, weight: FontWeight.Regular); public static FontUsage Inter => GetFont(Typeface.Inter, weight: FontWeight.Regular); diff --git a/osu.Game/Graphics/OsuIcon.cs b/osu.Game/Graphics/OsuIcon.cs index 9879ef5d14..84ff86a5e5 100644 --- a/osu.Game/Graphics/OsuIcon.cs +++ b/osu.Game/Graphics/OsuIcon.cs @@ -115,6 +115,7 @@ namespace osu.Game.Graphics public static IconUsage ChangelogB => get(OsuIconMapping.ChangelogB); public static IconUsage Chat => get(OsuIconMapping.Chat); public static IconUsage CheckCircle => get(OsuIconMapping.CheckCircle); + public static IconUsage Clock => get(OsuIconMapping.Clock); public static IconUsage CollapseA => get(OsuIconMapping.CollapseA); public static IconUsage Collections => get(OsuIconMapping.Collections); public static IconUsage Cross => get(OsuIconMapping.Cross); @@ -141,6 +142,7 @@ namespace osu.Game.Graphics public static IconUsage Input => get(OsuIconMapping.Input); public static IconUsage Maintenance => get(OsuIconMapping.Maintenance); public static IconUsage Megaphone => get(OsuIconMapping.Megaphone); + public static IconUsage Metronome => get(OsuIconMapping.Metronome); public static IconUsage Music => get(OsuIconMapping.Music); public static IconUsage News => get(OsuIconMapping.News); public static IconUsage Next => get(OsuIconMapping.Next); @@ -204,6 +206,9 @@ namespace osu.Game.Graphics [Description(@"check-circle")] CheckCircle, + [Description(@"clock")] + Clock, + [Description(@"collapse-a")] CollapseA, @@ -282,6 +287,9 @@ namespace osu.Game.Graphics [Description(@"megaphone")] Megaphone, + [Description(@"metronome")] + Metronome, + [Description(@"music")] Music, diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs index c39f41bf72..423d9637b8 100644 --- a/osu.Game/Graphics/UserInterface/DialogButton.cs +++ b/osu.Game/Graphics/UserInterface/DialogButton.cs @@ -129,7 +129,7 @@ namespace osu.Game.Graphics.UserInterface Radius = 5, }, Colour = ButtonColour, - Shear = new Vector2(0.2f, 0), + Shear = OsuGame.SHEAR, Children = new Drawable[] { new Box @@ -149,7 +149,7 @@ namespace osu.Game.Graphics.UserInterface RelativeSizeAxes = Axes.Both, TriangleScale = 4, ColourDark = OsuColour.Gray(0.88f), - Shear = new Vector2(-0.2f, 0), + Shear = -OsuGame.SHEAR, ClampAxes = Axes.Y }, }, diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index 87d269ccd4..a059490aa8 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -11,7 +11,6 @@ using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; -using osuTK; namespace osu.Game.Graphics.UserInterface { @@ -66,8 +65,6 @@ namespace osu.Game.Graphics.UserInterface private readonly Box background; private readonly OsuSpriteText text; - private const float shear = OsuGame.SHEAR; - private Colour4? darkerColour; private Colour4? lighterColour; private Colour4? textColour; @@ -91,10 +88,10 @@ namespace osu.Game.Graphics.UserInterface public ShearedButton(float? width = null, float height = DEFAULT_HEIGHT) { Height = height; - Padding = new MarginPadding { Horizontal = shear * height }; + Padding = new MarginPadding { Horizontal = OsuGame.SHEAR.X * height }; Content.CornerRadius = CORNER_RADIUS; - Content.Shear = new Vector2(shear, 0); + Content.Shear = OsuGame.SHEAR; Content.Masking = true; Content.Anchor = Content.Origin = Anchor.Centre; @@ -117,7 +114,7 @@ namespace osu.Game.Graphics.UserInterface Anchor = Anchor.Centre, Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, - Shear = new Vector2(-shear, 0), + Shear = -OsuGame.SHEAR, Child = text = new OsuSpriteText { Font = OsuFont.TorusAlternate.With(size: 17), diff --git a/osu.Game/Graphics/UserInterface/ShearedNub.cs b/osu.Game/Graphics/UserInterface/ShearedNub.cs index 7485f68525..17b50b5d58 100644 --- a/osu.Game/Graphics/UserInterface/ShearedNub.cs +++ b/osu.Game/Graphics/UserInterface/ShearedNub.cs @@ -26,8 +26,6 @@ namespace osu.Game.Graphics.UserInterface public const int HEIGHT = 30; public const float EXPANDED_SIZE = 50; - public static readonly Vector2 SHEAR = new Vector2(0.15f, 0); - private readonly Box fill; private readonly Container main; @@ -40,7 +38,7 @@ namespace osu.Game.Graphics.UserInterface Size = new Vector2(EXPANDED_SIZE, HEIGHT); InternalChild = main = new Container { - Shear = SHEAR, + Shear = OsuGame.SHEAR, BorderColour = Colour4.White, BorderThickness = BORDER_WIDTH, Masking = true, diff --git a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs index c6565726b5..f5fbb3411f 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs @@ -52,7 +52,7 @@ namespace osu.Game.Graphics.UserInterface public ShearedSearchTextBox() { Height = 42; - Shear = new Vector2(OsuGame.SHEAR, 0); + Shear = OsuGame.SHEAR; Masking = true; CornerRadius = corner_radius; @@ -115,7 +115,7 @@ namespace osu.Game.Graphics.UserInterface PlaceholderText = CommonStrings.InputSearch; CornerRadius = corner_radius; - TextContainer.Shear = new Vector2(-OsuGame.SHEAR, 0); + TextContainer.Shear = -OsuGame.SHEAR; } protected override SpriteText CreatePlaceholder() => new SearchPlaceholder(); diff --git a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs index a36b9c7a4c..e7b57f5c9e 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs @@ -58,7 +58,7 @@ namespace osu.Game.Graphics.UserInterface public ShearedSliderBar() { - Shear = SHEAR; + Shear = OsuGame.SHEAR; Height = HEIGHT; RangePadding = EXPANDED_SIZE / 2; Children = new Drawable[] @@ -98,11 +98,11 @@ namespace osu.Game.Graphics.UserInterface }, nubContainer = new Container { - Shear = -SHEAR, + Shear = -OsuGame.SHEAR, RelativeSizeAxes = Axes.Both, Child = Nub = new ShearedNub { - X = -SHEAR.X * HEIGHT / 2f, + X = -OsuGame.SHEAR.X * HEIGHT / 2f, Origin = Anchor.TopCentre, RelativePositionAxes = Axes.X, Current = { Value = true }, diff --git a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs index 0b9c5f294c..609f77dd7e 100644 --- a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs @@ -62,8 +62,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected partial class ShearedDropdownMenu : OsuDropdown.OsuDropdownMenu { - private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); - public new MarginPadding Padding { get => base.Padding; @@ -72,7 +70,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 public ShearedDropdownMenu() { - Shear = shear; + Shear = OsuGame.SHEAR; Margin = new MarginPadding { Top = 5f }; } @@ -84,12 +82,10 @@ namespace osu.Game.Graphics.UserInterfaceV2 public partial class ShearedMenuItem : DrawableOsuDropdownMenuItem { - private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); - public ShearedMenuItem(MenuItem item) : base(item) { - Foreground.Shear = -shear; + Foreground.Shear = -OsuGame.SHEAR; } } } @@ -125,14 +121,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 public ShearedDropdown Dropdown = null!; private ShearedDropdownSearchBar searchBar = null!; - private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); - [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; public ShearedDropdownHeader() { - Shear = shear; + Shear = OsuGame.SHEAR; CornerRadius = corner_radius; Masking = true; @@ -167,7 +161,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { Margin = new MarginPadding { Horizontal = 10f, Vertical = 8f }, Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold), - Shear = -shear, + Shear = -OsuGame.SHEAR, }, }, }, @@ -178,7 +172,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Horizontal = 10f }, - Shear = -shear, + Shear = -OsuGame.SHEAR, Children = new Drawable[] { valueText = new TruncatingSpriteText @@ -286,12 +280,10 @@ namespace osu.Game.Graphics.UserInterfaceV2 private partial class DropdownSearchTextBox : OsuTextBox { - private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); - [BackgroundDependencyLoader] private void load(OverlayColourProvider? colourProvider) { - TextContainer.Shear = -shear; + TextContainer.Shear = -OsuGame.SHEAR; BackgroundUnfocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255); BackgroundFocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255); } diff --git a/osu.Game/Localisation/DrawableRoomPlaylistItemStrings.cs b/osu.Game/Localisation/DrawableRoomPlaylistItemStrings.cs new file mode 100644 index 0000000000..44616c03ca --- /dev/null +++ b/osu.Game/Localisation/DrawableRoomPlaylistItemStrings.cs @@ -0,0 +1,19 @@ +// 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.Localisation; + +namespace osu.Game.Localisation +{ + public static class DrawableRoomPlaylistItemStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.DrawableRoomPlaylistItem"; + + /// + /// "You have completed this beatmap" + /// + public static LocalisableString CompletedTooltip => new TranslatableString(getKey(@"completed_tooltip"), @"You have completed this beatmap"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Online/API/Requests/GetScoresRequest.cs b/osu.Game/Online/API/Requests/GetScoresRequest.cs index f2a2daccb5..ed26c77dd9 100644 --- a/osu.Game/Online/API/Requests/GetScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetScoresRequest.cs @@ -36,7 +36,7 @@ namespace osu.Game.Online.API.Requests this.mods = mods ?? Array.Empty(); } - protected override string Target => $@"beatmaps/{beatmapInfo.OnlineID}/solo-scores{createQueryParameters()}"; + protected override string Target => $@"beatmaps/{beatmapInfo.OnlineID}/scores{createQueryParameters()}"; private string createQueryParameters() { diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index 055d2dd8e2..20494a1cbf 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -32,6 +32,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"playcount")] public int PlayCount { get; set; } + [JsonProperty(@"current_user_playcount")] + public int UserPlayCount { get; set; } + [JsonProperty(@"passcount")] public int PassCount { get; set; } diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs new file mode 100644 index 0000000000..ff3fe39a96 --- /dev/null +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -0,0 +1,172 @@ +// 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.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Extensions; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; +using osu.Game.Screens.Select.Leaderboards; +using Realms; + +namespace osu.Game.Online.Leaderboards +{ + public partial class LeaderboardManager : Component + { + public IBindable Scores => scores; + private readonly Bindable scores = new Bindable(); + + public LeaderboardCriteria? CurrentCriteria { get; private set; } + + private IDisposable? localScoreSubscription; + private TaskCompletionSource? localFetchCompletionSource; + private TaskCompletionSource? lastFetchCompletionSource; + private GetScoresRequest? inFlightOnlineRequest; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + public Task FetchWithCriteriaAsync(LeaderboardCriteria newCriteria) + { + if (CurrentCriteria?.Equals(newCriteria) == true && lastFetchCompletionSource?.Task.IsFaulted == false) + return lastFetchCompletionSource?.Task ?? Task.FromResult(Scores.Value); + + CurrentCriteria = newCriteria; + localScoreSubscription?.Dispose(); + inFlightOnlineRequest?.Cancel(); + lastFetchCompletionSource?.TrySetCanceled(); + scores.Value = null; + + switch (newCriteria.Scope) + { + case BeatmapLeaderboardScope.Local: + { + // this task completion source will be marked completed in the `localScoresChanged()` below. + // yes it's twisty, but such are the costs of trying to reconcile data-push / subscription and data-pull / explicit fetch flows. + lastFetchCompletionSource = localFetchCompletionSource = new TaskCompletionSource(); + localScoreSubscription = realm.RegisterForNotifications(r => + r.All().Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0" + + $" AND {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" + + $" AND {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1" + + $" AND {nameof(ScoreInfo.DeletePending)} == false" + , newCriteria.Beatmap.ID, newCriteria.Ruleset.ShortName), localScoresChanged); + return localFetchCompletionSource.Task; + } + + default: + { + var onlineFetchCompletionSource = new TaskCompletionSource(); + lastFetchCompletionSource = onlineFetchCompletionSource; + + IReadOnlyList? requestMods = null; + + if (newCriteria.ExactMods != null) + { + if (!newCriteria.ExactMods.Any()) + // add nomod for the request + requestMods = new Mod[] { new ModNoMod() }; + else + requestMods = newCriteria.ExactMods; + } + + var newRequest = new GetScoresRequest(newCriteria.Beatmap, newCriteria.Ruleset, newCriteria.Scope, requestMods); + newRequest.Success += response => + { + if (inFlightOnlineRequest != null && !newRequest.Equals(inFlightOnlineRequest)) + return; + + var result = new LeaderboardScores + ( + response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)).OrderByTotalScore(), + response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap) + ); + inFlightOnlineRequest = null; + if (onlineFetchCompletionSource.TrySetResult(result)) + scores.Value = result; + }; + newRequest.Failure += ex => onlineFetchCompletionSource.TrySetException(ex); + api.Queue(inFlightOnlineRequest = newRequest); + return onlineFetchCompletionSource.Task; + } + } + } + + private void localScoresChanged(IRealmCollection sender, ChangeSet? changes) + { + Debug.Assert(CurrentCriteria != null); + + // This subscription may fire from changes to linked beatmaps, which we don't care about. + // It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications. + if (changes?.HasCollectionChanges() == false) + return; + + var newScores = sender.AsEnumerable(); + + if (CurrentCriteria.ExactMods != null) + { + if (!CurrentCriteria.ExactMods.Any()) + { + // we need to filter out all scores that have any mods to get all local nomod scores + newScores = newScores.Where(s => !s.Mods.Any()); + } + else + { + // otherwise find all the scores that have all of the currently selected mods (similar to how web applies mod filters) + // we're creating and using a string HashSet representation of selected mods so that it can be translated into the DB query itself + var selectedMods = CurrentCriteria.ExactMods.Select(m => m.Acronym).ToHashSet(); + + newScores = newScores.Where(s => selectedMods.SetEquals(s.Mods.Select(m => m.Acronym))); + } + } + + newScores = newScores.Detach().OrderByTotalScore(); + + scores.Value = new LeaderboardScores(newScores, null); + + if (localFetchCompletionSource != null && localFetchCompletionSource == lastFetchCompletionSource) + { + localFetchCompletionSource.SetResult(scores.Value); + localFetchCompletionSource = lastFetchCompletionSource = null; + } + } + } + + public record LeaderboardCriteria( + BeatmapInfo Beatmap, + RulesetInfo Ruleset, + BeatmapLeaderboardScope Scope, + Mod[]? ExactMods + ); + + public record LeaderboardScores(IEnumerable TopScores, ScoreInfo? UserScore) + { + public IEnumerable AllScores + { + get + { + foreach (var score in TopScores) + yield return score; + + if (UserScore != null && TopScores.All(topScore => !topScore.Equals(UserScore) && !topScore.MatchesOnlineID(UserScore))) + yield return UserScore; + } + } + } +} diff --git a/osu.Game/Online/Rooms/ItemAttemptsCount.cs b/osu.Game/Online/Rooms/ItemAttemptsCount.cs index dc86897660..9ea2235500 100644 --- a/osu.Game/Online/Rooms/ItemAttemptsCount.cs +++ b/osu.Game/Online/Rooms/ItemAttemptsCount.cs @@ -10,10 +10,22 @@ namespace osu.Game.Online.Rooms /// public class ItemAttemptsCount { + /// + /// The playlist item this object describes. + /// [JsonProperty("id")] public int PlaylistItemID { get; set; } + /// + /// The number of times the user attempted the playlist item. + /// [JsonProperty("attempts")] public int Attempts { get; set; } + + /// + /// Whether the user has a passing score on the playlist item. + /// + [JsonProperty("passed")] + public bool Passed { get; set; } } } diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 427f31fc64..8ba62fd0e2 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -85,6 +85,11 @@ namespace osu.Game.Online.Rooms private readonly Bindable valid = new BindableBool(true); + [JsonIgnore] + public IBindable Completed => completed; + + private readonly Bindable completed = new BindableBool(false); + [JsonConstructor] private PlaylistItem() : this(new APIBeatmap()) @@ -118,6 +123,8 @@ namespace osu.Game.Online.Rooms public void MarkInvalid() => valid.Value = false; + public void MarkCompleted() => completed.Value = true; + #region Newtonsoft.Json implicit ShouldSerialize() methods // The properties in this region are used implicitly by Newtonsoft.Json to not serialise certain fields in some cases. diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 19b80bfba4..0c6a06a8fc 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -47,6 +47,7 @@ using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Online; using osu.Game.Online.Chat; +using osu.Game.Online.Leaderboards; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; @@ -67,6 +68,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Seasonal; using osu.Game.Skinning; using osu.Game.Updater; @@ -100,7 +102,12 @@ namespace osu.Game /// /// A common shear factor applied to most components of the game. /// - public const float SHEAR = 0.2f; + public static readonly Vector2 SHEAR = new Vector2(0.2f, 0); + + /// + /// For elements placed close to the screen edge, this is the margin to leave to the edge. + /// + public const float SCREEN_EDGE_MARGIN = 12f; public Toolbar Toolbar { get; private set; } @@ -784,6 +791,24 @@ namespace osu.Game if (!Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap)) Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap); + var currentLeaderboard = LeaderboardManager.CurrentCriteria; + + bool leaderboardBeatmapMatches = currentLeaderboard != null && databasedBeatmap.Equals(currentLeaderboard.Beatmap); + bool leaderboardRulesetMatches = currentLeaderboard != null && databasedScore.ScoreInfo.Ruleset.Equals(currentLeaderboard.Ruleset); + + if (!leaderboardBeatmapMatches || !leaderboardRulesetMatches) + { + var newLeaderboard = currentLeaderboard != null + ? currentLeaderboard with { Beatmap = databasedBeatmap, Ruleset = databasedScore.ScoreInfo.Ruleset } + : new LeaderboardCriteria(databasedBeatmap, databasedScore.ScoreInfo.Ruleset, BeatmapLeaderboardScope.Global, null); + LeaderboardManager.FetchWithCriteriaAsync(newLeaderboard) + .ContinueWith(t => + { + if (t.Exception != null) + Logger.Log($@"Failed to fetch leaderboards when displaying results: {t.Exception}", LoggingTarget.Network); + }); + } + switch (presentType) { case ScorePresentType.Gameplay: diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3eaabd9d2a..4cc9ab7936 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -49,6 +49,7 @@ using osu.Game.Localisation; using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.Chat; +using osu.Game.Online.Leaderboards; using osu.Game.Online.Metadata; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; @@ -203,6 +204,7 @@ namespace osu.Game private UserLookupCache userCache; private BeatmapLookupCache beatmapCache; + protected LeaderboardManager LeaderboardManager { get; private set; } private RulesetConfigCache rulesetConfigCache; @@ -365,6 +367,9 @@ namespace osu.Game dependencies.CacheAs>(Beatmap); dependencies.CacheAs(Beatmap); + dependencies.Cache(LeaderboardManager = new LeaderboardManager()); + base.Content.Add(LeaderboardManager); + // add api components to hierarchy. if (API is APIAccess apiAccess) base.Content.Add(apiAccess); diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index c72c2a6698..9b10f6156d 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -177,7 +177,6 @@ namespace osu.Game.Overlays.BeatmapSet { onlineStatusPill = new BeatmapSetOnlineStatusPill { - AutoSizeAxes = Axes.Both, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, TextSize = 14, diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs index 6107f130ec..3741852993 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs @@ -18,6 +18,7 @@ using osu.Game.Online.Chat; using osu.Game.Overlays.Chat.Listing; using osu.Game.Users.Drawables; using osuTK; +using osuTK.Input; namespace osu.Game.Overlays.Chat.ChannelList { @@ -160,6 +161,17 @@ namespace osu.Game.Overlays.Chat.ChannelList }; } + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Middle) + { + close?.TriggerClick(); + return true; + } + + return base.OnMouseDown(e); + } + private ChannelListItemCloseButton? createCloseButton() { if (isSelector || !CanLeave) diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index dedd1e336e..3cefa07cfa 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -20,7 +20,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Utils; -using osuTK; namespace osu.Game.Overlays.Mods { @@ -66,21 +65,19 @@ namespace osu.Game.Overlays.Mods [BackgroundDependencyLoader] private void load() { - const float shear = OsuGame.SHEAR; - LeftContent.AddRange(new Drawable[] { starRatingDisplay = new StarRatingDisplay(default, animated: true) { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Shear = new Vector2(-shear, 0), + Shear = -OsuGame.SHEAR, }, bpmDisplay = new BPMDisplay { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Shear = new Vector2(-shear, 0), + Shear = -OsuGame.SHEAR, AutoSizeAxes = Axes.Y, Width = 75, } @@ -89,10 +86,10 @@ namespace osu.Game.Overlays.Mods RightContent.Alpha = 0; RightContent.AddRange(new Drawable[] { - circleSizeDisplay = new VerticalAttributeDisplay("CS") { Shear = new Vector2(-shear, 0), }, - drainRateDisplay = new VerticalAttributeDisplay("HP") { Shear = new Vector2(-shear, 0), }, - overallDifficultyDisplay = new VerticalAttributeDisplay("OD") { Shear = new Vector2(-shear, 0), }, - approachRateDisplay = new VerticalAttributeDisplay("AR") { Shear = new Vector2(-shear, 0), }, + circleSizeDisplay = new VerticalAttributeDisplay("CS") { Shear = -OsuGame.SHEAR, }, + drainRateDisplay = new VerticalAttributeDisplay("HP") { Shear = -OsuGame.SHEAR, }, + overallDifficultyDisplay = new VerticalAttributeDisplay("OD") { Shear = -OsuGame.SHEAR, }, + approachRateDisplay = new VerticalAttributeDisplay("AR") { Shear = -OsuGame.SHEAR, }, }); } diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 326394a207..7d2ce54074 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.CentreLeft, Scale = new Vector2(0.8f), RelativeSizeAxes = Axes.X, - Shear = new Vector2(-OsuGame.SHEAR, 0) + Shear = -OsuGame.SHEAR }); ItemsFlow.Padding = new MarginPadding { diff --git a/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs b/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs index 6665a3b8dc..db42200775 100644 --- a/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs +++ b/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomRight, AutoSizeAxes = Axes.X, Height = ShearedButton.DEFAULT_HEIGHT, - Shear = new Vector2(OsuGame.SHEAR, 0), + Shear = OsuGame.SHEAR, CornerRadius = ShearedButton.CORNER_RADIUS, BorderThickness = ShearedButton.BORDER_THICKNESS, Masking = true, diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index b85904f22b..df72692f48 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.Centre, Origin = Anchor.Centre, Active = { BindTarget = Active }, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE) }; } diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index 8a499a391c..92c75e3494 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -70,7 +70,7 @@ namespace osu.Game.Overlays.Mods { Width = WIDTH; RelativeSizeAxes = Axes.Y; - Shear = new Vector2(OsuGame.SHEAR, 0); + Shear = OsuGame.SHEAR; InternalChildren = new Drawable[] { @@ -96,7 +96,7 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.X, Height = header_height, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Velocity = 0.7f, ClampAxes = Axes.Y }, @@ -111,7 +111,7 @@ namespace osu.Game.Overlays.Mods AutoSizeAxes = Axes.Y, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Padding = new MarginPadding { Horizontal = 17, diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index d36092ebed..9ba3b3774f 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -168,7 +168,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Direction = FillDirection.Horizontal, - Shear = new Vector2(OsuGame.SHEAR, 0), + Shear = OsuGame.SHEAR, RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Margin = new MarginPadding { Horizontal = 70 }, @@ -726,7 +726,7 @@ namespace osu.Game.Overlays.Mods // DrawWidth/DrawPosition do not include shear effects, and we want to know the full extents of the columns post-shear, // so we have to manually compensate. var topLeft = column.ToSpaceOfOtherDrawable(Vector2.Zero, ScrollContent); - var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * OsuGame.SHEAR, 0), ScrollContent); + var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * OsuGame.SHEAR.X, 0), ScrollContent); bool isCurrentlyVisible = Precision.AlmostBigger(topLeft.X, leftVisibleBound) && Precision.DefinitelyBigger(rightVisibleBound, bottomRight.X); diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index 284356f37e..6d48576742 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -20,7 +20,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -87,7 +86,7 @@ namespace osu.Game.Overlays.Mods Content.CornerRadius = CORNER_RADIUS; Content.BorderThickness = 2; - Shear = new Vector2(OsuGame.SHEAR, 0); + Shear = OsuGame.SHEAR; Children = new Drawable[] { @@ -128,10 +127,10 @@ namespace osu.Game.Overlays.Mods { Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Margin = new MarginPadding { - Left = -18 * OsuGame.SHEAR + Left = -18 * OsuGame.SHEAR.X }, ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`. }, @@ -139,7 +138,7 @@ namespace osu.Game.Overlays.Mods { Font = OsuFont.Default.With(size: 12), RelativeSizeAxes = Axes.X, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`. } } diff --git a/osu.Game/Overlays/Mods/RankingInformationDisplay.cs b/osu.Game/Overlays/Mods/RankingInformationDisplay.cs index 75a8f289d8..11c963f616 100644 --- a/osu.Game/Overlays/Mods/RankingInformationDisplay.cs +++ b/osu.Game/Overlays/Mods/RankingInformationDisplay.cs @@ -15,7 +15,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Utils; -using osuTK; namespace osu.Game.Overlays.Mods { @@ -52,7 +51,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, RelativeSizeAxes = Axes.Both, - Shear = new Vector2(OsuGame.SHEAR, 0), + Shear = OsuGame.SHEAR, CornerRadius = ShearedButton.CORNER_RADIUS, Masking = true, Children = new Drawable[] @@ -79,7 +78,7 @@ namespace osu.Game.Overlays.Mods { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold) } } @@ -94,7 +93,7 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.Centre, Child = counter = new EffectCounter { - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Anchor = Anchor.Centre, Origin = Anchor.Centre, Current = { BindTarget = ModMultiplier } diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs index cb0d738a2c..d15008f858 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs @@ -23,6 +23,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface { LabelText = UserInterfaceStrings.ShowConvertedBeatmaps, Current = config.GetBindable(OsuSetting.ShowConvertedBeatmaps), + Keywords = new[] { "converts", "converted" } }, new SettingsEnumDropdown { diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 39fc8b357b..01d800a351 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -54,6 +54,23 @@ namespace osu.Game.Rulesets.Scoring return result; } + /// + /// Calculates the average hit offset/error for a sequence of s, where negative numbers mean the user hit too early on average. + /// + /// + /// A non-null value if unstable rate could be calculated, + /// and if unstable rate cannot be calculated due to being empty. + /// + public static double? CalculateAverageHitError(this IEnumerable hitEvents) + { + double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).ToArray(); + + if (timeOffsets.Length == 0) + return null; + + return timeOffsets.Average(); + } + /// /// Calculates the median hit offset/error for a sequence of s, where negative numbers mean the user hit too early on average. /// diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index f75250a832..94f4ceeb1a 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -104,14 +104,14 @@ namespace osu.Game.Screens.Footer }, BackButton = new ScreenBackButton { - Margin = new MarginPadding { Bottom = 15f, Left = 12f }, + Margin = new MarginPadding { Bottom = OsuGame.SCREEN_EDGE_MARGIN, Left = OsuGame.SCREEN_EDGE_MARGIN }, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Action = onBackPressed, }, hiddenButtonsContainer = new Container { - Margin = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding }, + Margin = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding }, Y = 10f, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index 6515203ca0..5e96eadfea 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -25,16 +25,12 @@ namespace osu.Game.Screens.Footer { public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler { - private const float shear = OsuGame.SHEAR; - protected const int CORNER_RADIUS = 10; protected const int BUTTON_HEIGHT = 75; protected const int BUTTON_WIDTH = 116; public Bindable OverlayState = new Bindable(); - protected static readonly Vector2 BUTTON_SHEAR = new Vector2(shear, 0); - [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -89,7 +85,7 @@ namespace osu.Game.Screens.Footer Colour = Colour4.Black.Opacity(0.25f), Offset = new Vector2(0, 2), }, - Shear = BUTTON_SHEAR, + Shear = OsuGame.SHEAR, Masking = true, CornerRadius = CORNER_RADIUS, RelativeSizeAxes = Axes.Both, @@ -108,7 +104,7 @@ namespace osu.Game.Screens.Footer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, RelativeSizeAxes = Axes.Both, Children = new Drawable[] { @@ -135,7 +131,7 @@ namespace osu.Game.Screens.Footer }, new Container { - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, Anchor = Anchor.BottomCentre, Origin = Anchor.Centre, Y = -CORNER_RADIUS, diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index 5b423fbc6d..3ec9217aa4 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -116,7 +116,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = new Vector2(OsuGame.SHEAR, 0f), + Shear = OsuGame.SHEAR, Children = new Drawable[] { titleContainer = new Container @@ -147,7 +147,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Origin = Anchor.Centre, Text = "Today's Challenge", Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), }, } @@ -173,7 +173,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Origin = Anchor.Centre, Text = room.Name.Split(':', StringSplitOptions.TrimEntries).Last(), Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), }, } @@ -246,7 +246,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, MaxWidth = horizontal_info_size, Text = beatmap.BeatmapSet!.Metadata.GetDisplayTitleRomanisable(false), Padding = new MarginPadding { Horizontal = 5f }, @@ -257,7 +257,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Text = $"Difficulty: {beatmap.DifficultyName}", Font = OsuFont.GetFont(size: 20, italics: true), MaxWidth = horizontal_info_size, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, @@ -266,13 +266,13 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Text = $"by {beatmap.Metadata.Author.Username}", Font = OsuFont.GetFont(size: 16, italics: true), MaxWidth = horizontal_info_size, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, starRatingDisplay = new StarRatingDisplay(default) { - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Margin = new MarginPadding(5), Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -301,7 +301,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Current = { Value = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray() @@ -329,7 +329,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Origin = Anchor.Centre, FillMode = FillMode.Fit, Scale = new Vector2(1.2f), - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, }, c => { beatmapBackground.Add(c); diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs index 4736ba28db..401053599e 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -17,7 +17,7 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; -using osu.Game.Screens.SelectV2.Leaderboards; +using osu.Game.Screens.SelectV2; using osuTK; namespace osu.Game.Screens.OnlinePlay.DailyChallenge @@ -40,7 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private readonly Room room; private readonly PlaylistItem playlistItem; - private FillFlowContainer scoreFlow = null!; + private FillFlowContainer scoreFlow = null!; private Container userBestContainer = null!; private SectionHeader userBestHeader = null!; private LoadingLayer loadingLayer = null!; @@ -91,7 +91,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge new OsuScrollContainer { RelativeSizeAxes = Axes.Both, - Child = scoreFlow = new FillFlowContainer + Child = scoreFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -158,7 +158,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } else { - LoadComponentsAsync(best.Select((s, index) => new LeaderboardScoreV2(s, sheared: false) + LoadComponentsAsync(best.Select((s, index) => new BeatmapLeaderboardScore(s, sheared: false) { Rank = index + 1, IsPersonalBest = s.UserID == api.LocalUser.Value.Id, @@ -178,7 +178,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge if (userBest != null) { - userBestContainer.Add(new LeaderboardScoreV2(userBest, sheared: false) + userBestContainer.Add(new BeatmapLeaderboardScore(userBest, sheared: false) { Rank = userBest.Position, IsPersonalBest = true, diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 9e585d584d..85a87b0dff 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -31,13 +31,13 @@ using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapSet; -using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play.HUD; using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; +using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { @@ -76,6 +76,7 @@ namespace osu.Game.Screens.OnlinePlay private readonly DelayedLoadWrapper onScreenLoader; private readonly IBindable valid = new Bindable(); + private readonly IBindable completed = new Bindable(); private IBeatmapInfo? beatmap; private IRulesetInfo? ruleset; @@ -128,6 +129,7 @@ namespace osu.Game.Screens.OnlinePlay Item = item; valid.BindTo(item.Valid); + completed.BindTo(item.Completed); } [BackgroundDependencyLoader] @@ -525,9 +527,27 @@ namespace osu.Game.Screens.OnlinePlay private IEnumerable createButtons() => new[] { - beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap), + new CompletionIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Visible = { BindTarget = completed } + }, + beatmap == null + ? Empty().With(d => + { + d.Anchor = Anchor.Centre; + d.Origin = Anchor.Centre; + }) + : new PlaylistDownloadButton(beatmap) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie) { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Size = new Vector2(30, 30), Action = () => RequestResults?.Invoke(Item), Alpha = AllowShowingResults ? 1 : 0, @@ -535,13 +555,17 @@ namespace osu.Game.Screens.OnlinePlay }, editButton = new PlaylistEditButton { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Size = new Vector2(30, 30), Alpha = AllowEditing ? 1 : 0, Action = () => RequestEdit?.Invoke(Item), - TooltipText = CommonStrings.ButtonsEdit + TooltipText = Resources.Localisation.Web.CommonStrings.ButtonsEdit }, removeButton = new PlaylistRemoveButton { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Size = new Vector2(30, 30), Alpha = AllowDeletion ? 1 : 0, Action = () => RequestDeletion?.Invoke(Item), @@ -768,5 +792,64 @@ namespace osu.Game.Screens.OnlinePlay this.allowInteraction = allowInteraction; } } + + private partial class CompletionIcon : CompositeDrawable, IHasTooltip + { + public readonly BindableBool Visible = new BindableBool(); + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(16), + Masking = true, + Colour = colours.Lime0, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Scale = new Vector2(0.5f), + Colour = OsuColour.Gray(0.5f), + Icon = FontAwesome.Solid.Check + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Visible.BindValueChanged(onVisibleChanged, true); + } + + private void onVisibleChanged(ValueChangedEvent visible) + { + if (visible.NewValue) + { + Size = new Vector2(16); + Alpha = 1; + } + else + { + Size = Vector2.Zero; + Alpha = 0; + } + } + + public LocalisableString TooltipText => DrawableRoomPlaylistItemStrings.CompletedTooltip; + } } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 053e3b97af..9834598ac0 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -465,6 +465,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }); updateSetupState(); + updateUserScore(); updateGameplayState(); } @@ -480,6 +481,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists case nameof(Room.RoomID): updateSetupState(); break; + + case nameof(Room.UserScore): + updateUserScore(); + break; } } @@ -507,12 +512,32 @@ namespace osu.Game.Screens.OnlinePlay.Playlists progressSection.Alpha = room.MaxAttempts != null ? 1 : 0; drawablePlaylist.Items.ReplaceRange(0, drawablePlaylist.Items.Count, room.Playlist); + updateUserScore(); + // Select an initial item for the user to help them get into a playable state quicker. SelectedItem.Value = room.Playlist.FirstOrDefault(); }); } } + /// + /// Responds to changes in to mark playlist items as completed. + /// + private void updateUserScore() + { + if (room.UserScore == null) + return; + + if (drawablePlaylist.Items.Count == 0) + return; + + foreach (var item in room.UserScore.PlaylistItemAttempts) + { + if (item.Passed) + drawablePlaylist.Items.Single(i => i.ID == item.PlaylistItemID).MarkCompleted(); + } + } + /// /// Adjusts the rate at which the is updated. /// diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index ba572f6014..a5952f3ff3 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -14,6 +14,7 @@ using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Input.Bindings; +using osu.Game.Online.Leaderboards; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; @@ -59,6 +60,12 @@ namespace osu.Game.Screens.Play this.createScore = createScore; } + [Resolved] + private LeaderboardManager leaderboardManager { get; set; } = null!; + + private readonly IBindable globalScores = new Bindable(); + private readonly BindableList localScores = new BindableList(); + /// /// Add a settings group to the HUD overlay. Intended to be used by rulesets to add replay-specific settings. /// @@ -87,6 +94,20 @@ namespace osu.Game.Screens.Play HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); } + protected override void LoadComplete() + { + base.LoadComplete(); + + globalScores.BindTo(leaderboardManager.Scores); + globalScores.BindValueChanged(_ => + { + localScores.Clear(); + + if (globalScores.Value is LeaderboardScores g) + localScores.AddRange(g.AllScores.OrderByTotalScore()); + }, true); + } + protected override void PrepareReplay() { DrawableRuleset?.SetReplayScore(Score); @@ -97,13 +118,11 @@ namespace osu.Game.Screens.Play // Don't re-import replay scores as they're already present in the database. protected override Task ImportScore(Score score) => Task.CompletedTask; - public readonly BindableList LeaderboardScores = new BindableList(); - protected override GameplayLeaderboard CreateGameplayLeaderboard() => new SoloGameplayLeaderboard(Score.ScoreInfo.User) { AlwaysVisible = { Value = true }, - Scores = { BindTarget = LeaderboardScores } + Scores = { BindTarget = localScores } }; protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score) diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index f4cf2da364..ed5dea98cd 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -6,10 +6,12 @@ using System; using System.Diagnostics; using System.Threading.Tasks; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Online.API; +using osu.Game.Online.Leaderboards; using osu.Game.Online.Rooms; using osu.Game.Online.Solo; using osu.Game.Scoring; @@ -29,6 +31,26 @@ namespace osu.Game.Screens.Play { } + [Resolved] + private LeaderboardManager leaderboardManager { get; set; } = null!; + + private readonly IBindable globalScores = new Bindable(); + private readonly BindableList localScores = new BindableList(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + globalScores.BindTo(leaderboardManager.Scores); + globalScores.BindValueChanged(_ => + { + localScores.Clear(); + + if (globalScores.Value is LeaderboardScores g) + localScores.AddRange(g.AllScores.OrderByTotalScore()); + }, true); + } + protected override APIRequest CreateTokenRequest() { int beatmapId = Beatmap.Value.BeatmapInfo.OnlineID; @@ -43,13 +65,11 @@ namespace osu.Game.Screens.Play return new CreateSoloScoreRequest(Beatmap.Value.BeatmapInfo, rulesetId, Game.VersionHash); } - public readonly BindableList LeaderboardScores = new BindableList(); - protected override GameplayLeaderboard CreateGameplayLeaderboard() => new SoloGameplayLeaderboard(Score.ScoreInfo.User) { AlwaysVisible = { Value = false }, - Scores = { BindTarget = LeaderboardScores } + Scores = { BindTarget = localScores } }; protected override bool ShouldExitOnTokenRetrievalFailure(Exception exception) => false; @@ -59,7 +79,7 @@ namespace osu.Game.Screens.Play // Before importing a score, stop binding the leaderboard with its score source. // This avoids a case where the imported score may cause a leaderboard refresh // (if the leaderboard's source is local). - LeaderboardScores.UnbindBindings(); + globalScores.UnbindBindings(); return base.ImportScore(score); } diff --git a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs index 29df085c62..fb7107cc88 100644 --- a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs +++ b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs @@ -8,18 +8,18 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Ranking.Statistics { /// - /// Displays the average hit error statistic for a given play. + /// Displays the unstable rate statistic for a given play. /// public partial class AverageHitError : SimpleStatisticItem { /// /// Creates and computes an statistic. /// - /// Sequence of s to calculate the average hit error based on. + /// Sequence of s to calculate the unstable rate based on. public AverageHitError(IEnumerable hitEvents) : base("Average Hit Error") { - Value = hitEvents.CalculateMedianHitError(); + Value = hitEvents.CalculateAverageHitError(); } protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} ms {(value.Value < 0 ? "early" : "late")}"; diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index fd1c944689..5a09780943 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -263,7 +263,6 @@ namespace osu.Game.Screens.Select }, new BeatmapSetOnlineStatusPill { - AutoSizeAxes = Axes.Both, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Shear = -wedged_container_shear, diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 8d6fbbf256..c3ded16bd2 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -77,7 +77,6 @@ namespace osu.Game.Screens.Select.Carousel }, new BeatmapSetOnlineStatusPill { - AutoSizeAxes = Axes.Both, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, TextSize = 11, diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 46705aaa28..2896e7eab4 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -3,21 +3,17 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; -using Realms; namespace osu.Game.Screens.Select.Leaderboards { @@ -67,6 +63,8 @@ namespace osu.Game.Screens.Select.Leaderboards } } + private readonly Bindable fetchedScores = new Bindable(); + [Resolved] private IBindable ruleset { get; set; } = null!; @@ -77,14 +75,7 @@ namespace osu.Game.Screens.Select.Leaderboards private IAPIProvider api { get; set; } = null!; [Resolved] - private RulesetStore rulesets { get; set; } = null!; - - [Resolved] - private RealmAccess realm { get; set; } = null!; - - private IDisposable? scoreSubscription; - - private GetScoresRequest? scoreRetrievalRequest; + private LeaderboardManager leaderboardManager { get; set; } = null!; [BackgroundDependencyLoader] private void load() @@ -95,15 +86,13 @@ namespace osu.Game.Screens.Select.Leaderboards if (filterMods) RefetchScores(); }; + ((IBindable)fetchedScores).BindTo(leaderboardManager.Scores); } protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; protected override APIRequest? FetchScores(CancellationToken cancellationToken) { - scoreRetrievalRequest?.Cancel(); - scoreRetrievalRequest = null; - var fetchBeatmapInfo = BeatmapInfo; if (fetchBeatmapInfo == null) @@ -114,13 +103,7 @@ namespace osu.Game.Screens.Select.Leaderboards var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; - if (Scope == BeatmapLeaderboardScope.Local) - { - subscribeToLocalScores(fetchBeatmapInfo, cancellationToken); - return null; - } - - if (!api.IsLoggedIn) + if (!api.IsLoggedIn && IsOnlineScope) { SetErrorState(LeaderboardState.NotLoggedIn); return null; @@ -132,7 +115,7 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } - if (fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) + if ((fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) && IsOnlineScope) { SetErrorState(LeaderboardState.BeatmapUnavailable); return null; @@ -150,29 +133,24 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } - IReadOnlyList? requestMods = null; + leaderboardManager.FetchWithCriteriaAsync(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.ToArray() : null)) + .ContinueWith(t => + { + if (t.Exception != null && !t.IsCanceled) + { + Schedule(() => SetErrorState(LeaderboardState.NetworkFailure)); + return; + } - if (filterMods && !mods.Value.Any()) - // add nomod for the request - requestMods = new Mod[] { new ModNoMod() }; - else if (filterMods) - requestMods = mods.Value; + fetchedScores.UnbindEvents(); + fetchedScores.BindValueChanged(scores => + { + if (scores.NewValue != null) + Schedule(() => SetScores(scores.NewValue.TopScores, scores.NewValue.UserScore)); + }, true); + }, cancellationToken); - var newRequest = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods); - newRequest.Success += response => Schedule(() => - { - // Request may have changed since fetch request. - // Can't rely on request cancellation due to Schedule inside SetScores so let's play it safe. - if (!newRequest.Equals(scoreRetrievalRequest)) - return; - - SetScores( - response.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo)).OrderByTotalScore(), - response.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo) - ); - }); - - return scoreRetrievalRequest = newRequest; + return null; } protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope, Scope != BeatmapLeaderboardScope.Friend) @@ -184,59 +162,5 @@ namespace osu.Game.Screens.Select.Leaderboards { Action = () => ScoreSelected?.Invoke(model) }; - - private void subscribeToLocalScores(BeatmapInfo beatmapInfo, CancellationToken cancellationToken) - { - Debug.Assert(beatmapInfo != null); - - scoreSubscription?.Dispose(); - scoreSubscription = null; - - scoreSubscription = realm.RegisterForNotifications(r => - r.All().Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0" - + $" AND {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" - + $" AND {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1" - + $" AND {nameof(ScoreInfo.DeletePending)} == false" - , beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged); - - void localScoresChanged(IRealmCollection sender, ChangeSet? changes) - { - if (cancellationToken.IsCancellationRequested) - return; - - // This subscription may fire from changes to linked beatmaps, which we don't care about. - // It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications. - if (changes?.HasCollectionChanges() == false) - return; - - var scores = sender.AsEnumerable(); - - if (filterMods && !mods.Value.Any()) - { - // we need to filter out all scores that have any mods to get all local nomod scores - scores = scores.Where(s => !s.Mods.Any()); - } - else if (filterMods) - { - // otherwise find all the scores that have all of the currently selected mods (similar to how web applies mod filters) - // we're creating and using a string HashSet representation of selected mods so that it can be translated into the DB query itself - var selectedMods = mods.Value.Select(m => m.Acronym).ToHashSet(); - - scores = scores.Where(s => selectedMods.SetEquals(s.Mods.Select(m => m.Acronym))); - } - - scores = scores.Detach().OrderByTotalScore(); - - SetScores(scores); - } - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - scoreSubscription?.Dispose(); - scoreRetrievalRequest?.Cancel(); - } } } diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs index 045a518525..572b2427b1 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs @@ -89,7 +89,7 @@ namespace osu.Game.Screens.Select.Options Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Shear = new Vector2(0.2f, 0f), + Shear = OsuGame.SHEAR, Masking = true, EdgeEffect = new EdgeEffectParameters { diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 7b1479f392..c49b7c2ef2 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -129,17 +129,11 @@ namespace osu.Game.Screens.Select if (replayGeneratingMod != null) { - player = new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods)) - { - LeaderboardScores = { BindTarget = playBeatmapDetailArea.Leaderboard.Scores } - }; + player = new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods)); } else { - player = new SoloPlayer - { - LeaderboardScores = { BindTarget = playBeatmapDetailArea.Leaderboard.Scores } - }; + player = new SoloPlayer(); } return player; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 994b0fb6c0..9cb7d152de 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Pooling; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Select; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 8f9d5cc31b..3360437544 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs index 3cdbbb4fed..22a67321db 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Utils; diff --git a/osu.Game/Screens/SelectV2/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/SelectV2/BeatmapInfoWedgeV2.cs deleted file mode 100644 index b294896c77..0000000000 --- a/osu.Game/Screens/SelectV2/BeatmapInfoWedgeV2.cs +++ /dev/null @@ -1,330 +0,0 @@ -// 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.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets; -using osu.Game.Screens.Select; -using osuTK; - -namespace osu.Game.Screens.SelectV2 -{ - public partial class BeatmapInfoWedgeV2 : VisibilityContainer - { - public const float WEDGE_HEIGHT = 120; - private const float shear_width = 21; - private const float transition_duration = 250; - private const float corner_radius = 10; - private const float colour_bar_width = 30; - - /// Todo: move this const out to song select when more new design elements are implemented for the beatmap details area, since it applies to text alignment of various elements - private const float text_margin = 62; - - private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / WEDGE_HEIGHT, 0); - - [Resolved] - private IBindable ruleset { get; set; } = null!; - - [Resolved] - private OsuColour colours { get; set; } = null!; - - [Resolved] - private BeatmapDifficultyCache difficultyCache { get; set; } = null!; - - protected Container? DisplayedContent { get; private set; } - - protected WedgeInfoText? Info { get; private set; } - - private Container difficultyColourBar = null!; - private StarCounter starCounter = null!; - private StarRatingDisplay starRatingDisplay = null!; - private BeatmapSetOnlineStatusPill statusPill = null!; - private Container content = null!; - - private IBindable? starDifficulty; - private CancellationTokenSource? cancellationSource; - - public BeatmapInfoWedgeV2() - { - Height = WEDGE_HEIGHT; - Shear = wedged_container_shear; - Masking = true; - Margin = new MarginPadding { Left = -corner_radius }; - EdgeEffect = new EdgeEffectParameters - { - Colour = Colour4.Black.Opacity(0.2f), - Type = EdgeEffectType.Shadow, - Radius = 3, - }; - CornerRadius = corner_radius; - } - - [BackgroundDependencyLoader] - private void load() - { - Child = content = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - // These elements can't be grouped with the rest of the content, due to being present either outside or under the backgrounds area - difficultyColourBar = new Container - { - Colour = Colour4.Transparent, - Depth = float.MaxValue, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, - - // By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it. - Width = colour_bar_width + corner_radius, - Child = new Box { RelativeSizeAxes = Axes.Both } - }, - new Container - { - // Applying the shear to this container and nesting the starCounter inside avoids - // the deformation that occurs if the shear is applied to the starCounter whilst rotated - Shear = -wedged_container_shear, - X = -colour_bar_width / 2, - Anchor = Anchor.CentreRight, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Width = colour_bar_width, - Child = starCounter = new StarCounter - { - Rotation = (float)(Math.Atan(shear_width / WEDGE_HEIGHT) * (180 / Math.PI)), - Colour = Colour4.Transparent, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(0.35f), - Direction = FillDirection.Vertical - } - }, - new FillFlowContainer - { - Name = "Topright-aligned metadata", - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Top = 3, Right = colour_bar_width + 8 }, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(0, 5), - Depth = float.MinValue, - Children = new Drawable[] - { - starRatingDisplay = new StarRatingDisplay(default, animated: true) - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Shear = -wedged_container_shear, - Alpha = 0, - }, - statusPill = new BeatmapSetOnlineStatusPill - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Shear = -wedged_container_shear, - TextSize = 11, - TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, - Alpha = 0, - } - } - }, - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - ruleset.BindValueChanged(_ => updateDisplay()); - - starRatingDisplay.Current.BindValueChanged(s => - { - // use actual stars as star counter has its own animation - starCounter.Current = (float)s.NewValue.Stars; - }, true); - - starRatingDisplay.DisplayedStars.BindValueChanged(s => - { - // sync color with star rating display - starCounter.Colour = s.NewValue >= 6.5 ? colours.Orange1 : Colour4.Black.Opacity(0.75f); - difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue)); - }, true); - } - - private const double animation_duration = 600; - - protected override void PopIn() - { - this.MoveToX(0, animation_duration, Easing.OutQuint); - this.FadeIn(200, Easing.In); - } - - protected override void PopOut() - { - this.MoveToX(-150, animation_duration, Easing.OutQuint); - this.FadeOut(200, Easing.OutQuint); - } - - private WorkingBeatmap beatmap = null!; - - public WorkingBeatmap Beatmap - { - get => beatmap; - set - { - if (beatmap == value) return; - - beatmap = value; - - updateDisplay(); - } - } - - private Container? loadingInfo; - - private void updateDisplay() - { - statusPill.Status = beatmap.BeatmapInfo.Status; - - starDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token); - - starDifficulty.BindValueChanged(s => - { - starRatingDisplay.Current.Value = s.NewValue ?? default; - - starRatingDisplay.FadeIn(transition_duration); - }); - - Scheduler.AddOnce(() => - { - LoadComponentAsync(loadingInfo = new Container - { - Padding = new MarginPadding { Right = colour_bar_width }, - RelativeSizeAxes = Axes.Both, - Depth = DisplayedContent?.Depth + 1 ?? 0, - Child = new Container - { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - // TODO: New wedge design uses a coloured horizontal gradient for its background, however this lacks implementation information in the figma draft. - // pending https://www.figma.com/file/DXKwqZhD5yyb1igc3mKo1P?node-id=2980:3361#340801912 being answered. - new BeatmapInfoWedgeBackground(beatmap) { Shear = -Shear }, - Info = new WedgeInfoText(beatmap) { Shear = -Shear } - } - } - }, d => - { - // Ensure we are the most recent loaded wedge. - if (d != loadingInfo) return; - - removeOldInfo(); - content.Add(DisplayedContent = d); - }); - }); - - void removeOldInfo() - { - DisplayedContent?.FadeOut(transition_duration); - DisplayedContent?.Expire(); - DisplayedContent = null; - } - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - cancellationSource?.Cancel(); - } - - public partial class WedgeInfoText : Container - { - public OsuSpriteText TitleLabel { get; private set; } = null!; - public OsuSpriteText ArtistLabel { get; private set; } = null!; - - private readonly WorkingBeatmap working; - - public WedgeInfoText(WorkingBeatmap working) - { - this.working = working; - - RelativeSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load(SongSelect? songSelect, LocalisationManager localisation) - { - var metadata = working.Metadata; - - var titleText = new RomanisableString(metadata.TitleUnicode, metadata.Title); - var artistText = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); - - Child = new FillFlowContainer - { - Name = "Top-left aligned metadata", - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Left = text_margin, Top = 12 }, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Children = new Drawable[] - { - new OsuHoverContainer - { - AutoSizeAxes = Axes.Both, - Action = () => songSelect?.Search(titleText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)), - Child = TitleLabel = new TruncatingSpriteText - { - Shadow = true, - Text = titleText, - Font = OsuFont.TorusAlternate.With(size: 40, weight: FontWeight.SemiBold), - }, - }, - new OsuHoverContainer - { - AutoSizeAxes = Axes.Both, - Action = () => songSelect?.Search(artistText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)), - Child = ArtistLabel = new TruncatingSpriteText - { - // TODO : figma design has a diffused shadow, instead of the solid one present here, not possible currently as far as i'm aware. - Shadow = true, - Text = artistText, - // Not sure if this should be semi bold or medium - Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), - }, - }, - } - }; - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - // best effort to confine the auto-sized text to wedge bounds - // the artist label doesn't have an extra text_margin as it doesn't touch the right metadata - TitleLabel.MaxWidth = DrawWidth - text_margin * 2 - shear_width; - ArtistLabel.MaxWidth = DrawWidth - text_margin - shear_width; - } - } - } -} diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs similarity index 96% rename from osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs rename to osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index 16599a2080..c9413a9414 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -41,9 +41,9 @@ using osuTK; using osuTK.Graphics; using CommonStrings = osu.Game.Localisation.CommonStrings; -namespace osu.Game.Screens.SelectV2.Leaderboards +namespace osu.Game.Screens.SelectV2 { - public partial class LeaderboardScoreV2 : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip + public partial class BeatmapLeaderboardScore : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip { public Bindable> SelectedMods = new Bindable>(); @@ -117,12 +117,12 @@ namespace osu.Game.Screens.SelectV2.Leaderboards public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(); public virtual ScoreInfo TooltipContent => score; - public LeaderboardScoreV2(ScoreInfo score, bool sheared = true) + public BeatmapLeaderboardScore(ScoreInfo score, bool sheared = true) { this.score = score; this.sheared = sheared; - Shear = new Vector2(sheared ? OsuGame.SHEAR : 0, 0); + Shear = sheared ? OsuGame.SHEAR : Vector2.Zero; RelativeSizeAxes = Axes.X; Height = height; } @@ -255,7 +255,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { RelativeSizeAxes = Axes.Both, User = score.User, - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.FromHex(@"222A27").Opacity(1)), @@ -286,7 +286,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1.1f), - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, RelativeSizeAxes = Axes.Both, }) { @@ -326,7 +326,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { flagBadgeAndDateContainer = new FillFlowContainer { - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Direction = FillDirection.Horizontal, Spacing = new Vector2(5), AutoSizeAxes = Axes.Both, @@ -356,7 +356,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards nameLabel = new TruncatingSpriteText { RelativeSizeAxes = Axes.X, - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Text = user.Username, Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold) } @@ -372,7 +372,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Name = @"Statistics container", Padding = new MarginPadding { Right = 40 }, Spacing = new Vector2(25, 0), - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, @@ -430,7 +430,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards }, RankContainer = new Container { - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Y, @@ -488,7 +488,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Anchor = Anchor.TopRight, Origin = Anchor.TopRight, UseFullGlyphHeight = false, - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Current = scoreManager.GetBindableTotalScoreString(score), Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), }, @@ -496,7 +496,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(2f, 0f), @@ -704,7 +704,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Child = new OsuSpriteText { - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold, italics: true), diff --git a/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs b/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs deleted file mode 100644 index fb2e32dfdc..0000000000 --- a/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs +++ /dev/null @@ -1,195 +0,0 @@ -// 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.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.LocalisationExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; -using osu.Framework.Localisation; -using osu.Game.Beatmaps; -using osu.Game.Collections; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Localisation; -using osu.Game.Overlays; -using osuTK; -using osuTK.Graphics; -using osuTK.Input; -using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; - -namespace osu.Game.Screens.SelectV2.Footer -{ - public partial class BeatmapOptionsPopover : OsuPopover - { - private FillFlowContainer buttonFlow = null!; - private readonly ScreenFooterButtonOptions footerButton; - - [Cached] - private readonly OverlayColourProvider colourProvider; - - private WorkingBeatmap beatmapWhenOpening = null!; - - [Resolved] - private IBindable beatmap { get; set; } = null!; - - public BeatmapOptionsPopover(ScreenFooterButtonOptions footerButton, OverlayColourProvider colourProvider) - { - this.footerButton = footerButton; - this.colourProvider = colourProvider; - } - - [BackgroundDependencyLoader] - private void load(ManageCollectionsDialog? manageCollectionsDialog, OsuColour colours, BeatmapManager? beatmapManager) - { - Content.Padding = new MarginPadding(5); - - Child = buttonFlow = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(3), - }; - - beatmapWhenOpening = beatmap.Value; - - addHeader(CommonStrings.General); - addButton(SongSelectStrings.ManageCollections, FontAwesome.Solid.Book, () => manageCollectionsDialog?.Show()); - - addHeader(SongSelectStrings.ForAllDifficulties, beatmapWhenOpening.BeatmapSetInfo.ToString()); - addButton(SongSelectStrings.DeleteBeatmap, FontAwesome.Solid.Trash, () => { }, colours.Red1); // songSelect?.DeleteBeatmap(beatmapWhenOpening.BeatmapSetInfo); - - addHeader(SongSelectStrings.ForSelectedDifficulty, beatmapWhenOpening.BeatmapInfo.DifficultyName); - // TODO: make work, and make show "unplayed" or "played" based on status. - addButton(SongSelectStrings.MarkAsPlayed, FontAwesome.Regular.TimesCircle, null); - addButton(SongSelectStrings.ClearAllLocalScores, FontAwesome.Solid.Eraser, () => { }, colours.Red1); // songSelect?.ClearScores(beatmapWhenOpening.BeatmapInfo); - - // if (songSelect != null && songSelect.AllowEditing) - addButton(SongSelectStrings.EditBeatmap, FontAwesome.Solid.PencilAlt, () => { }); // songSelect.Edit(beatmapWhenOpening.BeatmapInfo); - - addButton(WebCommonStrings.ButtonsHide.ToSentence(), FontAwesome.Solid.Magic, () => beatmapManager?.Hide(beatmapWhenOpening.BeatmapInfo)); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - ScheduleAfterChildren(() => GetContainingFocusManager()!.ChangeFocus(this)); - - beatmap.BindValueChanged(_ => Hide()); - } - - private void addHeader(LocalisableString text, string? context = null) - { - var textFlow = new OsuTextFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Padding = new MarginPadding(10), - }; - - textFlow.AddText(text, t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold)); - - if (context != null) - { - textFlow.NewLine(); - textFlow.AddText(context, t => - { - t.Colour = colourProvider.Content2; - t.Font = t.Font.With(size: 13); - }); - } - - buttonFlow.Add(textFlow); - } - - private void addButton(LocalisableString text, IconUsage icon, Action? action, Color4? colour = null) - { - var button = new OptionButton - { - Text = text, - Icon = icon, - TextColour = colour, - Action = () => - { - Scheduler.AddDelayed(Hide, 50); - action?.Invoke(); - }, - }; - - buttonFlow.Add(button); - } - - private partial class OptionButton : OsuButton - { - public IconUsage Icon { get; init; } - public Color4? TextColour { get; init; } - - public OptionButton() - { - Size = new Vector2(265, 50); - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - BackgroundColour = colourProvider.Background3; - - SpriteText.Colour = TextColour ?? Color4.White; - Content.CornerRadius = 10; - - Add(new SpriteIcon - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(17), - X = 15, - Icon = Icon, - Colour = TextColour ?? Color4.White, - }); - } - - protected override SpriteText CreateText() => new OsuSpriteText - { - Depth = -1, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - X = 40 - }; - } - - protected override bool OnKeyDown(KeyDownEvent e) - { - // don't absorb control as ToolbarRulesetSelector uses control + number to navigate - if (e.ControlPressed) return false; - - if (!e.Repeat && e.Key >= Key.Number1 && e.Key <= Key.Number9) - { - int requested = e.Key - Key.Number1; - - OptionButton? found = buttonFlow.Children.OfType().ElementAtOrDefault(requested); - - if (found != null) - { - found.TriggerClick(); - return true; - } - } - - return base.OnKeyDown(e); - } - - protected override void UpdateState(ValueChangedEvent state) - { - base.UpdateState(state); - footerButton.OverlayState.Value = state.NewValue; - } - } -} diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs b/osu.Game/Screens/SelectV2/FooterButtonMods.cs similarity index 96% rename from osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs rename to osu.Game/Screens/SelectV2/FooterButtonMods.cs index 869aef1470..833ea96139 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonMods.cs @@ -28,9 +28,9 @@ using osu.Game.Utils; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.SelectV2.Footer +namespace osu.Game.Screens.SelectV2 { - public partial class ScreenFooterButtonMods : ScreenFooterButton, IHasCurrentValue> + public partial class FooterButtonMods : ScreenFooterButton, IHasCurrentValue> { private const float bar_height = 30f; private const float mod_display_portion = 0.65f; @@ -58,7 +58,7 @@ namespace osu.Game.Screens.SelectV2.Footer [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public ScreenFooterButtonMods(ModSelectOverlay overlay) + public FooterButtonMods(ModSelectOverlay overlay) : base(overlay) { } @@ -78,7 +78,7 @@ namespace osu.Game.Screens.SelectV2.Footer Y = -5f, Depth = float.MaxValue, Origin = Anchor.BottomLeft, - Shear = BUTTON_SHEAR, + Shear = OsuGame.SHEAR, CornerRadius = CORNER_RADIUS, Size = new Vector2(BUTTON_WIDTH, bar_height), Masking = true, @@ -108,7 +108,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, UseFullGlyphHeight = false, Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold) } @@ -130,7 +130,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, Scale = new Vector2(0.5f), Current = { BindTarget = Current }, ExpansionMode = ExpansionMode.AlwaysContracted, @@ -139,7 +139,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold), Mods = { BindTarget = Current }, } @@ -305,7 +305,7 @@ namespace osu.Game.Screens.SelectV2.Footer Y = -5f; Depth = float.MaxValue; Origin = Anchor.BottomLeft; - Shear = BUTTON_SHEAR; + Shear = OsuGame.SHEAR; CornerRadius = CORNER_RADIUS; AutoSizeAxes = Axes.X; Height = bar_height; @@ -329,7 +329,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, Text = ModSelectOverlayStrings.Unranked.ToUpper(), Margin = new MarginPadding { Horizontal = 15 }, UseFullGlyphHeight = false, diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonOptions.cs b/osu.Game/Screens/SelectV2/FooterButtonOptions.cs similarity index 76% rename from osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonOptions.cs rename to osu.Game/Screens/SelectV2/FooterButtonOptions.cs index 72409b5566..41edaf2a02 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonOptions.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonOptions.cs @@ -5,15 +5,14 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Screens.Footer; -namespace osu.Game.Screens.SelectV2.Footer +namespace osu.Game.Screens.SelectV2 { - public partial class ScreenFooterButtonOptions : ScreenFooterButton, IHasPopover + public partial class FooterButtonOptions : ScreenFooterButton, IHasPopover { [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -29,6 +28,6 @@ namespace osu.Game.Screens.SelectV2.Footer Action = this.ShowPopover; } - public Popover GetPopover() => new BeatmapOptionsPopover(this, colourProvider); + public Framework.Graphics.UserInterface.Popover GetPopover() => new Popover(this, colourProvider); } } diff --git a/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs b/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs new file mode 100644 index 0000000000..76b841ee99 --- /dev/null +++ b/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs @@ -0,0 +1,198 @@ +// 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.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Collections; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; +using osuTK.Input; +using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class FooterButtonOptions + { + public partial class Popover : OsuPopover + { + private FillFlowContainer buttonFlow = null!; + private readonly FooterButtonOptions footerButton; + + [Cached] + private readonly OverlayColourProvider colourProvider; + + private WorkingBeatmap beatmapWhenOpening = null!; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + public Popover(FooterButtonOptions footerButton, OverlayColourProvider colourProvider) + { + this.footerButton = footerButton; + this.colourProvider = colourProvider; + } + + [BackgroundDependencyLoader] + private void load(ManageCollectionsDialog? manageCollectionsDialog, OsuColour colours, BeatmapManager? beatmapManager) + { + Content.Padding = new MarginPadding(5); + + Child = buttonFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(3), + }; + + beatmapWhenOpening = beatmap.Value; + + addHeader(CommonStrings.General); + addButton(SongSelectStrings.ManageCollections, FontAwesome.Solid.Book, () => manageCollectionsDialog?.Show()); + + addHeader(SongSelectStrings.ForAllDifficulties, beatmapWhenOpening.BeatmapSetInfo.ToString()); + addButton(SongSelectStrings.DeleteBeatmap, FontAwesome.Solid.Trash, () => { }, colours.Red1); // songSelect?.DeleteBeatmap(beatmapWhenOpening.BeatmapSetInfo); + + addHeader(SongSelectStrings.ForSelectedDifficulty, beatmapWhenOpening.BeatmapInfo.DifficultyName); + // TODO: make work, and make show "unplayed" or "played" based on status. + addButton(SongSelectStrings.MarkAsPlayed, FontAwesome.Regular.TimesCircle, null); + addButton(SongSelectStrings.ClearAllLocalScores, FontAwesome.Solid.Eraser, () => { }, colours.Red1); // songSelect?.ClearScores(beatmapWhenOpening.BeatmapInfo); + + // if (songSelect != null && songSelect.AllowEditing) + addButton(SongSelectStrings.EditBeatmap, FontAwesome.Solid.PencilAlt, () => { }); // songSelect.Edit(beatmapWhenOpening.BeatmapInfo); + + addButton(WebCommonStrings.ButtonsHide.ToSentence(), FontAwesome.Solid.Magic, () => beatmapManager?.Hide(beatmapWhenOpening.BeatmapInfo)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ScheduleAfterChildren(() => GetContainingFocusManager()!.ChangeFocus(this)); + + beatmap.BindValueChanged(_ => Hide()); + } + + private void addHeader(LocalisableString text, string? context = null) + { + var textFlow = new OsuTextFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Padding = new MarginPadding(10), + }; + + textFlow.AddText(text, t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold)); + + if (context != null) + { + textFlow.NewLine(); + textFlow.AddText(context, t => + { + t.Colour = colourProvider.Content2; + t.Font = t.Font.With(size: 13); + }); + } + + buttonFlow.Add(textFlow); + } + + private void addButton(LocalisableString text, IconUsage icon, Action? action, Color4? colour = null) + { + var button = new OptionButton + { + Text = text, + Icon = icon, + TextColour = colour, + Action = () => + { + Scheduler.AddDelayed(Hide, 50); + action?.Invoke(); + }, + }; + + buttonFlow.Add(button); + } + + private partial class OptionButton : OsuButton + { + public IconUsage Icon { get; init; } + public Color4? TextColour { get; init; } + + public OptionButton() + { + Size = new Vector2(265, 50); + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + BackgroundColour = colourProvider.Background3; + + SpriteText.Colour = TextColour ?? Color4.White; + Content.CornerRadius = 10; + + Add(new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(17), + X = 15, + Icon = Icon, + Colour = TextColour ?? Color4.White, + }); + } + + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + X = 40 + }; + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + // don't absorb control as ToolbarRulesetSelector uses control + number to navigate + if (e.ControlPressed) return false; + + if (!e.Repeat && e.Key >= Key.Number1 && e.Key <= Key.Number9) + { + int requested = e.Key - Key.Number1; + + OptionButton? found = buttonFlow.Children.OfType().ElementAtOrDefault(requested); + + if (found != null) + { + found.TriggerClick(); + return true; + } + } + + return base.OnKeyDown(e); + } + + protected override void UpdateState(ValueChangedEvent state) + { + base.UpdateState(state); + footerButton.OverlayState.Value = state.NewValue; + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonRandom.cs b/osu.Game/Screens/SelectV2/FooterButtonRandom.cs similarity index 97% rename from osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonRandom.cs rename to osu.Game/Screens/SelectV2/FooterButtonRandom.cs index dbdb6fe79b..88b139da97 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonRandom.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonRandom.cs @@ -14,9 +14,9 @@ using osu.Game.Screens.Footer; using osuTK; using osuTK.Input; -namespace osu.Game.Screens.SelectV2.Footer +namespace osu.Game.Screens.SelectV2 { - public partial class ScreenFooterButtonRandom : ScreenFooterButton + public partial class FooterButtonRandom : ScreenFooterButton { public Action? NextRandom { get; set; } public Action? PreviousRandom { get; set; } diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/Panel.cs similarity index 98% rename from osu.Game/Screens/SelectV2/PanelBase.cs rename to osu.Game/Screens/SelectV2/Panel.cs index 05a1a55c03..c22a88a55f 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/Panel.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Graphics; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; @@ -19,7 +20,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public abstract partial class PanelBase : PoolableDrawable, ICarouselPanel + public abstract partial class Panel : PoolableDrawable, ICarouselPanel { private const float corner_radius = 10; @@ -159,7 +160,7 @@ namespace osu.Game.Screens.SelectV2 keyboardSelectionLayer = new Box { Alpha = 0, - Colour = colours.Yellow.Opacity(0.1f), + Colour = colourProvider.Highlight1.Opacity(0.1f), Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, }, diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index d4bf3519fa..6742577389 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -22,7 +23,7 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class PanelBeatmap : PanelBase + public partial class PanelBeatmap : Panel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; @@ -30,7 +31,7 @@ namespace osu.Game.Screens.SelectV2 private ConstrainedIconContainer difficultyIcon = null!; private OsuSpriteText keyCountText = null!; private StarRatingDisplay starRatingDisplay = null!; - private TopLocalRank difficultyRank = null!; + private PanelLocalRankDisplay localRank = null!; private OsuSpriteText difficultyText = null!; private OsuSpriteText authorText = null!; @@ -100,7 +101,7 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - difficultyRank = new TopLocalRank + localRank = new PanelLocalRankDisplay { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -174,7 +175,7 @@ namespace osu.Game.Screens.SelectV2 difficultyIcon.Icon = beatmap.Ruleset.CreateInstance().CreateIcon(); - difficultyRank.Beatmap = beatmap; + localRank.Beatmap = beatmap; difficultyText.Text = beatmap.DifficultyName; authorText.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username); @@ -186,7 +187,7 @@ namespace osu.Game.Screens.SelectV2 { base.FreeAfterUse(); - difficultyRank.Beatmap = null; + localRank.Beatmap = null; starDifficultyBindable = null; } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index c599c3e534..179d4d6444 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -11,22 +11,23 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class PanelBeatmapSet : PanelBase + public partial class PanelBeatmapSet : Panel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - private BeatmapSetPanelBackground background = null!; + private PanelSetBackground background = null!; private OsuSpriteText titleText = null!; private OsuSpriteText artistText = null!; private Drawable chevronIcon = null!; - private UpdateBeatmapSetButton updateButton = null!; + private PanelUpdateBeatmapButton updateButton = null!; private BeatmapSetOnlineStatusPill statusPill = null!; private DifficultySpectrumDisplay difficultiesDisplay = null!; @@ -60,7 +61,7 @@ namespace osu.Game.Screens.SelectV2 }, }; - Background = background = new BeatmapSetPanelBackground + Background = background = new PanelSetBackground { RelativeSizeAxes = Axes.Both, }; @@ -89,7 +90,7 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Top = 5f }, Children = new Drawable[] { - updateButton = new UpdateBeatmapSetButton + updateButton = new PanelUpdateBeatmapButton { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -97,7 +98,6 @@ namespace osu.Game.Screens.SelectV2 }, statusPill = new BeatmapSetOnlineStatusPill { - AutoSizeAxes = Axes.Both, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, TextSize = 11, diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index 948311a86e..a0d7484587 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -13,6 +13,7 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; @@ -23,7 +24,7 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class PanelBeatmapStandalone : PanelBase + public partial class PanelBeatmapStandalone : Panel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; @@ -48,17 +49,17 @@ namespace osu.Game.Screens.SelectV2 private IBindable? starDifficultyBindable; private CancellationTokenSource? starDifficultyCancellationSource; - private BeatmapSetPanelBackground background = null!; + private PanelSetBackground background = null!; private OsuSpriteText titleText = null!; private OsuSpriteText artistText = null!; - private UpdateBeatmapSetButton updateButton = null!; + private PanelUpdateBeatmapButton updateButton = null!; private BeatmapSetOnlineStatusPill statusPill = null!; private ConstrainedIconContainer difficultyIcon = null!; private FillFlowContainer difficultyLine = null!; private StarRatingDisplay difficultyStarRating = null!; - private TopLocalRank difficultyRank = null!; + private PanelLocalRankDisplay difficultyRank = null!; private OsuSpriteText difficultyKeyCountText = null!; private OsuSpriteText difficultyName = null!; private OsuSpriteText difficultyAuthor = null!; @@ -80,7 +81,7 @@ namespace osu.Game.Screens.SelectV2 Colour = colourProvider.Background5, }; - Background = background = new BeatmapSetPanelBackground + Background = background = new PanelSetBackground { RelativeSizeAxes = Axes.Both, }; @@ -109,7 +110,7 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Top = 5f }, Children = new Drawable[] { - updateButton = new UpdateBeatmapSetButton + updateButton = new PanelUpdateBeatmapButton { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -117,7 +118,6 @@ namespace osu.Game.Screens.SelectV2 }, statusPill = new BeatmapSetOnlineStatusPill { - AutoSizeAxes = Axes.Both, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, TextSize = 11, @@ -137,7 +137,7 @@ namespace osu.Game.Screens.SelectV2 Scale = new Vector2(8f / 9f), Margin = new MarginPadding { Right = 5f }, }, - difficultyRank = new TopLocalRank + difficultyRank = new PanelLocalRankDisplay { Scale = new Vector2(8f / 11), Origin = Anchor.CentreLeft, diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index ecb64f4797..ac4857d2f3 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -5,10 +5,13 @@ using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; @@ -16,31 +19,60 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class PanelGroup : PanelBase + public partial class PanelGroup : Panel { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.2f; - private Drawable chevronIcon = null!; + private Drawable iconContainer = null!; private OsuSpriteText titleText = null!; + private TrianglesV2 triangles = null!; + private Box glow = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load() { Height = HEIGHT; - Icon = chevronIcon = new SpriteIcon + Icon = iconContainer = new Container { AlwaysPresent = true, - Icon = FontAwesome.Solid.ChevronDown, - Size = new Vector2(12), - Margin = new MarginPadding { Horizontal = 5f }, - X = 2f, - Colour = colourProvider.Background3, + RelativeSizeAxes = Axes.Y, + Child = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + Colour = colourProvider.Background3, + }, }; - Background = new Box + Background = new Container { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Dark1, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + triangles = new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + Thickness = 0.02f, + SpawnRatio = 0.6f, + Colour = ColourInfo.GradientHorizontal(colourProvider.Background6, colourProvider.Background5) + }, + glow = new Box + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Colour = ColourInfo.GradientHorizontal(colourProvider.Highlight1, colourProvider.Highlight1.Opacity(0f)), + }, + }, }; AccentColour = colourProvider.Highlight1; Content.Children = new Drawable[] @@ -49,6 +81,8 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + Font = OsuFont.Style.Heading2, + UseFullGlyphHeight = false, X = 10f, }, new CircularContainer @@ -69,13 +103,13 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), // TODO: requires Carousel/CarouselItem-side implementation Text = "43", UseFullGlyphHeight = false, } }, - } + }, }; } @@ -90,8 +124,15 @@ namespace osu.Game.Screens.SelectV2 { const float duration = 500; - chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); - chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + iconContainer.ResizeWidthTo(Expanded.Value ? 20f : 5f, duration, Easing.OutQuint); + iconContainer.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + + ColourInfo colour = Expanded.Value + ? ColourInfo.GradientHorizontal(colourProvider.Highlight1.Opacity(0.25f), colourProvider.Highlight1.Opacity(0f)) + : ColourInfo.GradientHorizontal(colourProvider.Background6, colourProvider.Background5); + + triangles.FadeColour(colour, duration, Easing.OutQuint); + glow.FadeTo(Expanded.Value ? 0.4f : 0, duration, Easing.OutQuint); } protected override void PrepareForUse() diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs index 0dc5a2f365..4ef3bd724c 100644 --- a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -5,21 +5,20 @@ using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class PanelGroupStarDifficulty : PanelBase + public partial class PanelGroupStarDifficulty : Panel { [Resolved] private OsuColour colours { get; set; } = null!; @@ -27,28 +26,50 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - private Drawable chevronIcon = null!; + private Drawable iconContainer = null!; private Box contentBackground = null!; - private StarRatingDisplay starRatingDisplay = null!; - private StarCounter starCounter = null!; + private OsuSpriteText starRatingText = null!; + private TrianglesV2 triangles = null!; + private Box glow = null!; [BackgroundDependencyLoader] private void load() { Height = PanelGroup.HEIGHT; - Icon = chevronIcon = new SpriteIcon + Icon = iconContainer = new Container { AlwaysPresent = true, - Icon = FontAwesome.Solid.ChevronDown, - Size = new Vector2(12), - Margin = new MarginPadding { Horizontal = 5f }, - X = 2f, + RelativeSizeAxes = Axes.Y, + Child = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + }, }; - Background = contentBackground = new Box + Background = new Container { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Dark1, + Children = new Drawable[] + { + contentBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + triangles = new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + Thickness = 0.02f, + SpawnRatio = 0.6f, + }, + glow = new Box + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + }, + }, }; AccentColour = colourProvider.Highlight1; Content.Children = new Drawable[] @@ -62,17 +83,13 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Left = 10f }, Children = new Drawable[] { - starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + starRatingText = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - }, - starCounter = new StarCounter - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Scale = new Vector2(8f / 20f), - }, + UseFullGlyphHeight = false, + Font = OsuFont.Style.Heading2, + } } }, new CircularContainer @@ -93,7 +110,7 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), // TODO: requires Carousel/CarouselItem-side implementation Text = "43", UseFullGlyphHeight = false, @@ -110,6 +127,8 @@ namespace osu.Game.Screens.SelectV2 Expanded.BindValueChanged(_ => onExpanded(), true); } + private Color4 ratingColour; + protected override void PrepareForUse() { base.PrepareForUse(); @@ -118,25 +137,50 @@ namespace osu.Game.Screens.SelectV2 int starNumber = (int)((GroupDefinition)Item.Model).Data; - Color4 colour = starNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(starNumber); - Color4 contentColour = starNumber >= 7 ? colours.Orange1 : colourProvider.Background5; + ratingColour = starNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(starNumber); - AccentColour = colour; - contentBackground.Colour = colour.Darken(0.3f); + AccentColour = ratingColour; + contentBackground.Colour = ratingColour.Darken(1f); + glow.Colour = ColourInfo.GradientHorizontal(ratingColour, ratingColour.Opacity(0f)); - starRatingDisplay.Current.Value = new StarDifficulty(starNumber, 0); - starCounter.Current = starNumber; + switch (starNumber) + { + case 0: + starRatingText.Text = @"Below 1 Star"; + break; - chevronIcon.Colour = contentColour; - starCounter.Colour = contentColour; + case 1: + starRatingText.Text = @"1 Star"; + break; + + default: + starRatingText.Text = $"{starNumber} Stars"; + break; + } + + iconContainer.Colour = starNumber >= 7 ? colourProvider.Content1 : colourProvider.Background5; + starRatingText.Colour = colourProvider.Content1; + + ColourInfo colour; + + if (starNumber >= 8) + colour = ColourInfo.GradientHorizontal(ratingColour, ratingColour.Darken(0.2f)); + else + colour = ColourInfo.GradientHorizontal(ratingColour.Darken(0.6f), ratingColour.Darken(0.8f)); + + triangles.Colour = colour; + + onExpanded(); } private void onExpanded() { const float duration = 500; - chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); - chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + iconContainer.ResizeWidthTo(Expanded.Value ? 20f : 5f, duration, Easing.OutQuint); + iconContainer.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + + glow.FadeTo(Expanded.Value ? 0.4f : 0, duration, Easing.OutQuint); } } } diff --git a/osu.Game/Screens/SelectV2/TopLocalRank.cs b/osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs similarity index 96% rename from osu.Game/Screens/SelectV2/TopLocalRank.cs rename to osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs index 2a72a05db7..588e7e650e 100644 --- a/osu.Game/Screens/SelectV2/TopLocalRank.cs +++ b/osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs @@ -19,7 +19,7 @@ using Realms; namespace osu.Game.Screens.SelectV2 { - public partial class TopLocalRank : CompositeDrawable + public partial class PanelLocalRankDisplay : CompositeDrawable { private BeatmapInfo? beatmap; @@ -48,7 +48,7 @@ namespace osu.Game.Screens.SelectV2 private readonly UpdateableRank updateable; - public TopLocalRank(BeatmapInfo? beatmap = null) + public PanelLocalRankDisplay(BeatmapInfo? beatmap = null) { AutoSizeAxes = Axes.Both; diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs b/osu.Game/Screens/SelectV2/PanelSetBackground.cs similarity index 97% rename from osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs rename to osu.Game/Screens/SelectV2/PanelSetBackground.cs index 798acf62ee..99dbf90556 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs +++ b/osu.Game/Screens/SelectV2/PanelSetBackground.cs @@ -14,7 +14,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class BeatmapSetPanelBackground : ModelBackedDrawable + public partial class PanelSetBackground : ModelBackedDrawable { protected override double TransformDuration => 400; diff --git a/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs b/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs similarity index 96% rename from osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs rename to osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs index e2c841f88a..2a850321a6 100644 --- a/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs +++ b/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs @@ -22,7 +22,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class UpdateBeatmapSetButton : OsuAnimatedButton + public partial class PanelUpdateBeatmapButton : OsuAnimatedButton { private BeatmapSetInfo? beatmapSet; @@ -53,7 +53,7 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IDialogOverlay? dialogOverlay { get; set; } - public UpdateBeatmapSetButton() + public PanelUpdateBeatmapButton() { Size = new Vector2(75f, 22f); } @@ -69,7 +69,7 @@ namespace osu.Game.Screens.SelectV2 Content.Anchor = Anchor.Centre; Content.Origin = Anchor.Centre; - Content.Shear = new Vector2(OsuGame.SHEAR, 0); + Content.Shear = OsuGame.SHEAR; Content.AddRange(new Drawable[] { @@ -87,7 +87,7 @@ namespace osu.Game.Screens.SelectV2 AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(4), - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Children = new Drawable[] { new Container diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index e295656a21..67ca110dab 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -11,7 +11,6 @@ using osu.Game.Overlays.Mods; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.Select; -using osu.Game.Screens.SelectV2.Footer; namespace osu.Game.Screens.SelectV2 { @@ -77,9 +76,9 @@ namespace osu.Game.Screens.SelectV2 public override IReadOnlyList CreateFooterButtons() => new ScreenFooterButton[] { - new ScreenFooterButtonMods(modSelectOverlay) { Current = Mods }, - new ScreenFooterButtonRandom(), - new ScreenFooterButtonOptions(), + new FooterButtonMods(modSelectOverlay) { Current = Mods }, + new FooterButtonRandom(), + new FooterButtonOptions(), }; protected override void LoadComplete() diff --git a/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs b/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs deleted file mode 100644 index 4a3dc34cf9..0000000000 --- a/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs +++ /dev/null @@ -1,88 +0,0 @@ -// 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 osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Localisation; -using osu.Game.Overlays; - -namespace osu.Game.Screens.SelectV2.Wedge -{ - public abstract partial class DifficultyNameContent : CompositeDrawable - { - protected OsuSpriteText DifficultyName = null!; - private OsuSpriteText mappedByLabel = null!; - protected OsuHoverContainer MapperLink = null!; - protected OsuSpriteText MapperName = null!; - - protected DifficultyNameContent() - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - } - - [BackgroundDependencyLoader] - private void load() - { - InternalChild = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - DifficultyName = new TruncatingSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - }, - mappedByLabel = new OsuSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - // TODO: better null display? beatmap carousel panels also just show this text currently. - Text = " mapped by ", - Font = OsuFont.GetFont(size: 14), - }, - // This is not a `LinkFlowContainer` as there are single-frame layout issues when Update() - // is being used for layout, see https://github.com/ppy/osu-framework/issues/3369. - MapperLink = new MapperLinkContainer - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - AutoSizeAxes = Axes.Both, - Child = MapperName = new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 14), - } - }, - } - }; - } - - protected override void Update() - { - base.Update(); - - // truncate difficulty name when width exceeds bounds, prioritizing mapper name display - DifficultyName.MaxWidth = Math.Max(DrawWidth - mappedByLabel.DrawWidth - - MapperName.DrawWidth, 0); - } - - private partial class MapperLinkContainer : OsuHoverContainer - { - [BackgroundDependencyLoader] - private void load(OverlayColourProvider? overlayColourProvider, OsuColour colours) - { - TooltipText = ContextMenuStrings.ViewProfile; - IdleColour = overlayColourProvider?.Light2 ?? colours.Blue; - } - } - } -} diff --git a/osu.Game/Screens/SelectV2/Wedge/LocalDifficultyNameContent.cs b/osu.Game/Screens/SelectV2/Wedge/LocalDifficultyNameContent.cs deleted file mode 100644 index 66f8cb02b2..0000000000 --- a/osu.Game/Screens/SelectV2/Wedge/LocalDifficultyNameContent.cs +++ /dev/null @@ -1,34 +0,0 @@ -// 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.Game.Beatmaps; -using osu.Game.Online; -using osu.Game.Online.Chat; - -namespace osu.Game.Screens.SelectV2.Wedge -{ - public partial class LocalDifficultyNameContent : DifficultyNameContent - { - [Resolved] - private IBindable beatmap { get; set; } = null!; - - [Resolved] - private ILinkHandler? linkHandler { get; set; } - - protected override void LoadComplete() - { - base.LoadComplete(); - - beatmap.BindValueChanged(b => - { - DifficultyName.Text = b.NewValue.BeatmapInfo.DifficultyName; - - // TODO: should be the mapper of the guest difficulty, but that isn't stored correctly yet (see https://github.com/ppy/osu/issues/12965) - MapperName.Text = b.NewValue.Metadata.Author.Username; - MapperLink.Action = () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, b.NewValue.Metadata.Author)); - }, true); - } - } -}