mirror of
https://github.com/ppy/osu.git
synced 2025-01-15 06:33:20 +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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
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)
|
||||
=> 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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
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)
|
||||
=> 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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@ -9,5 +11,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
public class TaikoModRelax : ModRelax
|
||||
{
|
||||
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 TaikoModClassic(),
|
||||
new TaikoModSwap(),
|
||||
new TaikoModSingleTap(),
|
||||
};
|
||||
|
||||
case ModType.Automation:
|
||||
|
@ -33,6 +33,8 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true);
|
||||
|
||||
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
|
||||
|
||||
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
|
||||
|
||||
protected override bool UserScrollSpeedAdjustment => false;
|
||||
|
Loading…
Reference in New Issue
Block a user