diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index c0a6d3a755..9af5051f45 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
- [TestCase(6.718709884850683d, 239, "diffcalc-test")]
- [TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
- [TestCase(0.42630400627180914d, 4, "very-fast-slider")]
+ [TestCase(6.6860329680488437d, 239, "diffcalc-test")]
+ [TestCase(1.4485740324170036d, 54, "zero-length-sliders")]
+ [TestCase(0.43052813047866129d, 4, "very-fast-slider")]
[TestCase(0.14143808967817237d, 2, "nan-slider")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(9.6343245007055653d, 239, "diffcalc-test")]
- [TestCase(1.7550169162648608d, 54, "zero-length-sliders")]
- [TestCase(0.55231632896800109d, 4, "very-fast-slider")]
+ [TestCase(9.6300773538770041d, 239, "diffcalc-test")]
+ [TestCase(1.7550155729445993d, 54, "zero-length-sliders")]
+ [TestCase(0.55785578988249407d, 4, "very-fast-slider")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
- [TestCase(6.718709884850683d, 239, "diffcalc-test")]
- [TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
- [TestCase(0.42630400627180914d, 4, "very-fast-slider")]
+ [TestCase(6.6860329680488437d, 239, "diffcalc-test")]
+ [TestCase(1.4485740324170036d, 54, "zero-length-sliders")]
+ [TestCase(0.43052813047866129d, 4, "very-fast-slider")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
index fdf94719ed..e279ed889a 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
public static class AimEvaluator
{
private const double wide_angle_multiplier = 1.5;
- private const double acute_angle_multiplier = 2.7;
+ private const double acute_angle_multiplier = 2.6;
private const double slider_multiplier = 1.35;
private const double velocity_change_multiplier = 0.75;
private const double wiggle_multiplier = 1.02;
@@ -80,17 +80,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
double angleBonus = Math.Min(currVelocity, prevVelocity);
wideAngleBonus = calcWideAngleBonus(currAngle);
+ acuteAngleBonus = calcAcuteAngleBonus(currAngle);
+
+ // Penalize angle repetition.
+ wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3));
+ acuteAngleBonus *= 0.1 + 0.9 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
+
+ // Apply full wide angle bonus for distance more than one diameter
+ wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter);
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
- acuteAngleBonus = calcAcuteAngleBonus(currAngle) *
- angleBonus *
- DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) *
- DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2);
-
- // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
- wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)));
- // Penalize acute angles if they're repeated, reducing the penalty as the lastAngle gets more obtuse.
- acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
+ acuteAngleBonus *= angleBonus *
+ DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) *
+ DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2);
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
// https://www.desmos.com/calculator/dp0v0nvowc
@@ -140,8 +142,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
return aimStrain;
}
- private static double calcWideAngleBonus(double angle) => Math.Pow(Math.Sin(3.0 / 4 * (Math.Min(5.0 / 6 * Math.PI, Math.Max(Math.PI / 6, angle)) - Math.PI / 6)), 2);
+ private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(40), double.DegreesToRadians(140));
- private static double calcAcuteAngleBonus(double angle) => 1 - calcWideAngleBonus(angle);
+ private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(140), double.DegreesToRadians(40));
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs
index e5e9769081..769220ece0 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
private const double min_speed_bonus = 200; // 200 BPM 1/4th
private const double speed_balancing_factor = 40;
- private const double distance_multiplier = 0.94;
+ private const double distance_multiplier = 0.9;
///
/// Evaluates the difficulty of tapping the current object, based on:
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
index b46f5258bd..f41413b9bb 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
@@ -78,21 +78,33 @@ namespace osu.Game.Rulesets.Osu.Difficulty
///
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
///
- ///
- /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
- ///
[JsonProperty("approach_rate")]
public double ApproachRate { get; set; }
///
/// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc).
///
- ///
- /// Rate-adjusting mods don't directly affect the overall difficulty value, but have a perceived effect as a result of adjusting audio timing.
- ///
[JsonProperty("overall_difficulty")]
public double OverallDifficulty { get; set; }
+ ///
+ /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
+ ///
+ [JsonProperty("great_hit_window")]
+ public double GreatHitWindow { get; set; }
+
+ ///
+ /// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc).
+ ///
+ [JsonProperty("ok_hit_window")]
+ public double OkHitWindow { get; set; }
+
+ ///
+ /// The perceived hit window for a MEH hit inclusive of rate-adjusting mods (DT/HT/etc).
+ ///
+ [JsonProperty("meh_hit_window")]
+ public double MehHitWindow { get; set; }
+
///
/// The beatmap's drain rate. This doesn't scale with rate-adjusting mods.
///
@@ -123,6 +135,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
+ yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
if (ShouldSerializeFlashlightDifficulty())
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
@@ -133,6 +146,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount);
+
+ yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
+ yield return (ATTRIB_ID_MEH_HIT_WINDOW, MehHitWindow);
}
public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo)
@@ -144,12 +160,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
StarRating = values[ATTRIB_ID_DIFFICULTY];
+ GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT];
SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT];
+ OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW];
+ MehHitWindow = values[ATTRIB_ID_MEH_HIT_WINDOW];
DrainRate = onlineInfo.DrainRate;
HitCircleCount = onlineInfo.CircleCount;
SliderCount = onlineInfo.SliderCount;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index 78a1c95066..1f5334d198 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -105,6 +105,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
+ double hitWindowOk = hitWindows.WindowFor(HitResult.Ok) / clockRate;
+ double hitWindowMeh = hitWindows.WindowFor(HitResult.Meh) / clockRate;
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
{
@@ -122,6 +124,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
SpeedTopWeightedSliderFactor = speedTopWeightedSliderFactor,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6,
+ GreatHitWindow = hitWindowGreat,
+ OkHitWindow = hitWindowOk,
+ MehHitWindow = hitWindowMeh,
DrainRate = drainRate,
MaxCombo = beatmap.GetMaxCombo(),
HitCircleCount = hitCirclesCount,
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
index 0aeaf7669f..de4491a31b 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
@@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
[JsonProperty("effective_miss_count")]
public double EffectiveMissCount { get; set; }
+ [JsonProperty("speed_deviation")]
+ public double? SpeedDeviation { get; set; }
+
public override IEnumerable GetAttributesForDisplay()
{
foreach (var attribute in base.GetAttributesForDisplay())
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 8f09147c9f..462c47c420 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -10,6 +10,7 @@ using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
+using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Difficulty
{
@@ -41,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
///
private double effectiveMissCount;
+ private double? speedDeviation;
+
public OsuPerformanceCalculator()
: base(new OsuRuleset())
{
@@ -111,10 +114,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
}
+ speedDeviation = calculateSpeedDeviation(osuAttributes);
+
double aimValue = computeAimValue(score, osuAttributes);
double speedValue = computeSpeedValue(score, osuAttributes);
double accuracyValue = computeAccuracyValue(score, osuAttributes);
double flashlightValue = computeFlashlightValue(score, osuAttributes);
+
double totalValue =
Math.Pow(
Math.Pow(aimValue, 1.1) +
@@ -130,6 +136,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
Accuracy = accuracyValue,
Flashlight = flashlightValue,
EffectiveMissCount = effectiveMissCount,
+ SpeedDeviation = speedDeviation,
Total = totalValue
};
}
@@ -195,14 +202,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= accuracy;
// It is important to consider accuracy difficulty when scaling with accuracy.
- aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
+ aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500;
return aimValue;
}
private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
- if (score.Mods.Any(h => h is OsuModRelax))
+ if (score.Mods.Any(h => h is OsuModRelax) || speedDeviation == null)
return 0.0;
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
@@ -237,6 +244,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
}
+ double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes);
+ speedValue *= speedHighDeviationMultiplier;
+
// Calculate accuracy assuming the worst case scenario
double relevantTotalDiff = totalHits - attributes.SpeedNoteCount;
double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
@@ -245,10 +255,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
// Scale the speed value with accuracy and OD.
- speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2);
-
- // Scale the speed value with # of 50s to punish doubletapping.
- speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
+ speedValue *= (0.95 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2);
return speedValue;
}
@@ -312,11 +319,113 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Scale the flashlight value with accuracy _slightly_.
flashlightValue *= 0.5 + accuracy / 2.0;
// It is important to also consider accuracy difficulty when doing that.
- flashlightValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
+ flashlightValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500;
return flashlightValue;
}
+ ///
+ /// Estimates player's deviation on speed notes using , assuming worst-case.
+ /// Treats all speed notes as hit circles.
+ ///
+ private double? calculateSpeedDeviation(OsuDifficultyAttributes attributes)
+ {
+ if (totalSuccessfulHits == 0)
+ return null;
+
+ // Calculate accuracy assuming the worst case scenario
+ double speedNoteCount = attributes.SpeedNoteCount;
+ speedNoteCount += (totalHits - attributes.SpeedNoteCount) * 0.1;
+
+ // Assume worst case: all mistakes were on speed notes
+ double relevantCountMiss = Math.Min(countMiss, speedNoteCount);
+ double relevantCountMeh = Math.Min(countMeh, speedNoteCount - relevantCountMiss);
+ double relevantCountOk = Math.Min(countOk, speedNoteCount - relevantCountMiss - relevantCountMeh);
+ double relevantCountGreat = Math.Max(0, speedNoteCount - relevantCountMiss - relevantCountMeh - relevantCountOk);
+
+ return calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss);
+ }
+
+ ///
+ /// Estimates the player's tap deviation based on the OD, given number of greats, oks, mehs and misses,
+ /// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings
+ /// will always return the same deviation. Misses are ignored because they are usually due to misaiming.
+ /// Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution.
+ ///
+ private double? calculateDeviation(OsuDifficultyAttributes attributes, double relevantCountGreat, double relevantCountOk, double relevantCountMeh, double relevantCountMiss)
+ {
+ if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0)
+ return null;
+
+ double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss;
+
+ double hitWindowGreat = attributes.GreatHitWindow;
+ double hitWindowOk = attributes.OkHitWindow;
+ double hitWindowMeh = attributes.MehHitWindow;
+
+ // The probability that a player hits a circle is unknown, but we can estimate it to be
+ // the number of greats on circles divided by the number of circles, and then add one
+ // to the number of circles as a bias correction.
+ double n = Math.Max(1, objectCount - relevantCountMiss - relevantCountMeh);
+ const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
+
+ // Proportion of greats hit on circles, ignoring misses and 50s.
+ double p = relevantCountGreat / n;
+
+ // We can be 99% confident that p is at least this value.
+ double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
+
+ // Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed.
+ // Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than:
+ double deviation = hitWindowGreat / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
+
+ double randomValue = Math.Sqrt(2 / Math.PI) * hitWindowOk * Math.Exp(-0.5 * Math.Pow(hitWindowOk / deviation, 2))
+ / (deviation * SpecialFunctions.Erf(hitWindowOk / (Math.Sqrt(2) * deviation)));
+
+ deviation *= Math.Sqrt(1 - randomValue);
+
+ // Value deviation approach as greatCount approaches 0
+ double limitValue = hitWindowOk / Math.Sqrt(3);
+
+ // If precision is not enough to compute true deviation - use limit value
+ if (pLowerBound == 0 || randomValue >= 1 || deviation > limitValue)
+ deviation = limitValue;
+
+ // Then compute the variance for mehs.
+ double mehVariance = (hitWindowMeh * hitWindowMeh + hitWindowOk * hitWindowMeh + hitWindowOk * hitWindowOk) / 3;
+
+ // Find the total deviation.
+ deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh));
+
+ return deviation;
+ }
+
+ // Calculates multiplier for speed to account for improper tapping based on the deviation and speed difficulty
+ // https://www.desmos.com/calculator/dmogdhzofn
+ private double calculateSpeedHighDeviationNerf(OsuDifficultyAttributes attributes)
+ {
+ if (speedDeviation == null)
+ return 0;
+
+ double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
+
+ // Decides a point where the PP value achieved compared to the speed deviation is assumed to be tapped improperly. Any PP above this point is considered "excess" speed difficulty.
+ // This is used to cause PP above the cutoff to scale logarithmically towards the original speed value thus nerfing the value.
+ double excessSpeedDifficultyCutoff = 100 + 220 * Math.Pow(22 / speedDeviation.Value, 6.5);
+
+ if (speedValue <= excessSpeedDifficultyCutoff)
+ return 1.0;
+
+ const double scale = 50;
+ double adjustedSpeedValue = scale * (Math.Log((speedValue - excessSpeedDifficultyCutoff) / scale + 1) + excessSpeedDifficultyCutoff / scale);
+
+ // 200 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible
+ double lerp = 1 - Math.Clamp((speedDeviation.Value - 20) / (24 - 20), 0, 1);
+ adjustedSpeedValue = double.Lerp(adjustedSpeedValue, speedValue, lerp);
+
+ return adjustedSpeedValue / speedValue;
+ }
+
// 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.
@@ -334,6 +443,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
}
private int totalHits => countGreat + countOk + countMeh + countMiss;
+ private int totalSuccessfulHits => countGreat + countOk + countMeh;
private int totalImperfectHits => countOk + countMeh + countMiss;
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index 10d5c1e27e..240a360cb9 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
public class Speed : OsuStrainSkill
{
- private double skillMultiplier => 1.430;
+ private double skillMultiplier => 1.46;
private double strainDecayBase => 0.3;
private double currentStrain;
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index ba247c68d4..de3bec5fcf 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
- [TestCase(3.0950934814938953d, 200, "diffcalc-test")]
- [TestCase(3.0950934814938953d, 200, "diffcalc-test-strong")]
+ [TestCase(2.837609165845338d, 200, "diffcalc-test")]
+ [TestCase(2.837609165845338d, 200, "diffcalc-test-strong")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(4.0839365008715403d, 200, "diffcalc-test")]
- [TestCase(4.0839365008715403d, 200, "diffcalc-test-strong")]
+ [TestCase(3.8005218640444949, 200, "diffcalc-test")]
+ [TestCase(3.8005218640444949, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
index 25428c8b2f..3ff5b87fb6 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
@@ -36,18 +36,70 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
}
+ ///
+ /// Calculates a consistency penalty based on the number of consecutive consistent intervals,
+ /// considering the delta time between each colour sequence.
+ ///
+ /// The current hitObject to consider.
+ /// The allowable margin of error for determining whether ratios are consistent.
+ /// The maximum objects to check per count of consistent ratio.
+ private static double consistentRatioPenalty(TaikoDifficultyHitObject hitObject, double threshold = 0.01, int maxObjectsToCheck = 64)
+ {
+ int consistentRatioCount = 0;
+ double totalRatioCount = 0.0;
+
+ TaikoDifficultyHitObject current = hitObject;
+
+ for (int i = 0; i < maxObjectsToCheck; i++)
+ {
+ // Break if there is no valid previous object
+ if (current.Index <= 1)
+ break;
+
+ var previousHitObject = (TaikoDifficultyHitObject)current.Previous(1);
+
+ double currentRatio = current.Rhythm.Ratio;
+ double previousRatio = previousHitObject.Rhythm.Ratio;
+
+ // A consistent interval is defined as the percentage difference between the two rhythmic ratios with the margin of error.
+ if (Math.Abs(1 - currentRatio / previousRatio) <= threshold)
+ {
+ consistentRatioCount++;
+ totalRatioCount += currentRatio;
+ break;
+ }
+
+ // Move to the previous object
+ current = previousHitObject;
+ }
+
+ // Ensure no division by zero
+ double ratioPenalty = 1 - totalRatioCount / (consistentRatioCount + 1) * 0.80;
+
+ return ratioPenalty;
+ }
+
+ ///
+ /// Evaluate the difficulty of the first hitobject within a colour streak.
+ ///
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject)
{
- TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour;
+ var taikoObject = (TaikoDifficultyHitObject)hitObject;
+ TaikoDifficultyHitObjectColour colour = taikoObject.Colour;
double difficulty = 0.0d;
if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak
difficulty += EvaluateDifficultyOf(colour.MonoStreak);
+
if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern
difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern);
+
if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern);
+ double consistencyPenalty = consistentRatioPenalty(taikoObject);
+ difficulty *= consistencyPenalty;
+
return difficulty;
}
}
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
index f5ed5a180b..1d6cee043b 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
@@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Difficulty
protected const int ATTRIB_ID_OK_HIT_WINDOW = 27;
protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29;
protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31;
+ protected const int ATTRIB_ID_MEH_HIT_WINDOW = 33;
///
/// The mods which were applied to the beatmap.
diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
index 497a1f8234..aeccf2fd55 100644
--- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
+++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
@@ -66,6 +66,19 @@ namespace osu.Game.Rulesets.Difficulty.Utils
/// The output of the bell curve function of
public static double BellCurve(double x, double mean, double width, double multiplier = 1.0) => multiplier * Math.Exp(Math.E * -(Math.Pow(x - mean, 2) / Math.Pow(width, 2)));
+ ///
+ /// Smoothstep function (https://en.wikipedia.org/wiki/Smoothstep)
+ ///
+ /// Value to calculate the function for
+ /// Value at which function returns 0
+ /// Value at which function returns 1
+ public static double Smoothstep(double x, double start, double end)
+ {
+ x = Math.Clamp((x - start) / (end - start), 0.0, 1.0);
+
+ return x * x * (3.0 - 2.0 * x);
+ }
+
///
/// Smootherstep function (https://en.wikipedia.org/wiki/Smoothstep#Variations)
///