@@ -12,87 +12,61 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Aim
1212{
1313 public static class FlowAimEvaluator
1414 {
15- private const double velocity_change_multiplier = 2.0 ;
15+ private const int radius = OsuDifficultyHitObject . NORMALISED_RADIUS ;
16+ private const int diameter = OsuDifficultyHitObject . NORMALISED_DIAMETER ;
1617
1718 /// <summary>
1819 /// Evaluates difficulty of "flow aim" - aiming pattern where player doesn't stop their cursor on every object and instead "flows" through them.
1920 /// </summary>
2021 public static double EvaluateDifficultyOf ( DifficultyHitObject current , bool withSliderTravelDistance )
2122 {
23+ const double flow_multiplier = 0.95 ;
24+
2225 if ( current . BaseObject is Spinner || current . Index <= 1 || current . Previous ( 0 ) . BaseObject is Spinner )
2326 return 0 ;
2427
2528 var osuCurrObj = ( OsuDifficultyHitObject ) current ;
26- var osuLastObj = ( OsuDifficultyHitObject ) current . Previous ( 0 ) ;
27- var osuLastLastObj = ( OsuDifficultyHitObject ) current . Previous ( 1 ) ;
28-
29- double currDistance = withSliderTravelDistance ? osuCurrObj . LazyJumpDistance : osuCurrObj . JumpDistance ;
30- double prevDistance = withSliderTravelDistance ? osuLastObj . LazyJumpDistance : osuLastObj . JumpDistance ;
31-
32- double currVelocity = currDistance / osuCurrObj . AdjustedDeltaTime ;
33-
34- if ( osuLastObj . BaseObject is Slider && withSliderTravelDistance )
35- {
36- // If the last object is a slider, then we extend the travel velocity through the slider into the current object.
37- double sliderDistance = osuLastObj . LazyTravelDistance + osuCurrObj . LazyJumpDistance ;
38- currVelocity = Math . Max ( currVelocity , sliderDistance / osuCurrObj . AdjustedDeltaTime ) ;
39- }
40-
41- double prevVelocity = prevDistance / osuLastObj . AdjustedDeltaTime ;
29+ var osuLast0Obj = ( OsuDifficultyHitObject ) current . Previous ( 0 ) ;
30+ var osuLast1Obj = ( OsuDifficultyHitObject ) current . Previous ( 1 ) ;
4231
43- double flowDifficulty = currVelocity ;
44-
45- // Apply high circle size bonus to the base velocity
46- flowDifficulty *= osuCurrObj . SmallCircleBonus ;
47-
48- // Rhythm changes are harder to flow
49- flowDifficulty *= 1 + Math . Min ( 0.25 ,
50- Math . Pow ( ( Math . Max ( osuCurrObj . AdjustedDeltaTime , osuLastObj . AdjustedDeltaTime ) - Math . Min ( osuCurrObj . AdjustedDeltaTime , osuLastObj . AdjustedDeltaTime ) ) / 50 , 4 ) ) ;
32+ double distance = withSliderTravelDistance ? osuCurrObj . LazyJumpDistance : osuCurrObj . JumpDistance ;
33+ double flowDifficulty = distance / osuCurrObj . AdjustedDeltaTime ;
5134
5235 if ( osuCurrObj . AngularVelocity != null )
5336 {
5437 // Low angular velocity flow (angles are consistent) is easier to follow than erratic flow
5538 flowDifficulty *= 0.8 + Math . Sqrt ( osuCurrObj . AngularVelocity . Value / 270.0 ) ;
5639 }
5740
58- // If all three notes are overlapping - don't reward bonuses as you don't have to do additional movement
59- double overlappedNotesWeight = 1 ;
41+ double angleBonus = 0 ;
6042
61- if ( current . Index > 2 )
43+ if ( osuCurrObj . AngleSigned != null && osuLast0Obj . AngleSigned != null && osuLast1Obj . AngleSigned != null )
6244 {
63- double o1 = calculateOverlapFactor ( osuCurrObj , osuLastObj ) ;
64- double o2 = calculateOverlapFactor ( osuCurrObj , osuLastLastObj ) ;
65- double o3 = calculateOverlapFactor ( osuLastObj , osuLastLastObj ) ;
45+ double acuteAngleBonus = CalculateFlowAcuteAngleBonus ( current ) ;
46+ double angleChangeBonus = CalculateFlowAngleChangeBonus ( current ) ;
6647
67- overlappedNotesWeight = 1 - o1 * o2 * o3 ;
68- }
48+ // If all three notes are overlapping - don't reward angle bonuses as you don't have to do additional movement
49+ double overlappedNotesWeight = 1 ;
6950
70- if ( osuCurrObj . Angle != null && osuLastObj . Angle != null )
71- {
72- // Acute angles are also hard to flow
73- // We square root velocity to make acute angle switches in streams aren't having difficulty higher than snap
74- flowDifficulty += Math . Sqrt ( currVelocity ) *
75- SnapAimEvaluator . CalcAcuteAngleBonus ( osuCurrObj . Angle . Value ) *
76- overlappedNotesWeight ;
77- }
78-
79- if ( Math . Max ( prevVelocity , currVelocity ) != 0 )
80- {
81- if ( withSliderTravelDistance )
51+ if ( current . Index > 2 )
8252 {
83- currVelocity = currDistance / osuCurrObj . AdjustedDeltaTime ;
84- prevVelocity = prevDistance / osuLastObj . AdjustedDeltaTime ;
53+ double o1 = calculateOverlapFactor ( osuCurrObj , osuLast0Obj ) ;
54+ double o2 = calculateOverlapFactor ( osuCurrObj , osuLast1Obj ) ;
55+ double o3 = calculateOverlapFactor ( osuLast0Obj , osuLast1Obj ) ;
56+
57+ overlappedNotesWeight = 1 - o1 * o2 * o3 ;
8558 }
8659
87- // Scale with ratio of difference compared to 0.5 * max dist.
88- double distRatio = DifficultyCalculationUtils . Smoothstep ( Math . Abs ( prevVelocity - currVelocity ) / Math . Max ( prevVelocity , currVelocity ) , 0 , 1 ) ;
60+ angleBonus = Math . Max ( acuteAngleBonus , angleChangeBonus ) * overlappedNotesWeight ;
61+ }
8962
90- // Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
91- double overlapVelocityBuff = Math . Min ( OsuDifficultyHitObject . NORMALISED_DIAMETER * 1.25 / Math . Min ( osuCurrObj . AdjustedDeltaTime , osuLastObj . AdjustedDeltaTime ) ,
92- Math . Abs ( prevVelocity - currVelocity ) ) ;
63+ // Add all bonuses
64+ flowDifficulty += angleBonus ;
9365
94- flowDifficulty += overlapVelocityBuff * distRatio * velocity_change_multiplier ;
95- }
66+ // Apply high circle size bonus to the base velocity
67+ flowDifficulty *= osuCurrObj . SmallCircleBonus ;
68+
69+ flowDifficulty *= flow_multiplier ;
9670
9771 if ( osuCurrObj . BaseObject is Slider )
9872 {
@@ -113,5 +87,62 @@ private static double calculateOverlapFactor(OsuDifficultyHitObject first, OsuDi
11387 double distance = Vector2 . Distance ( firstBase . StackedPosition , secondBase . StackedPosition ) ;
11488 return Math . Clamp ( 1 - Math . Pow ( Math . Max ( distance - objectRadius , 0 ) / objectRadius , 2 ) , 0 , 1 ) ;
11589 }
90+
91+ // This bonus accounts for the fact that flow is circular movement, therefore flowing on sharp angles is harder.
92+ public static double CalculateFlowAcuteAngleBonus ( DifficultyHitObject current )
93+ {
94+ const double acute_angle_bonus_multiplier = 1.05 ;
95+
96+ if ( current . BaseObject is Spinner || current . Index <= 1 || current . Previous ( 0 ) . BaseObject is Spinner )
97+ return 0 ;
98+
99+ var osuCurrObj = ( OsuDifficultyHitObject ) current ;
100+
101+ if ( osuCurrObj . Angle == null )
102+ return 0 ;
103+
104+ double currAngle = ( double ) osuCurrObj . Angle ;
105+
106+ double bonusBase = osuCurrObj . JumpDistance / osuCurrObj . AdjustedDeltaTime ;
107+
108+ double acuteAngleBonus = bonusBase * SnapAimEvaluator . CalcAcuteAngleBonus ( currAngle ) ;
109+
110+ // If spacing is too low - decrease reward
111+ acuteAngleBonus *= DifficultyCalculationUtils . ReverseLerp ( osuCurrObj . JumpDistance , radius , diameter ) ;
112+
113+ return acuteAngleBonus * acute_angle_bonus_multiplier ;
114+ }
115+
116+ // This bonus accounts for flow aim being harder when angle is changing.
117+ public static double CalculateFlowAngleChangeBonus ( DifficultyHitObject current )
118+ {
119+ const double angle_change_bonus_multiplier = 1.0 ;
120+
121+ if ( current . BaseObject is Spinner || current . Index <= 1 || current . Previous ( 0 ) . BaseObject is Spinner )
122+ return 0 ;
123+
124+ var osuCurrObj = ( OsuDifficultyHitObject ) current ;
125+ var osuLast0Obj = ( OsuDifficultyHitObject ) current . Previous ( 0 ) ;
126+ var osuLast1Obj = ( OsuDifficultyHitObject ) current . Previous ( 1 ) ;
127+
128+ if ( osuCurrObj . AngleSigned == null || osuLast0Obj . AngleSigned == null )
129+ return 0 ;
130+
131+ double currAngle = osuCurrObj . AngleSigned . Value ;
132+ double lastAngle = osuLast0Obj . AngleSigned . Value ;
133+
134+ // Take min velocity to avoid abuse with very small spacing
135+ double currVelocity = osuCurrObj . JumpDistance / osuCurrObj . AdjustedDeltaTime ;
136+ double prevVelocity = osuLast0Obj . JumpDistance / osuLast0Obj . AdjustedDeltaTime ;
137+
138+ double bonusBase = Math . Min ( currVelocity , prevVelocity ) ;
139+
140+ double angleChangeBonus = Math . Pow ( Math . Sin ( ( currAngle - lastAngle ) / 2 ) , 2 ) * bonusBase ;
141+
142+ // Take the largest of last 3 distances and if it's too small - decrease flow angle change bonus, because it's cheesable
143+ angleChangeBonus *= DifficultyCalculationUtils . ReverseLerp ( Math . Max ( osuCurrObj . JumpDistance , osuLast1Obj . JumpDistance ) , 0 , diameter ) ;
144+
145+ return angleChangeBonus * angle_change_bonus_multiplier ;
146+ }
116147 }
117148}
0 commit comments