mirror of
https://github.com/ppy/osu.git
synced 2025-01-15 12:42:54 +08:00
Merge pull request #22746 from OpenSauce04/taiko-single-tap
Implement Single Tap mod for Taiko
This commit is contained in:
commit
6161192c65
212
osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSingleTap.cs
Normal file
212
osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSingleTap.cs
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
// 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.Replays;
|
||||||
|
using osu.Game.Rulesets.Taiko.Mods;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.Replays;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
||||||
|
{
|
||||||
|
public partial class TestSceneTaikoModSingleTap : TaikoModTestScene
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestInputAlternate() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new TaikoModSingleTap(),
|
||||||
|
Autoplay = false,
|
||||||
|
Beatmap = new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new Hit
|
||||||
|
{
|
||||||
|
StartTime = 100,
|
||||||
|
Type = HitType.Rim
|
||||||
|
},
|
||||||
|
new Hit
|
||||||
|
{
|
||||||
|
StartTime = 300,
|
||||||
|
Type = HitType.Rim
|
||||||
|
},
|
||||||
|
new Hit
|
||||||
|
{
|
||||||
|
StartTime = 500,
|
||||||
|
Type = HitType.Rim
|
||||||
|
},
|
||||||
|
new Hit
|
||||||
|
{
|
||||||
|
StartTime = 700,
|
||||||
|
Type = HitType.Rim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReplayFrames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new TaikoReplayFrame(100, TaikoAction.RightRim),
|
||||||
|
new TaikoReplayFrame(120),
|
||||||
|
new TaikoReplayFrame(300, TaikoAction.LeftRim),
|
||||||
|
new TaikoReplayFrame(320),
|
||||||
|
new TaikoReplayFrame(500, TaikoAction.RightRim),
|
||||||
|
new TaikoReplayFrame(520),
|
||||||
|
new TaikoReplayFrame(700, TaikoAction.LeftRim),
|
||||||
|
new TaikoReplayFrame(720),
|
||||||
|
},
|
||||||
|
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 1
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInputSameKey() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new TaikoModSingleTap(),
|
||||||
|
Autoplay = false,
|
||||||
|
Beatmap = new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new Hit
|
||||||
|
{
|
||||||
|
StartTime = 100,
|
||||||
|
Type = HitType.Rim
|
||||||
|
},
|
||||||
|
new Hit
|
||||||
|
{
|
||||||
|
StartTime = 300,
|
||||||
|
Type = HitType.Rim
|
||||||
|
},
|
||||||
|
new Hit
|
||||||
|
{
|
||||||
|
StartTime = 500,
|
||||||
|
Type = HitType.Rim
|
||||||
|
},
|
||||||
|
new Hit
|
||||||
|
{
|
||||||
|
StartTime = 700,
|
||||||
|
Type = HitType.Rim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReplayFrames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new TaikoReplayFrame(100, TaikoAction.RightRim),
|
||||||
|
new TaikoReplayFrame(120),
|
||||||
|
new TaikoReplayFrame(300, TaikoAction.RightRim),
|
||||||
|
new TaikoReplayFrame(320),
|
||||||
|
new TaikoReplayFrame(500, TaikoAction.RightRim),
|
||||||
|
new TaikoReplayFrame(520),
|
||||||
|
new TaikoReplayFrame(700, TaikoAction.RightRim),
|
||||||
|
new TaikoReplayFrame(720),
|
||||||
|
},
|
||||||
|
PassCondition = () => Player.ScoreProcessor.Combo.Value == 4
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInputIntro() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new TaikoModSingleTap(),
|
||||||
|
Autoplay = false,
|
||||||
|
Beatmap = new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new Hit
|
||||||
|
{
|
||||||
|
StartTime = 100,
|
||||||
|
Type = HitType.Rim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReplayFrames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new TaikoReplayFrame(0, TaikoAction.RightRim),
|
||||||
|
new TaikoReplayFrame(20),
|
||||||
|
new TaikoReplayFrame(100, TaikoAction.LeftRim),
|
||||||
|
new TaikoReplayFrame(120),
|
||||||
|
},
|
||||||
|
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInputStrong() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new TaikoModSingleTap(),
|
||||||
|
Autoplay = false,
|
||||||
|
Beatmap = new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new Hit
|
||||||
|
{
|
||||||
|
StartTime = 100,
|
||||||
|
Type = HitType.Rim
|
||||||
|
},
|
||||||
|
new Hit
|
||||||
|
{
|
||||||
|
StartTime = 300,
|
||||||
|
Type = HitType.Rim,
|
||||||
|
IsStrong = true
|
||||||
|
},
|
||||||
|
new Hit
|
||||||
|
{
|
||||||
|
StartTime = 500,
|
||||||
|
Type = HitType.Rim,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReplayFrames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new TaikoReplayFrame(100, TaikoAction.RightRim),
|
||||||
|
new TaikoReplayFrame(120),
|
||||||
|
new TaikoReplayFrame(300, TaikoAction.LeftRim),
|
||||||
|
new TaikoReplayFrame(320),
|
||||||
|
new TaikoReplayFrame(500, TaikoAction.LeftRim),
|
||||||
|
new TaikoReplayFrame(520),
|
||||||
|
},
|
||||||
|
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInputBreaks() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new TaikoModSingleTap(),
|
||||||
|
Autoplay = false,
|
||||||
|
Beatmap = new Beatmap
|
||||||
|
{
|
||||||
|
Breaks = new List<BreakPeriod>
|
||||||
|
{
|
||||||
|
new BreakPeriod(100, 1600),
|
||||||
|
},
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new Hit
|
||||||
|
{
|
||||||
|
StartTime = 100,
|
||||||
|
Type = HitType.Rim
|
||||||
|
},
|
||||||
|
new Hit
|
||||||
|
{
|
||||||
|
StartTime = 2000,
|
||||||
|
Type = HitType.Rim,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReplayFrames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new TaikoReplayFrame(100, TaikoAction.RightRim),
|
||||||
|
new TaikoReplayFrame(120),
|
||||||
|
// Press different key after break but before hit object.
|
||||||
|
new TaikoReplayFrame(1900, TaikoAction.LeftRim),
|
||||||
|
new TaikoReplayFrame(1820),
|
||||||
|
// Press original key at second hitobject and ensure it has been hit.
|
||||||
|
new TaikoReplayFrame(2000, TaikoAction.RightRim),
|
||||||
|
new TaikoReplayFrame(2020),
|
||||||
|
},
|
||||||
|
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Taiko.Replays;
|
using osu.Game.Rulesets.Taiko.Replays;
|
||||||
@ -12,5 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
|||||||
{
|
{
|
||||||
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||||
=> new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" });
|
=> new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" });
|
||||||
|
|
||||||
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
@ -13,5 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
|||||||
{
|
{
|
||||||
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||||
=> new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" });
|
=> new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" });
|
||||||
|
|
||||||
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
@ -9,5 +11,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
|||||||
public class TaikoModRelax : ModRelax
|
public class TaikoModRelax : ModRelax
|
||||||
{
|
{
|
||||||
public override LocalisableString Description => @"No ninja-like spinners, demanding drumrolls or unexpected katus.";
|
public override LocalisableString Description => @"No ninja-like spinners, demanding drumrolls or unexpected katus.";
|
||||||
|
|
||||||
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
127
osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs
Normal file
127
osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
// 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 osu.Framework.Localisation;
|
||||||
|
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.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Mods
|
||||||
|
{
|
||||||
|
public partial class TaikoModSingleTap : Mod, IApplicableToDrawableRuleset<TaikoHitObject>, IUpdatableByPlayfield
|
||||||
|
{
|
||||||
|
public override string Name => @"Single Tap";
|
||||||
|
public override string Acronym => @"SG";
|
||||||
|
public override LocalisableString Description => @"One key for dons, one key for kats.";
|
||||||
|
|
||||||
|
public override double ScoreMultiplier => 1.0;
|
||||||
|
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(TaikoModCinema) };
|
||||||
|
public override ModType Type => ModType.Conversion;
|
||||||
|
|
||||||
|
private DrawableTaikoRuleset ruleset = null!;
|
||||||
|
|
||||||
|
private TaikoPlayfield playfield { get; set; } = null!;
|
||||||
|
|
||||||
|
private TaikoAction? lastAcceptedCentreAction { get; set; }
|
||||||
|
private TaikoAction? lastAcceptedRimAction { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A tracker for periods where single tap should not be enforced (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<TaikoHitObject> drawableRuleset)
|
||||||
|
{
|
||||||
|
ruleset = (DrawableTaikoRuleset)drawableRuleset;
|
||||||
|
ruleset.KeyBindingInputManager.Add(new InputInterceptor(this));
|
||||||
|
playfield = (TaikoPlayfield)ruleset.Playfield;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(Playfield playfield)
|
||||||
|
{
|
||||||
|
if (!nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime)) return;
|
||||||
|
|
||||||
|
lastAcceptedCentreAction = null;
|
||||||
|
lastAcceptedRimAction = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool checkCorrectAction(TaikoAction action)
|
||||||
|
{
|
||||||
|
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// If next hit object is strong, allow usage of all actions. Strong drumrolls are ignored in this check.
|
||||||
|
if (playfield.HitObjectContainer.AliveObjects.FirstOrDefault(h => h.Result?.HasResult != true)?.HitObject is TaikoStrongableHitObject hitObject
|
||||||
|
&& hitObject.IsStrong
|
||||||
|
&& hitObject is not DrumRoll)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if ((action == TaikoAction.LeftCentre || action == TaikoAction.RightCentre)
|
||||||
|
&& (lastAcceptedCentreAction == null || lastAcceptedCentreAction == action))
|
||||||
|
{
|
||||||
|
lastAcceptedCentreAction = action;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((action == TaikoAction.LeftRim || action == TaikoAction.RightRim)
|
||||||
|
&& (lastAcceptedRimAction == null || lastAcceptedRimAction == action))
|
||||||
|
{
|
||||||
|
lastAcceptedRimAction = action;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class InputInterceptor : Component, IKeyBindingHandler<TaikoAction>
|
||||||
|
{
|
||||||
|
private readonly TaikoModSingleTap mod;
|
||||||
|
|
||||||
|
public InputInterceptor(TaikoModSingleTap mod)
|
||||||
|
{
|
||||||
|
this.mod = mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
|
||||||
|
// if the pressed action is incorrect, block it from reaching gameplay.
|
||||||
|
=> !mod.checkCorrectAction(e.Action);
|
||||||
|
|
||||||
|
public void OnReleased(KeyBindingReleaseEvent<TaikoAction> e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -158,6 +158,7 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
new TaikoModDifficultyAdjust(),
|
new TaikoModDifficultyAdjust(),
|
||||||
new TaikoModClassic(),
|
new TaikoModClassic(),
|
||||||
new TaikoModSwap(),
|
new TaikoModSwap(),
|
||||||
|
new TaikoModSingleTap(),
|
||||||
};
|
};
|
||||||
|
|
||||||
case ModType.Automation:
|
case ModType.Automation:
|
||||||
|
@ -33,6 +33,8 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
|
|
||||||
public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true);
|
public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true);
|
||||||
|
|
||||||
|
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
|
||||||
|
|
||||||
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
|
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
|
||||||
|
|
||||||
protected override bool UserScrollSpeedAdjustment => false;
|
protected override bool UserScrollSpeedAdjustment => false;
|
||||||
|
Loading…
Reference in New Issue
Block a user