From 01d9c526d9342943771da83bb0bbe3d8d011b28c Mon Sep 17 00:00:00 2001 From: Givikap120 <89256026+Givikap120@users.noreply.github.com> Date: Mon, 26 May 2025 13:16:48 +0300 Subject: [PATCH] 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 --- .../Difficulty/OsuDifficultyCalculator.cs | 98 ++++++++++++++++--- .../Difficulty/OsuPerformanceCalculator.cs | 11 ++- 2 files changed, 90 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index dd9d4d4c23..c048fedd02 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -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; } + /// + /// Calculates a visibility bonus that is applicable to Hidden and Traceable. + /// + 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().Any(m => !m.OnlyFadeApproachCircles.Value) || mods.OfType().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 CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { List objects = new List(); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 8802c4a1c2..e5e42e6d4f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -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;