Skip to content
Merged
Show file tree
Hide file tree
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
24 changes: 24 additions & 0 deletions osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,30 @@ public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxComb
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());

[TestCase(239, "diffcalc-test")]
[TestCase(54, "zero-length-sliders")]
[TestCase(4, "very-fast-slider")]
public void TestOffsetChanges(int expectedMaxCombo, string name)
{
const double offset_iterations = 400;
var beatmap = GetBeatmap(name);

var attributes = CreateDifficultyCalculator(beatmap).Calculate();
double expectedStarRating = attributes.StarRating;

for (int i = 0; i < offset_iterations; i++)
{
foreach (var beatmapHitObject in beatmap.Beatmap.HitObjects)
beatmapHitObject.StartTime++;

attributes = CreateDifficultyCalculator(beatmap).Calculate();

// Platform-dependent math functions (Pow, Cbrt, Exp, etc) may result in minute differences.
Assert.That(attributes.StarRating, Is.EqualTo(expectedStarRating).Within(0.00001));
Assert.That(attributes.MaxCombo, Is.EqualTo(expectedMaxCombo));
}
}

protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap);

protected override Ruleset CreateRuleset() => new OsuRuleset();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@

using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;

namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Aim
{
public static class AgilityEvaluator
{
private const double distance_cap = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
private const double distance_cap = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.2; // 1.25 circles distance between centers

/// <summary>
/// Evaluates the difficulty of fast aiming
Expand All @@ -31,13 +30,13 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current)

double strain = distanceScaled * 1000 / osuCurrObj.AdjustedDeltaTime;

strain *= osuCurrObj.SmallCircleBonus;
strain *= Math.Pow(osuCurrObj.SmallCircleBonus, 1.5);

strain *= highBpmBonus(osuCurrObj.AdjustedDeltaTime);

return strain * DifficultyCalculationUtils.Smootherstep(distance, 0, OsuDifficultyHitObject.NORMALISED_RADIUS);
return strain;
}

private static double highBpmBonus(double ms) => 1 / (1 - Math.Pow(0.3, Math.Pow(ms / 1000, 0.9)));
private static double highBpmBonus(double ms) => 1 / (1 - Math.Pow(0.2, ms / 1000));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Aim
{
public static class FlowAimEvaluator
{
private const double velocity_change_multiplier = 2.0;
private const double velocity_change_multiplier = 0.52;

/// <summary>
/// Evaluates difficulty of "flow aim" - aiming pattern where player doesn't stop their cursor on every object and instead "flows" through them.
Expand Down Expand Up @@ -44,7 +44,7 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool with

// Apply high circle size bonus to the base velocity.
// We use reduced CS bonus here because the bonus was made for an evaluator with a different d/t scaling
flowDifficulty *= Math.Pow(osuCurrObj.SmallCircleBonus, 0.75);
flowDifficulty *= Math.Sqrt(osuCurrObj.SmallCircleBonus);

// Rhythm changes are harder to flow
flowDifficulty *= 1 + Math.Min(0.25,
Expand Down Expand Up @@ -77,7 +77,7 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool with
// Acute angles are also hard to flow
// We square root velocity to make acute angle switches in streams aren't having difficulty higher than snap
flowDifficulty += Math.Sqrt(currVelocity) *
SnapAimEvaluator.CalcAcuteAngleBonus(osuCurrObj.Angle.Value) *
SnapAimEvaluator.CalcAngleAcuteness(osuCurrObj.Angle.Value) *
overlappedNotesWeight;
}

Expand Down Expand Up @@ -108,7 +108,10 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool with
}

// 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
return Math.Pow(flowDifficulty, 1.45);
flowDifficulty = Math.Pow(flowDifficulty, 1.45);

// Reduce difficulty for low spacing since spacing below radius is always to be flowed
return flowDifficulty * DifficultyCalculationUtils.Smootherstep(currDistance, 0, OsuDifficultyHitObject.NORMALISED_RADIUS);
}

private static double calculateOverlapFactor(OsuDifficultyHitObject first, OsuDifficultyHitObject second)
Expand Down
93 changes: 41 additions & 52 deletions osu.Game.Rulesets.Osu/Difficulty/Evaluators/Aim/SnapAimEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,51 +55,39 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool with
double prevDistance = withSliderTravelDistance ? osuLastObj.LazyJumpDistance : osuLastObj.JumpDistance;
double prevVelocity = prevDistance / osuLastObj.AdjustedDeltaTime;

double wideAngleBonus = 0;
double acuteAngleBonus = 0;
double sliderBonus = 0;
double velocityChangeBonus = 0;
double wiggleBonus = 0;

double aimStrain = currVelocity; // Start strain with regular velocity.

// Penalize angle repetition.
aimStrain *= vectorAngleRepetition(osuCurrObj, osuLastObj);

