diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 0bbf1d3df6..9cab454142 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -75,8 +75,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("speed_difficult_strain_count")] public double SpeedDifficultStrainCount { get; set; } - [JsonProperty("slider_nested_score_per_object")] - public double SliderNestedScorePerObject { get; set; } + [JsonProperty("nested_score_per_object")] + public double NestedScorePerObject { get; set; } [JsonProperty("legacy_score_base_multiplier")] public double LegacyScoreBaseMultiplier { get; set; } @@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount); yield return (ATTRIB_ID_AIM_TOP_WEIGHTED_SLIDER_FACTOR, AimTopWeightedSliderFactor); yield return (ATTRIB_ID_SPEED_TOP_WEIGHTED_SLIDER_FACTOR, SpeedTopWeightedSliderFactor); - yield return (ATTRIB_ID_SLIDER_NESTED_SCORE_PER_OBJECT, SliderNestedScorePerObject); + yield return (ATTRIB_ID_NESTED_SCORE_PER_OBJECT, NestedScorePerObject); yield return (ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER, LegacyScoreBaseMultiplier); yield return (ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE, MaximumLegacyComboScore); } @@ -144,7 +144,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT]; AimTopWeightedSliderFactor = values[ATTRIB_ID_AIM_TOP_WEIGHTED_SLIDER_FACTOR]; SpeedTopWeightedSliderFactor = values[ATTRIB_ID_SPEED_TOP_WEIGHTED_SLIDER_FACTOR]; - SliderNestedScorePerObject = values[ATTRIB_ID_SLIDER_NESTED_SCORE_PER_OBJECT]; + NestedScorePerObject = values[ATTRIB_ID_NESTED_SCORE_PER_OBJECT]; LegacyScoreBaseMultiplier = values[ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER]; MaximumLegacyComboScore = values[ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE]; DrainRate = onlineInfo.DrainRate; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 7c8de87884..dd9d4d4c23 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty ? Math.Cbrt(multiplier) * star_rating_multiplier * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; - double sliderNestedScorePerObject = LegacyScoreUtils.CalculateSliderNestedScorePerObject(beatmap, totalHits); + double sliderNestedScorePerObject = LegacyScoreUtils.CalculateNestedScorePerObject(beatmap, totalHits); double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(beatmap); var simulator = new OsuLegacyScoreSimulator(); @@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty HitCircleCount = hitCircleCount, SliderCount = sliderCount, SpinnerCount = spinnerCount, - SliderNestedScorePerObject = sliderNestedScorePerObject, + NestedScorePerObject = sliderNestedScorePerObject, LegacyScoreBaseMultiplier = legacyScoreBaseMultiplier, MaximumLegacyComboScore = scoreAttributes.ComboScore }; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreMissCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreMissCalculator.cs index 53837b78a0..207ecde81a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreMissCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreMissCalculator.cs @@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double objectsHit = (totalHits - countMiss) * combo / attributes.MaxCombo; // Score also has a non-combo portion we need to create the final score value. - double nonComboScore = (300 + attributes.SliderNestedScorePerObject) * score.Accuracy * objectsHit; + double nonComboScore = (300 + attributes.NestedScorePerObject) * score.Accuracy * objectsHit; return comboScore + nonComboScore; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Utils/LegacyScoreUtils.cs b/osu.Game.Rulesets.Osu/Difficulty/Utils/LegacyScoreUtils.cs index d1df378b47..df1683fb29 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Utils/LegacyScoreUtils.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Utils/LegacyScoreUtils.cs @@ -12,9 +12,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Utils public static class LegacyScoreUtils { /// - /// Calculates the average amount of score per object that is caused by slider ticks. + /// Calculates the average amount of score per object that is caused by nested judgements such as slider-ticks and spinners. /// - public static double CalculateSliderNestedScorePerObject(IBeatmap beatmap, int objectCount) + public static double CalculateNestedScorePerObject(IBeatmap beatmap, int objectCount) { const double big_tick_score = 30; const double small_tick_score = 10; @@ -29,9 +29,59 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Utils int amountOfSmallTicks = sliders.Select(s => s.NestedHitObjects.Count(nho => nho is SliderTick)).Sum(); - double totalScore = amountOfBigTicks * big_tick_score + amountOfSmallTicks * small_tick_score; + double sliderScore = amountOfBigTicks * big_tick_score + amountOfSmallTicks * small_tick_score; - return totalScore / objectCount; + double spinnerScore = 0; + + foreach (var spinner in beatmap.HitObjects.OfType()) + { + spinnerScore += calculateSpinnerScore(spinner); + } + + return (sliderScore + spinnerScore) / objectCount; + } + + /// + /// Logic borrowed from for basic score calculations. + /// + private static double calculateSpinnerScore(Spinner spinner) + { + const int spin_score = 100; + const int bonus_spin_score = 1000; + + // The spinner object applies a lenience because gameplay mechanics differ from osu-stable. + // We'll redo the calculations to match osu-stable here... + const double maximum_rotations_per_second = 477.0 / 60; + + // Normally, this value depends on the final overall difficulty. For simplicity, we'll only consider the worst case that maximises bonus score. + // As we're primarily concerned with computing the maximum theoretical final score, + // this will have the final effect of slightly underestimating bonus score achieved on stable when converting from score V1. + const double minimum_rotations_per_second = 3; + + double secondsDuration = spinner.Duration / 1000; + + // The total amount of half spins possible for the entire spinner. + int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2); + // The amount of half spins that are required to successfully complete the spinner (i.e. get a 300). + int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimum_rotations_per_second); + // To be able to receive bonus points, the spinner must be rotated another 1.5 times. + int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3; + + long score = 0; + + int fullSpins = (totalHalfSpinsPossible / 2); + + // Normal spin score + score += spin_score * fullSpins; + + int bonusSpins = (totalHalfSpinsPossible - halfSpinsRequiredBeforeBonus) / 2; + + // Reduce amount of bonus spins because we want to represent the more average case, rather than the best one. + bonusSpins = Math.Max(0, bonusSpins - fullSpins / 2); + + score += bonus_spin_score * bonusSpins; + + return score; } public static int CalculateDifficultyPeppyStars(IBeatmap beatmap) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index e01ce6fde5..5e792d1b75 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31; protected const int ATTRIB_ID_AIM_TOP_WEIGHTED_SLIDER_FACTOR = 33; protected const int ATTRIB_ID_SPEED_TOP_WEIGHTED_SLIDER_FACTOR = 35; - protected const int ATTRIB_ID_SLIDER_NESTED_SCORE_PER_OBJECT = 37; + protected const int ATTRIB_ID_NESTED_SCORE_PER_OBJECT = 37; protected const int ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER = 39; protected const int ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE = 41;