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)