// 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.Beatmaps; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring { /// /// A structure containing timing data for hit window based gameplay. /// public class HitWindows { private static readonly DifficultyRange[] base_ranges = { new DifficultyRange(HitResult.Perfect, 22.4D, 19.4D, 13.9D), new DifficultyRange(HitResult.Great, 64, 49, 34), new DifficultyRange(HitResult.Good, 97, 82, 67), new DifficultyRange(HitResult.Ok, 127, 112, 97), new DifficultyRange(HitResult.Meh, 151, 136, 121), new DifficultyRange(HitResult.Miss, 188, 173, 158), }; private double perfect; private double great; private double good; private double ok; private double meh; private double miss; /// /// An empty with only and . /// No time values are provided (meaning instantaneous hit or miss). /// public static HitWindows Empty => new EmptyHitWindows(); public HitWindows() { Debug.Assert(GetRanges().Any(r => r.Result == HitResult.Miss), $"{nameof(GetRanges)} should always contain {nameof(HitResult.Miss)}"); Debug.Assert(GetRanges().Any(r => r.Result != HitResult.Miss), $"{nameof(GetRanges)} should always contain at least one result type other than {nameof(HitResult.Miss)}."); } /// /// Retrieves the with the largest hit window that produces a successful hit. /// /// The lowest allowed successful . protected HitResult LowestSuccessfulHitResult() { for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result) { if (IsHitResultAllowed(result)) return result; } return HitResult.None; } /// /// Retrieves a mapping of s to their timing windows for all allowed s. /// public IEnumerable<(HitResult result, double length)> GetAllAvailableWindows() { for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result) { if (IsHitResultAllowed(result)) yield return (result, WindowFor(result)); } } /// /// Check whether it is possible to achieve the provided . /// /// The result type to check. /// Whether the can be achieved. public virtual bool IsHitResultAllowed(HitResult result) => true; /// /// Sets hit windows with values that correspond to a difficulty parameter. /// /// The parameter. public void SetDifficulty(double difficulty) { foreach (var range in GetRanges()) { double value = IBeatmapDifficultyInfo.DifficultyRange(difficulty, (range.Min, range.Average, range.Max)); switch (range.Result) { case HitResult.Miss: miss = value; break; case HitResult.Meh: meh = value; break; case HitResult.Ok: ok = value; break; case HitResult.Good: good = value; break; case HitResult.Great: great = value; break; case HitResult.Perfect: perfect = value; break; } } } /// /// Retrieves the for a time offset. /// /// The time offset. /// The hit result, or if doesn't result in a judgement. public HitResult ResultFor(double timeOffset) { timeOffset = Math.Abs(timeOffset); for (var result = HitResult.Perfect; result >= HitResult.Miss; --result) { if (IsHitResultAllowed(result) && timeOffset <= WindowFor(result)) return result; } return HitResult.None; } /// /// Retrieves the hit window for a . /// This is the number of +/- milliseconds allowed for the requested result (so the actual hittable range is double this). /// /// The expected . /// One half of the hit window for . public double WindowFor(HitResult result) { switch (result) { case HitResult.Perfect: return perfect; case HitResult.Great: return great; case HitResult.Good: return good; case HitResult.Ok: return ok; case HitResult.Meh: return meh; case HitResult.Miss: return miss; default: throw new ArgumentException("Unknown enum member", nameof(result)); } } /// /// Given a time offset, whether the can ever be hit in the future with a non- result. /// This happens if is less than what is required for . /// /// The time offset. /// Whether the can be hit at any point in the future from this time offset. public bool CanBeHit(double timeOffset) => timeOffset <= WindowFor(LowestSuccessfulHitResult()); /// /// Retrieve a valid list of s representing hit windows. /// Defaults are provided but can be overridden to customise for a ruleset. /// protected virtual DifficultyRange[] GetRanges() => base_ranges; public class EmptyHitWindows : HitWindows { private static readonly DifficultyRange[] ranges = { new DifficultyRange(HitResult.Perfect, 0, 0, 0), new DifficultyRange(HitResult.Miss, 0, 0, 0), }; public override bool IsHitResultAllowed(HitResult result) { switch (result) { case HitResult.Perfect: case HitResult.Miss: return true; } return false; } protected override DifficultyRange[] GetRanges() => ranges; } } public struct DifficultyRange { public readonly HitResult Result; public double Min; public double Average; public double Max; public DifficultyRange(HitResult result, double min, double average, double max) { Result = result; Min = min; Average = average; Max = max; } } }