diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 8ccd9db513..af17789b59 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -24,9 +24,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty private int countOk; private int countMeh; private int countMiss; - private int countLargeTickMiss; + + /// + /// Missed slider ticks that includes missed reverse arrows + /// + private int countSliderTickMiss; + + /// + /// Amount of missed slider tails that don't break combo + /// private int countSliderEndsDropped; + /// + /// Estimated total amount of combo breaks + /// private double effectiveMissCount; public OsuPerformanceCalculator() @@ -46,12 +57,36 @@ namespace osu.Game.Rulesets.Osu.Difficulty countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); + + if (osuAttributes.SliderCount > 0) + { + // Consider that full combo is maximum combo minus dropped sliders since missed tails don't contribute to score combo but also don't break it + // In classic scores we can't know the amount of dropped sliders so we use 10% of all sliders on the map + double droppedSliderTailsAmount = usingClassicSliderAccuracy + ? 0.1 * osuAttributes.SliderCount + : countSliderEndsDropped; + + double fullComboThreshold = attributes.MaxCombo - droppedSliderTailsAmount; + + if (scoreMaxCombo < fullComboThreshold) + effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); + } + if (!usingClassicSliderAccuracy) { countSliderEndsDropped = osuAttributes.SliderCount - score.Statistics.GetValueOrDefault(HitResult.SliderTailHit); - countLargeTickMiss = score.Statistics.GetValueOrDefault(HitResult.LargeTickMiss); + countSliderTickMiss = score.Statistics.GetValueOrDefault(HitResult.LargeTickMiss); + + // Combine regular misses with tick misses since tick misses break combo as well + effectiveMissCount = Math.Min(effectiveMissCount, countSliderTickMiss + countMiss); } - effectiveMissCount = calculateEffectiveMissCount(osuAttributes); + else + { + // In classic scores there can't be more misses than a sum of all non-perfect judgements + effectiveMissCount = Math.Min(effectiveMissCount, totalImperfectHits); + } + + effectiveMissCount = Math.Max(countMiss, effectiveMissCount); double multiplier = PERFORMANCE_BASE_MULTIPLIER; @@ -131,11 +166,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (attributes.SliderCount > 0) { - double estimateSliderEndsDropped = usingClassicSliderAccuracy - ? Math.Clamp(Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders) - : Math.Min(countSliderEndsDropped + countLargeTickMiss, estimateDifficultSliders); + double estimateImproperlyFollowedDifficultSliders; - double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + attributes.SliderFactor; + if (usingClassicSliderAccuracy) + { + // When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders + int maximumPossibleDroppedSliders = totalImperfectHits; + estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders); + } + else + { + // We add tick misses here since they too mean that the player didn't follow the slider properly + // We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly + estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, estimateDifficultSliders); + } + + double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor; aimValue *= sliderNerfFactor; } @@ -257,29 +303,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty return flashlightValue; } - private double calculateEffectiveMissCount(OsuDifficultyAttributes attributes) - { - double comboBasedMissCount = 0.0; - - if (attributes.SliderCount > 0) - { - // Guess the number of misses + slider breaks from combo - double fullComboThreshold = usingClassicSliderAccuracy ? attributes.MaxCombo - 0.1 * attributes.SliderCount : attributes.MaxCombo - countSliderEndsDropped; - if (scoreMaxCombo < fullComboThreshold) - comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); - } - - // Clamp miss count to maximum amount of possible breaks - comboBasedMissCount = usingClassicSliderAccuracy ? Math.Min(comboBasedMissCount, countOk + countMeh + countMiss) : Math.Min(comboBasedMissCount, countLargeTickMiss + countMiss); - - return Math.Max(countMiss, comboBasedMissCount); - } - // Miss penalty assumes that a player will miss on the hardest parts of a map, // so we use the amount of relatively difficult sections to adjust miss penalty // to make it more punishing on maps with lower amount of hard sections. private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1); private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0); private int totalHits => countGreat + countOk + countMeh + countMiss; + private int totalImperfectHits => countOk + countMeh + countMiss; } }