From c53188cf450bf5eb9efb903e5e295b7435971386 Mon Sep 17 00:00:00 2001 From: StanR Date: Tue, 14 Jan 2025 18:18:02 +0500 Subject: [PATCH] Use total deviation to scale accuracy on aim, general aim buff (#31498) * Make aim accuracy scaling harsher * Use deviation-based scaling * Bring the balancing multiplier down * Adjust multipliers, fix incorrect deviation when using slider accuracy * Adjust multipliers * Update osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs Co-authored-by: James Wilson * Change high speed deviation threshold to 22-27 instead of 20-24 * Update tests --------- Co-authored-by: James Wilson --- .../OsuDifficultyCalculatorTest.cs | 12 ++-- .../Difficulty/Evaluators/AimEvaluator.cs | 2 +- .../Difficulty/OsuPerformanceAttributes.cs | 3 + .../Difficulty/OsuPerformanceCalculator.cs | 57 +++++++++++++++++-- .../Difficulty/Skills/Aim.cs | 2 +- 5 files changed, 63 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 9af5051f45..a68d9dad39 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,21 +15,21 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.6860329680488437d, 239, "diffcalc-test")] - [TestCase(1.4485740324170036d, 54, "zero-length-sliders")] + [TestCase(6.7443067697205539d, 239, "diffcalc-test")] + [TestCase(1.4630292101418947d, 54, "zero-length-sliders")] [TestCase(0.43052813047866129d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.6300773538770041d, 239, "diffcalc-test")] - [TestCase(1.7550155729445993d, 54, "zero-length-sliders")] + [TestCase(9.7058844423552308d, 239, "diffcalc-test")] + [TestCase(1.7724929629205366d, 54, "zero-length-sliders")] [TestCase(0.55785578988249407d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.6860329680488437d, 239, "diffcalc-test")] - [TestCase(1.4485740324170036d, 54, "zero-length-sliders")] + [TestCase(6.7443067697205539d, 239, "diffcalc-test")] + [TestCase(1.4630292101418947d, 54, "zero-length-sliders")] [TestCase(0.43052813047866129d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index e279ed889a..858ce673ee 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Penalize angle repetition. wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)); - acuteAngleBonus *= 0.1 + 0.9 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); + acuteAngleBonus *= 0.09 + 0.91 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); // Apply full wide angle bonus for distance more than one diameter wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index de4491a31b..9c30c0f7c7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } + [JsonProperty("total_deviation")] + public double? TotalDeviation { get; set; } + [JsonProperty("speed_deviation")] public double? SpeedDeviation { get; set; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 91cd270966..a03e3fd6ef 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; @@ -41,6 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// private double effectiveMissCount; + private double? totalDeviation; private double? speedDeviation; public OsuPerformanceCalculator() @@ -113,6 +115,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits); } + totalDeviation = calculateTotalDeviation(osuAttributes); speedDeviation = calculateSpeedDeviation(osuAttributes); double aimValue = computeAimValue(score, osuAttributes); @@ -135,6 +138,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty Accuracy = accuracyValue, Flashlight = flashlightValue, EffectiveMissCount = effectiveMissCount, + TotalDeviation = totalDeviation, SpeedDeviation = speedDeviation, Total = totalValue }; @@ -145,6 +149,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModAutopilot)) return 0.0; + if (totalDeviation == null) + return 0; + double aimDifficulty = attributes.AimDifficulty; if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0) @@ -196,9 +203,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - aimValue *= accuracy; - // It is important to consider accuracy difficulty when scaling with accuracy. - aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; + aimValue *= SpecialFunctions.Erf(25.0 / (Math.Sqrt(2) * totalDeviation.Value)); return aimValue; } @@ -317,6 +322,48 @@ namespace osu.Game.Rulesets.Osu.Difficulty return flashlightValue; } + /// + /// Using estimates player's deviation on accuracy objects. + /// Returns deviation for circles and sliders if score was set with slideracc. + /// Returns the min between deviation of circles and deviation on circles and sliders (assuming slider hits are 50s), if score was set without slideracc. + /// + private double? calculateTotalDeviation(OsuDifficultyAttributes attributes) + { + if (totalSuccessfulHits == 0) + return null; + + int accuracyObjectCount = attributes.HitCircleCount; + + if (!usingClassicSliderAccuracy) + accuracyObjectCount += attributes.SliderCount; + + // Assume worst case: all mistakes was on accuracy objects + int relevantCountMiss = Math.Min(countMiss, accuracyObjectCount); + int relevantCountMeh = Math.Min(countMeh, accuracyObjectCount - relevantCountMiss); + int relevantCountOk = Math.Min(countOk, accuracyObjectCount - relevantCountMiss - relevantCountMeh); + int relevantCountGreat = Math.Max(0, accuracyObjectCount - relevantCountMiss - relevantCountMeh - relevantCountOk); + + // Calculate deviation on accuracy objects + double? deviation = calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss); + if (deviation == null) + return null; + + if (!usingClassicSliderAccuracy) + return deviation.Value; + + // If score was set without slider accuracy - also compute deviation with sliders + // Assume that all hits was 50s + int totalCountWithSliders = attributes.HitCircleCount + attributes.SliderCount; + int missCountWithSliders = Math.Min(totalCountWithSliders, countMiss); + int hitCountWithSliders = totalCountWithSliders - missCountWithSliders; + + double hitProbabilityWithSliders = hitCountWithSliders / (totalCountWithSliders + 1.0); + double deviationWithSliders = attributes.MehHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(hitProbabilityWithSliders)); + + // Min is needed for edgecase maps with 1 circle and 999 sliders, as deviation on sliders can be lower in this case + return Math.Min(deviation.Value, deviationWithSliders); + } + /// /// Estimates player's deviation on speed notes using , assuming worst-case. /// Treats all speed notes as hit circles. @@ -412,8 +459,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty const double scale = 50; double adjustedSpeedValue = scale * (Math.Log((speedValue - excessSpeedDifficultyCutoff) / scale + 1) + excessSpeedDifficultyCutoff / scale); - // 200 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible - double lerp = 1 - Math.Clamp((speedDeviation.Value - 20) / (24 - 20), 0, 1); + // 220 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible + double lerp = 1 - DifficultyCalculationUtils.ReverseLerp(speedDeviation.Value, 22.0, 27.0); adjustedSpeedValue = double.Lerp(adjustedSpeedValue, speedValue, lerp); return adjustedSpeedValue / speedValue; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 400bc97fbc..69211b610f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 25.18; + private double skillMultiplier => 25.7; private double strainDecayBase => 0.15; private readonly List sliderStrains = new List();