diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs new file mode 100644 index 0000000000..d8064d36ea --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -0,0 +1,65 @@ +// 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.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public class TestSceneOsuModSpunOut : OsuModTestScene + { + protected override bool AllowFail => true; + + [Test] + public void TestSpinnerAutoCompleted() => CreateModTest(new ModTestData + { + Mod = new OsuModSpunOut(), + Autoplay = false, + Beatmap = singleSpinnerBeatmap, + PassCondition = () => Player.ChildrenOfType().Single().Progress >= 1 + }); + + [TestCase(null)] + [TestCase(typeof(OsuModDoubleTime))] + [TestCase(typeof(OsuModHalfTime))] + public void TestSpinRateUnaffectedByMods(Type additionalModType) + { + var mods = new List { new OsuModSpunOut() }; + if (additionalModType != null) + mods.Add((Mod)Activator.CreateInstance(additionalModType)); + + CreateModTest(new ModTestData + { + Mods = mods, + Autoplay = false, + Beatmap = singleSpinnerBeatmap, + PassCondition = () => Precision.AlmostEquals(Player.ChildrenOfType().Single().SpinsPerMinute, 286, 1) + }); + } + + private Beatmap singleSpinnerBeatmap => new Beatmap + { + HitObjects = new List + { + new Spinner + { + Position = new Vector2(256, 192), + StartTime = 500, + Duration = 2000 + } + } + }; + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs deleted file mode 100644 index d1210db6b1..0000000000 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs +++ /dev/null @@ -1,59 +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.Graphics; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Tests.Visual; - -namespace osu.Game.Rulesets.Osu.Tests -{ - [TestFixture] - public class TestSceneSpinnerSpunOut : OsuTestScene - { - [SetUp] - public void SetUp() => Schedule(() => - { - SelectedMods.Value = new[] { new OsuModSpunOut() }; - }); - - [Test] - public void TestSpunOut() - { - DrawableSpinner spinner = null; - - AddStep("create spinner", () => spinner = createSpinner()); - - AddUntilStep("wait for end", () => Time.Current > spinner.LifetimeEnd); - - AddAssert("spinner is completed", () => spinner.Progress >= 1); - } - - private DrawableSpinner createSpinner() - { - var spinner = new Spinner - { - StartTime = Time.Current + 500, - EndTime = Time.Current + 2500 - }; - spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - - var drawableSpinner = new DrawableSpinner(spinner) - { - Anchor = Anchor.Centre - }; - - foreach (var mod in SelectedMods.Value.OfType()) - mod.ApplyToDrawableHitObjects(new[] { drawableSpinner }); - - Add(drawableSpinner); - return drawableSpinner; - } - } -} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 47d765fecd..f080e11933 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -41,7 +41,16 @@ namespace osu.Game.Rulesets.Osu.Mods var spinner = (DrawableSpinner)drawable; spinner.RotationTracker.Tracking = true; - spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f)); + + // early-return if we were paused to avoid division-by-zero in the subsequent calculations. + if (Precision.AlmostEquals(spinner.Clock.Rate, 0)) + return; + + // because the spinner is under the gameplay clock, it is affected by rate adjustments on the track; + // for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time. + // for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here. + var rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate; + spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * 0.03f)); } } } diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 23b5ad0bd8..a71d008eb9 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -40,8 +40,8 @@ namespace osu.Game.Tests.Visual { var mods = new List(SelectedMods.Value); - if (currentTestData.Mod != null) - mods.Add(currentTestData.Mod); + if (currentTestData.Mods != null) + mods.AddRange(currentTestData.Mods); if (currentTestData.Autoplay) mods.Add(ruleset.GetAutoplayMod()); @@ -85,9 +85,18 @@ namespace osu.Game.Tests.Visual public Func PassCondition; /// - /// The this test case tests. + /// The s this test case tests. /// - public Mod Mod; + public IReadOnlyList Mods; + + /// + /// Convenience property for setting if only + /// a single mod is to be tested. + /// + public Mod Mod + { + set => Mods = new[] { value }; + } } } }