1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 14:50:54 +08:00

Move speed distance bonus to aim (#36168)

This doesn't solve _flow_ aim in any way, but makes it so that `Speed`
doesn't have distance scaling (read as "aim") anymore which fixes some
issues related to that like the length bonus behaving incorrectly, and
in general `Speed` being an aim+tap skill instead of just a tapping
skill

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
This commit is contained in:
StanR
2026-01-26 02:21:20 +05:00
committed by GitHub
Unverified
parent 562f1f8004
commit 0afcebe8bf
6 changed files with 80 additions and 40 deletions
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
private const int history_time_max = 5 * 1000; // 5 seconds
private const int history_objects_max = 32;
private const double rhythm_overall_multiplier = 1.0;
private const double rhythm_ratio_multiplier = 17.0;
private const double rhythm_ratio_multiplier = 30.0;
/// <summary>
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
@@ -0,0 +1,50 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
{
public static class SpeedAimEvaluator
{
private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
/// <summary>
/// Evaluates the difficulty of aiming the current object, based on:
/// <list type="bullet">
/// <item><description>distance between the previous and current object</description></item>
/// </list>
/// </summary>
public static double EvaluateDifficultyOf(DifficultyHitObject current)
{
if (current.BaseObject is Spinner)
return 0;
var osuCurrObj = (OsuDifficultyHitObject)current;
var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null;
double travelDistance = osuPrevObj?.LazyTravelDistance ?? 0;
double distance = travelDistance + osuCurrObj.LazyJumpDistance;
// Cap distance at single_spacing_threshold
distance = Math.Min(distance, single_spacing_threshold);
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95);
// Apply reduced small circle bonus because flow aim difficulty on small circles doesn't scale as hard as jumps
distanceBonus *= Math.Sqrt(osuCurrObj.SmallCircleBonus);
double strain = distanceBonus * 1000 / osuCurrObj.AdjustedDeltaTime;
strain *= highBpmBonus(osuCurrObj.AdjustedDeltaTime);
return strain;
}
private static double highBpmBonus(double ms) => 1 / (1 - Math.Pow(0.3, ms / 1000));
}
}
@@ -2,40 +2,31 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
{
public static class SpeedEvaluator
{
private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
private const double min_speed_bonus = 200; // 200 BPM 1/4th
private const double speed_balancing_factor = 40;
private const double distance_multiplier = 0.8;
/// <summary>
/// Evaluates the difficulty of tapping the current object, based on:
/// <list type="bullet">
/// <item><description>time between pressing the previous and current object,</description></item>
/// <item><description>distance between those objects,</description></item>
/// <item><description>and how easily they can be cheesed.</description></item>
/// </list>
/// </summary>
public static double EvaluateDifficultyOf(DifficultyHitObject current, IReadOnlyList<Mod> mods)
public static double EvaluateDifficultyOf(DifficultyHitObject current)
{
if (current.BaseObject is Spinner)
return 0;
// derive strainTime for calculation
var osuCurrObj = (OsuDifficultyHitObject)current;
var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null;
double strainTime = osuCurrObj.AdjustedDeltaTime;
double doubletapness = 1.0 - osuCurrObj.GetDoubletapness((OsuDifficultyHitObject?)osuCurrObj.Next(0));
@@ -51,23 +42,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
if (DifficultyCalculationUtils.MillisecondsToBPM(strainTime) > min_speed_bonus)
speedBonus = 0.75 * Math.Pow((DifficultyCalculationUtils.BPMToMilliseconds(min_speed_bonus) - strainTime) / speed_balancing_factor, 2);
double travelDistance = osuPrevObj?.LazyTravelDistance ?? 0;
double distance = travelDistance + osuCurrObj.LazyJumpDistance;
// Cap distance at single_spacing_threshold
distance = Math.Min(distance, single_spacing_threshold);
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier;
// Apply reduced small circle bonus because flow aim difficulty on small circles doesn't scale as hard as jumps
distanceBonus *= Math.Sqrt(osuCurrObj.SmallCircleBonus);
if (mods.OfType<OsuModAutopilot>().Any())
distanceBonus = 0;
// Base difficulty with all bonuses
double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime;
double difficulty = (1 + speedBonus) * 1000 / strainTime;
difficulty *= highBpmBonus(osuCurrObj.AdjustedDeltaTime);
@@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double aimValue = OsuStrainSkill.DifficultyToPerformance(aimDifficulty);
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
double lengthBonus = 0.95 + 0.3 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
aimValue *= lengthBonus;
+24 -10
View File
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
@@ -26,28 +27,41 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
IncludeSliders = includeSliders;
}
private double currentStrain;
private double currentAimStrain;
private double currentSpeedStrain;
private double skillMultiplier => 26.6;
private double strainDecayBase => 0.15;
private double skillMultiplierAim => 26.0;
private double skillMultiplierSpeed => 1.3;
private double skillMultiplierTotal => 1.02;
private double meanExponent => 1.2;
private readonly List<double> sliderStrains = new List<double>();
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
private double strainDecayAim(double ms) => Math.Pow(0.15, ms / 1000);
private double strainDecaySpeed(double ms) => Math.Pow(0.3, ms / 1000);
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime);
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) =>
DifficultyCalculationUtils.Norm(meanExponent,
currentAimStrain * strainDecayAim(time - current.Previous(0).StartTime),
currentSpeedStrain * strainDecaySpeed(time - current.Previous(0).StartTime));
protected override double StrainValueAt(DifficultyHitObject current)
{
double decay = strainDecay(((OsuDifficultyHitObject)current).AdjustedDeltaTime);
double decayAim = strainDecayAim(((OsuDifficultyHitObject)current).AdjustedDeltaTime);
double decaySpeed = strainDecaySpeed(((OsuDifficultyHitObject)current).AdjustedDeltaTime);
currentStrain *= decay;
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * (1 - decay) * skillMultiplier;
currentAimStrain *= decayAim;
currentAimStrain += AimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * (1 - decayAim) * skillMultiplierAim;
currentSpeedStrain *= decaySpeed;
currentSpeedStrain += SpeedAimEvaluator.EvaluateDifficultyOf(current) * (1 - decaySpeed) * skillMultiplierSpeed;
double totalStrain = DifficultyCalculationUtils.Norm(meanExponent, currentAimStrain, currentSpeedStrain);
if (current.BaseObject is Slider)
sliderStrains.Add(currentStrain);
sliderStrains.Add(totalStrain);
return currentStrain;
return totalStrain * skillMultiplierTotal;
}
public double GetDifficultSliders()
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// </summary>
public class Speed : HarmonicSkill
{
private double skillMultiplier => 0.93;
private double skillMultiplier => 0.95;
private readonly List<double> sliderStrains = new List<double>();
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
double decay = strainDecay(((OsuDifficultyHitObject)current).AdjustedDeltaTime);
currentDifficulty *= decay;
currentDifficulty += SpeedEvaluator.EvaluateDifficultyOf(current, Mods) * (1 - decay) * skillMultiplier;
currentDifficulty += SpeedEvaluator.EvaluateDifficultyOf(current) * (1 - decay) * skillMultiplier;
double currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);