Skip to content

Commit 8ee5c2c

Browse files
committed
Add proper flow aim evaluator
1 parent 1e01001 commit 8ee5c2c

File tree

4 files changed

+92
-62
lines changed

4 files changed

+92
-62
lines changed

osu.Game.Rulesets.Osu/Difficulty/Evaluators/Aim/AgilityEvaluator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current)
3333

3434
strain *= highBpmBonus(osuCurrObj.AdjustedDeltaTime);
3535

36-
return strain * DifficultyCalculationUtils.Smootherstep(distance, 0, OsuDifficultyHitObject.NORMALISED_RADIUS);
36+
return strain;
3737
}
3838

3939
private static double highBpmBonus(double ms) => 1 / (1 - Math.Pow(0.3, Math.Pow(ms / 1000, 0.9)));

osu.Game.Rulesets.Osu/Difficulty/Evaluators/Aim/FlowAimEvaluator.cs

Lines changed: 85 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

osu.Game.Rulesets.Osu/Difficulty/Evaluators/Aim/SnapAimEvaluator.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,7 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool with
173173
return aimStrain;
174174
}
175175

176-
// We decrease strain for distances <radius to fix cases where doubles with no aim requirement
177-
// have their strain buffed incredibly high due to the delta time.
178-
// These objects do not require any movement, so it does not make sense to award them.
179-
private static double highBpmBonus(double ms, double distance) => 1 / (1 - Math.Pow(0.03, Math.Pow(ms / 1000, 0.65)))
180-
* DifficultyCalculationUtils.Smootherstep(distance, 0, OsuDifficultyHitObject.NORMALISED_RADIUS);
176+
private static double highBpmBonus(double ms, double distance) => 1 / (1 - Math.Pow(0.03, Math.Pow(ms / 1000, 0.65)));
181177

182178
private static double vectorAngleRepetition(OsuDifficultyHitObject current, OsuDifficultyHitObject previous)
183179
{

osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ public class OsuDifficultyHitObject : DifficultyHitObject
116116
/// </summary>
117117
public double? Angle { get; private set; }
118118

119+
public double? AngleSigned { get; private set; }
120+
119121
public double? AngularVelocity { get; private set; }
120122

121123
/// <summary>
@@ -270,7 +272,8 @@ private void setDistances(double clockRate)
270272
Vector2 v = BaseObject.StackedPosition - lastCursorPosition;
271273
NormalisedVectorAngle = Math.Atan2(Math.Abs(v.Y), Math.Abs(v.X));
272274

273-
Angle = Math.Min(angle, sliderAngle);
275+
AngleSigned = Math.Abs(angle) <= Math.Abs(sliderAngle) ? angle : sliderAngle;
276+
Angle = Math.Abs((double)AngleSigned);
274277

275278
if (lastLastDifficultyObject.Angle != null)
276279
{
@@ -410,7 +413,7 @@ private double calculateAngle(Vector2 currentPosition, Vector2 lastPosition, Vec
410413
float dot = Vector2.Dot(v1, v2);
411414
float det = v1.X * v2.Y - v1.Y * v2.X;
412415

413-
return Math.Abs(Math.Atan2(det, dot));
416+
return Math.Atan2(det, dot);
414417
}
415418

416419
private Vector2 getEndCursorPosition(OsuDifficultyHitObject difficultyHitObject)

0 commit comments

Comments
 (0)