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;