if (osuCurrObj.Angle != null && osuLastObj.Angle != null)
{
double currAngle = osuCurrObj.Angle.Value;
double lastAngle = osuLastObj.Angle.Value;

// Rewarding angles, take the smaller velocity as base.
double angleBonus = Math.Min(currVelocity, prevVelocity);
double velocityInfluence = Math.Min(currVelocity, prevVelocity);

double acuteAngleBonus = 0;

if (Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) < 1.25 * Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime)) // If rhythms are the same.
{
acuteAngleBonus = CalcAcuteAngleBonus(currAngle);
acuteAngleBonus = CalcAngleAcuteness(currAngle);

// Penalize angle repetition.
acuteAngleBonus *= 0.08 + 0.92 * (1 - Math.Min(acuteAngleBonus, Math.Pow(CalcAcuteAngleBonus(lastAngle), 3)));
// Penalize angle repetition. It is important to do it _before_ multiplying by anything because we compare raw acuteness here
acuteAngleBonus *= 0.08 + 0.92 * (1 - Math.Min(acuteAngleBonus, Math.Pow(CalcAngleAcuteness(lastAngle), 3)));

// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
acuteAngleBonus *= angleBonus *
DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.AdjustedDeltaTime, 2), 300, 400) *
acuteAngleBonus *= velocityInfluence * DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.AdjustedDeltaTime, 2), 300, 400) *
DifficultyCalculationUtils.Smootherstep(currDistance, 0, diameter * 2);
}

wideAngleBonus = calcWideAngleBonus(currAngle);
double wideAngleBonus = calcAngleWideness(currAngle);

// Penalize angle repetition.
wideAngleBonus *= 0.25 + 0.75 * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)));
// Penalize angle repetition. It is important to do it _before_ multiplying by velocity because we compare raw wideness here
wideAngleBonus *= 0.25 + 0.75 * (1 - Math.Min(wideAngleBonus, Math.Pow(calcAngleWideness(lastAngle), 3)));

wideAngleBonus *= angleBonus;

// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
// https://www.desmos.com/calculator/dp0v0nvowc
wiggleBonus = angleBonus
* DifficultyCalculationUtils.Smootherstep(currDistance, radius, diameter)
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(currDistance, diameter * 3, diameter), 1.8)
* DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60))
* DifficultyCalculationUtils.Smootherstep(prevDistance, radius, diameter)
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(prevDistance, diameter * 3, diameter), 1.8)
* DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60));
wideAngleBonus *= velocityInfluence;

if (osuLast2Obj != null)
{
Expand All @@ -115,6 +103,21 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool with
wideAngleBonus *= 1 - 0.55 * (1 - distance);
}
}

// Add in acute angle bonus or wide angle bonus, whichever is larger.
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier);

// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
// https://www.desmos.com/calculator/dp0v0nvowc
double wiggleBonus = velocityInfluence
* DifficultyCalculationUtils.Smootherstep(currDistance, radius, diameter)
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(currDistance, diameter * 3, diameter), 1.8)
* DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60))
* DifficultyCalculationUtils.Smootherstep(prevDistance, radius, diameter)
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(prevDistance, diameter * 3, diameter), 1.8)
* DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60));

aimStrain += wiggleBonus * wiggle_multiplier;
}

if (Math.Max(prevVelocity, currVelocity) != 0)
Expand All @@ -131,44 +134,30 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool with
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
double overlapVelocityBuff = Math.Min(diameter * 1.25 / Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), Math.Abs(prevVelocity - currVelocity));

velocityChangeBonus = overlapVelocityBuff * distRatio;
double velocityChangeBonus = overlapVelocityBuff * distRatio;

// Penalize for rhythm changes.
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) / Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), 2);
}

if (osuCurrObj.BaseObject is Slider)
{
// Reward sliders based on velocity.
sliderBonus = osuCurrObj.TravelDistance / osuCurrObj.TravelTime;
aimStrain += velocityChangeBonus * velocity_change_multiplier;
}

// Penalize angle repetition.
aimStrain *= vectorAngleRepetition(osuCurrObj, osuLastObj);

aimStrain += wiggleBonus * wiggle_multiplier;
aimStrain += velocityChangeBonus * velocity_change_multiplier;

// Add in acute angle bonus or wide angle bonus, whichever is larger.
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier);

// Add in additional slider velocity bonus.
if (withSliderTravelDistance)
// Reward sliders based on velocity.
if (osuCurrObj.BaseObject is Slider && withSliderTravelDistance)
{
double sliderBonus = osuCurrObj.TravelDistance / osuCurrObj.TravelTime;
aimStrain += (sliderBonus < 1 ? sliderBonus : Math.Pow(sliderBonus, 0.75)) * slider_multiplier;
}

// Apply high circle size bonus
aimStrain *= osuCurrObj.SmallCircleBonus;

