diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/Aim/FlowAimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/Aim/FlowAimEvaluator.cs index 3707d4061342..592c9a121bcc 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/Aim/FlowAimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/Aim/FlowAimEvaluator.cs @@ -20,24 +20,13 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool with if (current.BaseObject is Spinner || current.Index <= 1 || current.Previous(0).BaseObject is Spinner) return 0; - const double velocity_change_multiplier = 0.52; - var osuCurrObj = (OsuDifficultyHitObject)current; var osuLastObj = (OsuDifficultyHitObject)current.Previous(0); - var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1); double currDistance = withSliderTravelDistance ? osuCurrObj.LazyJumpDistance : osuCurrObj.JumpDistance; double prevDistance = withSliderTravelDistance ? osuLastObj.LazyJumpDistance : osuLastObj.JumpDistance; - double currVelocity = currDistance / osuCurrObj.AdjustedDeltaTime; - - if (osuLastObj.BaseObject is Slider && withSliderTravelDistance) - { - // If the last object is a slider, then we extend the travel velocity through the slider into the current object. - double sliderDistance = osuLastObj.LazyTravelDistance + osuCurrObj.LazyJumpDistance; - currVelocity = Math.Max(currVelocity, sliderDistance / osuCurrObj.AdjustedDeltaTime); - } - + double currVelocity = calculateCurrentVelocity(osuCurrObj, osuLastObj, currDistance, withSliderTravelDistance); double prevVelocity = prevDistance / osuLastObj.AdjustedDeltaTime; double flowDifficulty = currVelocity; @@ -46,64 +35,18 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool with // We use reduced CS bonus here because the bonus was made for an evaluator with a different d/t scaling flowDifficulty *= Math.Sqrt(osuCurrObj.SmallCircleBonus); - // Rhythm changes are harder to flow - flowDifficulty *= 1 + Math.Min(0.25, - DiffUtils.Pow((Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) - Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime)) / 50, 4)); - - if (osuCurrObj.Angle != null && osuLastObj.Angle != null) - { - double angleDifference = Math.Abs(osuCurrObj.Angle.Value - osuLastObj.Angle.Value); - double angleDifferenceAdjusted = Math.Sin(angleDifference / 2) * 180.0; - double angularVelocity = angleDifferenceAdjusted / (osuCurrObj.AdjustedDeltaTime * 0.1); - - // Low angular velocity flow (angles are consistent) is easier to follow than erratic flow - flowDifficulty *= 0.8 + Math.Sqrt(angularVelocity / 270.0); - } + flowDifficulty *= calculateRhythmChangeBonus(osuCurrObj, osuLastObj); + flowDifficulty *= calculateAngularVelocityBonus(osuCurrObj, osuLastObj); // If all three notes are overlapping - don't reward bonuses as you don't have to do additional movement - double overlappedNotesWeight = 1; - - if (current.Index > 2) - { - double o1 = calculateOverlapFactor(osuCurrObj, osuLastObj); - double o2 = calculateOverlapFactor(osuCurrObj, osuLastLastObj); - double o3 = calculateOverlapFactor(osuLastObj, osuLastLastObj); + double overlappedNotesWeight = calculateOverlappedNotesWeight(osuCurrObj, osuLastObj); - overlappedNotesWeight = 1 - o1 * o2 * o3; - } - - if (osuCurrObj.Angle != null) - { - // Acute angles are also hard to flow - flowDifficulty += currVelocity * - SnapAimEvaluator.CalcAngleAcuteness(osuCurrObj.Angle.Value) * - overlappedNotesWeight; - } - - if (Math.Max(prevVelocity, currVelocity) != 0) - { - if (withSliderTravelDistance) - { - currVelocity = currDistance / osuCurrObj.AdjustedDeltaTime; - } - - // Scale with ratio of difference compared to 0.5 * max dist. - double distRatio = DiffUtils.Smoothstep(Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity), 0, 1); - - // Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing. - double overlapVelocityBuff = Math.Min(OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25 / Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), - Math.Abs(prevVelocity - currVelocity)); - - flowDifficulty += overlapVelocityBuff * - distRatio * - overlappedNotesWeight * - velocity_change_multiplier; - } + flowDifficulty += calculateAcuteAngleBonus(osuCurrObj, currVelocity, overlappedNotesWeight); + flowDifficulty += calculateVelocityChangeBonus(osuCurrObj, osuLastObj, currVelocity, prevVelocity, currDistance, overlappedNotesWeight, withSliderTravelDistance); if (osuCurrObj.BaseObject is Slider && withSliderTravelDistance) { - // Include slider velocity to make velocity more consistent with snap - flowDifficulty += osuCurrObj.TravelDistance / osuCurrObj.TravelTime; + flowDifficulty += calculateSliderBonus(osuCurrObj); } // Final velocity is being raised to a power because flow difficulty scales harder with both high distance and time, and we want to account for that @@ -113,6 +56,89 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool with return flowDifficulty * DiffUtils.Smootherstep(currDistance, 0, OsuDifficultyHitObject.NORMALISED_RADIUS); } + private static double calculateRhythmChangeBonus(OsuDifficultyHitObject osuCurrObj, OsuDifficultyHitObject osuLastObj) => + 1 + Math.Min(0.25, + DiffUtils.Pow((Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) - Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime)) / 50, 4)); + + /// + /// Scales flow difficulty by angular velocity. + /// This nerfs consistent angles whilst buffing "erratic" flow. + /// + private static double calculateAngularVelocityBonus(OsuDifficultyHitObject osuCurrObj, OsuDifficultyHitObject osuLastObj) + { + if (osuCurrObj.Angle == null || osuLastObj.Angle == null) + return 1; + + double angleDifference = Math.Abs(osuCurrObj.Angle.Value - osuLastObj.Angle.Value); + double angleDifferenceAdjusted = Math.Sin(angleDifference / 2) * 180.0; + double angularVelocity = angleDifferenceAdjusted / (osuCurrObj.AdjustedDeltaTime * 0.1); + + return 0.8 + Math.Sqrt(angularVelocity / 270.0); + } + + private static double calculateAcuteAngleBonus(OsuDifficultyHitObject osuCurrObj, double currVelocity, double overlappedNotesWeight) + { + if (osuCurrObj.Angle == null) + return 0; + + return currVelocity * SnapAimEvaluator.CalcAngleAcuteness(osuCurrObj.Angle.Value) * overlappedNotesWeight; + } + + private static double calculateVelocityChangeBonus(OsuDifficultyHitObject osuCurrObj, OsuDifficultyHitObject osuLastObj, double currVelocity, double prevVelocity, + double currDistance, double overlappedNotesWeight, bool withSliderTravelDistance) + { + const double velocity_change_multiplier = 0.52; + + if (Math.Max(prevVelocity, currVelocity) == 0) + return 0; + + if (withSliderTravelDistance) + currVelocity = currDistance / osuCurrObj.AdjustedDeltaTime; + + // Scale with ratio of difference compared to 0.5 * max dist. + double distRatio = DiffUtils.Smoothstep(Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity), 0, 1); + + // Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing. + double overlapVelocityBuff = Math.Min(OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25 / Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), + Math.Abs(prevVelocity - currVelocity)); + + return overlapVelocityBuff * distRatio * overlappedNotesWeight * velocity_change_multiplier; + } + + private static double calculateSliderBonus(OsuDifficultyHitObject osuCurrObj) + => osuCurrObj.TravelDistance / osuCurrObj.TravelTime; + + private static double calculateCurrentVelocity(OsuDifficultyHitObject osuCurrObj, OsuDifficultyHitObject osuLastObj, double currDistance, bool withSliderTravelDistance) + { + double currVelocity = currDistance / osuCurrObj.AdjustedDeltaTime; + + // If the last object is a slider, then we extend the travel velocity through the slider into the current object. + if (osuLastObj.BaseObject is Slider && withSliderTravelDistance) + { + double sliderDistance = osuLastObj.LazyTravelDistance + osuCurrObj.LazyJumpDistance; + currVelocity = Math.Max(currVelocity, sliderDistance / osuCurrObj.AdjustedDeltaTime); + } + + return currVelocity; + } + + /// + /// Reduces bonuses when the current and previous two objects all overlap, as no additional movement is required. + /// + private static double calculateOverlappedNotesWeight(OsuDifficultyHitObject osuCurrObj, OsuDifficultyHitObject osuLastObj) + { + if (osuCurrObj.Index <= 2) + return 1; + + var osuLastLastObj = (OsuDifficultyHitObject)osuCurrObj.Previous(1); + + double o1 = calculateOverlapFactor(osuCurrObj, osuLastObj); + double o2 = calculateOverlapFactor(osuCurrObj, osuLastLastObj); + double o3 = calculateOverlapFactor(osuLastObj, osuLastLastObj); + + return 1 - o1 * o2 * o3; + } + private static double calculateOverlapFactor(OsuDifficultyHitObject first, OsuDifficultyHitObject second) { var firstBase = (OsuHitObject)first.BaseObject;