// 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.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.UI; using static osu.Game.Input.Handlers.ReplayInputHandler; namespace osu.Game.Rulesets.Osu.Mods { public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset { public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray(); /// /// How early before a hitobject's start time to trigger a hit. /// private const float relax_leniency = 3; private bool wasHit; private bool wasLeft; private OsuInputManager osuInputManager; public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { // grab the input manager for future use. osuInputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager; osuInputManager.AllowUserPresses = false; } public void Update(Playfield playfield) { bool requiresHold = false; bool requiresHit = false; double time = playfield.Clock.CurrentTime; foreach (var h in playfield.HitObjectContainer.AliveObjects.OfType()) { // we are not yet close enough to the object. if (time < h.HitObject.StartTime - relax_leniency) break; // already hit or beyond the hittable end time. if (h.IsHit || (h.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime)) continue; switch (h) { case DrawableHitCircle circle: handleHitCircle(circle); break; case DrawableSlider slider: // Handles cases like "2B" beatmaps, where sliders may be overlapping and simply holding is not enough. if (!slider.HeadCircle.IsHit) handleHitCircle(slider.HeadCircle); requiresHold |= slider.Ball.IsHovered || h.IsHovered; break; case DrawableSpinner _: requiresHold = true; break; } } if (requiresHit) { addAction(false); addAction(true); } addAction(requiresHold); void handleHitCircle(DrawableHitCircle circle) { if (!circle.IsHovered) return; Debug.Assert(circle.HitObject.HitWindows != null); requiresHit |= circle.HitObject.HitWindows.CanBeHit(time - circle.HitObject.StartTime); } } private void addAction(bool hitting) { if (wasHit == hitting) return; wasHit = hitting; var state = new ReplayState { PressedActions = new List() }; if (hitting) { state.PressedActions.Add(wasLeft ? OsuAction.LeftButton : OsuAction.RightButton); wasLeft = !wasLeft; } state.Apply(osuInputManager.CurrentState, osuInputManager); } } }