aimStrain *= highBpmBonus(osuCurrObj.AdjustedDeltaTime, osuCurrObj.LazyJumpDistance);
aimStrain *= highBpmBonus(osuCurrObj.AdjustedDeltaTime);

return aimStrain;
}

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

private static double vectorAngleRepetition(OsuDifficultyHitObject current, OsuDifficultyHitObject previous)
{
Expand Down Expand Up @@ -208,13 +197,13 @@ private static double vectorAngleRepetition(OsuDifficultyHitObject current, OsuD

double angleDifferenceAdjusted = Math.Cos(2 * Math.Min(double.DegreesToRadians(45), Math.Abs(currAngle - lastAngle) * stackFactor));

double baseNerf = 1 - maximum_repetition_nerf * CalcAcuteAngleBonus(lastAngle) * angleDifferenceAdjusted;
double baseNerf = 1 - maximum_repetition_nerf * CalcAngleAcuteness(lastAngle) * angleDifferenceAdjusted;

return Math.Pow(baseNerf + (1 - baseNerf) * vectorRepetition * maximum_vector_influence * stackFactor, 2);
}

private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(40), double.DegreesToRadians(140));
private static double calcAngleWideness(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(40), double.DegreesToRadians(140));

public static double CalcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(140), double.DegreesToRadians(40));
public static double CalcAngleAcuteness(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(140), double.DegreesToRadians(40));
}
}
32 changes: 31 additions & 1 deletion osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool hidd

double difficulty = DifficultyCalculationUtils.Norm(1.5, preemptDifficulty, hiddenDifficulty, noteDensityDifficulty);

// Having less time to process information is harder
difficulty *= highBpmBonus(currObj.AdjustedDeltaTime);

return difficulty;
}

Expand Down Expand Up @@ -208,6 +211,10 @@ private static double getConstantAngleNerfFactor(OsuDifficultyHitObject current)
int index = 0;
double currentTimeGap = 0;

OsuDifficultyHitObject loopObjPrev0 = current;
OsuDifficultyHitObject? loopObjPrev1 = null;
OsuDifficultyHitObject? loopObjPrev2 = null;

while (currentTimeGap < minimum_angle_relevancy_time)
{
var loopObj = (OsuDifficultyHitObject)current.Previous(index);
Expand All @@ -221,13 +228,34 @@ private static double getConstantAngleNerfFactor(OsuDifficultyHitObject current)
if (loopObj.Angle.IsNotNull() && current.Angle.IsNotNull())
{
double angleDifference = Math.Abs(current.Angle.Value - loopObj.Angle.Value);
double angleDifferenceAlternating = Math.PI;

if (loopObjPrev0.Angle != null && loopObjPrev1?.Angle != null && loopObjPrev2?.Angle != null)
{
angleDifferenceAlternating = Math.Abs(loopObjPrev1.Angle.Value - loopObj.Angle.Value);
angleDifferenceAlternating += Math.Abs(loopObjPrev2.Angle.Value - loopObjPrev0.Angle.Value);

double weight = 1.0;

// Be sure that one of the angles is very sharp, when other is wide
weight *= DifficultyCalculationUtils.ReverseLerp(Math.Min(loopObj.Angle.Value, loopObjPrev0.Angle.Value) * 180 / Math.PI, 20, 5);
weight *= DifficultyCalculationUtils.ReverseLerp(Math.Max(loopObj.Angle.Value, loopObjPrev0.Angle.Value) * 180 / Math.PI, 60, 120);

// Lerp between max angle difference and rescaled alternating difference, with more harsh scaling compared to normal difference
angleDifferenceAlternating = double.Lerp(Math.PI, 0.1 * angleDifferenceAlternating, weight);
}

double stackFactor = DifficultyCalculationUtils.Smootherstep(loopObj.LazyJumpDistance, 0, OsuDifficultyHitObject.NORMALISED_RADIUS);

constantAngleCount += Math.Cos(3 * Math.Min(double.DegreesToRadians(30), angleDifference * stackFactor)) * longIntervalFactor;
constantAngleCount += Math.Cos(3 * Math.Min(double.DegreesToRadians(30), Math.Min(angleDifference, angleDifferenceAlternating) * stackFactor)) * longIntervalFactor;
}

currentTimeGap = current.StartTime - loopObj.StartTime;
index++;

loopObjPrev2 = loopObjPrev1;
loopObjPrev1 = loopObjPrev0;
loopObjPrev0 = loopObj;
}

return Math.Clamp(2 / constantAngleCount, 0.2, 1);
Expand All @@ -238,5 +266,7 @@ private static double getTimeNerfFactor(double deltaTime)
{
return Math.Clamp(2 - deltaTime / (reading_window_size / 2), 0, 1);
}

private static double highBpmBonus(double ms) => 1 / (1 - Math.Pow(0.8, ms / 1000));
}
}
Loading
Loading