// 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.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 { /// /// Indicates that the object has not been judged yet. /// [Description(@"")] [EnumMember(Value = "none")] [Order(14)] None, /// /// Indicates that the object has been judged as a miss. /// /// /// 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). /// [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, /// /// This is an optional timing window tighter than . /// /// /// By default, this does not give any bonus accuracy or score. /// To have it affect scoring, consider adding a nested bonus object. /// [Description(@"Perfect")] [EnumMember(Value = "perfect")] [Order(0)] Perfect, /// /// Indicates small tick miss. /// [EnumMember(Value = "small_tick_miss")] [Order(11)] SmallTickMiss, /// /// Indicates a small tick hit. /// [Description(@"S Tick")] [EnumMember(Value = "small_tick_hit")] [Order(7)] SmallTickHit, /// /// Indicates a large tick miss. /// [EnumMember(Value = "large_tick_miss")] [Description("-")] [Order(10)] LargeTickMiss, /// /// Indicates a large tick hit. /// [Description(@"L Tick")] [EnumMember(Value = "large_tick_hit")] [Order(6)] LargeTickHit, /// /// Indicates a small bonus. /// [Description("S Bonus")] [EnumMember(Value = "small_bonus")] [Order(9)] SmallBonus, /// /// Indicates a large bonus. /// [Description("L Bonus")] [EnumMember(Value = "large_bonus")] [Order(8)] LargeBonus, /// /// Indicates a miss that should be ignored for scoring purposes. /// [EnumMember(Value = "ignore_miss")] [Description("-")] [Order(13)] IgnoreMiss, /// /// Indicates a hit that should be ignored for scoring purposes. /// [EnumMember(Value = "ignore_hit")] [Order(12)] IgnoreHit, /// /// Indicates that a combo break should occur, but does not otherwise affect score. /// /// /// May be paired with . /// [EnumMember(Value = "combo_break")] [Order(15)] ComboBreak, /// /// A special judgement similar to that's used to increase the valuation of the final tick of a slider. /// [EnumMember(Value = "slider_tail_hit")] [Order(16)] SliderTailHit, /// /// 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. /// /// /// 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. /// [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 order = EnumExtensions.GetValuesInOrder().ToList(); /// /// Whether a increases the combo. /// public static bool IncreasesCombo(this HitResult result) => AffectsCombo(result) && IsHit(result); /// /// Whether a breaks the combo and resets it back to zero. /// public static bool BreaksCombo(this HitResult result) => AffectsCombo(result) && !IsHit(result); /// /// Whether a increases or breaks the combo. /// 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; } } /// /// Whether a affects the accuracy portion of the score. /// 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); } } /// /// Whether a is a non-tick and non-bonus result. /// 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); } } /// /// Whether a should be counted as a tick. /// 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; } } /// /// Whether a should be counted as bonus score. /// public static bool IsBonus(this HitResult result) { switch (result) { case HitResult.SmallBonus: case HitResult.LargeBonus: return true; default: return false; } } /// /// Whether a represents a miss of any type. /// /// /// Of note, both and return for . /// 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; } } /// /// Whether a represents a successful hit. /// /// /// Of note, both and return for . /// 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; } } /// /// Whether a is scorable. /// 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; } } /// /// An array of all scorable s. /// public static readonly HitResult[] ALL_TYPES = Enum.GetValues().Except(new[] { HitResult.LegacyComboIncrease }).ToArray(); /// /// Whether a is valid within a given range. /// /// The to check. /// The minimum . /// The maximum . /// Whether falls between and . 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; } /// /// Ordered index of a . Used for consistent order when displaying hit results to the user. /// /// The to get the index of. /// The index of . 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 }