Merge pull request #18017 from frenzibyte/improve-alternate-after-break

Improve "Alternate" mod to reset at first object after break
This commit is contained in:
Bartłomiej Dach
2022-05-02 13:00:56 +02:00
committed by GitHub
2 changed files with 83 additions and 51 deletions

View File

@ -16,31 +16,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{ {
public class TestSceneOsuModAlternate : OsuModTestScene 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<HitObject>
{
new HitCircle
{
StartTime = 1000,
Position = new Vector2(100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(200)),
new OsuReplayFrame(1000, new Vector2(100), OsuAction.LeftButton),
}
});
[Test] [Test]
public void TestInputAlternating() => CreateModTest(new ModTestData public void TestInputAlternating() => CreateModTest(new ModTestData
{ {
@ -116,17 +91,50 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
} }
}); });
/// <summary>
/// Ensures alternation is reset before the first hitobject after intro.
/// </summary>
[Test]
public void TestInputSingularAtIntro() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 1000,
Position = new Vector2(100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
// 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),
}
});
/// <summary>
/// Ensures alternation is reset before the first hitobject after a break.
/// </summary>
[Test] [Test]
public void TestInputSingularWithBreak() => CreateModTest(new ModTestData public void TestInputSingularWithBreak() => CreateModTest(new ModTestData
{ {
Mod = new OsuModAlternate(), Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2, PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2,
Autoplay = false, Autoplay = false,
Beatmap = new Beatmap Beatmap = new Beatmap
{ {
Breaks = new List<BreakPeriod> Breaks = new List<BreakPeriod>
{ {
new BreakPeriod(500, 2250), new BreakPeriod(500, 2000),
}, },
HitObjects = new List<HitObject> HitObjects = new List<HitObject>
{ {
@ -138,16 +146,29 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
new HitCircle new HitCircle
{ {
StartTime = 2500, StartTime = 2500,
Position = new Vector2(100), Position = new Vector2(500, 100),
} },
new HitCircle
{
StartTime = 3000,
Position = new Vector2(500, 100),
},
} }
}, },
ReplayFrames = new List<ReplayFrame> ReplayFrames = new List<ReplayFrame>
{ {
// first press to start alternate lock.
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton), new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)), new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(2500, new Vector2(100), OsuAction.LeftButton), // press same key after break but before hit object.
new OsuReplayFrame(2501, new Vector2(100)), 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)),
} }
}); });
} }

View File

@ -2,21 +2,24 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModAlternate : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer public class OsuModAlternate : Mod, IApplicableToDrawableRuleset<OsuHitObject>
{ {
public override string Name => @"Alternate"; public override string Name => @"Alternate";
public override string Acronym => @"AL"; public override string Acronym => @"AL";
@ -26,9 +29,16 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Conversion; public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => FontAwesome.Solid.Keyboard; public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
private double firstObjectValidJudgementTime;
private IBindable<bool> isBreakTime;
private const double flash_duration = 1000; private const double flash_duration = 1000;
/// <summary>
/// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
/// </summary>
/// <remarks>
/// This is different from <see cref="Player.IsBreakTime"/> in that the periods here end strictly at the first object after the break, rather than the break's end time.
/// </remarks>
private PeriodTracker nonGameplayPeriods;
private OsuAction? lastActionPressed; private OsuAction? lastActionPressed;
private DrawableRuleset<OsuHitObject> ruleset; private DrawableRuleset<OsuHitObject> ruleset;
@ -39,29 +49,30 @@ namespace osu.Game.Rulesets.Osu.Mods
ruleset = drawableRuleset; ruleset = drawableRuleset;
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this)); drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
var firstHitObject = ruleset.Objects.FirstOrDefault(); var periods = new List<Period>();
firstObjectValidJudgementTime = (firstHitObject?.StartTime ?? 0) - (firstHitObject?.HitWindows.WindowFor(HitResult.Meh) ?? 0);
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; 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) private bool checkCorrectAction(OsuAction action)
{ {
if (isBreakTime.Value) if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
return true; {
lastActionPressed = null;
if (gameplayClock.CurrentTime < firstObjectValidJudgementTime)
return true; return true;
}
switch (action) switch (action)
{ {