1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-06 23:24:53 +08:00

Rebalance HD bonus (#33237)

* initial commit

* changed HD curve

* removed AR variable

* update for new rework

* nerf HD acc bonus for AR>10

* add another HD nerf for AR>10

* Update OsuDifficultyCalculator.cs

* fix speed part being missing

* Update OsuDifficultyCalculator.cs

* rework to difficulty-based high AR nerf

* move TC back to perfcalc

* fix nvicka

* fix comment

* use utils function instead of manual one

* Clean up

* Use "visibility" term instead

* Store `mechanicalDifficultyRating` field

* Rename `isFullyHidden` to `isAlwaysPartiallyVisible` and clarify intent

* Remove redundant comment

* Add `calculateDifficultyRating` method

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
This commit is contained in:
Givikap120
2025-05-26 13:16:48 +03:00
committed by GitHub
Unverified
parent ace74824b8
commit 01d9c526d9
2 changed files with 90 additions and 19 deletions
@@ -8,6 +8,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Skills;
@@ -27,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public override int Version => 20250306;
private double mechanicalDifficultyRating;
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
@@ -42,6 +45,30 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return multiplier;
}
/// <summary>
/// Calculates a visibility bonus that is applicable to Hidden and Traceable.
/// </summary>
public static double CalculateVisibilityBonus(Mod[] mods, double approachRate, double visibilityFactor = 1)
{
// NOTE: TC's effect is only noticeable in performance calculations until lazer mods are accounted for server-side.
bool isAlwaysPartiallyVisible = mods.OfType<OsuModHidden>().Any(m => !m.OnlyFadeApproachCircles.Value) || mods.OfType<OsuModTraceable>().Any();
// Start from normal curve, rewarding lower AR up to AR5
double readingBonus = 0.04 * (12.0 - Math.Max(approachRate, 5));
readingBonus *= visibilityFactor;
// For AR up to 0 - reduce reward for very low ARs when object is visible
if (approachRate < 5)
readingBonus += (isAlwaysPartiallyVisible ? 0.04 : 0.03) * (5.0 - Math.Max(approachRate, 0));
// Starting from AR0 - cap values so they won't grow to infinity
if (approachRate < 0)
readingBonus += (isAlwaysPartiallyVisible ? 0.1 : 0.075) * (1 - Math.Pow(1.5, approachRate));
return readingBonus;
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
{
if (beatmap.HitObjects.Count == 0)
@@ -85,9 +112,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double drainRate = beatmap.Difficulty.DrainRate;
double aimRating = computeAimRating(aim.DifficultyValue(), mods, totalHits, approachRate, overallDifficulty);
double aimRatingNoSliders = computeAimRating(aimWithoutSliders.DifficultyValue(), mods, totalHits, approachRate, overallDifficulty);
double speedRating = computeSpeedRating(speed.DifficultyValue(), mods, totalHits, approachRate, overallDifficulty);
double aimDifficultyValue = aim.DifficultyValue();
double aimNoSlidersDifficultyValue = aimWithoutSliders.DifficultyValue();
double speedDifficultyValue = speed.DifficultyValue();
mechanicalDifficultyRating = calculateMechanicalDifficultyRating(aimDifficultyValue, speedDifficultyValue);
double aimRating = computeAimRating(aimDifficultyValue, mods, totalHits, approachRate, overallDifficulty);
double aimRatingNoSliders = computeAimRating(aimNoSlidersDifficultyValue, mods, totalHits, approachRate, overallDifficulty);
double speedRating = computeSpeedRating(speedDifficultyValue, mods, totalHits, approachRate, overallDifficulty);
double flashlightRating = 0.0;
@@ -108,10 +141,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
);
double multiplier = CalculateDifficultyMultiplier(mods, totalHits, spinnerCount);
double starRating = basePerformance > 0.00001
? Math.Cbrt(multiplier) * star_rating_multiplier * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4)
: 0;
double starRating = calculateStarRating(basePerformance, multiplier);
double sliderNestedScorePerObject = LegacyScoreUtils.CalculateNestedScorePerObject(beatmap, totalHits);
double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(beatmap);
@@ -151,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (mods.Any(m => m is OsuModAutopilot))
return 0;
double aimRating = Math.Sqrt(aimDifficultyValue) * difficulty_multiplier;
double aimRating = calculateDifficultyRating(aimDifficultyValue);
if (mods.Any(m => m is OsuModTouchDevice))
aimRating = Math.Pow(aimRating, 0.8);
@@ -183,8 +213,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (mods.Any(m => m is OsuModHidden))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
ratingMultiplier *= 1.0 + 0.04 * (12.0 - approachRate);
double visibilityFactor = calculateAimVisibilityFactor(approachRate);
ratingMultiplier *= 1.0 + CalculateVisibilityBonus(mods, approachRate, visibilityFactor);
}
// It is important to consider accuracy difficulty when scaling with accuracy.
@@ -198,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (mods.Any(m => m is OsuModRelax))
return 0;
double speedRating = Math.Sqrt(speedDifficultyValue) * difficulty_multiplier;
double speedRating = calculateDifficultyRating(speedDifficultyValue);
if (mods.Any(m => m is OsuModAutopilot))
speedRating *= 0.5;
@@ -226,8 +256,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (mods.Any(m => m is OsuModHidden))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
ratingMultiplier *= 1.0 + 0.04 * (12.0 - approachRate);
double visibilityFactor = calculateSpeedVisibilityFactor(approachRate);
ratingMultiplier *= 1.0 + CalculateVisibilityBonus(mods, approachRate, visibilityFactor);
}
ratingMultiplier *= 0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750;
@@ -240,7 +270,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (!mods.Any(m => m is OsuModFlashlight))
return 0;
double flashlightRating = Math.Sqrt(flashlightDifficultyValue) * difficulty_multiplier;
double flashlightRating = calculateDifficultyRating(flashlightDifficultyValue);
if (mods.Any(m => m is OsuModTouchDevice))
flashlightRating = Math.Pow(flashlightRating, 0.8);
@@ -268,6 +298,46 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return flashlightRating * Math.Sqrt(ratingMultiplier);
}
private double calculateAimVisibilityFactor(double approachRate)
{
const double ar_factor_end_point = 11.5;
double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10);
double arFactorStartingPoint = double.Lerp(9, 10.33, mechanicalDifficultyFactor);
return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint);
}
private double calculateSpeedVisibilityFactor(double approachRate)
{
const double ar_factor_end_point = 11.5;
double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10);
double arFactorStartingPoint = double.Lerp(10, 10.33, mechanicalDifficultyFactor);
return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint);
}
private static double calculateMechanicalDifficultyRating(double aimDifficultyValue, double speedDifficultyValue)
{
double aimValue = OsuStrainSkill.DifficultyToPerformance(calculateDifficultyRating(aimDifficultyValue));
double speedValue = OsuStrainSkill.DifficultyToPerformance(calculateDifficultyRating(speedDifficultyValue));
double totalValue = Math.Pow(Math.Pow(aimValue, 1.1) + Math.Pow(speedValue, 1.1), 1 / 1.1);
return calculateStarRating(totalValue, performance_base_multiplier);
}
private static double calculateStarRating(double basePerformance, double multiplier)
{
if (basePerformance <= 0.00001)
return 0;
return Math.Cbrt(multiplier) * star_rating_multiplier * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4);
}
private static double calculateDifficultyRating(double difficultyValue) => Math.Sqrt(difficultyValue) * difficulty_multiplier;
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
@@ -210,8 +210,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate);
else if (score.Mods.Any(m => m is OsuModTraceable))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
aimValue *= 1.0 + 0.04 * (12.0 - approachRate);
aimValue *= 1.0 + OsuDifficultyCalculator.CalculateVisibilityBonus(score.Mods, approachRate);
}
aimValue *= accuracy;
@@ -244,8 +243,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
}
else if (score.Mods.Any(m => m is OsuModTraceable))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
speedValue *= 1.0 + 0.04 * (12.0 - approachRate);
speedValue *= 1.0 + OsuDifficultyCalculator.CalculateVisibilityBonus(score.Mods, approachRate);
}
double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes);
@@ -295,7 +293,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (score.Mods.Any(m => m is OsuModBlinds))
accuracyValue *= 1.14;
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
accuracyValue *= 1.08;
{
// Decrease bonus for AR > 10
accuracyValue *= 1 + 0.08 * Math.Clamp((11.5 - approachRate) / (11.5 - 10), 0, 1);
}
if (score.Mods.Any(m => m is OsuModFlashlight))
accuracyValue *= 1.02;