1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 17:43:05 +08:00

Merge pull request #16701 from LeNitrous/mod-alternate-osu

Add "Alternate" mod for osu! ruleset
This commit is contained in:
Dean Herbert 2022-02-03 10:41:23 +09:00 committed by GitHub
commit 2731d8e3c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 292 additions and 2 deletions

View File

@ -0,0 +1,154 @@
// 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 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]
public void TestInputAlternating() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 4,
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.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)),
}
});
[Test]
public void TestInputSingular() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
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.LeftButton),
}
});
[Test]
public void TestInputSingularWithBreak() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
Autoplay = false,
Beatmap = new Beatmap
{
Breaks = new List<BreakPeriod>
{
new BreakPeriod(500, 2250),
},
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 2500,
Position = new Vector2(100),
}
}
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(2500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(2501, new Vector2(100)),
}
});
}
}

View File

@ -0,0 +1,106 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAlternate : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
{
public override string Name => @"Alternate";
public override string Acronym => @"AL";
public override string Description => @"Don't use the same key twice in a row!";
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay) };
public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
private double firstObjectValidJudgementTime;
private IBindable<bool> isBreakTime;
private const double flash_duration = 1000;
private OsuAction? lastActionPressed;
private DrawableRuleset<OsuHitObject> ruleset;
private IFrameStableClock gameplayClock;
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
ruleset = drawableRuleset;
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
var firstHitObject = ruleset.Objects.FirstOrDefault();
firstObjectValidJudgementTime = (firstHitObject?.StartTime ?? 0) - (firstHitObject?.HitWindows.WindowFor(HitResult.Meh) ?? 0);
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)
{
if (isBreakTime.Value)
return true;
if (gameplayClock.CurrentTime < firstObjectValidJudgementTime)
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 (lastActionPressed != action)
{
// User alternated correctly.
lastActionPressed = action;
return true;
}
ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
return false;
}
private class InputInterceptor : Component, IKeyBindingHandler<OsuAction>
{
private readonly OsuModAlternate mod;
public InputInterceptor(OsuModAlternate 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)
{
}
}
}
}

View File

@ -169,6 +169,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModClassic(),
new OsuModRandom(),
new OsuModMirror(),
new OsuModAlternate(),
};
case ModType.Automation:

View File

@ -5,8 +5,12 @@ using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays;
using osu.Game.Scoring;
namespace osu.Game.Tests.Visual
{
@ -50,18 +54,37 @@ namespace osu.Game.Tests.Visual
return CreateModPlayer(ruleset);
}
protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(AllowFail);
protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(currentTestData, AllowFail);
protected class ModTestPlayer : TestPlayer
{
private readonly bool allowFail;
private readonly ModTestData currentTestData;
protected override bool CheckModsAllowFailure() => allowFail;
public ModTestPlayer(bool allowFail)
public ModTestPlayer(ModTestData data, bool allowFail)
: base(false, false)
{
this.allowFail = allowFail;
currentTestData = data;
}
protected override void PrepareReplay()
{
if (currentTestData.Autoplay && currentTestData.ReplayFrames?.Count > 0)
throw new InvalidOperationException(@$"{nameof(ModTestData.Autoplay)} must be false when {nameof(ModTestData.ReplayFrames)} is specified.");
if (currentTestData.ReplayFrames != null)
{
DrawableRuleset?.SetReplayScore(new Score
{
Replay = new Replay { Frames = currentTestData.ReplayFrames },
ScoreInfo = new ScoreInfo { User = new APIUser { Username = @"Test" } },
});
}
base.PrepareReplay();
}
}
@ -72,6 +95,12 @@ namespace osu.Game.Tests.Visual
/// </summary>
public bool Autoplay = true;
/// <summary>
/// The frames to use for replay. <see cref="Autoplay"/> must be set to false.
/// </summary>
[CanBeNull]
public List<ReplayFrame> ReplayFrames;
/// <summary>
/// The beatmap for this test case.
/// </summary>