// 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 osu.Game.Beatmaps; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Objects { public class HitWindows { private static readonly IReadOnlyDictionary base_ranges = new Dictionary { { HitResult.Perfect, (44.8, 38.8, 27.8) }, { HitResult.Great, (128, 98, 68) }, { HitResult.Good, (194, 164, 134) }, { HitResult.Ok, (254, 224, 194) }, { HitResult.Meh, (302, 272, 242) }, { HitResult.Miss, (376, 346, 316) }, }; /// /// Hit window for a result. /// public double Perfect { get; protected set; } /// /// Hit window for a result. /// public double Great { get; protected set; } /// /// Hit window for a result. /// public double Good { get; protected set; } /// /// Hit window for an result. /// public double Ok { get; protected set; } /// /// Hit window for a result. /// public double Meh { get; protected set; } /// /// Hit window for a result. /// public double Miss { get; protected set; } /// /// 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 half window timing for all allowed s. /// /// public IEnumerable<(HitResult result, double length)> GetAllAvailableHalfWindows() { for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result) { if (IsHitResultAllowed(result)) yield return (result, HalfWindowFor(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) { switch (result) { case HitResult.Perfect: case HitResult.Ok: return false; default: return true; } } /// /// Sets hit windows with values that correspond to a difficulty parameter. /// /// The parameter. public virtual void SetDifficulty(double difficulty) { Perfect = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Perfect]); Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]); Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]); Ok = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Ok]); Meh = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Meh]); Miss = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Miss]); } /// /// 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 <= HalfWindowFor(result)) return result; } return HitResult.None; } /// /// Retrieves half the hit window for a . /// This is useful if the hit window for one half of the hittable range of a is required. /// /// The expected . /// One half of the hit window for . public double HalfWindowFor(HitResult result) { switch (result) { case HitResult.Perfect: return Perfect / 2; case HitResult.Great: return Great / 2; case HitResult.Good: return Good / 2; case HitResult.Ok: return Ok / 2; case HitResult.Meh: return Meh / 2; case HitResult.Miss: return Miss / 2; default: throw new ArgumentException(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 <= HalfWindowFor(LowestSuccessfulHitResult()); } }