diff --git a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs index e22ad13451..0faf509136 100644 --- a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs +++ b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs @@ -1,8 +1,13 @@ // Copyright (c) ppy Pty Ltd . 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.Diagnostics; +using System.Linq; using osu.Framework.Input.Events; +using osu.Game.Rulesets.Mods; +using osuTK.Input; namespace osu.Game.Overlays.Mods.Input { @@ -11,6 +16,65 @@ namespace osu.Game.Overlays.Mods.Input /// public class ClassicModHotkeyHandler : IModHotkeyHandler { - public bool HandleHotkeyPressed(KeyDownEvent e, IEnumerable availableMods) => false; // TODO + private static readonly Dictionary mod_type_lookup = new Dictionary + { + [Key.Q] = new[] { typeof(ModEasy) }, + [Key.W] = new[] { typeof(ModNoFail) }, + [Key.E] = new[] { typeof(ModHalfTime) }, + [Key.A] = new[] { typeof(ModHardRock) }, + [Key.S] = new[] { typeof(ModSuddenDeath), typeof(ModPerfect) }, + [Key.D] = new[] { typeof(ModDoubleTime), typeof(ModNightcore) }, + [Key.F] = new[] { typeof(ModHidden) }, + [Key.G] = new[] { typeof(ModFlashlight) }, + [Key.Z] = new[] { typeof(ModRelax) }, + [Key.V] = new[] { typeof(ModAutoplay), typeof(ModCinema) } + }; + + public bool HandleHotkeyPressed(KeyDownEvent e, IEnumerable availableMods) + { + if (!mod_type_lookup.TryGetValue(e.Key, out var typesToMatch)) + return false; + + var matchingMods = availableMods.Where(modState => matches(modState, typesToMatch)).ToArray(); + + if (matchingMods.Length == 0) + return false; + + if (matchingMods.Length == 1) + { + matchingMods.Single().Active.Toggle(); + return true; + } + + // we're assuming that only one mod from the group can be active at a time. + // this is mostly ensured by `IncompatibleMods` definitions, but let's make sure just in case. + Debug.Assert(matchingMods.Count(modState => modState.Active.Value) <= 1); + int currentSelectedIndex = Array.FindIndex(matchingMods, modState => modState.Active.Value); + + // `FindIndex` will return -1 if it doesn't find the item. + // this is convenient in the forward direction, since if we add 1 then we'll end up at the first item, + // but less so in the backwards direction. + // for convenience, detect this situation and set the index to one index past the last item. + // this makes it so that if we subtract 1 then we'll end up at the last item again. + if (currentSelectedIndex < 0 && e.ShiftPressed) + currentSelectedIndex = matchingMods.Length; + + int indexToSelect = e.ShiftPressed ? currentSelectedIndex - 1 : currentSelectedIndex + 1; + + // `currentSelectedIndex` and `indexToSelect` can both be equal to -1 or `matchingMods.Length`. + // if the former is beyond array range, it means nothing was previously selected and so there's nothing to deselect. + // if the latter is beyond array range, it means that either the previous selection was first and we're going backwards, + // or it was last and we're going forwards. + // in either case there is nothing to select. + if (currentSelectedIndex >= 0 && currentSelectedIndex <= matchingMods.Length - 1) + matchingMods[currentSelectedIndex].Active.Value = false; + if (indexToSelect >= 0 && indexToSelect <= matchingMods.Length - 1) + matchingMods[indexToSelect].Active.Value = true; + + return true; + } + + private static bool matches(ModState modState, Type[] typesToMatch) + => typesToMatch.Any(typeToMatch => typeToMatch.IsInstanceOfType(modState.Mod)); } }