diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 0b370cf911..0de2060e2d 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps switch (obj) { - case IHasCurve curveData: + case IHasPathWithRepeats curveData: return new JuiceStream { StartTime = obj.StartTime, diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 7c8d57e689..2c96ee2b19 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Catch.Objects { - public class JuiceStream : CatchHitObject, IHasCurve + public class JuiceStream : CatchHitObject, IHasPathWithRepeats { /// /// Positional distance that results in a duration of one second, before any speed adjustments. diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-left.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-left.png new file mode 100644 index 0000000000..03ca371c4e Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-left.png differ diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-right.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-right.png new file mode 100644 index 0000000000..45b7be0255 Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-right.png differ diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index d8d5b67c0e..1bd796511b 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -474,7 +474,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// private IList sampleInfoListAt(double time) { - if (!(HitObject is IHasCurve curveData)) + if (!(HitObject is IHasPathWithRepeats curveData)) return HitObject.Samples; double segmentTime = (EndTime - HitObject.StartTime) / spanCount; diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs index 7680526ac4..f177284399 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs @@ -52,10 +52,10 @@ namespace osu.Game.Rulesets.Mania.Skinning base.Update(); if (leftSprite?.Height > 0) - leftSprite.Scale = new Vector2(DrawHeight / leftSprite.Height); + leftSprite.Scale = new Vector2(1, DrawHeight / leftSprite.Height); if (rightSprite?.Height > 0) - rightSprite.Scale = new Vector2(DrawHeight / rightSprite.Height); + rightSprite.Scale = new Vector2(1, DrawHeight / rightSprite.Height); } } } diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index 3490d96b61..fcad356a1c 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps switch (original) { - case IHasCurve curveData: + case IHasPathWithRepeats curveData: return new Slider { StartTime = original.StartTime, diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 984a9bf956..705e88040f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -17,7 +17,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { - public class Slider : OsuHitObject, IHasCurve + public class Slider : OsuHitObject, IHasPathWithRepeats { public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 1de7d488f3..79a6ea7e92 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { - internal class OsuScoreProcessor : ScoreProcessor + public class OsuScoreProcessor : ScoreProcessor { protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement); diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index a4ec7211ca..78550ed270 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength) { - List> allSamples = obj is IHasCurve curveData ? curveData.NodeSamples : new List>(new[] { samples }); + List> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List>(new[] { samples }); int i = 0; diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 7b11bce520..5f52160be1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -3,9 +3,7 @@ using osu.Game.Rulesets.Objects.Types; using System; -using System.Collections.Generic; using System.Threading; -using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; @@ -17,7 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.Taiko.Objects { - public class DrumRoll : TaikoHitObject, IHasCurve + public class DrumRoll : TaikoHitObject, IHasPath { /// /// Drum roll distance that results in a duration of 1 speed-adjusted beat length. @@ -115,11 +113,7 @@ namespace osu.Game.Rulesets.Taiko.Objects double IHasDistance.Distance => Duration * Velocity; - int IHasRepeats.RepeatCount { get => 0; set { } } - - List> IHasRepeats.NodeSamples => new List>(); - - SliderPath IHasCurve.Path + SliderPath IHasPath.Path => new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER); #endregion diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index acb30a6277..dab923d75b 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -365,7 +365,7 @@ namespace osu.Game.Tests.Beatmaps.Formats { var hitObjects = decoder.Decode(stream).HitObjects; - var curveData = hitObjects[0] as IHasCurve; + var curveData = hitObjects[0] as IHasPathWithRepeats; var positionData = hitObjects[0] as IHasPosition; Assert.IsNotNull(positionData); diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index b034e66616..b4c78ce273 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Beatmaps.Formats { var beatmap = decodeAsJson(normal); - var curveData = beatmap.HitObjects[0] as IHasCurve; + var curveData = beatmap.HitObjects[0] as IHasPathWithRepeats; var positionData = beatmap.HitObjects[0] as IHasPosition; Assert.IsNotNull(positionData); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index a35437a286..1809332bce 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -28,6 +28,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep(@"locally available state", () => downloadButton.SetDownloadState(DownloadState.LocallyAvailable)); AddStep(@"not downloaded state", () => downloadButton.SetDownloadState(DownloadState.NotDownloaded)); createButton(false); + createButtonNoScore(); } private void createButton(bool withReplay) @@ -40,6 +41,22 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.Centre, }; }); + + AddUntilStep("wait for load", () => downloadButton.IsLoaded); + } + + private void createButtonNoScore() + { + AddStep("create button with null score", () => + { + Child = downloadButton = new TestReplayDownloadButton(null) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + }); + + AddUntilStep("wait for load", () => downloadButton.IsLoaded); } private ScoreInfo getScoreInfo(bool replayAvailable) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs new file mode 100644 index 0000000000..76cfe75b59 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking; +using osu.Game.Screens.Ranking.Contracted; +using osuTK; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneContractedPanelMiddleContent : OsuTestScene + { + [Resolved] + private RulesetStore rulesetStore { get; set; } + + [Test] + public void TestShowPanel() + { + AddStep("show example score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), new TestScoreInfo(new OsuRuleset().RulesetInfo))); + } + + private void showPanel(WorkingBeatmap workingBeatmap, ScoreInfo score) + { + Child = new ContractedPanelMiddleContentContainer(workingBeatmap, score); + } + + private class ContractedPanelMiddleContentContainer : Container + { + [Cached] + private Bindable workingBeatmap { get; set; } + + public ContractedPanelMiddleContentContainer(WorkingBeatmap beatmap, ScoreInfo score) + { + workingBeatmap = new Bindable(beatmap); + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Size = new Vector2(ScorePanel.CONTRACTED_WIDTH, 460); + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#353535"), + }, + new ContractedPanelMiddleContent(score), + }; + } + } + } +} diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 106b4187ee..69511b85c0 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.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 System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -14,10 +13,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking.Expanded; @@ -37,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking { var author = new User { Username = "mapper_name" }; - AddStep("show example score", () => showPanel(createTestBeatmap(author), createTestScore())); + AddStep("show example score", () => showPanel(createTestBeatmap(author), new TestScoreInfo(new OsuRuleset().RulesetInfo))); AddAssert("mapper name present", () => this.ChildrenOfType().Any(spriteText => spriteText.Text == "mapper_name")); } @@ -45,7 +41,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestMapWithUnknownMapper() { - AddStep("show example score", () => showPanel(createTestBeatmap(null), createTestScore())); + AddStep("show example score", () => showPanel(createTestBeatmap(null), new TestScoreInfo(new OsuRuleset().RulesetInfo))); AddAssert("mapped by text not present", () => this.ChildrenOfType().All(spriteText => !containsAny(spriteText.Text, "mapped", "by"))); @@ -66,29 +62,6 @@ namespace osu.Game.Tests.Visual.Ranking return new TestWorkingBeatmap(beatmap); } - private ScoreInfo createTestScore() => new ScoreInfo - { - User = new User - { - Id = 2, - Username = "peppy", - }, - Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, - Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, - TotalScore = 999999, - Accuracy = 0.95, - MaxCombo = 999, - Rank = ScoreRank.S, - Date = DateTimeOffset.Now, - Statistics = - { - { HitResult.Miss, 1 }, - { HitResult.Meh, 50 }, - { HitResult.Good, 100 }, - { HitResult.Great, 300 }, - } - }; - private bool containsAny(string text, params string[] stringsToMatch) => stringsToMatch.Any(text.Contains); private class ExpandedPanelMiddleContentContainer : Container diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs index afaa607099..a32bcbe7f0 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs @@ -5,8 +5,8 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Osu; using osu.Game.Screens.Ranking.Expanded; -using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#444"), }, - new ExpandedPanelTopContent(new User { Id = 2, Username = "peppy" }), + new ExpandedPanelTopContent(new TestScoreInfo(new OsuRuleset().RulesetInfo).User), } }; } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index aa0ce89d93..242766ad4b 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -1,8 +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 System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -11,13 +9,10 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; -using osu.Game.Tests.Beatmaps; -using osu.Game.Users; namespace osu.Game.Tests.Visual.Ranking { @@ -41,26 +36,7 @@ namespace osu.Game.Tests.Visual.Ranking Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); } - private TestSoloResults createResultsScreen() => new TestSoloResults(new ScoreInfo - { - TotalScore = 2845370, - Accuracy = 0.98, - MaxCombo = 123, - Rank = ScoreRank.A, - Date = DateTimeOffset.Now, - Statistics = new Dictionary - { - { HitResult.Great, 50 }, - { HitResult.Good, 20 }, - { HitResult.Meh, 50 }, - { HitResult.Miss, 1 } - }, - Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, - User = new User - { - Username = "peppy", - } - }); + private TestSoloResults createResultsScreen() => new TestSoloResults(new TestScoreInfo(new OsuRuleset().RulesetInfo)); [Test] public void ResultsWithoutPlayer() diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs index 880e331b92..250fdc5ebd 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs @@ -1,28 +1,23 @@ // 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.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking; -using osu.Game.Tests.Beatmaps; -using osu.Game.Users; namespace osu.Game.Tests.Visual.Ranking { public class TestSceneScorePanel : OsuTestScene { + private ScorePanel panel; + [Test] public void TestDRank() { - var score = createScore(); - score.Accuracy = 0.5; - score.Rank = ScoreRank.D; + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.5, Rank = ScoreRank.D }; addPanelStep(score); } @@ -30,9 +25,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestCRank() { - var score = createScore(); - score.Accuracy = 0.75; - score.Rank = ScoreRank.C; + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.75, Rank = ScoreRank.C }; addPanelStep(score); } @@ -40,9 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestBRank() { - var score = createScore(); - score.Accuracy = 0.85; - score.Rank = ScoreRank.B; + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.85, Rank = ScoreRank.B }; addPanelStep(score); } @@ -50,9 +41,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestARank() { - var score = createScore(); - score.Accuracy = 0.925; - score.Rank = ScoreRank.A; + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A }; addPanelStep(score); } @@ -60,9 +49,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestSRank() { - var score = createScore(); - score.Accuracy = 0.975; - score.Rank = ScoreRank.S; + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.975, Rank = ScoreRank.S }; addPanelStep(score); } @@ -70,9 +57,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAlmostSSRank() { - var score = createScore(); - score.Accuracy = 0.9999; - score.Rank = ScoreRank.S; + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.9999, Rank = ScoreRank.S }; addPanelStep(score); } @@ -80,9 +65,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestSSRank() { - var score = createScore(); - score.Accuracy = 1; - score.Rank = ScoreRank.X; + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 1, Rank = ScoreRank.X }; addPanelStep(score); } @@ -90,44 +73,42 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAllHitResults() { - var score = createScore(); - score.Statistics[HitResult.Perfect] = 350; - score.Statistics[HitResult.Ok] = 200; + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Statistics = { [HitResult.Perfect] = 350, [HitResult.Ok] = 200 } }; addPanelStep(score); } - private void addPanelStep(ScoreInfo score) => AddStep("add panel", () => + [Test] + public void TestContractedPanel() { - Child = new ScorePanel(score) + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A }; + + addPanelStep(score, PanelState.Contracted); + } + + [Test] + public void TestExpandAndContract() + { + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A }; + + addPanelStep(score, PanelState.Contracted); + AddWaitStep("wait for transition", 10); + + AddStep("expand panel", () => panel.State = PanelState.Expanded); + AddWaitStep("wait for transition", 10); + + AddStep("contract panel", () => panel.State = PanelState.Contracted); + AddWaitStep("wait for transition", 10); + } + + private void addPanelStep(ScoreInfo score, PanelState state = PanelState.Expanded) => AddStep("add panel", () => + { + Child = panel = new ScorePanel(score) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - State = PanelState.Expanded + State = state }; }); - - private ScoreInfo createScore() => new ScoreInfo - { - User = new User - { - Id = 2, - Username = "peppy", - }, - Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, - Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, - TotalScore = 2845370, - Accuracy = 0.95, - MaxCombo = 999, - Rank = ScoreRank.S, - Date = DateTimeOffset.Now, - Statistics = - { - { HitResult.Miss, 1 }, - { HitResult.Meh, 50 }, - { HitResult.Good, 100 }, - { HitResult.Great, 300 }, - } - }; } } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs new file mode 100644 index 0000000000..e65dcb19b1 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs @@ -0,0 +1,208 @@ +// 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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Rulesets.Osu; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneScorePanelList : OsuManualInputManagerTestScene + { + private ScorePanelList list; + + [Test] + public void TestEmptyList() + { + createListStep(() => new ScorePanelList()); + } + + [Test] + public void TestEmptyListWithSelectedScore() + { + createListStep(() => new ScorePanelList + { + SelectedScore = { Value = new TestScoreInfo(new OsuRuleset().RulesetInfo) } + }); + } + + [Test] + public void TestAddPanelAfterSelectingScore() + { + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); + + createListStep(() => new ScorePanelList + { + SelectedScore = { Value = score } + }); + + AddStep("add panel", () => list.AddScore(score)); + + assertScoreState(score, true); + assertExpandedPanelCentred(); + } + + [Test] + public void TestAddPanelBeforeSelectingScore() + { + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); + + createListStep(() => new ScorePanelList()); + + AddStep("add panel", () => list.AddScore(score)); + + assertScoreState(score, false); + assertFirstPanelCentred(); + + AddStep("select score", () => list.SelectedScore.Value = score); + + assertScoreState(score, true); + assertExpandedPanelCentred(); + } + + [Test] + public void TestAddManyNonExpandedPanels() + { + createListStep(() => new ScorePanelList()); + + AddStep("add many scores", () => + { + for (int i = 0; i < 20; i++) + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + }); + + assertFirstPanelCentred(); + } + + [Test] + public void TestAddManyScoresAfterExpandedPanel() + { + var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); + + createListStep(() => new ScorePanelList()); + + AddStep("add initial panel and select", () => + { + list.AddScore(initialScore); + list.SelectedScore.Value = initialScore; + }); + + AddStep("add many scores", () => + { + for (int i = 0; i < 20; i++) + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore - i - 1 }); + }); + + assertScoreState(initialScore, true); + assertExpandedPanelCentred(); + } + + [Test] + public void TestAddManyScoresBeforeExpandedPanel() + { + var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); + + createListStep(() => new ScorePanelList()); + + AddStep("add initial panel and select", () => + { + list.AddScore(initialScore); + list.SelectedScore.Value = initialScore; + }); + + AddStep("add scores", () => + { + for (int i = 0; i < 20; i++) + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 }); + }); + + assertScoreState(initialScore, true); + assertExpandedPanelCentred(); + } + + [Test] + public void TestAddManyPanelsOnBothSidesOfExpandedPanel() + { + var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); + + createListStep(() => new ScorePanelList()); + + AddStep("add initial panel and select", () => + { + list.AddScore(initialScore); + list.SelectedScore.Value = initialScore; + }); + + AddStep("add scores after", () => + { + for (int i = 0; i < 20; i++) + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore - i - 1 }); + + for (int i = 0; i < 20; i++) + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 }); + }); + + assertScoreState(initialScore, true); + assertExpandedPanelCentred(); + } + + [Test] + public void TestSelectMultipleScores() + { + var firstScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); + var secondScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); + + createListStep(() => new ScorePanelList()); + + AddStep("add scores and select first", () => + { + list.AddScore(firstScore); + list.AddScore(secondScore); + list.SelectedScore.Value = firstScore; + }); + + assertScoreState(firstScore, true); + assertScoreState(secondScore, false); + + AddStep("select second score", () => + { + InputManager.MoveMouseTo(list.ChildrenOfType().Single(p => p.Score == secondScore)); + InputManager.Click(MouseButton.Left); + }); + + assertScoreState(firstScore, false); + assertScoreState(secondScore, true); + assertExpandedPanelCentred(); + } + + private void createListStep(Func creationFunc) + { + AddStep("create list", () => Child = list = creationFunc().With(d => + { + d.Anchor = Anchor.Centre; + d.Origin = Anchor.Centre; + })); + + AddUntilStep("wait for load", () => list.IsLoaded); + } + + private void assertExpandedPanelCentred() => AddUntilStep("expanded panel centred", () => + { + var expandedPanel = list.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, list.ScreenSpaceDrawQuad.Centre.X, 1); + }); + + private void assertFirstPanelCentred() + => AddUntilStep("first panel centred", () => Precision.AlmostEquals(list.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre.X, list.ScreenSpaceDrawQuad.Centre.X, 1)); + + private void assertScoreState(ScoreInfo score, bool expanded) + => AddUntilStep($"score expanded = {expanded}", () => (list.ChildrenOfType().Single(p => p.Score == score).State == PanelState.Expanded) == expanded); + } +} diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index a8982238cf..cefb47893c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -233,9 +233,9 @@ namespace osu.Game.Beatmaps.Formats writer.Write(FormattableString.Invariant($"{(int)getObjectType(hitObject)},")); writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},")); - if (hitObject is IHasCurve curveData) + if (hitObject is IHasPath path) { - addCurveData(writer, curveData, position); + addPathData(writer, path, position); writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true)); } else @@ -263,7 +263,7 @@ namespace osu.Game.Beatmaps.Formats switch (hitObject) { - case IHasCurve _: + case IHasPath _: type |= LegacyHitObjectType.Slider; break; @@ -282,13 +282,13 @@ namespace osu.Game.Beatmaps.Formats return type; } - private void addCurveData(TextWriter writer, IHasCurve curveData, Vector2 position) + private void addPathData(TextWriter writer, IHasPath pathData, Vector2 position) { PathType? lastType = null; - for (int i = 0; i < curveData.Path.ControlPoints.Count; i++) + for (int i = 0; i < pathData.Path.ControlPoints.Count; i++) { - PathControlPoint point = curveData.Path.ControlPoints[i]; + PathControlPoint point = pathData.Path.ControlPoints[i]; if (point.Type.Value != null) { @@ -325,23 +325,28 @@ namespace osu.Game.Beatmaps.Formats if (i != 0) { writer.Write(FormattableString.Invariant($"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}")); - writer.Write(i != curveData.Path.ControlPoints.Count - 1 ? "|" : ","); + writer.Write(i != pathData.Path.ControlPoints.Count - 1 ? "|" : ","); } } - writer.Write(FormattableString.Invariant($"{curveData.RepeatCount + 1},")); - writer.Write(FormattableString.Invariant($"{curveData.Path.Distance},")); + var curveData = pathData as IHasPathWithRepeats; - for (int i = 0; i < curveData.NodeSamples.Count; i++) - { - writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}")); - writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); - } + writer.Write(FormattableString.Invariant($"{(curveData?.RepeatCount ?? 0) + 1},")); + writer.Write(FormattableString.Invariant($"{pathData.Path.Distance},")); - for (int i = 0; i < curveData.NodeSamples.Count; i++) + if (curveData != null) { - writer.Write(getSampleBank(curveData.NodeSamples[i], true)); - writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); + for (int i = 0; i < curveData.NodeSamples.Count; i++) + { + writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}")); + writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); + } + + for (int i = 0; i < curveData.NodeSamples.Count; i++) + { + writer.Write(getSampleBank(curveData.NodeSamples[i], true)); + writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); + } } } diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 0c3272c7de..dde45b5aeb 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -27,7 +27,15 @@ namespace osu.Game.Online.API.Requests private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc"; - public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, Cursor cursor = null, SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending) + public SearchBeatmapSetsRequest( + string query, + RulesetInfo ruleset, + Cursor cursor = null, + SearchCategory searchCategory = SearchCategory.Any, + SortCriteria sortCriteria = SortCriteria.Ranked, + SortDirection sortDirection = SortDirection.Descending, + SearchGenre genre = SearchGenre.Any, + SearchLanguage language = SearchLanguage.Any) { this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); this.ruleset = ruleset; @@ -36,8 +44,8 @@ namespace osu.Game.Online.API.Requests SearchCategory = searchCategory; SortCriteria = sortCriteria; SortDirection = sortDirection; - Genre = SearchGenre.Any; - Language = SearchLanguage.Any; + Genre = genre; + Language = language; } protected override WebRequest CreateWebRequest() diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 41c99d5d03..494a0df8f8 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -177,7 +177,9 @@ namespace osu.Game.Overlays.BeatmapListing lastResponse?.Cursor, searchControl.Category.Value, sortControl.Current.Value, - sortControl.SortDirection.Value); + sortControl.SortDirection.Value, + searchControl.Genre.Value, + searchControl.Language.Value); getSetsRequest.Success += response => { @@ -186,6 +188,9 @@ namespace osu.Game.Overlays.BeatmapListing if (sets.Count == 0) noMoreResults = true; + if (CurrentPage == 0) + searchControl.BeatmapSet = sets.FirstOrDefault(); + lastResponse = response; getSetsRequest = null; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 38576e02a0..6dd6e6815b 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -48,8 +48,6 @@ namespace osu.Game.Rulesets.Edit protected ComposeBlueprintContainer BlueprintContainer { get; private set; } - public override Playfield Playfield => drawableRulesetWrapper.Playfield; - private DrawableEditRulesetWrapper drawableRulesetWrapper; protected readonly Container LayerBelowRuleset = new Container { RelativeSizeAxes = Axes.Both }; @@ -61,7 +59,6 @@ namespace osu.Game.Rulesets.Edit protected HitObjectComposer(Ruleset ruleset) { Ruleset = ruleset; - RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] @@ -137,6 +134,49 @@ namespace osu.Game.Rulesets.Edit EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged; } + protected override void LoadComplete() + { + base.LoadComplete(); + + inputManager = GetContainingInputManager(); + } + + public override Playfield Playfield => drawableRulesetWrapper.Playfield; + + public override IEnumerable HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects; + + public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position); + + /// + /// Defines all available composition tools, listed on the left side of the editor screen as button controls. + /// This should usually define one tool for each type used in the target ruleset. + /// + /// + /// A "select" tool is automatically added as the first tool. + /// + protected abstract IReadOnlyList CompositionTools { get; } + + /// + /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. + /// + protected abstract ComposeBlueprintContainer CreateBlueprintContainer(); + + /// + /// Construct a drawable ruleset for the provided ruleset. + /// + /// + /// Can be overridden to add editor-specific logical changes to a 's standard . + /// For example, hit animations or judgement logic may be changed to give a better editor user experience. + /// + /// The ruleset used to construct its drawable counterpart. + /// The loaded beatmap. + /// The mods to be applied. + /// An editor-relevant . + protected virtual DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + => (DrawableRuleset)ruleset.CreateDrawableRulesetWith(beatmap, mods); + + #region Tool selection logic + protected override bool OnKeyDown(KeyDownEvent e) { if (e.Key >= Key.Number1 && e.Key <= Key.Number9) @@ -153,13 +193,6 @@ namespace osu.Game.Rulesets.Edit return base.OnKeyDown(e); } - protected override void LoadComplete() - { - base.LoadComplete(); - - inputManager = GetContainingInputManager(); - } - private void selectionChanged(object sender, NotifyCollectionChangedEventArgs changedArgs) { if (EditorBeatmap.SelectedHitObjects.Any()) @@ -179,15 +212,9 @@ namespace osu.Game.Rulesets.Edit EditorBeatmap.SelectedHitObjects.Clear(); } - public override IEnumerable HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects; + #endregion - public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position); - - protected abstract IReadOnlyList CompositionTools { get; } - - protected abstract ComposeBlueprintContainer CreateBlueprintContainer(); - - protected abstract DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null); + #region IPlacementHandler public void BeginPlacement(HitObject hitObject) { @@ -209,6 +236,17 @@ namespace osu.Game.Rulesets.Edit public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject); + #endregion + + #region IPositionSnapProvider + + /// + /// Retrieve the relevant at a specified screen-space position. + /// In cases where a ruleset doesn't require custom logic (due to nested playfields, for example) + /// this will return the ruleset's main playfield. + /// + /// The screen-space position to query. + /// The most relevant . protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield; public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) @@ -257,8 +295,14 @@ namespace osu.Game.Rulesets.Edit return DurationToDistance(referenceTime, snappedEndTime - referenceTime); } + + #endregion } + /// + /// A non-generic definition of a HitObject composer class. + /// Generally used to access certain methods without requiring a generic type for . + /// [Cached(typeof(HitObjectComposer))] [Cached(typeof(IPositionSnapProvider))] public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider @@ -268,10 +312,13 @@ namespace osu.Game.Rulesets.Edit RelativeSizeAxes = Axes.Both; } + /// + /// The target ruleset's playfield. + /// public abstract Playfield Playfield { get; } /// - /// All the s. + /// All s in currently loaded beatmap. /// public abstract IEnumerable HitObjects { get; } @@ -280,6 +327,8 @@ namespace osu.Game.Rulesets.Edit /// public abstract bool CursorInPlacementArea { get; } + #region IPositionSnapProvider + public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); public abstract float GetBeatSnapDistanceAt(double referenceTime); @@ -291,5 +340,7 @@ namespace osu.Game.Rulesets.Edit public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance); public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance); + + #endregion } } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index 0a3c1b40fd..c522dc623c 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -9,7 +9,10 @@ using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Objects.Legacy { - internal abstract class ConvertSlider : ConvertHitObject, IHasCurve, IHasLegacyLastTickOffset + internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset, +#pragma warning disable 618 + IHasCurve +#pragma warning restore 618 { /// /// Scoring distance with a speed-adjusted beat length of 1 second. diff --git a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs index e98a888bd7..26f50ffa31 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs @@ -1,13 +1,12 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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 osuTK; namespace osu.Game.Rulesets.Objects.Types { - /// - /// A HitObject that has a curve. - /// + [Obsolete("Use IHasPathWithRepeats instead.")] // can be removed 20201126 public interface IHasCurve : IHasDistance, IHasRepeats { /// @@ -16,6 +15,8 @@ namespace osu.Game.Rulesets.Objects.Types SliderPath Path { get; } } +#pragma warning disable 618 + [Obsolete("Use IHasPathWithRepeats instead.")] // can be removed 20201126 public static class HasCurveExtensions { /// @@ -50,4 +51,5 @@ namespace osu.Game.Rulesets.Objects.Types public static int SpanAt(this IHasCurve obj, double progress) => (int)(progress * obj.SpanCount()); } +#pragma warning restore 618 } diff --git a/osu.Game/Rulesets/Objects/Types/IHasPath.cs b/osu.Game/Rulesets/Objects/Types/IHasPath.cs new file mode 100644 index 0000000000..567c24a4a2 --- /dev/null +++ b/osu.Game/Rulesets/Objects/Types/IHasPath.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Objects.Types +{ + public interface IHasPath : IHasDistance + { + /// + /// The curve. + /// + SliderPath Path { get; } + } +} diff --git a/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs b/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs new file mode 100644 index 0000000000..279946b44e --- /dev/null +++ b/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osuTK; + +namespace osu.Game.Rulesets.Objects.Types +{ + /// + /// A HitObject that has a curve. + /// + // ReSharper disable once RedundantExtendsListEntry + public interface IHasPathWithRepeats : IHasPath, IHasRepeats + { + } + + public static class HasPathWithRepeatsExtensions + { + /// + /// Computes the position on the curve relative to how much of the has been completed. + /// + /// The curve. + /// [0, 1] where 0 is the start time of the and 1 is the end time of the . + /// The position on the curve. + public static Vector2 CurvePositionAt(this IHasPathWithRepeats obj, double progress) + => obj.Path.PositionAt(obj.ProgressAt(progress)); + + /// + /// Computes the progress along the curve relative to how much of the has been completed. + /// + /// The curve. + /// [0, 1] where 0 is the start time of the and 1 is the end time of the . + /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. + public static double ProgressAt(this IHasPathWithRepeats obj, double progress) + { + double p = progress * obj.SpanCount() % 1; + if (obj.SpanAt(progress) % 2 == 1) + p = 1 - p; + return p; + } + + /// + /// Determines which span of the curve the progress point is on. + /// + /// The curve. + /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. + /// [0, SpanCount) where 0 is the first run. + public static int SpanAt(this IHasPathWithRepeats obj, double progress) + => (int)(progress * obj.SpanCount()); + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3d4b20bec4..36198bcc65 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -479,7 +479,7 @@ namespace osu.Game.Screens.Play protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; - protected virtual ResultsScreen CreateResults(ScoreInfo score) => new ResultsScreen(score); + protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score); #region Fail Logic diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index f0c76163f1..b443603128 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Play this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo)); } - protected override ResultsScreen CreateResults(ScoreInfo score) => new ResultsScreen(score, false); + protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); protected override ScoreInfo CreateScore() => score.ScoreInfo; } diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs new file mode 100644 index 0000000000..8cd0e7025e --- /dev/null +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -0,0 +1,222 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Leaderboards; +using osu.Game.Scoring; +using osu.Game.Screens.Play.HUD; +using osu.Game.Users; +using osu.Game.Users.Drawables; +using osu.Game.Utils; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Ranking.Contracted +{ + /// + /// The content that appears in the middle of a contracted . + /// + public class ContractedPanelMiddleContent : CompositeDrawable + { + private readonly ScoreInfo score; + + /// + /// Creates a new . + /// + /// The to display. + public ContractedPanelMiddleContent(ScoreInfo score) + { + this.score = score; + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerExponent = 2.5f, + CornerRadius = 20, + EdgeEffect = new EdgeEffectParameters + { + Colour = Color4.Black.Opacity(0.25f), + Type = EdgeEffectType.Shadow, + Radius = 1, + Offset = new Vector2(0, 4) + }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("444") + }, + new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + User = score.User, + Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.5f), Color4Extensions.FromHex("#444").Opacity(0)) + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Children = new Drawable[] + { + new UpdateableAvatar(score.User) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(110), + Masking = true, + CornerExponent = 2.5f, + CornerRadius = 20, + EdgeEffect = new EdgeEffectParameters + { + Colour = Color4.Black.Opacity(0.25f), + Type = EdgeEffectType.Shadow, + Radius = 8, + Offset = new Vector2(0, 4), + } + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = score.UserString, + Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold) + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + ChildrenEnumerable = score.SortedStatistics.Select(s => createStatistic(s.Key.GetDescription(), s.Value.ToString())) + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10 }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new[] + { + createStatistic("Max Combo", $"x{score.MaxCombo}"), + createStatistic("Accuracy", $"{score.Accuracy.FormatAccuracy()}"), + } + }, + new ModDisplay + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + ExpansionMode = ExpansionMode.AlwaysExpanded, + DisplayUnrankedText = false, + Current = { Value = score.Mods }, + Scale = new Vector2(0.5f), + } + } + } + } + }, + }, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Vertical = 5 }, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = score.TotalScore.ToString("N0"), + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, fixedWidth: true), + Spacing = new Vector2(-1, 0) + }, + }, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 2 }, + Child = new DrawableRank(score.Rank) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } + }, + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + } + } + } + }, + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 45), + } + }; + } + + private Drawable createStatistic(string key, string value) => new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = key, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) + }, + new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Text = value, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + Colour = Color4Extensions.FromHex("#FFDD55") + } + } + }; + } +} diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index a36c86eafc..9d4e3af230 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -13,6 +14,8 @@ namespace osu.Game.Screens.Ranking { public class ReplayDownloadButton : DownloadTrackingComposite { + public Bindable Score => Model; + private DownloadButton button; private ShakeContainer shakeContainer; @@ -23,7 +26,7 @@ namespace osu.Game.Screens.Ranking if (State.Value == DownloadState.LocallyAvailable) return ReplayAvailability.Local; - if (!string.IsNullOrEmpty(Model.Value.Hash)) + if (!string.IsNullOrEmpty(Model.Value?.Hash)) return ReplayAvailability.Online; return ReplayAvailability.NotAvailable; diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index cfba1e6e3e..fbb9b95478 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -10,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Scoring; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Play; @@ -17,7 +20,7 @@ using osuTK; namespace osu.Game.Screens.Ranking { - public class ResultsScreen : OsuScreen + public abstract class ResultsScreen : OsuScreen { protected const float BACKGROUND_BLUR = 20; @@ -28,19 +31,26 @@ namespace osu.Game.Screens.Ranking protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); + public readonly Bindable SelectedScore = new Bindable(); + + public readonly ScoreInfo Score; + private readonly bool allowRetry; + [Resolved(CanBeNull = true)] private Player player { get; set; } - public readonly ScoreInfo Score; - - private readonly bool allowRetry; + [Resolved] + private IAPIProvider api { get; set; } private Drawable bottomPanel; + private ScorePanelList panels; - public ResultsScreen(ScoreInfo score, bool allowRetry = true) + protected ResultsScreen(ScoreInfo score, bool allowRetry = true) { Score = score; this.allowRetry = allowRetry; + + SelectedScore.Value = score; } [BackgroundDependencyLoader] @@ -48,47 +58,68 @@ namespace osu.Game.Screens.Ranking { FillFlowContainer buttons; - InternalChildren = new[] + InternalChild = new GridContainer { - new ResultsScrollContainer + RelativeSizeAxes = Axes.Both, + Content = new[] { - Child = new ScorePanel(Score) + new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - State = PanelState.Expanded + new ResultsScrollContainer + { + Child = panels = new ScorePanelList + { + RelativeSizeAxes = Axes.Both, + SelectedScore = { BindTarget = SelectedScore } + } + } }, - }, - bottomPanel = new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = TwoLayerButton.SIZE_EXTENDED.Y, - Alpha = 0, - Children = new Drawable[] + new[] { - new Box + bottomPanel = new Container { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#333") - }, - buttons = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5), - Direction = FillDirection.Horizontal, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = TwoLayerButton.SIZE_EXTENDED.Y, + Alpha = 0, Children = new Drawable[] { - new ReplayDownloadButton(Score) { Width = 300 }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333") + }, + buttons = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new ReplayDownloadButton(null) + { + Score = { BindTarget = SelectedScore }, + Width = 300 + }, + } + } } } } + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) } }; + if (Score != null) + panels.AddScore(Score); + if (player != null && allowRetry) { buttons.Add(new RetryButton { Width = 300 }); @@ -105,6 +136,27 @@ namespace osu.Game.Screens.Ranking } } + protected override void LoadComplete() + { + base.LoadComplete(); + + var req = FetchScores(scores => Schedule(() => + { + foreach (var s in scores) + panels.AddScore(s); + })); + + if (req != null) + api.Queue(req); + } + + /// + /// Performs a fetch/refresh of scores to be displayed. + /// + /// A callback which should be called when fetching is completed. Scheduling is not required. + /// An responsible for the fetch operation. This will be queued and performed automatically. + protected virtual APIRequest FetchScores(Action> scoresCallback) => null; + public override void OnEntering(IScreen last) { base.OnEntering(last); @@ -142,7 +194,7 @@ namespace osu.Game.Screens.Ranking protected override void Update() { base.Update(); - content.Height = Math.Max(768, DrawHeight); + content.Height = Math.Max(768 - TwoLayerButton.SIZE_EXTENDED.Y, DrawHeight); } } } diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index c055df7ccc..a99b48e8f0 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -9,7 +9,9 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Contracted; using osu.Game.Screens.Ranking.Expanded; using osuTK; using osuTK.Graphics; @@ -21,12 +23,12 @@ namespace osu.Game.Screens.Ranking /// /// Width of the panel when contracted. /// - private const float contracted_width = 160; + public const float CONTRACTED_WIDTH = 130; /// /// Height of the panel when contracted. /// - private const float contracted_height = 320; + private const float contracted_height = 355; /// /// Width of the panel when expanded. @@ -46,7 +48,7 @@ namespace osu.Game.Screens.Ranking /// /// Height of the top layer when the panel is contracted. /// - private const float contracted_top_layer_height = 40; + private const float contracted_top_layer_height = 30; /// /// Duration for the panel to resize into its expanded/contracted size. @@ -71,11 +73,12 @@ namespace osu.Game.Screens.Ranking private static readonly ColourInfo expanded_top_layer_colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#444"), Color4Extensions.FromHex("#333")); private static readonly ColourInfo expanded_middle_layer_colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#555"), Color4Extensions.FromHex("#333")); private static readonly Color4 contracted_top_layer_colour = Color4Extensions.FromHex("#353535"); - private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#444"); + private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#353535"); public event Action StateChanged; + public readonly ScoreInfo Score; - private readonly ScoreInfo score; + private Container content; private Container topLayerContainer; private Drawable topLayerBackground; @@ -89,47 +92,54 @@ namespace osu.Game.Screens.Ranking public ScorePanel(ScoreInfo score) { - this.score = score; + Score = score; } [BackgroundDependencyLoader] private void load() { - InternalChildren = new Drawable[] + InternalChild = content = new Container { - topLayerContainer = new Container + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(40), + Children = new Drawable[] { - Name = "Top layer", - RelativeSizeAxes = Axes.X, - Height = 120, - Children = new Drawable[] + topLayerContainer = new Container { - new Container + Name = "Top layer", + RelativeSizeAxes = Axes.X, + Alpha = 0, + Height = 120, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - CornerRadius = 20, - CornerExponent = 2.5f, - Masking = true, - Child = topLayerBackground = new Box { RelativeSizeAxes = Axes.Both } - }, - topLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } - } - }, - middleLayerContainer = new Container - { - Name = "Middle layer", - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 20, + CornerExponent = 2.5f, + Masking = true, + Child = topLayerBackground = new Box { RelativeSizeAxes = Axes.Both } + }, + topLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } + } + }, + middleLayerContainer = new Container { - new Container + Name = "Middle layer", + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - CornerRadius = 20, - CornerExponent = 2.5f, - Masking = true, - Child = middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both } - }, - middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } + new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 20, + CornerExponent = 2.5f, + Masking = true, + Child = middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both } + }, + middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } + } } } }; @@ -174,34 +184,40 @@ namespace osu.Game.Screens.Ranking private void updateState() { - topLayerContainer.MoveToY(0, resize_duration, Easing.OutQuint); - middleLayerContainer.MoveToY(0, resize_duration, Easing.OutQuint); - topLayerContent?.FadeOut(content_fade_duration).Expire(); middleLayerContent?.FadeOut(content_fade_duration).Expire(); switch (state) { case PanelState.Expanded: - this.ResizeTo(new Vector2(EXPANDED_WIDTH, expanded_height), resize_duration, Easing.OutQuint); + Size = new Vector2(EXPANDED_WIDTH, expanded_height); topLayerBackground.FadeColour(expanded_top_layer_colour, resize_duration, Easing.OutQuint); middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint); - topLayerContentContainer.Add(middleLayerContent = new ExpandedPanelTopContent(score.User).With(d => d.Alpha = 0)); - middleLayerContentContainer.Add(topLayerContent = new ExpandedPanelMiddleContent(score).With(d => d.Alpha = 0)); + topLayerContentContainer.Add(middleLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0)); + middleLayerContentContainer.Add(topLayerContent = new ExpandedPanelMiddleContent(Score).With(d => d.Alpha = 0)); break; case PanelState.Contracted: - this.ResizeTo(new Vector2(contracted_width, contracted_height), resize_duration, Easing.OutQuint); + Size = new Vector2(CONTRACTED_WIDTH, contracted_height); topLayerBackground.FadeColour(contracted_top_layer_colour, resize_duration, Easing.OutQuint); middleLayerBackground.FadeColour(contracted_middle_layer_colour, resize_duration, Easing.OutQuint); + + middleLayerContentContainer.Add(topLayerContent = new ContractedPanelMiddleContent(Score).With(d => d.Alpha = 0)); break; } - using (BeginDelayedSequence(resize_duration + top_layer_expand_delay, true)) + content.ResizeTo(Size, resize_duration, Easing.OutQuint); + + bool topLayerExpanded = topLayerContainer.Y < 0; + + // If the top layer was already expanded, then we don't need to wait for the resize and can instead transform immediately. This looks better when changing the panel state. + using (BeginDelayedSequence(topLayerExpanded ? 0 : resize_duration + top_layer_expand_delay, true)) { + topLayerContainer.FadeIn(); + switch (state) { case PanelState.Expanded: @@ -219,5 +235,13 @@ namespace osu.Game.Screens.Ranking middleLayerContent?.FadeIn(content_fade_duration); } } + + protected override bool OnClick(ClickEvent e) + { + if (State == PanelState.Contracted) + State = PanelState.Expanded; + + return true; + } } } diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs new file mode 100644 index 0000000000..18db3f2af4 --- /dev/null +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -0,0 +1,185 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Screens.Ranking +{ + public class ScorePanelList : CompositeDrawable + { + /// + /// Normal spacing between all panels. + /// + private const float panel_spacing = 5; + + /// + /// Spacing around both sides of the expanded panel. This is added on top of . + /// + private const float expanded_panel_spacing = 15; + + public readonly Bindable SelectedScore = new Bindable(); + + private readonly Flow flow; + private readonly Scroll scroll; + private ScorePanel expandedPanel; + + public ScorePanelList() + { + RelativeSizeAxes = Axes.Both; + + InternalChild = scroll = new Scroll + { + RelativeSizeAxes = Axes.Both, + Child = flow = new Flow + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(panel_spacing, 0), + AutoSizeAxes = Axes.Both, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + SelectedScore.BindValueChanged(selectedScoreChanged, true); + } + + /// + /// Adds a to this list. + /// + /// The to add. + public void AddScore(ScoreInfo score) + { + flow.Add(new ScorePanel(score) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }.With(p => + { + p.StateChanged += s => + { + if (s == PanelState.Expanded) + SelectedScore.Value = p.Score; + }; + })); + + if (SelectedScore.Value == score) + selectedScoreChanged(new ValueChangedEvent(SelectedScore.Value, SelectedScore.Value)); + else + { + // We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done. + // But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel. + if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score)) + { + // A somewhat hacky property is used here because we need to: + // 1) Scroll after the scroll container's visible range is updated. + // 2) Scroll before the scroll container's scroll position is updated. + // Without this, we would have a 1-frame positioning error which looks very jarring. + scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing; + } + } + } + + /// + /// Brings a to the centre of the screen and expands it. + /// + /// The to present. + private void selectedScoreChanged(ValueChangedEvent score) + { + // Contract the old panel. + foreach (var p in flow.Where(p => p.Score == score.OldValue)) + { + p.State = PanelState.Contracted; + p.Margin = new MarginPadding(); + } + + // Find the panel corresponding to the new score. + expandedPanel = flow.SingleOrDefault(p => p.Score == score.NewValue); + + if (expandedPanel == null) + return; + + // Expand the new panel. + expandedPanel.State = PanelState.Expanded; + expandedPanel.Margin = new MarginPadding { Horizontal = expanded_panel_spacing }; + + // Scroll to the new panel. This is done manually since we need: + // 1) To scroll after the scroll container's visible range is updated. + // 2) To account for the centre anchor/origins of panels. + // In the end, it's easier to compute the scroll position manually. + float scrollOffset = flow.GetPanelIndex(expandedPanel.Score) * (ScorePanel.CONTRACTED_WIDTH + panel_spacing); + scroll.ScrollTo(scrollOffset); + } + + protected override void Update() + { + base.Update(); + + float offset = DrawWidth / 2f; + + // Add padding to both sides such that the centre of an expanded panel on either side is in the middle of the screen. + + if (SelectedScore.Value != null) + { + // The expanded panel has extra padding applied to it, so it needs to be included into the offset. + offset -= ScorePanel.EXPANDED_WIDTH / 2f + expanded_panel_spacing; + } + else + offset -= ScorePanel.CONTRACTED_WIDTH / 2f; + + flow.Padding = new MarginPadding { Horizontal = offset }; + } + + private class Flow : FillFlowContainer + { + public override IEnumerable FlowingChildren => applySorting(AliveInternalChildren); + + public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Score != score).Count(); + + private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() + .OrderByDescending(s => s.Score.TotalScore) + .ThenBy(s => s.Score.OnlineScoreID); + } + + private class Scroll : OsuScrollContainer + { + public new float Target => base.Target; + + public Scroll() + : base(Direction.Horizontal) + { + } + + /// + /// The target that will be scrolled to instantaneously next frame. + /// + public float? InstantScrollTarget; + + protected override void UpdateAfterChildren() + { + if (InstantScrollTarget != null) + { + ScrollTo(InstantScrollTarget.Value, false); + InstantScrollTarget = null; + } + + base.UpdateAfterChildren(); + } + + public override bool HandlePositionalInput => false; + + public override bool HandleNonPositionalInput => false; + } + } +} diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs new file mode 100644 index 0000000000..3ae723683a --- /dev/null +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; +using osu.Game.Scoring; + +namespace osu.Game.Screens.Ranking +{ + public class SoloResultsScreen : ResultsScreen + { + [Resolved] + private RulesetStore rulesets { get; set; } + + public SoloResultsScreen(ScoreInfo score, bool allowRetry = true) + : base(score, allowRetry) + { + } + + protected override APIRequest FetchScores(Action> scoresCallback) + { + var req = new GetScoresRequest(Score.Beatmap, Score.Ruleset); + req.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineScoreID != Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets))); + return req; + } + } +} diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 21ddc5685d..0a4c0e2085 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Select } protected void PresentScore(ScoreInfo score) => - FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new ResultsScreen(score))); + FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score))); protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); diff --git a/osu.Game/Tests/TestScoreInfo.cs b/osu.Game/Tests/TestScoreInfo.cs new file mode 100644 index 0000000000..1193a29d70 --- /dev/null +++ b/osu.Game/Tests/TestScoreInfo.cs @@ -0,0 +1,52 @@ +// 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.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Tests.Beatmaps; +using osu.Game.Users; + +namespace osu.Game.Tests +{ + public class TestScoreInfo : ScoreInfo + { + public TestScoreInfo(RulesetInfo ruleset) + { + User = new User + { + Id = 2, + Username = "peppy", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }; + + Beatmap = new TestBeatmap(ruleset).BeatmapInfo; + Ruleset = ruleset; + RulesetID = ruleset.ID ?? 0; + Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() }; + + TotalScore = 2845370; + Accuracy = 0.95; + MaxCombo = 999; + Rank = ScoreRank.S; + Date = DateTimeOffset.Now; + + Statistics[HitResult.Miss] = 1; + Statistics[HitResult.Meh] = 50; + Statistics[HitResult.Good] = 100; + Statistics[HitResult.Great] = 300; + } + + private class TestModHardRock : ModHardRock + { + public override double ScoreMultiplier => 1; + } + + private class TestModDoubleTime : ModDoubleTime + { + public override double ScoreMultiplier => 1; + } + } +} diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index e3b64c03b9..b9fc3de734 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -1,4 +1,4 @@ - + True True True @@ -905,14 +905,17 @@ private void load() True True True + True True True True True True True + True True True True + True True True