diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index bd4c0f2ad5..a08fe3b7c5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -14,6 +14,7 @@ public class OsuDifficultyAttributes : DifficultyAttributes public double OverallDifficulty { get; set; } public double DrainRate { get; set; } public int HitCircleCount { get; set; } + public int SliderCount { get; set; } public int SpinnerCount { get; set; } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 790aa0eb7d..b0a764dc4d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -64,6 +64,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat maxCombo += beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1); int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); + int sliderCount = beatmap.HitObjects.Count(h => h is Slider); int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner); return new OsuDifficultyAttributes @@ -78,6 +79,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat DrainRate = drainRate, MaxCombo = maxCombo, HitCircleCount = hitCirclesCount, + SliderCount = sliderCount, SpinnerCount = spinnerCount, Skills = skills }; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 4e4dbc02a1..4bca87204a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -25,6 +25,8 @@ public class OsuPerformanceCalculator : PerformanceCalculator private int countMeh; private int countMiss; + private int effectiveMissCount; + public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) : base(ruleset, attributes, score) { @@ -39,19 +41,20 @@ public override double Calculate(Dictionary categoryRatings = nu countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok); countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); + effectiveMissCount = calculateEffectiveMissCount(); double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. // Custom multipliers for NoFail and SpunOut. if (mods.Any(m => m is OsuModNoFail)) - multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss); + multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount); if (mods.Any(m => m is OsuModSpunOut)) multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); if (mods.Any(h => h is OsuModRelax)) { - countMiss += countOk + countMeh; + effectiveMissCount += countOk + countMeh; multiplier *= 0.6; } @@ -97,8 +100,8 @@ private double computeAimValue() aimValue *= lengthBonus; // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. - if (countMiss > 0) - aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss); + if (effectiveMissCount > 0) + aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), effectiveMissCount); // Combo scaling. if (Attributes.MaxCombo > 0) @@ -115,7 +118,7 @@ private double computeAimValue() double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; if (mods.Any(m => m is OsuModBlinds)) - aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * countMiss)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate); + aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate); else if (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. @@ -142,8 +145,8 @@ private double computeSpeedValue() speedValue *= lengthBonus; // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. - if (countMiss > 0) - speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875)); + if (effectiveMissCount > 0) + speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); // Combo scaling. if (Attributes.MaxCombo > 0) @@ -231,8 +234,8 @@ private double computeFlashlightValue() flashlightValue *= 1.3; // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. - if (countMiss > 0) - flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875)); + if (effectiveMissCount > 0) + flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); // Combo scaling. if (Attributes.MaxCombo > 0) @@ -250,6 +253,24 @@ private double computeFlashlightValue() return flashlightValue; } + private int calculateEffectiveMissCount() + { + // guess the number of misses + slider breaks from combo + double comboBasedMissCount = 0.0; + + if (Attributes.SliderCount > 0) + { + double fullComboThreshold = Attributes.MaxCombo - 0.1 * Attributes.SliderCount; + if (scoreMaxCombo < fullComboThreshold) + comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); + } + + // we're clamping misscount because since its derived from combo it can be higher than total hits and that breaks some calculations + comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits); + + return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount)); + } + private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; }