diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs index de1f61a0bd..5e46498aca 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs @@ -16,31 +16,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { public class TestSceneOsuModAlternate : OsuModTestScene { - [Test] - public void TestInputAtIntro() => CreateModTest(new ModTestData - { - Mod = new OsuModAlternate(), - PassCondition = () => Player.ScoreProcessor.Combo.Value == 1, - Autoplay = false, - Beatmap = new Beatmap - { - HitObjects = new List - { - new HitCircle - { - StartTime = 1000, - Position = new Vector2(100), - }, - }, - }, - ReplayFrames = new List - { - new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton), - new OsuReplayFrame(501, new Vector2(200)), - new OsuReplayFrame(1000, new Vector2(100), OsuAction.LeftButton), - } - }); - [Test] public void TestInputAlternating() => CreateModTest(new ModTestData { @@ -116,17 +91,50 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } }); + /// + /// Ensures alternation is reset before the first hitobject after intro. + /// + [Test] + public void TestInputSingularAtIntro() => CreateModTest(new ModTestData + { + Mod = new OsuModAlternate(), + PassCondition = () => Player.ScoreProcessor.Combo.Value == 1, + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new HitCircle + { + StartTime = 1000, + Position = new Vector2(100), + }, + }, + }, + ReplayFrames = new List + { + // first press during intro. + new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton), + new OsuReplayFrame(501, new Vector2(200)), + // press same key at hitobject and ensure it has been hit. + new OsuReplayFrame(1000, new Vector2(100), OsuAction.LeftButton), + } + }); + + /// + /// Ensures alternation is reset before the first hitobject after a break. + /// [Test] public void TestInputSingularWithBreak() => CreateModTest(new ModTestData { Mod = new OsuModAlternate(), - PassCondition = () => Player.ScoreProcessor.Combo.Value == 2, + PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2, Autoplay = false, Beatmap = new Beatmap { Breaks = new List { - new BreakPeriod(500, 2250), + new BreakPeriod(500, 2000), }, HitObjects = new List { @@ -138,16 +146,29 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods new HitCircle { StartTime = 2500, - Position = new Vector2(100), - } + Position = new Vector2(500, 100), + }, + new HitCircle + { + StartTime = 3000, + Position = new Vector2(500, 100), + }, } }, ReplayFrames = new List { + // first press to start alternate lock. new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton), new OsuReplayFrame(501, new Vector2(100)), - new OsuReplayFrame(2500, new Vector2(100), OsuAction.LeftButton), - new OsuReplayFrame(2501, new Vector2(100)), + // press same key after break but before hit object. + new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.LeftButton), + new OsuReplayFrame(2251, new Vector2(300, 100)), + // press same key at second hitobject and ensure it has been hit. + new OsuReplayFrame(2500, new Vector2(500, 100), OsuAction.LeftButton), + new OsuReplayFrame(2501, new Vector2(500, 100)), + // press same key at third hitobject and ensure it has been missed. + new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.LeftButton), + new OsuReplayFrame(3001, new Vector2(500, 100)), } }); } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs index 46b97dd23b..dfa28a537a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs @@ -2,21 +2,24 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; +using osu.Game.Utils; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModAlternate : Mod, IApplicableToDrawableRuleset, IApplicableToPlayer + public class OsuModAlternate : Mod, IApplicableToDrawableRuleset { public override string Name => @"Alternate"; public override string Acronym => @"AL"; @@ -26,9 +29,16 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Conversion; public override IconUsage? Icon => FontAwesome.Solid.Keyboard; - private double firstObjectValidJudgementTime; - private IBindable isBreakTime; private const double flash_duration = 1000; + + /// + /// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods). + /// + /// + /// This is different from in that the periods here end strictly at the first object after the break, rather than the break's end time. + /// + private PeriodTracker nonGameplayPeriods; + private OsuAction? lastActionPressed; private DrawableRuleset ruleset; @@ -39,29 +49,30 @@ namespace osu.Game.Rulesets.Osu.Mods ruleset = drawableRuleset; drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this)); - var firstHitObject = ruleset.Objects.FirstOrDefault(); - firstObjectValidJudgementTime = (firstHitObject?.StartTime ?? 0) - (firstHitObject?.HitWindows.WindowFor(HitResult.Meh) ?? 0); + var periods = new List(); + + if (drawableRuleset.Objects.Any()) + { + periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1)); + + foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks) + periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1)); + + static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh); + } + + nonGameplayPeriods = new PeriodTracker(periods); gameplayClock = drawableRuleset.FrameStableClock; } - public void ApplyToPlayer(Player player) - { - isBreakTime = player.IsBreakTime.GetBoundCopy(); - isBreakTime.ValueChanged += e => - { - if (e.NewValue) - lastActionPressed = null; - }; - } - private bool checkCorrectAction(OsuAction action) { - if (isBreakTime.Value) - return true; - - if (gameplayClock.CurrentTime < firstObjectValidJudgementTime) + if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime)) + { + lastActionPressed = null; return true; + } switch (action) {