diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
index a4b6668f7b..72168b00f3 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
@@ -70,6 +70,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
[JsonProperty("speed_difficult_strain_count")]
public double SpeedDifficultStrainCount { get; set; }
+ [JsonProperty("low_ar_difficult_strain_count")]
+ public double LowArDifficultStrainCount { get; set; }
+
+ [JsonProperty("hidden_difficult_strain_count")]
+ public double HiddenDifficultStrainCount { get; set; }
+
///
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
///
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index 7d1899bb93..612b477add 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -51,11 +51,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
+ double hiddenDifficultyStrainCount = 0;
double baseReadingHiddenPerformance = 0.0;
if (mods.Any(h => h is OsuModHidden))
{
hiddenRating = Math.Sqrt(skills[6].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
baseReadingHiddenPerformance = ReadingHidden.DifficultyToPerformance(hiddenRating);
+ hiddenDifficultyStrainCount = skills[5].CountDifficultStrains();
}
double baseFlashlightPerformance = 0.0;
@@ -63,8 +65,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating);
}
- double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountDifficultStrains();
- double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountDifficultStrains();
+
+ double aimDifficultyStrainCount = skills[0].CountDifficultStrains();
+ double speedDifficultyStrainCount = skills[2].CountDifficultStrains();
+ double lowArDifficultyStrainCount = skills[4].CountDifficultStrains();
if (mods.Any(m => m is OsuModTouchDevice))
{
@@ -135,13 +139,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
SliderFactor = sliderFactor,
AimDifficultStrainCount = aimDifficultyStrainCount,
SpeedDifficultStrainCount = speedDifficultyStrainCount,
+ LowArDifficultStrainCount = lowArDifficultyStrainCount,
+ HiddenDifficultStrainCount = hiddenDifficultyStrainCount,
ApproachRate = IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, 1800, 1200, 450),
OverallDifficulty = (80 - hitWindowGreat) / 6,
DrainRate = drainRate,
MaxCombo = beatmap.GetMaxCombo(),
HitCircleCount = hitCirclesCount,
SliderCount = sliderCount,
- SpinnerCount = spinnerCount,
+ SpinnerCount = spinnerCount
};
return attributes;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 9cc6f88ae1..a0f98216a8 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -216,6 +216,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (!usingClassicSliderAccuracy)
amountHitObjectsWithAccuracy += attributes.SliderCount;
+ if (score.Mods.OfType().All(m => !m.NoSliderHeadAccuracy.Value))
+ amountHitObjectsWithAccuracy += attributes.SliderCount;
+
if (amountHitObjectsWithAccuracy > 0)
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
else
@@ -289,9 +292,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)
- readingValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
-
- readingValue *= getComboScalingFactor(attributes);
+ readingValue *= calculateMissPenalty(effectiveMissCount, attributes.LowArDifficultStrainCount);
// Scale the reading value with accuracy _harshly_. Additional note: it would have it's own curve in Statistical Accuracy rework.
readingValue *= accuracy * accuracy;
@@ -305,12 +306,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
double highARValue = OsuStrainSkill.DifficultyToPerformance(attributes.ReadingDifficultyHighAR);
- // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
- if (effectiveMissCount > 0)
- highARValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount);
-
- highARValue *= getComboScalingFactor(attributes);
-
// Approximate how much of high AR difficulty is aim
double aimPerformance = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty);
double speedPerformance = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
@@ -330,6 +325,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimPartValue *= sliderNerfFactor;
}
+ if (effectiveMissCount > 0)
+ aimPartValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount);
+
aimPartValue *= accuracy;
// It is important to consider accuracy difficulty when scaling with accuracy.
aimPartValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
@@ -345,6 +343,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk));
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
+ if (effectiveMissCount > 0)
+ speedPartValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount);
+
// Scale the speed value with accuracy and OD.
speedPartValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
@@ -365,11 +366,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double lengthBonus = CalculateDefaultLengthBonus(totalHits);
hiddenValue *= lengthBonus;
- // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)
- hiddenValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
-
- hiddenValue *= getComboScalingFactor(attributes);
+ hiddenValue *= calculateMissPenalty(effectiveMissCount, attributes.HiddenDifficultStrainCount);
// Scale the reading value with accuracy _harshly_. Additional note: it would have it's own curve in Statistical Accuracy rework.
hiddenValue *= accuracy * accuracy;
@@ -442,13 +440,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return 1 + result;
}
- // 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 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;
-
// Limits reading difficulty by the difficulty of full-memorisation (assumed to be mechanicalPerformance + flashlightPerformance + 25)
// Desmos graph assuming that x = cognitionPerformance, while y = mechanicalPerformance + flaslightPerformance
// https://www.desmos.com/3d/vjygrxtkqs
@@ -464,7 +455,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return ratio * capPerformance;
}
+ // 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 getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
+
private static double softmin(double a, double b, double power = Math.E) => a * b / Math.Log(Math.Pow(power, a) + Math.Pow(power, b), power);
+
private static double logistic(double x) => 1 / (1 + Math.Exp(-x));
+
+ private int totalHits => countGreat + countOk + countMeh + countMiss;
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index f8359f87dd..35e7162de3 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
CurrentStrain *= StrainDecay(current.DeltaTime);
CurrentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * SkillMultiplier;
- ObjectStrains.Add(currentStrain);
+ ObjectStrains.Add(CurrentStrain);
return CurrentStrain;
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
index fc08e01f0e..d77952fbbe 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
@@ -26,8 +26,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
protected virtual double StrainDecayBase => 0.15;
protected double StrainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000);
- protected List ObjectStrains = new List();
- protected double Difficulty;
protected OsuStrainSkill(Mod[] mods)
: base(mods)
@@ -63,20 +61,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return Difficulty;
}
- ///
- /// Returns the number of strains weighted against the top strain.
- /// The result is scaled by clock rate as it affects the total number of strains.
- ///
- public double CountDifficultStrains()
- {
- if (Difficulty == 0)
- return 0.0;
-
- double consistentTopStrain = Difficulty / 10; // What would the top strain be if all strain values were identical
- // Use a weighted sum of all strains. Constants are arbitrary and give nice values
- return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88))));
- }
-
///
/// Converts difficulty value from to base performance.
///
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs
index e2c5597aad..7f80999eb1 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs
@@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
public class ReadingLowAR : GraphSkill
{
- private readonly List difficulties = new List();
private double skillMultiplier => 1.23;
private double aimComponentMultiplier => 0.4;
@@ -40,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
double totalDensityDifficulty = (currentDensityAimStrain + densityReadingDifficulty) * skillMultiplier;
- difficulties.Add(totalDensityDifficulty);
+ ObjectStrains.Add(totalDensityDifficulty);
if (current.Index == 0)
CurrentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
@@ -59,11 +58,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private double reducedNoteBaseline => 0.7;
public override double DifficultyValue()
{
- double difficulty = 0;
-
// Sections with 0 difficulty are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// These sections will not contribute to the difficulty.
- var peaks = difficulties.Where(p => p > 0);
+ var peaks = ObjectStrains.Where(p => p > 0);
List values = peaks.OrderByDescending(d => d).ToList();
@@ -75,14 +72,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
values = values.OrderByDescending(d => d).ToList();
+ Difficulty = 0;
+
// Difficulty is the weighted sum of the highest strains from every section.
// We're sorting from highest to lowest strain.
for (int i = 0; i < values.Count; i++)
{
- difficulty += values[i] / (i + 1);
+ Difficulty += values[i] / (i + 1);
}
- return difficulty;
+ return Difficulty;
}
public static double DifficultyToPerformance(double difficulty) => Math.Max(
Math.Max(Math.Pow(difficulty, 1.5) * 20, Math.Pow(difficulty, 2) * 17.0),
@@ -95,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
: base(mods, false)
{
}
- protected new double SkillMultiplier => 7.2;
+ protected new double SkillMultiplier => 7.632;
protected override double StrainValueAt(DifficultyHitObject current)
{
@@ -107,6 +106,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
hiddenDifficulty *= SkillMultiplier;
CurrentStrain += hiddenDifficulty;
+ ObjectStrains.Add(CurrentStrain);
return CurrentStrain;
}
@@ -204,8 +204,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
double adjustedDifficulty = performanceToDifficulty(totalPerformance);
double difficultyValue = Math.Pow(adjustedDifficulty / OsuDifficultyCalculator.DIFFICULTY_MULTIPLIER, 2.0);
+ Difficulty = skill_multiplier * Math.Pow(difficultyValue, MECHANICAL_PP_POWER);
+
// Sqrt value to make difficulty depend less on mechanical difficulty
- return skill_multiplier * Math.Pow(difficultyValue, MECHANICAL_PP_POWER);
+ return Difficulty;
}
public class HighARAimComponent : Aim
{
diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs
index 8b8892113b..4f81183b1f 100644
--- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs
+++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs
@@ -1,7 +1,9 @@
// 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.Collections.Generic;
+using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods;
@@ -27,6 +29,9 @@ namespace osu.Game.Rulesets.Difficulty.Skills
this.mods = mods;
}
+ protected List ObjectStrains = new List();
+ protected double Difficulty;
+
///
/// Process a .
///
@@ -37,5 +42,19 @@ namespace osu.Game.Rulesets.Difficulty.Skills
/// Returns the calculated difficulty value representing all s that have been processed up to this point.
///
public abstract double DifficultyValue();
+
+ ///
+ /// Returns the number of strains weighted against the top strain.
+ /// The result is scaled by clock rate as it affects the total number of strains.
+ ///
+ public double CountDifficultStrains()
+ {
+ if (Difficulty == 0)
+ return 0.0;
+
+ double consistentTopStrain = Difficulty / 10; // What would the top strain be if all strain values were identical
+ // Use a weighted sum of all strains. Constants are arbitrary and give nice values
+ return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88))));
+ }
}
}