Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 90 additions & 64 deletions osu.Game.Rulesets.Osu/Difficulty/Evaluators/Aim/FlowAimEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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));

/// <summary>
/// Scales flow difficulty by angular velocity.
/// This nerfs consistent angles whilst buffing "erratic" flow.
/// </summary>
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;
}

/// <summary>
/// Reduces bonuses when the current and previous two objects all overlap, as no additional movement is required.
/// </summary>
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;
Expand Down
Loading