// Copyright (c) ppy Pty Ltd <[email protected]>. 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.ComponentModel; using System.Diagnostics; using System.Linq; using System.Runtime.Serialization; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Utils; namespace osu.Game.Rulesets.Scoring { [HasOrderedElements] public enum HitResult { /// <summary> /// Indicates that the object has not been judged yet. /// </summary> [Description(@"")] [EnumMember(Value = "none")] [Order(15)] None, /// <summary> /// Indicates that the object has been judged as a miss. /// </summary> /// <remarks> /// This miss window should determine how early a hit can be before it is considered for judgement (as opposed to being ignored as /// "too far in the future"). It should also define when a forced miss should be triggered (as a result of no user input in time). /// </remarks> [Description(@"Miss")] [EnumMember(Value = "miss")] [Order(5)] Miss, [Description(@"Meh")] [EnumMember(Value = "meh")] [Order(4)] Meh, [Description(@"OK")] [EnumMember(Value = "ok")] [Order(3)] Ok, [Description(@"Good")] [EnumMember(Value = "good")] [Order(2)] Good, [Description(@"Great")] [EnumMember(Value = "great")] [Order(1)] Great, /// <summary> /// This is an optional timing window tighter than <see cref="Great"/>. /// </summary> /// <remarks> /// By default, this does not give any bonus accuracy or score. /// To have it affect scoring, consider adding a nested bonus object. /// </remarks> [Description(@"Perfect")] [EnumMember(Value = "perfect")] [Order(0)] Perfect, /// <summary> /// Indicates small tick miss. /// </summary> [EnumMember(Value = "small_tick_miss")] [Order(12)] SmallTickMiss, /// <summary> /// Indicates a small tick hit. /// </summary> [Description(@"S Tick")] [EnumMember(Value = "small_tick_hit")] [Order(7)] SmallTickHit, /// <summary> /// Indicates a large tick miss. /// </summary> [EnumMember(Value = "large_tick_miss")] [Order(11)] LargeTickMiss, /// <summary> /// Indicates a large tick hit. /// </summary> [Description(@"L Tick")] [EnumMember(Value = "large_tick_hit")] [Order(6)] LargeTickHit, /// <summary> /// Indicates a small bonus. /// </summary> [Description("S Bonus")] [EnumMember(Value = "small_bonus")] [Order(10)] SmallBonus, /// <summary> /// Indicates a large bonus. /// </summary> [Description("L Bonus")] [EnumMember(Value = "large_bonus")] [Order(9)] LargeBonus, /// <summary> /// Indicates a miss that should be ignored for scoring purposes. /// </summary> [EnumMember(Value = "ignore_miss")] [Order(14)] IgnoreMiss, /// <summary> /// Indicates a hit that should be ignored for scoring purposes. /// </summary> [EnumMember(Value = "ignore_hit")] [Order(13)] IgnoreHit, /// <summary> /// Indicates that a combo break should occur, but does not otherwise affect score. /// </summary> /// <remarks> /// May be paired with <see cref="IgnoreHit"/>. /// </remarks> [EnumMember(Value = "combo_break")] [Order(16)] ComboBreak, /// <summary> /// A special tick judgement to increase the valuation of the final tick of a slider. /// The default minimum result is <see cref="IgnoreMiss"/>, but may be overridden to <see cref="LargeTickMiss"/>. /// </summary> [EnumMember(Value = "slider_tail_hit")] [Order(8)] SliderTailHit, /// <summary> /// A special result used as a padding value for legacy rulesets. It is a hit type and affects combo, but does not affect the base score (does not affect accuracy). /// /// DO NOT USE FOR ANYTHING EVER. /// </summary> /// <remarks> /// This is used when dealing with legacy scores, which historically only have counts stored for 300/100/50/miss. /// For these scores, we pad the hit statistics with `LegacyComboIncrease` to meet the correct max combo for the score. /// </remarks> [EnumMember(Value = "legacy_combo_increase")] [Order(99)] [Obsolete("Do not use.")] LegacyComboIncrease = 99 } #pragma warning disable CS0618 public static class HitResultExtensions { private static readonly IList<HitResult> order = EnumExtensions.GetValuesInOrder<HitResult>().ToList(); /// <summary> /// Whether a <see cref="HitResult"/> increases the combo. /// </summary> public static bool IncreasesCombo(this HitResult result) => AffectsCombo(result) && IsHit(result); /// <summary> /// Whether a <see cref="HitResult"/> breaks the combo and resets it back to zero. /// </summary> public static bool BreaksCombo(this HitResult result) => AffectsCombo(result) && !IsHit(result); /// <summary> /// Whether a <see cref="HitResult"/> increases or breaks the combo. /// </summary> public static bool AffectsCombo(this HitResult result) { switch (result) { case HitResult.Miss: case HitResult.Meh: case HitResult.Ok: case HitResult.Good: case HitResult.Great: case HitResult.Perfect: case HitResult.LargeTickHit: case HitResult.LargeTickMiss: case HitResult.LegacyComboIncrease: case HitResult.ComboBreak: case HitResult.SliderTailHit: return true; default: return false; } } /// <summary> /// Whether a <see cref="HitResult"/> affects the accuracy portion of the score. /// </summary> public static bool AffectsAccuracy(this HitResult result) { switch (result) { // LegacyComboIncrease is a special non-gameplay type which is neither a basic, tick, bonus, or accuracy-affecting result. case HitResult.LegacyComboIncrease: return false; // ComboBreak is a special type that only affects combo. It cannot be considered as basic, tick, bonus, or accuracy-affecting. case HitResult.ComboBreak: return false; default: return IsScorable(result) && !IsBonus(result); } } /// <summary> /// Whether a <see cref="HitResult"/> is a non-tick and non-bonus result. /// </summary> public static bool IsBasic(this HitResult result) { switch (result) { // LegacyComboIncrease is a special non-gameplay type which is neither a basic, tick, bonus, or accuracy-affecting result. case HitResult.LegacyComboIncrease: return false; // ComboBreak is a special type that only affects combo. It cannot be considered as basic, tick, bonus, or accuracy-affecting. case HitResult.ComboBreak: return false; default: return IsScorable(result) && !IsTick(result) && !IsBonus(result); } } /// <summary> /// Whether a <see cref="HitResult"/> should be counted as a tick. /// </summary> public static bool IsTick(this HitResult result) { switch (result) { case HitResult.LargeTickHit: case HitResult.LargeTickMiss: case HitResult.SmallTickHit: case HitResult.SmallTickMiss: case HitResult.SliderTailHit: return true; default: return false; } } /// <summary> /// Whether a <see cref="HitResult"/> should be counted as bonus score. /// </summary> public static bool IsBonus(this HitResult result) { switch (result) { case HitResult.SmallBonus: case HitResult.LargeBonus: return true; default: return false; } } /// <summary> /// Whether a <see cref="HitResult"/> represents a miss of any type. /// </summary> /// <remarks> /// Of note, both <see cref="IsMiss"/> and <see cref="IsHit"/> return <see langword="false"/> for <see cref="HitResult.None"/>. /// </remarks> public static bool IsMiss(this HitResult result) { switch (result) { case HitResult.IgnoreMiss: case HitResult.Miss: case HitResult.SmallTickMiss: case HitResult.LargeTickMiss: case HitResult.ComboBreak: return true; default: return false; } } /// <summary> /// Whether a <see cref="HitResult"/> represents a successful hit. /// </summary> /// <remarks> /// Of note, both <see cref="IsMiss"/> and <see cref="IsHit"/> return <see langword="false"/> for <see cref="HitResult.None"/>. /// </remarks> public static bool IsHit(this HitResult result) { switch (result) { case HitResult.None: case HitResult.IgnoreMiss: case HitResult.Miss: case HitResult.SmallTickMiss: case HitResult.LargeTickMiss: case HitResult.ComboBreak: return false; default: return true; } } /// <summary> /// Whether a <see cref="HitResult"/> is scorable. /// </summary> public static bool IsScorable(this HitResult result) { switch (result) { // LegacyComboIncrease is not actually scorable (in terms of usable by rulesets for that purpose), but needs to be defined as such to be correctly included in statistics output. case HitResult.LegacyComboIncrease: return true; // ComboBreak is its own type that affects score via combo. case HitResult.ComboBreak: return true; case HitResult.SliderTailHit: return true; default: // Note that IgnoreHit and IgnoreMiss are excluded as they do not affect score. return result >= HitResult.Miss && result < HitResult.IgnoreMiss; } } /// <summary> /// An array of all scorable <see cref="HitResult"/>s. /// </summary> public static readonly HitResult[] ALL_TYPES = Enum.GetValues<HitResult>().Except(new[] { HitResult.LegacyComboIncrease }).ToArray(); /// <summary> /// Whether a <see cref="HitResult"/> is valid within a given <see cref="HitResult"/> range. /// </summary> /// <param name="result">The <see cref="HitResult"/> to check.</param> /// <param name="minResult">The minimum <see cref="HitResult"/>.</param> /// <param name="maxResult">The maximum <see cref="HitResult"/>.</param> /// <returns>Whether <see cref="HitResult"/> falls between <paramref name="minResult"/> and <paramref name="maxResult"/>.</returns> public static bool IsValidHitResult(this HitResult result, HitResult minResult, HitResult maxResult) { if (result == HitResult.None) return false; if (result == minResult || result == maxResult) return true; Debug.Assert(minResult <= maxResult); return result > minResult && result < maxResult; } /// <summary> /// Ordered index of a <see cref="HitResult"/>. Used for consistent order when displaying hit results to the user. /// </summary> /// <param name="result">The <see cref="HitResult"/> to get the index of.</param> /// <returns>The index of <paramref name="result"/>.</returns> public static int GetIndexForOrderedDisplay(this HitResult result) => order.IndexOf(result); public static void ValidateHitResultPair(HitResult maxResult, HitResult minResult) { if (maxResult == HitResult.None || !IsHit(maxResult)) throw new ArgumentOutOfRangeException(nameof(maxResult), $"{maxResult} is not a valid maximum judgement result."); if (minResult == HitResult.None || IsHit(minResult)) throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum judgement result."); if (maxResult == HitResult.IgnoreHit && minResult is not (HitResult.IgnoreMiss or HitResult.ComboBreak)) throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement."); if (maxResult.IsBonus() && minResult != HitResult.IgnoreMiss) throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.IgnoreMiss} is the only valid minimum result for a {maxResult} judgement."); if (minResult == HitResult.IgnoreMiss) return; if (maxResult == HitResult.SliderTailHit && minResult != HitResult.LargeTickMiss) throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.LargeTickMiss} is the only valid minimum result for a {maxResult} judgement."); if (maxResult == HitResult.LargeTickHit && minResult != HitResult.LargeTickMiss) throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.LargeTickMiss} is the only valid minimum result for a {maxResult} judgement."); if (maxResult == HitResult.SmallTickHit && minResult != HitResult.SmallTickMiss) throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.SmallTickMiss} is the only valid minimum result for a {maxResult} judgement."); if (maxResult.IsBasic() && minResult != HitResult.Miss) throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.Miss} is the only valid minimum result for a {maxResult} judgement."); } } #pragma warning restore CS0618 }