1
0
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:
Dan Balasescu 2023-03-09 14:00:30 +09:00 committed by GitHub
commit 6161192c65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 354 additions and 0 deletions

View 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
});
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View 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)
{
}
}
}
}

View File

@ -158,6 +158,7 @@ namespace osu.Game.Rulesets.Taiko
new TaikoModDifficultyAdjust(),
new TaikoModClassic(),
new TaikoModSwap(),
new TaikoModSingleTap(),
};
case ModType.Automation:

View File

@ -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;