-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #19089 from tsunyoku/singletap-mod
Add "Single Tap" mod for osu! ruleset, abstract Alternate & Single Tap into InputBlockingMod
- Loading branch information
Showing
8 changed files
with
314 additions
and
106 deletions.
There are no files selected for viewing
175 changes: 175 additions & 0 deletions
175
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. | ||
// See the LICENCE file in the repository root for full licence text. | ||
|
||
using System.Collections.Generic; | ||
using NUnit.Framework; | ||
using osu.Game.Beatmaps; | ||
using osu.Game.Beatmaps.Timing; | ||
using osu.Game.Rulesets.Objects; | ||
using osu.Game.Rulesets.Osu.Mods; | ||
using osu.Game.Rulesets.Osu.Objects; | ||
using osu.Game.Rulesets.Osu.Replays; | ||
using osu.Game.Rulesets.Replays; | ||
using osuTK; | ||
|
||
namespace osu.Game.Rulesets.Osu.Tests.Mods | ||
{ | ||
public class TestSceneOsuModSingleTap : OsuModTestScene | ||
{ | ||
[Test] | ||
public void TestInputSingular() => CreateModTest(new ModTestData | ||
{ | ||
Mod = new OsuModSingleTap(), | ||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2, | ||
Autoplay = false, | ||
Beatmap = new Beatmap | ||
{ | ||
HitObjects = new List<HitObject> | ||
{ | ||
new HitCircle | ||
{ | ||
StartTime = 500, | ||
Position = new Vector2(100), | ||
}, | ||
new HitCircle | ||
{ | ||
StartTime = 1000, | ||
Position = new Vector2(200, 100), | ||
}, | ||
new HitCircle | ||
{ | ||
StartTime = 1500, | ||
Position = new Vector2(300, 100), | ||
}, | ||
new HitCircle | ||
{ | ||
StartTime = 2000, | ||
Position = new Vector2(400, 100), | ||
}, | ||
}, | ||
}, | ||
ReplayFrames = new List<ReplayFrame> | ||
{ | ||
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton), | ||
new OsuReplayFrame(501, new Vector2(100)), | ||
new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.LeftButton), | ||
} | ||
}); | ||
|
||
[Test] | ||
public void TestInputAlternating() => CreateModTest(new ModTestData | ||
{ | ||
Mod = new OsuModSingleTap(), | ||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 1, | ||
Autoplay = false, | ||
Beatmap = new Beatmap | ||
{ | ||
HitObjects = new List<HitObject> | ||
{ | ||
new HitCircle | ||
{ | ||
StartTime = 500, | ||
Position = new Vector2(100), | ||
}, | ||
new HitCircle | ||
{ | ||
StartTime = 1000, | ||
Position = new Vector2(200, 100), | ||
}, | ||
}, | ||
}, | ||
ReplayFrames = new List<ReplayFrame> | ||
{ | ||
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton), | ||
new OsuReplayFrame(501, new Vector2(100)), | ||
new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.RightButton), | ||
new OsuReplayFrame(1001, new Vector2(200, 100)), | ||
new OsuReplayFrame(1500, new Vector2(300, 100), OsuAction.LeftButton), | ||
new OsuReplayFrame(1501, new Vector2(300, 100)), | ||
new OsuReplayFrame(2000, new Vector2(400, 100), OsuAction.RightButton), | ||
new OsuReplayFrame(2001, new Vector2(400, 100)), | ||
} | ||
}); | ||
|
||
/// <summary> | ||
/// Ensures singletapping is reset before the first hitobject after intro. | ||
/// </summary> | ||
[Test] | ||
public void TestInputAlternatingAtIntro() => CreateModTest(new ModTestData | ||
{ | ||
Mod = new OsuModSingleTap(), | ||
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 different key at hitobject and ensure it has been hit. | ||
new OsuReplayFrame(1000, new Vector2(100), OsuAction.RightButton), | ||
} | ||
}); | ||
|
||
/// <summary> | ||
/// Ensures singletapping is reset before the first hitobject after a break. | ||
/// </summary> | ||
[Test] | ||
public void TestInputAlternatingWithBreak() => CreateModTest(new ModTestData | ||
{ | ||
Mod = new OsuModSingleTap(), | ||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2, | ||
Autoplay = false, | ||
Beatmap = new Beatmap | ||
{ | ||
Breaks = new List<BreakPeriod> | ||
{ | ||
new BreakPeriod(500, 2000), | ||
}, | ||
HitObjects = new List<HitObject> | ||
{ | ||
new HitCircle | ||
{ | ||
StartTime = 500, | ||
Position = new Vector2(100), | ||
}, | ||
new HitCircle | ||
{ | ||
StartTime = 2500, | ||
Position = new Vector2(500, 100), | ||
}, | ||
new HitCircle | ||
{ | ||
StartTime = 3000, | ||
Position = new Vector2(500, 100), | ||
}, | ||
} | ||
}, | ||
ReplayFrames = new List<ReplayFrame> | ||
{ | ||
// first press to start singletap lock. | ||
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton), | ||
new OsuReplayFrame(501, new Vector2(100)), | ||
// press different key after break but before hit object. | ||
new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.RightButton), | ||
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 different key at third hitobject and ensure it has been missed. | ||
new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.RightButton), | ||
new OsuReplayFrame(3001, new Vector2(500, 100)), | ||
} | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Graphics; | ||
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 abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset<OsuHitObject> | ||
{ | ||
public override double ScoreMultiplier => 1.0; | ||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(OsuModCinema) }; | ||
public override ModType Type => ModType.Conversion; | ||
|
||
private const double flash_duration = 1000; | ||
|
||
private DrawableRuleset<OsuHitObject> ruleset = null!; | ||
|
||
protected OsuAction? LastAcceptedAction { get; private set; } | ||
|
||
/// <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 = null!; | ||
|
||
private IFrameStableClock gameplayClock = null!; | ||
|
||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset) | ||
{ | ||
ruleset = drawableRuleset; | ||
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this)); | ||
|
||
var periods = new List<Period>(); | ||
|
||
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; | ||
} | ||
|
||
protected abstract bool CheckValidNewAction(OsuAction action); | ||
|
||
private bool checkCorrectAction(OsuAction action) | ||
{ | ||
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime)) | ||
{ | ||
LastAcceptedAction = null; | ||
return true; | ||
} | ||
|
||
switch (action) | ||
{ | ||
case OsuAction.LeftButton: | ||
case OsuAction.RightButton: | ||
break; | ||
|
||
// Any action which is not left or right button should be ignored. | ||
default: | ||
return true; | ||
} | ||
|
||
if (CheckValidNewAction(action)) | ||
{ | ||
LastAcceptedAction = action; | ||
return true; | ||
} | ||
|
||
ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint); | ||
return false; | ||
} | ||
|
||
private class InputInterceptor : Component, IKeyBindingHandler<OsuAction> | ||
{ | ||
private readonly InputBlockingMod mod; | ||
|
||
public InputInterceptor(InputBlockingMod mod) | ||
{ | ||
this.mod = mod; | ||
} | ||
|
||
public bool OnPressed(KeyBindingPressEvent<OsuAction> e) | ||
// if the pressed action is incorrect, block it from reaching gameplay. | ||
=> !mod.checkCorrectAction(e.Action); | ||
|
||
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e) | ||
{ | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.