diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index afd94f457004..8d8387378e74 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -15,13 +15,13 @@ public class OsuDifficultyCalculatorTest : DifficultyCalculatorTest
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
- [TestCase(6.9311451172574934d, "diffcalc-test")]
- [TestCase(1.0736586907780401d, "zero-length-sliders")]
+ [TestCase(6.7568168283591499d, "diffcalc-test")]
+ [TestCase(1.0348244046058293d, "zero-length-sliders")]
public void Test(double expected, string name)
=> base.Test(expected, name);
- [TestCase(8.7212283220412345d, "diffcalc-test")]
- [TestCase(1.3212137158641493d, "zero-length-sliders")]
+ [TestCase(8.4783236764532557d, "diffcalc-test")]
+ [TestCase(1.2708532136987165d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime());
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index a76db4abe396..e6ab978dfbbc 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -103,22 +103,26 @@ private double computeAimValue()
double approachRateTotalHitsFactor = 1.0 / (1.0 + Math.Exp(-(0.007 * (totalHits - 400))));
- aimValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
+ double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
if (mods.Any(h => h is OsuModHidden))
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
+ double flashlightBonus = 1.0;
+
if (mods.Any(h => h is OsuModFlashlight))
{
// Apply object-based bonus for flashlight.
- aimValue *= 1.0 + 0.35 * Math.Min(1.0, totalHits / 200.0) +
- (totalHits > 200
- ? 0.3 * Math.Min(1.0, (totalHits - 200) / 300.0) +
- (totalHits > 500 ? (totalHits - 500) / 1200.0 : 0.0)
- : 0.0);
+ flashlightBonus = 1.0 + 0.35 * Math.Min(1.0, totalHits / 200.0) +
+ (totalHits > 200
+ ? 0.3 * Math.Min(1.0, (totalHits - 200) / 300.0) +
+ (totalHits > 500 ? (totalHits - 500) / 1200.0 : 0.0)
+ : 0.0);
}
+ aimValue *= Math.Max(flashlightBonus, approachRateBonus);
+
// Scale the aim value with accuracy _slightly_
aimValue *= 0.5 + accuracy / 2.0;
// It is important to also consider accuracy difficulty when doing that
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index cb819ec09067..16a18cbcb90c 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -3,7 +3,6 @@
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
@@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
/// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
///
- public class Aim : StrainSkill
+ public class Aim : OsuStrainSkill
{
private const double angle_bonus_begin = Math.PI / 3;
private const double timing_threshold = 107;
@@ -47,7 +46,7 @@ protected override double StrainValueOf(DifficultyHitObject current)
Math.Max(osuPrevious.JumpDistance - scale, 0)
* Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2)
* Math.Max(osuCurrent.JumpDistance - scale, 0));
- result = 1.5 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime);
+ result = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime);
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
new file mode 100644
index 000000000000..e47edc37cca9
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
@@ -0,0 +1,61 @@
+// 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 osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mods;
+using System.Linq;
+using osu.Framework.Utils;
+
+namespace osu.Game.Rulesets.Osu.Difficulty.Skills
+{
+ public abstract class OsuStrainSkill : StrainSkill
+ {
+ ///
+ /// The number of sections with the highest strains, which the peak strain reductions will apply to.
+ /// This is done in order to decrease their impact on the overall difficulty of the map for this skill.
+ ///
+ protected virtual int ReducedSectionCount => 10;
+
+ ///
+ /// The baseline multiplier applied to the section with the biggest strain.
+ ///
+ protected virtual double ReducedStrainBaseline => 0.75;
+
+ ///
+ /// The final multiplier to be applied to after all other calculations.
+ ///
+ protected virtual double DifficultyMultiplier => 1.06;
+
+ protected OsuStrainSkill(Mod[] mods)
+ : base(mods)
+ {
+ }
+
+ public override double DifficultyValue()
+ {
+ double difficulty = 0;
+ double weight = 1;
+
+ List strains = GetCurrentStrainPeaks().OrderByDescending(d => d).ToList();
+
+ // We are reducing the highest strains first to account for extreme difficulty spikes
+ for (int i = 0; i < Math.Min(strains.Count, ReducedSectionCount); i++)
+ {
+ double scale = Math.Log10(Interpolation.Lerp(1, 10, Math.Clamp((float)i / ReducedSectionCount, 0, 1)));
+ strains[i] *= Interpolation.Lerp(ReducedStrainBaseline, 1.0, scale);
+ }
+
+ // Difficulty is the weighted sum of the highest strains from every section.
+ // We're sorting from highest to lowest strain.
+ foreach (double strain in strains.OrderByDescending(d => d))
+ {
+ difficulty += strain * weight;
+ weight *= DecayWeight;
+ }
+
+ return difficulty * DifficultyMultiplier;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index fbac080fc6c9..f0eb199e5f6d 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -3,7 +3,6 @@
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
@@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
/// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit.
///
- public class Speed : StrainSkill
+ public class Speed : OsuStrainSkill
{
private const double single_spacing_threshold = 125;
@@ -23,6 +22,8 @@ public class Speed : StrainSkill
protected override double SkillMultiplier => 1400;
protected override double StrainDecayBase => 0.3;
+ protected override int ReducedSectionCount => 5;
+ protected override double DifficultyMultiplier => 1.04;
private const double min_speed_bonus = 75; // ~200BPM
private const double max_speed_bonus = 45; // ~330BPM
diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
index 71cee3681276..d4fcefab9b17 100644
--- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
+++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
@@ -109,7 +109,7 @@ private void startNewSectionFrom(double time)
///
/// Returns the calculated difficulty value representing all s that have been processed up to this point.
///
- public sealed override double DifficultyValue()
+ public override double DifficultyValue()
{
double difficulty = 0;
double weight = 1;