diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index 513352825f..337bda3221 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -8,7 +8,6 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
-using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Skills;
@@ -23,13 +22,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public class OsuDifficultyCalculator : DifficultyCalculator
{
private 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 const double difficulty_multiplier = 0.0675;
private const double star_rating_multiplier = 0.0265;
public override int Version => 20250306;
- private double mechanicalDifficultyRating;
-
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
@@ -45,30 +41,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return multiplier;
}
- ///
- /// Calculates a visibility bonus that is applicable to Hidden and Traceable.
- ///
- public static double CalculateVisibilityBonus(Mod[] mods, double approachRate, double visibilityFactor = 1)
- {
- // NOTE: TC's effect is only noticeable in performance calculations until lazer mods are accounted for server-side.
- bool isAlwaysPartiallyVisible = mods.OfType().Any(m => m.OnlyFadeApproachCircles.Value) || mods.OfType().Any();
-
- // Start from normal curve, rewarding lower AR up to AR5
- double readingBonus = 0.04 * (12.0 - Math.Max(approachRate, 5));
-
- readingBonus *= visibilityFactor;
-
- // For AR up to 0 - reduce reward for very low ARs when object is visible
- if (approachRate < 5)
- readingBonus += (isAlwaysPartiallyVisible ? 0.03 : 0.04) * (5.0 - Math.Max(approachRate, 0));
-
- // Starting from AR0 - cap values so they won't grow to infinity
- if (approachRate < 0)
- readingBonus += (isAlwaysPartiallyVisible ? 0.075 : 0.1) * (1 - Math.Pow(1.5, approachRate));
-
- return readingBonus;
- }
-
public static double CalculateRateAdjustedApproachRate(double approachRate, double clockRate)
{
double preempt = IBeatmapDifficultyInfo.DifficultyRange(approachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN) / clockRate;
@@ -125,19 +97,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double aimNoSlidersDifficultyValue = aimWithoutSliders.DifficultyValue();
double speedDifficultyValue = speed.DifficultyValue();
- mechanicalDifficultyRating = calculateMechanicalDifficultyRating(aimDifficultyValue, speedDifficultyValue);
+ double mechanicalDifficultyRating = calculateMechanicalDifficultyRating(aimDifficultyValue, speedDifficultyValue);
- double aimRating = computeAimRating(aimDifficultyValue, mods, totalHits, approachRate, overallDifficulty);
- double aimRatingNoSliders = computeAimRating(aimNoSlidersDifficultyValue, mods, totalHits, approachRate, overallDifficulty);
- double speedRating = computeSpeedRating(speedDifficultyValue, mods, totalHits, approachRate, overallDifficulty);
+ var osuRatingCalculator = new OsuRatingCalculator(mods, totalHits, approachRate, overallDifficulty, mechanicalDifficultyRating);
+
+ double aimRating = osuRatingCalculator.ComputeAimRating(aimDifficultyValue);
+ double aimRatingNoSliders = osuRatingCalculator.ComputeAimRating(aimNoSlidersDifficultyValue);
+ double speedRating = osuRatingCalculator.ComputeSpeedRating(speedDifficultyValue);
double flashlightRating = 0.0;
if (flashlight is not null)
- flashlightRating = computeFlashlightRating(flashlight.DifficultyValue(), mods, totalHits, overallDifficulty);
+ flashlightRating = osuRatingCalculator.ComputeFlashlightRating(flashlight.DifficultyValue());
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
+ double sliderNestedScorePerObject = LegacyScoreUtils.CalculateNestedScorePerObject(beatmap, totalHits);
+ double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(beatmap);
+
+ var simulator = new OsuLegacyScoreSimulator();
+ var scoreAttributes = simulator.Simulate(WorkingBeatmap, beatmap);
+
double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);
double baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating);
@@ -152,12 +132,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double multiplier = CalculateDifficultyMultiplier(mods, totalHits, spinnerCount);
double starRating = calculateStarRating(basePerformance, multiplier);
- double sliderNestedScorePerObject = LegacyScoreUtils.CalculateNestedScorePerObject(beatmap, totalHits);
- double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(beatmap);
-
- var simulator = new OsuLegacyScoreSimulator();
- var scoreAttributes = simulator.Simulate(WorkingBeatmap, beatmap);
-
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
{
StarRating = starRating,
@@ -185,152 +159,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return attributes;
}
- private double computeAimRating(double aimDifficultyValue, Mod[] mods, int totalHits, double approachRate, double overallDifficulty)
- {
- if (mods.Any(m => m is OsuModAutopilot))
- return 0;
-
- double aimRating = calculateDifficultyRating(aimDifficultyValue);
-
- if (mods.Any(m => m is OsuModTouchDevice))
- aimRating = Math.Pow(aimRating, 0.8);
-
- if (mods.Any(m => m is OsuModRelax))
- aimRating *= 0.9;
-
- if (mods.Any(m => m is OsuModMagnetised))
- {
- float magnetisedStrength = mods.OfType().First().AttractionStrength.Value;
- aimRating *= 1.0 - magnetisedStrength;
- }
-
- double ratingMultiplier = 1.0;
-
- double approachRateLengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
- (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
-
- double approachRateFactor = 0.0;
- if (approachRate > 10.33)
- approachRateFactor = 0.3 * (approachRate - 10.33);
- else if (approachRate < 8.0)
- approachRateFactor = 0.05 * (8.0 - approachRate);
-
- if (mods.Any(h => h is OsuModRelax))
- approachRateFactor = 0.0;
-
- ratingMultiplier *= 1.0 + approachRateFactor * approachRateLengthBonus; // Buff for longer maps with high AR.
-
- if (mods.Any(m => m is OsuModHidden))
- {
- double visibilityFactor = calculateAimVisibilityFactor(approachRate);
- ratingMultiplier *= 1.0 + CalculateVisibilityBonus(mods, approachRate, visibilityFactor);
- }
-
- // It is important to consider accuracy difficulty when scaling with accuracy.
- ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
-
- return aimRating * Math.Cbrt(ratingMultiplier);
- }
-
- private double computeSpeedRating(double speedDifficultyValue, Mod[] mods, int totalHits, double approachRate, double overallDifficulty)
- {
- if (mods.Any(m => m is OsuModRelax))
- return 0;
-
- double speedRating = calculateDifficultyRating(speedDifficultyValue);
-
- if (mods.Any(m => m is OsuModAutopilot))
- speedRating *= 0.5;
-
- if (mods.Any(m => m is OsuModMagnetised))
- {
- // reduce speed rating because of the speed distance scaling, with maximum reduction being 0.7x
- float magnetisedStrength = mods.OfType().First().AttractionStrength.Value;
- speedRating *= 1.0 - magnetisedStrength * 0.3;
- }
-
- double ratingMultiplier = 1.0;
-
- double approachRateLengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
- (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
-
- double approachRateFactor = 0.0;
- if (approachRate > 10.33)
- approachRateFactor = 0.3 * (approachRate - 10.33);
-
- if (mods.Any(m => m is OsuModAutopilot))
- approachRateFactor = 0.0;
-
- ratingMultiplier *= 1.0 + approachRateFactor * approachRateLengthBonus; // Buff for longer maps with high AR.
-
- if (mods.Any(m => m is OsuModHidden))
- {
- double visibilityFactor = calculateSpeedVisibilityFactor(approachRate);
- ratingMultiplier *= 1.0 + CalculateVisibilityBonus(mods, approachRate, visibilityFactor);
- }
-
- ratingMultiplier *= 0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750;
-
- return speedRating * Math.Cbrt(ratingMultiplier);
- }
-
- private double computeFlashlightRating(double flashlightDifficultyValue, Mod[] mods, int totalHits, double overallDifficulty)
- {
- if (!mods.Any(m => m is OsuModFlashlight))
- return 0;
-
- double flashlightRating = calculateDifficultyRating(flashlightDifficultyValue);
-
- if (mods.Any(m => m is OsuModTouchDevice))
- flashlightRating = Math.Pow(flashlightRating, 0.8);
-
- if (mods.Any(m => m is OsuModRelax))
- flashlightRating *= 0.7;
- else if (mods.Any(m => m is OsuModAutopilot))
- flashlightRating *= 0.4;
-
- if (mods.Any(m => m is OsuModMagnetised))
- {
- float magnetisedStrength = mods.OfType().First().AttractionStrength.Value;
- flashlightRating *= 1.0 - magnetisedStrength;
- }
-
- double ratingMultiplier = 1.0;
-
- // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
- ratingMultiplier *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
- (totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0);
-
- // It is important to consider accuracy difficulty when scaling with accuracy.
- ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
-
- return flashlightRating * Math.Sqrt(ratingMultiplier);
- }
-
- private double calculateAimVisibilityFactor(double approachRate)
- {
- const double ar_factor_end_point = 11.5;
-
- double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10);
- double arFactorStartingPoint = double.Lerp(9, 10.33, mechanicalDifficultyFactor);
-
- return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint);
- }
-
- private double calculateSpeedVisibilityFactor(double approachRate)
- {
- const double ar_factor_end_point = 11.5;
-
- double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10);
- double arFactorStartingPoint = double.Lerp(10, 10.33, mechanicalDifficultyFactor);
-
- return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint);
- }
-
private static double calculateMechanicalDifficultyRating(double aimDifficultyValue, double speedDifficultyValue)
{
- double aimValue = OsuStrainSkill.DifficultyToPerformance(calculateDifficultyRating(aimDifficultyValue));
- double speedValue = OsuStrainSkill.DifficultyToPerformance(calculateDifficultyRating(speedDifficultyValue));
+ double aimValue = OsuStrainSkill.DifficultyToPerformance(OsuRatingCalculator.CalculateDifficultyRating(aimDifficultyValue));
+ double speedValue = OsuStrainSkill.DifficultyToPerformance(OsuRatingCalculator.CalculateDifficultyRating(speedDifficultyValue));
double totalValue = Math.Pow(Math.Pow(aimValue, 1.1) + Math.Pow(speedValue, 1.1), 1 / 1.1);
@@ -345,8 +177,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return Math.Cbrt(multiplier) * star_rating_multiplier * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4);
}
- private static double calculateDifficultyRating(double difficultyValue) => Math.Sqrt(difficultyValue) * difficulty_multiplier;
-
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
List objects = new List();
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 49626eb7b6..11e9714ed8 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -209,7 +209,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
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 OsuModTraceable))
{
- aimValue *= 1.0 + OsuDifficultyCalculator.CalculateVisibilityBonus(score.Mods, approachRate);
+ aimValue *= 1.0 + OsuRatingCalculator.CalculateVisibilityBonus(score.Mods, approachRate);
}
aimValue *= accuracy;
@@ -245,7 +245,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
}
else if (score.Mods.Any(m => m is OsuModTraceable))
{
- speedValue *= 1.0 + OsuDifficultyCalculator.CalculateVisibilityBonus(score.Mods, approachRate);
+ speedValue *= 1.0 + OsuRatingCalculator.CalculateVisibilityBonus(score.Mods, approachRate);
}
double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes);
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuRatingCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuRatingCalculator.cs
new file mode 100644
index 0000000000..e505ed07e4
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuRatingCalculator.cs
@@ -0,0 +1,199 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Mods;
+
+namespace osu.Game.Rulesets.Osu.Difficulty
+{
+ public class OsuRatingCalculator
+ {
+ private const double difficulty_multiplier = 0.0675;
+
+ private readonly Mod[] mods;
+ private readonly int totalHits;
+ private readonly double approachRate;
+ private readonly double overallDifficulty;
+ private readonly double mechanicalDifficultyRating;
+
+ public OsuRatingCalculator(Mod[] mods, int totalHits, double approachRate, double overallDifficulty, double mechanicalDifficultyRating)
+ {
+ this.mods = mods;
+ this.totalHits = totalHits;
+ this.approachRate = approachRate;
+ this.overallDifficulty = overallDifficulty;
+ this.mechanicalDifficultyRating = mechanicalDifficultyRating;
+ }
+
+ public double ComputeAimRating(double aimDifficultyValue)
+ {
+ if (mods.Any(m => m is OsuModAutopilot))
+ return 0;
+
+ double aimRating = CalculateDifficultyRating(aimDifficultyValue);
+
+ if (mods.Any(m => m is OsuModTouchDevice))
+ aimRating = Math.Pow(aimRating, 0.8);
+
+ if (mods.Any(m => m is OsuModRelax))
+ aimRating *= 0.9;
+
+ if (mods.Any(m => m is OsuModMagnetised))
+ {
+ float magnetisedStrength = mods.OfType().First().AttractionStrength.Value;
+ aimRating *= 1.0 - magnetisedStrength;
+ }
+
+ double ratingMultiplier = 1.0;
+
+ double approachRateLengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
+ (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
+
+ double approachRateFactor = 0.0;
+ if (approachRate > 10.33)
+ approachRateFactor = 0.3 * (approachRate - 10.33);
+ else if (approachRate < 8.0)
+ approachRateFactor = 0.05 * (8.0 - approachRate);
+
+ if (mods.Any(h => h is OsuModRelax))
+ approachRateFactor = 0.0;
+
+ ratingMultiplier *= 1.0 + approachRateFactor * approachRateLengthBonus; // Buff for longer maps with high AR.
+
+ if (mods.Any(m => m is OsuModHidden))
+ {
+ double visibilityFactor = calculateAimVisibilityFactor(approachRate);
+ ratingMultiplier *= 1.0 + CalculateVisibilityBonus(mods, approachRate, visibilityFactor);
+ }
+
+ // It is important to consider accuracy difficulty when scaling with accuracy.
+ ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
+
+ return aimRating * Math.Cbrt(ratingMultiplier);
+ }
+
+ public double ComputeSpeedRating(double speedDifficultyValue)
+ {
+ if (mods.Any(m => m is OsuModRelax))
+ return 0;
+
+ double speedRating = CalculateDifficultyRating(speedDifficultyValue);
+
+ if (mods.Any(m => m is OsuModAutopilot))
+ speedRating *= 0.5;
+
+ if (mods.Any(m => m is OsuModMagnetised))
+ {
+ // reduce speed rating because of the speed distance scaling, with maximum reduction being 0.7x
+ float magnetisedStrength = mods.OfType().First().AttractionStrength.Value;
+ speedRating *= 1.0 - magnetisedStrength * 0.3;
+ }
+
+ double ratingMultiplier = 1.0;
+
+ double approachRateLengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
+ (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
+
+ double approachRateFactor = 0.0;
+ if (approachRate > 10.33)
+ approachRateFactor = 0.3 * (approachRate - 10.33);
+
+ if (mods.Any(m => m is OsuModAutopilot))
+ approachRateFactor = 0.0;
+
+ ratingMultiplier *= 1.0 + approachRateFactor * approachRateLengthBonus; // Buff for longer maps with high AR.
+
+ if (mods.Any(m => m is OsuModHidden))
+ {
+ double visibilityFactor = calculateSpeedVisibilityFactor(approachRate);
+ ratingMultiplier *= 1.0 + CalculateVisibilityBonus(mods, approachRate, visibilityFactor);
+ }
+
+ ratingMultiplier *= 0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750;
+
+ return speedRating * Math.Cbrt(ratingMultiplier);
+ }
+
+ public double ComputeFlashlightRating(double flashlightDifficultyValue)
+ {
+ if (!mods.Any(m => m is OsuModFlashlight))
+ return 0;
+
+ double flashlightRating = CalculateDifficultyRating(flashlightDifficultyValue);
+
+ if (mods.Any(m => m is OsuModTouchDevice))
+ flashlightRating = Math.Pow(flashlightRating, 0.8);
+
+ if (mods.Any(m => m is OsuModRelax))
+ flashlightRating *= 0.7;
+ else if (mods.Any(m => m is OsuModAutopilot))
+ flashlightRating *= 0.4;
+
+ if (mods.Any(m => m is OsuModMagnetised))
+ {
+ float magnetisedStrength = mods.OfType().First().AttractionStrength.Value;
+ flashlightRating *= 1.0 - magnetisedStrength;
+ }
+
+ double ratingMultiplier = 1.0;
+
+ // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
+ ratingMultiplier *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
+ (totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0);
+
+ // It is important to consider accuracy difficulty when scaling with accuracy.
+ ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
+
+ return flashlightRating * Math.Sqrt(ratingMultiplier);
+ }
+
+ private double calculateAimVisibilityFactor(double approachRate)
+ {
+ const double ar_factor_end_point = 11.5;
+
+ double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10);
+ double arFactorStartingPoint = double.Lerp(9, 10.33, mechanicalDifficultyFactor);
+
+ return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint);
+ }
+
+ private double calculateSpeedVisibilityFactor(double approachRate)
+ {
+ const double ar_factor_end_point = 11.5;
+
+ double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10);
+ double arFactorStartingPoint = double.Lerp(10, 10.33, mechanicalDifficultyFactor);
+
+ return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint);
+ }
+
+ ///
+ /// Calculates a visibility bonus that is applicable to Hidden and Traceable.
+ ///
+ public static double CalculateVisibilityBonus(Mod[] mods, double approachRate, double visibilityFactor = 1)
+ {
+ // NOTE: TC's effect is only noticeable in performance calculations until lazer mods are accounted for server-side.
+ bool isAlwaysPartiallyVisible = mods.OfType().Any(m => m.OnlyFadeApproachCircles.Value) || mods.OfType().Any();
+
+ // Start from normal curve, rewarding lower AR up to AR5
+ double readingBonus = 0.04 * (12.0 - Math.Max(approachRate, 5));
+
+ readingBonus *= visibilityFactor;
+
+ // For AR up to 0 - reduce reward for very low ARs when object is visible
+ if (approachRate < 5)
+ readingBonus += (isAlwaysPartiallyVisible ? 0.03 : 0.04) * (5.0 - Math.Max(approachRate, 0));
+
+ // Starting from AR0 - cap values so they won't grow to infinity
+ if (approachRate < 0)
+ readingBonus += (isAlwaysPartiallyVisible ? 0.075 : 0.1) * (1 - Math.Pow(1.5, approachRate));
+
+ return readingBonus;
+ }
+
+ public static double CalculateDifficultyRating(double difficultyValue) => Math.Sqrt(difficultyValue) * difficulty_multiplier;
+ }
+}