diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
index 118468cce6..3d1939acac 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
/// - and slider difficulty.
///
///
- public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliderTravelDistance, double clampPreemptTime = 0)
+ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliderTravelDistance)
{
if (current.BaseObject is Spinner || current.Index <= 1 || current.Previous(0).BaseObject is Spinner)
return 0;
@@ -121,17 +121,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier);
- if (clampPreemptTime > 0)
- {
- // Scale if AR is too high for high AR calc
- double multiplier = osuCurrObj.StrainTime / Math.Min(osuCurrObj.StrainTime, clampPreemptTime - 150); // 150ms is considered as reaction time
- double multiplierIfAR11 = osuCurrObj.StrainTime / Math.Min(osuCurrObj.StrainTime, 150);
-
- multiplier = Math.Min(multiplier, multiplierIfAR11);
-
- aimStrain *= multiplier;
- }
-
// Add in additional slider velocity bonus.
if (withSliderTravelDistance)
aimStrain += sliderBonus * slider_multiplier;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingEvaluator.cs
index 7c6c82fdb9..90fb1758d0 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingEvaluator.cs
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
{
+ // Main class with some util functions
public static class ReadingEvaluator
{
private const double reading_window_size = 3000;
@@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
double loopDifficulty = currObj.OpacityAt(loopObj.BaseObject.StartTime, false);
// Small distances means objects may be cheesed, so it doesn't matter whether they are arranged confusingly.
- loopDifficulty *= logistic((loopObj.MinimumJumpDistance - 90) / 15);
+ loopDifficulty *= logistic((loopObj.MinimumJumpDistance - 60) / 10);
//double timeBetweenCurrAndLoopObj = (currObj.BaseObject.StartTime - loopObj.BaseObject.StartTime) / clockRateEstimate;
double timeBetweenCurrAndLoopObj = currObj.StartTime - loopObj.StartTime;
@@ -79,57 +80,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
return difficulty;
}
- public static double EvaluateHighARDifficultyOf(DifficultyHitObject current, bool applyAdjust = false)
- {
- var currObj = (OsuDifficultyHitObject)current;
-
- double result = highArCurve(currObj.Preempt);
-
- if (applyAdjust)
- {
- double inpredictability = EvaluateInpredictabilityOf(current);
-
- // follow lines make high AR easier, so apply nerf if object isn't new combo
- inpredictability *= 1 + 0.1 * (800 - currObj.FollowLineTime) / 800;
-
- result *= 0.85 + 0.75 * inpredictability;
- }
-
- return result;
- }
-
- public static double EvaluateHiddenDifficultyOf(DifficultyHitObject current)
- {
- var currObj = (OsuDifficultyHitObject)current;
-
- double aimDifficulty = AimEvaluator.EvaluateDifficultyOf(current, false);
-
- double hdDifficulty = 0;
-
- double timeSpentInvisible = getDurationSpentInvisible(currObj) / currObj.ClockRate;
-
- double density = 1 + Math.Max(0, CalculateDenstityOf(currObj) - 1);
- density *= getConstantAngleNerfFactor(currObj);
-
- double timeDifficultyFactor = density / 1000;
-
- double visibleObjectFactor = Math.Clamp(retrieveCurrentVisibleObjects(currObj).Count - 2, 0, 15);
-
- hdDifficulty += Math.Pow(visibleObjectFactor * timeSpentInvisible * timeDifficultyFactor, 1) +
- (6 + visibleObjectFactor) * aimDifficulty;
-
- hdDifficulty *= 0.95 + 0.15 * EvaluateInpredictabilityOf(current); // Max multiplier is 1.1
-
- return hdDifficulty;
- }
-
// Returns value from 0 to 1, where 0 is very predictable and 1 is very unpredictable
public static double EvaluateInpredictabilityOf(DifficultyHitObject current)
{
// make the sum equal to 1
- const double velocity_change_part = 0.3;
- const double angle_change_part = 0.6;
- const double rhythm_change_part = 0.1;
+ const double velocity_change_part = 0.25;
+ const double angle_change_part = 0.45;
+ const double rhythm_change_part = 0.3;
if (current.BaseObject is Spinner || current.Index == 0 || current.Previous(0).BaseObject is Spinner)
return 0;
@@ -137,23 +94,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
var osuCurrObj = (OsuDifficultyHitObject)current;
var osuLastObj = (OsuDifficultyHitObject)current.Previous(0);
- double velocityChangeBonus = 0;
+ // Rhythm difference punishment for velocity and angle bonuses
+ double rhythmSimilarity = 1 - getRhythmDifference(osuCurrObj.StrainTime, osuLastObj.StrainTime);
+
+ // Make differentiation going from 1/4 to 1/2 and bigger difference
+ // To 1/3 to 1/2 and smaller difference
+ rhythmSimilarity = Math.Clamp(rhythmSimilarity, 0.5, 0.75);
+ rhythmSimilarity = 4 * (rhythmSimilarity - 0.5);
+
+ double velocityChangeBonus = getVelocityChangeFactor(osuCurrObj, osuLastObj) * rhythmSimilarity;
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
- // https://www.desmos.com/calculator/kqxmqc8pkg
- if (currVelocity > 0 || prevVelocity > 0)
- {
- double velocityChange = Math.Max(0,
- Math.Min(
- Math.Abs(prevVelocity - currVelocity) - 0.5 * Math.Min(currVelocity, prevVelocity),
- Math.Max(((OsuHitObject)osuCurrObj.BaseObject).Radius / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Min(currVelocity, prevVelocity))
- )); // Stealed from xexxar
- velocityChangeBonus = velocityChange / Math.Max(currVelocity, prevVelocity); // maxiumum is 0.4
- velocityChangeBonus /= 0.4;
- }
-
double angleChangeBonus = 0;
if (osuCurrObj.Angle != null && osuLastObj.Angle != null && currVelocity > 0 && prevVelocity > 0)
@@ -162,6 +115,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
angleChangeBonus *= Math.Min(currVelocity, prevVelocity) / Math.Max(currVelocity, prevVelocity); // Prevent cheesing
}
+ angleChangeBonus *= rhythmSimilarity;
+
+ // This bonus only awards rhythm changes if they're not filled with sliderends
double rhythmChangeBonus = 0;
if (current.Index > 1)
@@ -190,19 +146,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
return result;
}
- public static double EvaluateLowDensityBonusOf(DifficultyHitObject current)
+ private static double getVelocityChangeFactor(OsuDifficultyHitObject osuCurrObj, OsuDifficultyHitObject osuLastObj)
{
- //var currObj = (OsuDifficultyHitObject)current;
+ double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
+ double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
- //// Density = 2 in general means 3 notes on screen (it's not including current note)
- //double density = CalculateDenstityOf(currObj);
+ double velocityChangeFactor = 0;
- //// We are considering density = 1.5 as starting point, 1.0 is noticably uncomfy and 0.5 is severely uncomfy
- //double bonus = 1.5 - density;
- //if (bonus <= 0) return 0;
+ // https://www.desmos.com/calculator/kqxmqc8pkg
+ if (currVelocity > 0 || prevVelocity > 0)
+ {
+ double velocityChange = Math.Max(0,
+ Math.Min(
+ Math.Abs(prevVelocity - currVelocity) - 0.5 * Math.Min(currVelocity, prevVelocity),
+ Math.Max(((OsuHitObject)osuCurrObj.BaseObject).Radius / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Min(currVelocity, prevVelocity))
+ )); // Stealed from xexxar
+ velocityChangeFactor = velocityChange / Math.Max(currVelocity, prevVelocity); // maxiumum is 0.4
+ velocityChangeFactor /= 0.4;
+ }
- //return Math.Pow(bonus, 2);
- return 0;
+ return velocityChangeFactor;
}
// Returns a list of objects that are visible on screen at
@@ -222,35 +185,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
}
}
- private static List retrieveCurrentVisibleObjects(OsuDifficultyHitObject current)
- {
- List objects = new List();
-
- for (int i = 0; i < current.Count; i++)
- {
- OsuDifficultyHitObject hitObject = (OsuDifficultyHitObject)current.Next(i);
-
- if (hitObject.IsNull() ||
- (hitObject.StartTime - current.StartTime) > reading_window_size ||
- current.StartTime < hitObject.StartTime - hitObject.Preempt)
- break;
-
- objects.Add(hitObject);
- }
-
- return objects;
- }
-
- private static double getDurationSpentInvisible(OsuDifficultyHitObject current)
- {
- var baseObject = (OsuHitObject)current.BaseObject;
-
- double fadeOutStartTime = baseObject.StartTime - baseObject.TimePreempt + baseObject.TimeFadeIn;
- double fadeOutDuration = baseObject.TimePreempt * OsuModHidden.FADE_OUT_DURATION_MULTIPLIER;
-
- return (fadeOutStartTime + fadeOutDuration) - (baseObject.StartTime - baseObject.TimePreempt);
- }
-
private static double getConstantAngleNerfFactor(OsuDifficultyHitObject current)
{
const double time_limit = 2000;
@@ -327,19 +261,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
return Math.Clamp(2 - deltaTime / (reading_window_size / 2), 0, 1);
}
- // https://www.desmos.com/calculator/hbj7swzlth
- private static double highArCurve(double preempt)
- {
- double value = Math.Pow(3, 3 - 0.01 * preempt); // 1 for 300ms, 0.25 for 400ms, 0.0625 for 500ms
- value = softmin(value, 2, 1.7); // use softmin to achieve full-memory cap, 2 times more than AR11 (300ms)
- return value;
- }
-
private static double getRhythmDifference(double t1, double t2) => 1 - Math.Min(t1, t2) / Math.Max(t1, t2);
private static double logistic(double x) => 1 / (1 + Math.Exp(-x));
-
- // We are using mutiply and divide instead of add and subtract, so values won't be negative
- // https://www.desmos.com/calculator/fv5xerwpd2
- 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);
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingHiddenEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingHiddenEvaluator.cs
new file mode 100644
index 0000000000..6b25e4657a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingHiddenEvaluator.cs
@@ -0,0 +1,219 @@
+// 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.Framework.Extensions.ObjectExtensions;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
+
+namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
+{
+ // Class for HD calc. Split because there are a lot of things in HD calc.
+ public static class ReadingHiddenEvaluator
+ {
+ private const double reading_window_size = 3000;
+
+ public static double EvaluateDifficultyOf(DifficultyHitObject current)
+ {
+ var currObj = (OsuDifficultyHitObject)current;
+
+ double density = 0;
+ double densityAnglesNerf = -2; // we have threshold of 2, so 2 or same angles won't be punished
+
+ OsuDifficultyHitObject? prevObj0 = null;
+ OsuDifficultyHitObject? prevObj1 = null;
+ OsuDifficultyHitObject? prevObj2 = null;
+
+ double prevConstantAngle = 0;
+
+ foreach (var loopObj in retrievePastVisibleObjects(currObj).Reverse())
+ {
+ double loopDifficulty = currObj.OpacityAt(loopObj.BaseObject.StartTime, false);
+
+ // Small distances means objects may be cheesed, so it doesn't matter whether they are arranged confusingly.
+ // For HD: it's not subtracting anything cuz it's multiplied by the aim difficulty anyways.
+ // loopDifficulty *= logistic((loopObj.MinimumJumpDistance) / 15);
+
+ // Reduce density bonus for this object if they're too apart in time
+ // Nerf starts on 1500ms and reaches maximum (*=0) on 3000ms
+ double timeBetweenCurrAndLoopObj = currObj.StartTime - loopObj.StartTime;
+ loopDifficulty *= getTimeNerfFactor(timeBetweenCurrAndLoopObj);
+
+ if (prevObj0.IsNull())
+ {
+ prevObj0 = loopObj;
+ continue;
+ }
+
+ // HD-exclusive burst nerf
+
+ // Only if next object is slower, representing break from many notes in a row
+ if (loopObj.StrainTime > prevObj0.StrainTime)
+ {
+ // Get rhythm similarity: 1 on same rhythms, 0.5 on 1/4 to 1/2
+ double rhythmSimilarity = 1 - getRhythmDifference(loopObj.StrainTime, prevObj0.StrainTime);
+
+ // Make differentiation going from 1/4 to 1/2 and bigger difference
+ // To 1/3 to 1/2 and smaller difference
+ rhythmSimilarity = Math.Clamp(rhythmSimilarity, 0.5, 0.75);
+ rhythmSimilarity = 4 * (rhythmSimilarity - 0.5);
+
+ // Reduce density for this objects if rhythms are different
+ loopDifficulty *= rhythmSimilarity;
+ }
+
+ density += loopDifficulty;
+
+ // Angles nerf
+
+ if (loopObj.Angle.IsNotNull() && prevObj0.Angle.IsNotNull())
+ {
+ double angleDifference = Math.Abs(prevObj0.Angle.Value - loopObj.Angle.Value);
+
+ // Nerf alternating angles case
+ if (prevObj1.IsNotNull() && prevObj2.IsNotNull() && prevObj1.Angle.IsNotNull() && prevObj2.Angle.IsNotNull())
+ {
+ // Normalized difference
+ double angleDifference1 = Math.Abs(prevObj1.Angle.Value - loopObj.Angle.Value) / Math.PI;
+ double angleDifference2 = Math.Abs(prevObj2.Angle.Value - prevObj0.Angle.Value) / Math.PI;
+
+ // Will be close to 1 if angleDifference1 and angleDifference2 was both close to 0
+ double alternatingFactor = Math.Pow((1 - angleDifference1) * (1 - angleDifference2), 2);
+
+ // Be sure to nerf only same rhythms
+ double rhythmFactor = 1 - getRhythmDifference(loopObj.StrainTime, prevObj0.StrainTime); // 0 on different rhythm, 1 on same rhythm
+ rhythmFactor *= 1 - getRhythmDifference(prevObj0.StrainTime, prevObj1.StrainTime);
+ rhythmFactor *= 1 - getRhythmDifference(prevObj1.StrainTime, prevObj2.StrainTime);
+
+ double acuteAngleFactor = 1 - Math.Min(loopObj.Angle.Value, prevObj0.Angle.Value) / Math.PI;
+
+ double prevAngleAdjust = Math.Max(angleDifference - angleDifference1, 0);
+
+ prevAngleAdjust *= alternatingFactor; // Nerf if alternating
+ prevAngleAdjust *= rhythmFactor; // Nerf if same rhythms
+ prevAngleAdjust *= acuteAngleFactor;
+
+ angleDifference -= prevAngleAdjust;
+ }
+
+ // Reduce angles nerf if objects are too apart in time
+ // Angle nerf is starting being reduced from 200ms (150BPM jump) and it reduced to 0 on 2000ms
+ double longIntervalFactor = Math.Clamp(1 - (loopObj.StrainTime - 200) / (2000 - 200), 0, 1);
+
+ // Current angle nerf. Angle difference less than 15 degrees is considered the same
+ double currConstantAngle = Math.Cos(4 * Math.Min(Math.PI / 12, angleDifference)) * longIntervalFactor;
+
+ // Apply the nerf only when it's repeated
+ double currentAngleNerf = Math.Min(currConstantAngle, prevConstantAngle);
+
+ densityAnglesNerf += Math.Min(currentAngleNerf, loopDifficulty);
+ prevConstantAngle = currConstantAngle;
+ }
+
+ prevObj2 = prevObj1;
+ prevObj1 = prevObj0;
+ prevObj0 = loopObj;
+ }
+
+ // Apply angles nerf
+ density -= Math.Max(0, densityAnglesNerf);
+
+ // Consider that density matters only starting from 3rd note on the screen
+ double densityFactor = Math.Max(0, density - 1) / 4;
+
+ // This is kinda wrong cuz it returns value bigger than preempt
+ // double timeSpentInvisible = getDurationSpentInvisible(currObj) / 1000 / currObj.ClockRate;
+
+ // The closer timeSpentInvisible is to 0 -> the less difference there are between NM and HD
+ // So we will reduce base according to this
+ // It will be 0.354 on AR11 value
+ double invisibilityFactor = logistic(currObj.Preempt / 120 - 4);
+
+ double hdDifficulty = invisibilityFactor + densityFactor;
+
+ // Scale by inpredictability slightly
+ hdDifficulty *= 0.95 + 0.15 * ReadingEvaluator.EvaluateInpredictabilityOf(current); // Max multiplier is 1.1
+
+ return hdDifficulty;
+ }
+
+ //public static double EvaluateHiddenDifficultyOfOld(DifficultyHitObject current)
+ //{
+ // var currObj = (OsuDifficultyHitObject)current;
+
+ // double aimDifficulty = AimEvaluator.EvaluateDifficultyOf(current, false);
+
+ // double timeSpentInvisible = getDurationSpentInvisible(currObj) / currObj.ClockRate;
+
+ // double density = 1 + Math.Max(0, CalculateDenstityOf(currObj) - 1);
+
+ // double timeDifficultyFactor = density / 1000;
+ // timeDifficultyFactor *= getConstantAngleNerfFactor(currObj);
+
+ // double visibleObjectFactor = Math.Clamp(retrieveCurrentVisibleObjects(currObj).Count - 2, 0, 15);
+
+ // double hdDifficulty = visibleObjectFactor * timeSpentInvisible * timeDifficultyFactor +
+ // (6 + visibleObjectFactor) * aimDifficulty;
+
+ // hdDifficulty *= 0.95 + 0.15 * EvaluateInpredictabilityOf(current); // Max multiplier is 1.1
+
+ // return hdDifficulty;
+ //}
+
+ // Returns a list of objects that are visible on screen at
+ // the point in time at which the current object becomes visible.
+ private static IEnumerable retrievePastVisibleObjects(OsuDifficultyHitObject current)
+ {
+ for (int i = 0; i < current.Index; i++)
+ {
+ OsuDifficultyHitObject hitObject = (OsuDifficultyHitObject)current.Previous(i);
+
+ if (hitObject.IsNull() ||
+ current.StartTime - hitObject.StartTime > reading_window_size ||
+ hitObject.StartTime < current.StartTime - current.Preempt)
+ break;
+
+ yield return hitObject;
+ }
+ }
+
+ //private static double getDurationSpentInvisible(OsuDifficultyHitObject current)
+ //{
+ // var baseObject = (OsuHitObject)current.BaseObject;
+
+ // double fadeOutStartTime = baseObject.StartTime - baseObject.TimePreempt + baseObject.TimeFadeIn;
+ // double fadeOutDuration = baseObject.TimePreempt * OsuModHidden.FADE_OUT_DURATION_MULTIPLIER;
+
+ // return (fadeOutStartTime + fadeOutDuration) - (baseObject.StartTime - baseObject.TimePreempt);
+ //}
+
+ //private static List retrieveCurrentVisibleObjects(OsuDifficultyHitObject current)
+ //{
+ // List objects = new List();
+
+ // for (int i = 0; i < current.Count; i++)
+ // {
+ // OsuDifficultyHitObject hitObject = (OsuDifficultyHitObject)current.Next(i);
+
+ // if (hitObject.IsNull() ||
+ // (hitObject.StartTime - current.StartTime) > reading_window_size ||
+ // current.StartTime < hitObject.StartTime - hitObject.Preempt)
+ // break;
+
+ // objects.Add(hitObject);
+ // }
+
+ // return objects;
+ //}
+
+ private static double getTimeNerfFactor(double deltaTime)
+ {
+ return Math.Clamp(2 - deltaTime / (reading_window_size / 2), 0, 1);
+ }
+
+ private static double getRhythmDifference(double t1, double t2) => 1 - Math.Min(t1, t2) / Math.Max(t1, t2);
+ private static double logistic(double x) => 1 / (1 + Math.Exp(-x));
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingHighAREvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingHighAREvaluator.cs
new file mode 100644
index 0000000000..aea7f15ab2
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingHighAREvaluator.cs
@@ -0,0 +1,149 @@
+// 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.Framework.Extensions.ObjectExtensions;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+
+namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
+{
+ // Main class with some util functions
+ public static class ReadingHighAREvaluator
+ {
+ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool applyAdjust = false)
+ {
+ var currObj = (OsuDifficultyHitObject)current;
+
+ double result = highArCurve(currObj.Preempt);
+
+ if (applyAdjust)
+ {
+ double inpredictability = ReadingEvaluator.EvaluateInpredictabilityOf(current);
+
+ // follow lines make high AR easier, so apply nerf if object isn't new combo
+ inpredictability *= 1 + 0.1 * (800 - currObj.FollowLineTime) / 800;
+
+ result *= 0.85 + 1 * inpredictability;
+ result *= 1.05 - 0.4 * EvaluateFieryAnglePunishmentOf(current);
+ }
+
+ return result;
+ }
+
+ // Explicitely nerfs edgecased fiery-type jumps for high AR. The difference from Inpredictability is that this is not used in HD calc
+ public static double EvaluateFieryAnglePunishmentOf(DifficultyHitObject current)
+ {
+ if (current.Index <= 2)
+ return 0;
+
+ var currObj = (OsuDifficultyHitObject)current;
+ var lastObj0 = (OsuDifficultyHitObject)current.Previous(0);
+ var lastObj1 = (OsuDifficultyHitObject)current.Previous(1);
+ var lastObj2 = (OsuDifficultyHitObject)current.Previous(2);
+
+ if (currObj.Angle.IsNull() || lastObj0.Angle.IsNull() || lastObj1.Angle.IsNull() || lastObj2.Angle.IsNull())
+ return 0;
+
+ // Punishment will be reduced if velocity is changing
+ double velocityChangeFactor = getVelocityChangeFactor(currObj, lastObj0);
+ velocityChangeFactor = 1 - Math.Pow(velocityChangeFactor, 2);
+
+ double a1 = currObj.Angle.Value / Math.PI;
+ double a2 = lastObj0.Angle.Value / Math.PI;
+ double a3 = lastObj1.Angle.Value / Math.PI;
+ double a4 = lastObj2.Angle.Value / Math.PI;
+
+ // - 4 same sharp angles in a row: (0.3 0.3 0.3 0.3) -> max punishment
+
+ // Normalized difference
+ double angleDifference1 = Math.Abs(a1 - a2);
+ double angleDifference2 = Math.Abs(a1 - a3);
+ double angleDifference3 = Math.Abs(a1 - a4);
+
+ // Will be close to 1 if angleDifference1 and angleDifference2 was both close to 0
+ double sameAnglePunishment = Math.Pow((1 - angleDifference1) * (1 - angleDifference2) * (1 - angleDifference3), 3);
+
+ // Starting from 60 degrees - reduce same angle punishment
+ double angleSharpnessFactor = Math.Max(0, a1 - 1.0 / 3);
+ angleSharpnessFactor = 1 - angleSharpnessFactor;
+
+ sameAnglePunishment *= angleSharpnessFactor;
+ sameAnglePunishment *= velocityChangeFactor;
+ sameAnglePunishment *= 0.75;
+
+ // - Alternating angles with 0: (0.3 0 0.3 0) or (0 0.3 0 0.3) -> max punishment, (0.3 0 0.1 0) -> some punishment
+
+ double alternateWithZeroAnglePunishment = Math.Max(
+ getAlternateWithZeroAnglePunishment(a1, a2, a3, a4),
+ getAlternateWithZeroAnglePunishment(a2, a1, a4, a3));
+ alternateWithZeroAnglePunishment *= velocityChangeFactor;
+
+ return Math.Min(1, sameAnglePunishment + alternateWithZeroAnglePunishment);
+ }
+
+ private static double getVelocityChangeFactor(OsuDifficultyHitObject osuCurrObj, OsuDifficultyHitObject osuLastObj)
+ {
+ double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
+ double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
+
+ double velocityChangeFactor = 0;
+
+ // https://www.desmos.com/calculator/kqxmqc8pkg
+ if (currVelocity > 0 || prevVelocity > 0)
+ {
+ double velocityChange = Math.Max(0,
+ Math.Min(
+ Math.Abs(prevVelocity - currVelocity) - 0.5 * Math.Min(currVelocity, prevVelocity),
+ Math.Max(((OsuHitObject)osuCurrObj.BaseObject).Radius / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Min(currVelocity, prevVelocity))
+ )); // Stealed from xexxar
+ velocityChangeFactor = velocityChange / Math.Max(currVelocity, prevVelocity); // maxiumum is 0.4
+ velocityChangeFactor /= 0.4;
+ }
+
+ return velocityChangeFactor;
+ }
+
+ private static double getAlternateWithZeroAnglePunishment(double a1, double a2, double a3, double a4)
+ {
+ // We assume that a1 and a3 are 0
+ double zeroFactor = Math.Pow((1 - a1) * (1 - a3), 8);
+ zeroFactor *= Math.Pow(1 - Math.Abs(a1 - a3), 2);
+
+ double angleSimilarityFactor = 1 - Math.Abs(a2 - a4);
+ double angleSharpnessFactor = Math.Min(1 - Math.Max(0, a2 - 1.0 / 3), 1 - Math.Max(0, a4 - 1.0 / 3));
+
+ return zeroFactor * angleSimilarityFactor * angleSharpnessFactor;
+ }
+
+ public static double EvaluateLowDensityBonusOf(DifficultyHitObject current)
+ {
+ //var currObj = (OsuDifficultyHitObject)current;
+
+ //// Density = 2 in general means 3 notes on screen (it's not including current note)
+ //double density = CalculateDenstityOf(currObj);
+
+ //// We are considering density = 1.5 as starting point, 1.0 is noticably uncomfy and 0.5 is severely uncomfy
+ //double bonus = 1.5 - density;
+ //if (bonus <= 0) return 0;
+
+ //return Math.Pow(bonus, 2);
+ return 0;
+ }
+
+ // https://www.desmos.com/calculator/hbj7swzlth
+ private static double highArCurve(double preempt)
+ {
+ double value = Math.Pow(3, 3 - 0.01 * preempt); // 1 for 300ms, 0.25 for 400ms, 0.0625 for 500ms
+ value = softmin(value, 2, 1.7); // use softmin to achieve full-memory cap, 2 times more than AR11 (300ms)
+ return value;
+ }
+
+ // We are using mutiply and divide instead of add and subtract, so values won't be negative
+ // https://www.desmos.com/calculator/fv5xerwpd2
+ 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);
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index caf9588b94..364adc58a9 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -22,11 +22,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuDifficultyCalculator : DifficultyCalculator
{
- private const double difficulty_multiplier = 0.067;
-
+ public const double DIFFICULTY_MULTIPLIER = 0.067;
+ public const double SUM_POWER = 1.1;
+ public const double FL_SUM_POWER = 1.6;
public override int Version => 20220902;
- public static double SumPower => 1.1;
- public static double FLSumPower => 1.6;
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
@@ -38,16 +37,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (beatmap.HitObjects.Count == 0)
return new OsuDifficultyAttributes { Mods = mods };
- double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
- double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
- double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
+ double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
+ double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
+ double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
double speedNotes = ((Speed)skills[2]).RelevantNoteCount();
- double flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier;
+ double flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
- double readingLowARRating = Math.Sqrt(skills[4].DifficultyValue()) * difficulty_multiplier;
- double readingHighARRating = Math.Sqrt(skills[5].DifficultyValue()) * difficulty_multiplier;
+ double readingLowARRating = Math.Sqrt(skills[4].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
+ double readingHighARRating = Math.Sqrt(skills[5].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
double readingSlidersRating = 0;
- double hiddenRating = Math.Sqrt(skills[6].DifficultyValue()) * difficulty_multiplier;
+ double hiddenRating = Math.Sqrt(skills[6].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
@@ -64,8 +63,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
flashlightRating *= 0.7;
}
- double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
- double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
+ double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
+ double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);
// Cognition
double baseFlashlightPerformance = 0.0;
@@ -73,10 +72,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
baseFlashlightPerformance = Math.Pow(flashlightRating, 2.0) * 25.0;
double baseReadingLowARPerformance = Math.Pow(readingLowARRating, 2.5) * 17.0;
- double baseReadingHighARPerformance = Math.Pow(5 * Math.Max(1, readingHighARRating / 0.0675) - 4, 3) / 100000;
- double baseReadingARPerformance = Math.Pow(Math.Pow(baseReadingLowARPerformance, SumPower) + Math.Pow(baseReadingHighARPerformance, SumPower), 1.0 / SumPower);
+ double baseReadingHighARPerformance = OsuStrainSkill.DifficultyToPerformance(readingHighARRating);
+ double baseReadingARPerformance = Math.Pow(Math.Pow(baseReadingLowARPerformance, SUM_POWER) + Math.Pow(baseReadingHighARPerformance, SUM_POWER), 1.0 / SUM_POWER);
- double baseFlashlightARPerformance = Math.Pow(Math.Pow(baseFlashlightPerformance, FLSumPower) + Math.Pow(baseReadingARPerformance, FLSumPower), 1.0 / FLSumPower);
+ double baseFlashlightARPerformance = Math.Pow(Math.Pow(baseFlashlightPerformance, FL_SUM_POWER) + Math.Pow(baseReadingARPerformance, FL_SUM_POWER), 1.0 / FL_SUM_POWER);
double baseReadingHiddenPerformance = 0;
if (mods.Any(h => h is OsuModHidden))
@@ -86,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double baseReadingNonARPerformance = baseReadingHiddenPerformance + baseReadingSliderPerformance;
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
+
double drainRate = beatmap.Difficulty.DrainRate;
int maxCombo = beatmap.GetMaxCombo();
@@ -94,16 +94,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
// Limit cognition by full memorisation difficulty
- double cognitionPerformance = Math.Pow(Math.Pow(baseFlashlightARPerformance, SumPower) + Math.Pow(baseReadingNonARPerformance, SumPower), 1.0 / SumPower);
- double mechanicalPerformance = Math.Pow(Math.Pow(baseAimPerformance, SumPower) + Math.Pow(baseSpeedPerformance, SumPower), 1.0 / SumPower);
+ double cognitionPerformance = Math.Pow(Math.Pow(baseFlashlightARPerformance, SUM_POWER) + Math.Pow(baseReadingNonARPerformance, SUM_POWER), 1.0 / SUM_POWER);
+ double mechanicalPerformance = Math.Pow(Math.Pow(baseAimPerformance, SUM_POWER) + Math.Pow(baseSpeedPerformance, SUM_POWER), 1.0 / SUM_POWER);
double potentialFlashlightPerformance = OsuPerformanceCalculator.ComputePerfectFlashlightValue(flashlightRating, hitCirclesCount + sliderCount);
cognitionPerformance = OsuPerformanceCalculator.AdjustCognitionPerformance(cognitionPerformance, mechanicalPerformance, potentialFlashlightPerformance);
double basePerformance =
Math.Pow(
- Math.Pow(mechanicalPerformance, SumPower) +
- Math.Pow(cognitionPerformance, SumPower)
- , 1.0 / SumPower
+ Math.Pow(mechanicalPerformance, SUM_POWER) +
+ Math.Pow(cognitionPerformance, SUM_POWER)
+ , 1.0 / SUM_POWER
);
double starRating = basePerformance > 0.00001
@@ -115,6 +115,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
+ //var test = ((ReadingHighAR)skills[5]).GetAimSpeed();
+
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
{
StarRating = starRating,
@@ -128,7 +130,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
HiddenDifficulty = hiddenRating,
FlashlightDifficulty = flashlightRating,
SliderFactor = sliderFactor,
- ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
+ ApproachRate = IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, 1800, 1200, 450),
OverallDifficulty = (80 - hitWindowGreat) / 6,
DrainRate = drainRate,
MaxCombo = maxCombo,
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 979bda168f..384c1b3df7 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -4,8 +4,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Xml.Linq;
using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
double multiplier = PERFORMANCE_BASE_MULTIPLIER;
+ double power = OsuDifficultyCalculator.SUM_POWER;
if (score.Mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
@@ -62,8 +63,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
}
- double power = OsuDifficultyCalculator.SumPower;
-
double aimValue = computeAimValue(score, osuAttributes);
double speedValue = computeSpeedValue(score, osuAttributes);
double mechanicalValue = Math.Pow(Math.Pow(aimValue, power) + Math.Pow(speedValue, power), 1.0 / power);
@@ -75,12 +74,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double flashlightValue = potentialFlashlightValue;
if (!score.Mods.Any(h => h is OsuModFlashlight))
flashlightValue = 0.0;
- double readingARValue = computeReadingARValue(score, osuAttributes);
+
+ double lowARValue = computeReadingLowARValue(score, osuAttributes);
+ double readingHDValue = computeReadingHiddenValue(score, osuAttributes);
+ double readingSlidersValue = 0;
+
+ double highARValue = computeReadingHighARValue(score, osuAttributes);
+
+ double readingARValue = Math.Pow(
+ Math.Pow(lowARValue, power) +
+ Math.Pow(highARValue, power), 1.0 / power);
+
// Reduce AR reading bonus if FL is present
- double flPower = OsuDifficultyCalculator.FLSumPower;
+ double flPower = OsuDifficultyCalculator.FL_SUM_POWER;
double flashlightARValue = Math.Pow(Math.Pow(flashlightValue, flPower) + Math.Pow(readingARValue, flPower), 1.0 / flPower);
- double readingNonARValue = computeReadingNonARValue(score, osuAttributes);
+ double readingNonARValue = readingHDValue + readingSlidersValue;
double cognitionValue = Math.Pow(Math.Pow(flashlightARValue, power) + Math.Pow(readingNonARValue, power), 1.0 / power);
cognitionValue = AdjustCognitionPerformance(cognitionValue, mechanicalValue, potentialFlashlightValue);
@@ -106,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
- double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
+ double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty);
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
@@ -143,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (score.Mods.Any(h => h is OsuModRelax))
return 0.0;
- double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
+ double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
@@ -245,25 +254,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return flashlightValue;
}
- private double computeReadingARValue(ScoreInfo score, OsuDifficultyAttributes attributes)
- {
- //double readingARValue = Math.Max(computeReadingLowARValue(score, attributes), computeReadingHighARValue(score, attributes));
- double power = OsuDifficultyCalculator.SumPower;
- double readingValue = Math.Pow(
- Math.Pow(computeReadingLowARValue(score, attributes), power) +
- Math.Pow(computeReadingHighARValue(score, attributes), power), 1.0 / power);
-
- return readingValue;
- }
-
- private double computeReadingNonARValue(ScoreInfo score, OsuDifficultyAttributes attributes)
- {
- double readingHDValue = computeReadingHiddenValue(score, attributes);
- double readingSlidersValue = 0;
-
- return readingHDValue + readingSlidersValue;
- }
-
private double computeReadingLowARValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
double rawReading = attributes.ReadingDifficultyLowAR;
@@ -289,8 +279,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeReadingHighARValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
- // Copied from aim
- double highARValue = Math.Pow(5.0 * Math.Max(1.0, attributes.ReadingDifficultyHighAR / 0.0675) - 4.0, 3.0) / 100000.0;
+ double highARValue = OsuStrainSkill.DifficultyToPerformance(attributes.ReadingDifficultyHighAR);
// High AR should have length bonus, even more agressive than normal aim
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
@@ -303,11 +292,48 @@ namespace osu.Game.Rulesets.Osu.Difficulty
highARValue *= getComboScalingFactor(attributes);
- highARValue *= accuracy * accuracy;
- // It is important to consider accuracy difficulty when scaling with accuracy.
- highARValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
+ // Approximate how much of high AR difficulty is aim
+ double aimPerformance = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty);
+ double speedPerformance = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
- return highARValue;
+ double aimRatio = aimPerformance / (aimPerformance + speedPerformance);
+
+ // Aim part calculation
+ double aimPartValue = highARValue * aimRatio;
+ {
+ // We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator.
+ double estimateDifficultSliders = attributes.SliderCount * 0.15;
+
+ if (attributes.SliderCount > 0)
+ {
+ double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
+ double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + attributes.SliderFactor;
+ aimPartValue *= sliderNerfFactor;
+ }
+
+ aimPartValue *= accuracy;
+ // It is important to consider accuracy difficulty when scaling with accuracy.
+ aimPartValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
+ }
+
+ // Speed part calculation
+ double speedPartValue = highARValue * (1 - aimRatio);
+ {
+ // Calculate accuracy assuming the worst case scenario
+ double relevantTotalDiff = totalHits - attributes.SpeedNoteCount;
+ double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
+ double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat));
+ 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);
+
+ // 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);
+
+ // Scale the speed value with # of 50s to punish doubletapping.
+ speedPartValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
+ }
+
+ return aimPartValue + speedPartValue;
}
private double computeReadingHiddenValue(ScoreInfo score, OsuDifficultyAttributes attributes)
@@ -316,6 +342,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return 0.0;
double rawReading = attributes.HiddenDifficulty;
+ //double readingValue = Math.Pow(rawReading, 2.0) * 25.0;
double readingValue = Math.Pow(rawReading, 2.0) * 25.0;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index 2ba5c3cc10..19478cd753 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -95,11 +95,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
///
public IList OverlapObjects { get; private set; }
+ ///
+ /// Time in ms between appearence of this and moment to click on it.
+ ///
+ public readonly double Preempt;
+
+ ///
+ /// Preempt of follow line for this adjusted by clockrate.
+ /// Will be equal to 0 if object is New Combo.
+ ///
+ public readonly double FollowLineTime;
+
+ ///
+ /// Playback rate of beatmap.
+ /// Will be equal 1.5 on DT and 0.75 on HT.
+ ///
+ public readonly double ClockRate;
+
private readonly OsuHitObject? lastLastObject;
private readonly OsuHitObject lastObject;
- public readonly double Preempt;
- public readonly double FollowLineTime;
- public readonly double ClockRate;
public OsuDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject? lastLastObject, double clockRate, List objects, int index)
: base(hitObject, lastObject, clockRate, objects, index)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
index 15b20a5572..bee9832b1b 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
@@ -7,6 +7,7 @@ using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using System.Linq;
using osu.Framework.Utils;
+using System.Xml.Linq;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
@@ -67,5 +68,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return difficulty * DifficultyMultiplier;
}
+
+ ///
+ /// Converts difficulty value from to base performance.
+ ///
+ public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
+
+ ///
+ /// Converts base performance to difficulty value.s
+ ///
+ public static double PerformanceToDifficulty(double performance) => (Math.Pow(100000.0 * performance, 1.0 / 3.0) + 4.0) / 5.0 * 0.0675;
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs
index 13fc1ef8bb..1e5a4f73ac 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs
@@ -9,7 +9,6 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
-using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
@@ -75,65 +74,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
}
}
- public class ReadingHighAR : GraphSkill
+ public class ReadingHidden : OsuStrainSkill
{
- public ReadingHighAR(Mod[] mods)
- : base(mods)
- {
- aimComponent = new HighARAimComponent(mods);
- speedComponent = new HighARSpeedComponent(mods);
- }
-
- private HighARAimComponent aimComponent;
- private HighARSpeedComponent speedComponent;
-
- private readonly List difficulties = new List();
-
- public override void Process(DifficultyHitObject current)
- {
- aimComponent.Process(current);
- speedComponent.Process(current);
-
- double power = OsuDifficultyCalculator.SumPower;
- double mergedDifficulty = Math.Pow(
- Math.Pow(aimComponent.CurrentSectionPeak, power) +
- Math.Pow(speedComponent.CurrentSectionPeak, power), 1.0 / power);
-
- difficulties.Add(mergedDifficulty);
-
- if (current.Index == 0)
- CurrentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
-
- while (current.StartTime > CurrentSectionEnd)
- {
- StrainPeaks.Add(CurrentSectionPeak);
- CurrentSectionPeak = 0;
- CurrentSectionEnd += SectionLength;
- }
-
- CurrentSectionPeak = Math.Max(mergedDifficulty, CurrentSectionPeak);
- }
- public override double DifficultyValue()
- {
- double power = OsuDifficultyCalculator.SumPower;
- return Math.Pow(
- Math.Pow(aimComponent.DifficultyValue(), power) +
- Math.Pow(speedComponent.DifficultyValue(), power), 1.0 / power);
- }
- }
-
- public class HighARAimComponent : OsuStrainSkill
- {
- public HighARAimComponent(Mod[] mods)
+ public ReadingHidden(Mod[] mods)
: base(mods)
{
}
private double currentStrain;
- // private double currentRhythm;
-
- //private double skillMultiplier => 13;
- private double skillMultiplier => 14;
+ private double skillMultiplier => 5;
private double strainDecayBase => 0.15;
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
@@ -144,105 +93,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
currentStrain *= strainDecay(current.DeltaTime);
- double aimDifficulty = AimEvaluator.EvaluateDifficultyOf(current, true, ((OsuDifficultyHitObject)current).Preempt);
- aimDifficulty *= ReadingEvaluator.EvaluateHighARDifficultyOf(current, true);
- aimDifficulty *= skillMultiplier;
+ // We're not using slider aim because we assuming that HD doesn't makes sliders harder (what is not true, but we will ignore this for now)
+ double hiddenDifficulty = AimEvaluator.EvaluateDifficultyOf(current, false);
+ hiddenDifficulty *= ReadingHiddenEvaluator.EvaluateDifficultyOf(current);
+ hiddenDifficulty *= skillMultiplier;
- double totalStrain = currentStrain;
- currentStrain += aimDifficulty;
+ currentStrain += hiddenDifficulty;
- // Warning: this line is unstable, so increasing amount of objects can decrease pp
- totalStrain += aimDifficulty * (1 + ReadingEvaluator.EvaluateLowDensityBonusOf(current));
-
-
- //Console.WriteLine($"{current.StartTime} - {ReadingEvaluator.EvaluateLowDensityBonusOf(current)}");
-
- return totalStrain;
- }
- }
-
- public class HighARSpeedComponent : OsuStrainSkill
- {
- private double skillMultiplier => 675;
- private double strainDecayBase => 0.3;
-
- private double currentStrain;
- private double currentRhythm;
-
- public HighARSpeedComponent(Mod[] mods)
- : base(mods)
- {
- }
-
- private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
-
- protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => (currentStrain * currentRhythm) * strainDecay(time - current.Previous(0).StartTime);
-
- protected override double StrainValueAt(DifficultyHitObject current)
- {
- OsuDifficultyHitObject currODHO = (OsuDifficultyHitObject)current;
-
- currentStrain *= strainDecay(currODHO.StrainTime);
-
- double speedDifficulty = SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier;
- speedDifficulty *= ReadingEvaluator.EvaluateHighARDifficultyOf(current, false);
- currentStrain += speedDifficulty;
-
- currentRhythm = currODHO.RhythmDifficulty;
- // currentRhythm *= currentRhythm; // Squaring is broken cuz rhythm is broken ((((
-
- double totalStrain = currentStrain * currentRhythm;
- return totalStrain;
- }
- }
-
- public class ReadingHidden : GraphSkill
- {
- public ReadingHidden(Mod[] mods)
- : base(mods)
- {
- }
-
- private readonly List difficulties = new List();
- private double skillMultiplier => 2.3;
-
- public override void Process(DifficultyHitObject current)
- {
- double currentDifficulty = ReadingEvaluator.EvaluateHiddenDifficultyOf(current) * skillMultiplier;
-
- difficulties.Add(currentDifficulty);
-
- if (current.Index == 0)
- CurrentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
-
- while (current.StartTime > CurrentSectionEnd)
- {
- StrainPeaks.Add(CurrentSectionPeak);
- CurrentSectionPeak = 0;
- CurrentSectionEnd += SectionLength;
- }
-
- CurrentSectionPeak = Math.Max(currentDifficulty, CurrentSectionPeak);
- }
-
- 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);
-
- List values = peaks.OrderByDescending(d => d).ToList();
-
- // 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);
- }
-
- return difficulty;
+ return currentStrain;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/ReadingHighAR.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/ReadingHighAR.cs
new file mode 100644
index 0000000000..06fe2d3a84
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/ReadingHighAR.cs
@@ -0,0 +1,146 @@
+// 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.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
+using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
+
+namespace osu.Game.Rulesets.Osu.Difficulty.Skills
+{
+ public class ReadingHighAR : GraphSkill
+ {
+ public ReadingHighAR(Mod[] mods)
+ : base(mods)
+ {
+ aimComponent = new HighARAimComponent(mods);
+ speedComponent = new HighARSpeedComponent(mods);
+
+ aimComponentNoAdjust = new HighARAimComponent(mods, false);
+ }
+
+ private HighARAimComponent aimComponent;
+ private HighARAimComponent aimComponentNoAdjust;
+ private HighARSpeedComponent speedComponent;
+
+ private readonly List difficulties = new List();
+
+ public override void Process(DifficultyHitObject current)
+ {
+ aimComponent.Process(current);
+ speedComponent.Process(current);
+
+ aimComponentNoAdjust.Process(current);
+
+ double power = OsuDifficultyCalculator.SUM_POWER;
+ double mergedDifficulty = Math.Pow(
+ Math.Pow(aimComponent.CurrentSectionPeak, power) +
+ Math.Pow(speedComponent.CurrentSectionPeak, power), 1.0 / power);
+
+ difficulties.Add(mergedDifficulty);
+
+ if (current.Index == 0)
+ CurrentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
+
+ while (current.StartTime > CurrentSectionEnd)
+ {
+ StrainPeaks.Add(CurrentSectionPeak);
+ CurrentSectionPeak = 0;
+ CurrentSectionEnd += SectionLength;
+ }
+
+ CurrentSectionPeak = Math.Max(mergedDifficulty, CurrentSectionPeak);
+ }
+ public override double DifficultyValue()
+ {
+ Console.WriteLine($"Degree of High AR Complexity = {aimComponent.DifficultyValue() / aimComponentNoAdjust.DifficultyValue():0.##}");
+
+ // Simulating summing
+ double aimValue = Math.Sqrt(aimComponent.DifficultyValue()) * OsuDifficultyCalculator.DIFFICULTY_MULTIPLIER;
+ double speedValue = Math.Sqrt(speedComponent.DifficultyValue()) * OsuDifficultyCalculator.DIFFICULTY_MULTIPLIER;
+
+ double aimPerformance = OsuStrainSkill.DifficultyToPerformance(aimValue);
+ double speedPerformance = OsuStrainSkill.DifficultyToPerformance(speedValue);
+
+ double power = OsuDifficultyCalculator.SUM_POWER;
+ double totalPerformance = Math.Pow(Math.Pow(aimPerformance, power) + Math.Pow(speedPerformance, power), 1.0 / power);
+
+ double adjustedDifficulty = OsuStrainSkill.PerformanceToDifficulty(totalPerformance);
+
+ return Math.Pow(adjustedDifficulty / OsuDifficultyCalculator.DIFFICULTY_MULTIPLIER, 2.0);
+ }
+ }
+
+ public class HighARAimComponent : OsuStrainSkill
+ {
+ public HighARAimComponent(Mod[] mods, bool adjustHighAR = true)
+ : base(mods)
+ {
+ this.adjustHighAR = adjustHighAR;
+ }
+
+ private bool adjustHighAR;
+ private double currentStrain;
+
+ private double skillMultiplier => 19;
+ private double strainDecayBase => 0.15;
+
+ private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
+
+ protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime);
+
+ protected override double StrainValueAt(DifficultyHitObject current)
+ {
+ currentStrain *= strainDecay(current.DeltaTime);
+
+ double aimDifficulty = AimEvaluator.EvaluateDifficultyOf(current, true);
+ aimDifficulty *= ReadingHighAREvaluator.EvaluateDifficultyOf(current, adjustHighAR);
+ aimDifficulty *= skillMultiplier;
+
+ double totalStrain = currentStrain;
+
+ currentStrain += aimDifficulty;
+ totalStrain += aimDifficulty;
+
+ // Console.WriteLine($"{current.BaseObject.StartTime},{aimDifficulty:0.#}");
+
+ return totalStrain;
+ }
+ }
+
+ public class HighARSpeedComponent : OsuStrainSkill
+ {
+ private double skillMultiplier => 850;
+ private double strainDecayBase => 0.3;
+
+ private double currentStrain;
+ private double currentRhythm;
+
+ public HighARSpeedComponent(Mod[] mods)
+ : base(mods)
+ {
+ }
+
+ private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
+
+ protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => (currentStrain * currentRhythm) * strainDecay(time - current.Previous(0).StartTime);
+
+ protected override double StrainValueAt(DifficultyHitObject current)
+ {
+ OsuDifficultyHitObject currODHO = (OsuDifficultyHitObject)current;
+
+ currentStrain *= strainDecay(currODHO.StrainTime);
+
+ double speedDifficulty = SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier;
+ speedDifficulty *= ReadingHighAREvaluator.EvaluateDifficultyOf(current, false);
+ currentStrain += speedDifficulty;
+
+ currentRhythm = currODHO.RhythmDifficulty;
+ double totalStrain = currentStrain * currentRhythm;
+ return totalStrain;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 032f105ded..506145568e 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects
set
{
repeatCount = value;
- endPositionCache.Invalidate();
+ updateNestedPositions();
}
}
@@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public Slider()
{
SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples();
- Path.Version.ValueChanged += _ => endPositionCache.Invalidate();
+ Path.Version.ValueChanged += _ => updateNestedPositions();
}
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)