diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index e2c9ea77e3..c6395b24e6 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty; -using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -14,9 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public class OsuPerformanceCalculator : PerformanceCalculator { - public const double PERFORMANCE_BASE_MULTIPLIER = 1.15; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. - - private bool usingClassicSliderAccuracy; + public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. private double accuracy; private int scoreMaxCombo; @@ -25,19 +22,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty private int countMeh; private int countMiss; - /// - /// Missed slider ticks that includes missed reverse arrows. Will only be correct on non-classic scores - /// - private int countSliderTickMiss; - - /// - /// Amount of missed slider tails that don't break combo. Will only be correct on non-classic scores - /// - private int countSliderEndsDropped; - - /// - /// Estimated total amount of combo breaks - /// private double effectiveMissCount; public OsuPerformanceCalculator() @@ -49,45 +33,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty { var osuAttributes = (OsuDifficultyAttributes)attributes; - usingClassicSliderAccuracy = score.Mods.OfType().Any(m => m.NoSliderHeadAccuracy.Value); - scoreMaxCombo = score.MaxCombo; countGreat = score.Statistics.GetValueOrDefault(HitResult.Great); countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); - countSliderEndsDropped = osuAttributes.SliderCount - score.Statistics.GetValueOrDefault(HitResult.SliderTailHit); - countSliderTickMiss = score.Statistics.GetValueOrDefault(HitResult.LargeTickMiss); - - accuracy = calculateAccuracy(countGreat, countOk, countMeh, countMiss, totalHits); - - if (osuAttributes.SliderCount > 0) - { - if (usingClassicSliderAccuracy) - { - // Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it - // In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map - double fullComboThreshold = attributes.MaxCombo - 0.1 * osuAttributes.SliderCount; - - if (scoreMaxCombo < fullComboThreshold) - effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); - - // In classic scores there can't be more misses than a sum of all non-perfect judgements - effectiveMissCount = Math.Min(effectiveMissCount, totalImperfectHits); - } - else - { - double fullComboThreshold = attributes.MaxCombo - countSliderEndsDropped; - - if (scoreMaxCombo < fullComboThreshold) - effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); - - // Combine regular misses with tick misses since tick misses break combo as well - effectiveMissCount = Math.Min(effectiveMissCount, countSliderTickMiss + countMiss); - } - } - - effectiveMissCount = Math.Max(countMiss, effectiveMissCount); + effectiveMissCount = calculateEffectiveMissCount(osuAttributes); + accuracy = calculateEffectiveAccuracy(countGreat, countOk, countMeh, countMiss, totalHits); double multiplier = PERFORMANCE_BASE_MULTIPLIER; @@ -134,14 +86,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty); + double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); aimValue *= lengthBonus; + // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (effectiveMissCount > 0) - aimValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount); + aimValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount); + + aimValue *= getComboScalingFactor(attributes); double approachRateFactor = 0.0; if (attributes.ApproachRate > 10.33) @@ -156,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(m => m is OsuModBlinds)) aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate); - else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable)) + else if (score.Mods.Any(h => h is OsuModHidden)) { // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); @@ -167,22 +122,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (attributes.SliderCount > 0) { - double estimateImproperlyFollowedDifficultSliders; - - 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; + double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders); + double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + attributes.SliderFactor; aimValue *= sliderNerfFactor; } @@ -198,14 +139,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModRelax)) return 0.0; - double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty); + double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); speedValue *= lengthBonus; + // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (effectiveMissCount > 0) - speedValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount); + speedValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); + + speedValue *= getComboScalingFactor(attributes); double approachRateFactor = 0.0; if (attributes.ApproachRate > 10.33) @@ -218,7 +162,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given. speedValue *= 1.12; } - else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable)) + else if (score.Mods.Any(m => m is OsuModHidden)) { // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); @@ -230,10 +174,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat)); double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk)); double relevantCountMiss = Math.Max(0, countMiss - Math.Max(0, relevantTotalDiff - countGreat - countOk - countMeh)); - double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : calculateAccuracy(relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss, attributes.SpeedNoteCount); + double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : calculateEffectiveAccuracy(relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss, attributes.SpeedNoteCount); // Scale the speed value with accuracy and OD. - speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2); + speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2); // Scale the speed value with # of 50s to punish doubletapping. speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); @@ -249,11 +193,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window. double betterAccuracyPercentage; int amountHitObjectsWithAccuracy = attributes.HitCircleCount; - if (!usingClassicSliderAccuracy) - amountHitObjectsWithAccuracy += attributes.SliderCount; if (amountHitObjectsWithAccuracy > 0) - betterAccuracyPercentage = calculateAccuracy(countGreat - (totalHits - amountHitObjectsWithAccuracy), countOk, countMeh, countMiss, amountHitObjectsWithAccuracy); + betterAccuracyPercentage = calculateEffectiveAccuracy(countGreat - (totalHits - amountHitObjectsWithAccuracy), countOk, countMeh, countMiss, amountHitObjectsWithAccuracy); else betterAccuracyPercentage = 0; @@ -271,7 +213,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. if (score.Mods.Any(m => m is OsuModBlinds)) accuracyValue *= 1.14; - else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable)) + else if (score.Mods.Any(m => m is OsuModHidden)) accuracyValue *= 1.08; if (score.Mods.Any(m => m is OsuModFlashlight)) @@ -285,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (!score.Mods.Any(h => h is OsuModFlashlight)) return 0.0; - double flashlightValue = Flashlight.DifficultyToPerformance(attributes.FlashlightDifficulty); + double flashlightValue = Math.Pow(attributes.FlashlightDifficulty, 2.0) * 25.0; // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (effectiveMissCount > 0) @@ -305,18 +247,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty return flashlightValue; } - // 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 calculateEffectiveMissCount(OsuDifficultyAttributes attributes) + { + // Guess the number of misses + slider breaks from combo + double comboBasedMissCount = 0.0; - private double calculateAccuracy(double countGreat, double countOk, double countMeh, double countMiss, double totalHits) - => usingClassicSliderAccuracy - ? (countGreat * 6 + countOk * 2 + countMeh + countMiss * 2) / (totalHits * 6) - : (countGreat * 6 + countOk * 2 + countMeh) / ((totalHits - countMiss) * 6); + if (attributes.SliderCount > 0) + { + double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount; + if (scoreMaxCombo < fullComboThreshold) + comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); + } + + // Clamp miss count to maximum amount of possible breaks + comboBasedMissCount = Math.Min(comboBasedMissCount, countOk + countMeh + countMiss); + + return Math.Max(countMiss, comboBasedMissCount); + } + private double calculateEffectiveAccuracy(double countGreat, double countOk, double countMeh, double countMiss, double totalHits) => (countGreat * 6 + countOk * 2 + countMeh + countMiss * 2) / (double)(totalHits * 6); 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; } }