diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 2aef0fdfc2ba..8e5f23a12566 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -15,8 +15,6 @@ using osu.Game.Rulesets.Osu.Difficulty.Utils; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Scoring; -using osu.Game.Rulesets.Scoring; using osu.Game.Utils; namespace osu.Game.Rulesets.Osu.Difficulty @@ -30,22 +28,6 @@ public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) { } - public static double CalculateRateAdjustedApproachRate(double approachRate, double clockRate) - { - double preempt = IBeatmapDifficultyInfo.DifficultyRange(approachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN) / clockRate; - return IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN); - } - - public static double CalculateRateAdjustedOverallDifficulty(double overallDifficulty, double clockRate) - { - HitWindows hitWindows = new OsuHitWindows(); - hitWindows.SetDifficulty(overallDifficulty); - - double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate; - - return (79.5 - hitWindowGreat) / 6; - } - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills) { if (beatmap.HitObjects.Count == 0) @@ -78,8 +60,6 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat double difficultSliders = aim.GetDifficultSliders(); - double overallDifficulty = CalculateRateAdjustedOverallDifficulty(beatmap.Difficulty.OverallDifficulty, ModUtils.CalculateRateWithMods(mods)); - int hitCircleCount = beatmap.HitObjects.Count(h => h is HitCircle); int sliderCount = beatmap.HitObjects.Count(h => h is Slider); int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner); @@ -87,19 +67,17 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat int totalHits = beatmap.HitObjects.Count; double sliderFactor = aimDifficultyValue > 0 - ? OsuRatingCalculator.CalculateDifficultyRating(aimNoSlidersDifficultyValue) / OsuRatingCalculator.CalculateDifficultyRating(aimDifficultyValue) + ? calculateDifficultyRating(aimNoSlidersDifficultyValue) / calculateDifficultyRating(aimDifficultyValue) // TODO: this is intentionally left incorrect : 1; - var osuRatingCalculator = new OsuRatingCalculator(totalHits, overallDifficulty); - - double aimRating = osuRatingCalculator.ComputeAimRating(aimDifficultyValue); - double speedRating = osuRatingCalculator.ComputeSpeedRating(speedDifficultyValue); - double readingRating = osuRatingCalculator.ComputeReadingRating(readingDifficultyValue); + double aimRating = calculateAimDifficultyRating(aimDifficultyValue); + double speedRating = calculateDifficultyRating(speedDifficultyValue); + double readingRating = calculateDifficultyRating(readingDifficultyValue); double flashlightRating = 0.0; if (flashlight is not null) - flashlightRating = osuRatingCalculator.ComputeFlashlightRating(flashlight.DifficultyValue()); + flashlightRating = calculateDifficultyRating(flashlight.DifficultyValue()); double sliderNestedScorePerObject = LegacyScoreUtils.CalculateNestedScorePerObject(beatmap, totalHits); double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(WorkingBeatmap.Beatmap); @@ -157,6 +135,10 @@ public static double SumCognitionDifficulty(double reading, double flashlight) return DifficultyCalculationUtils.Norm(OsuPerformanceCalculator.PERFORMANCE_NORM_EXPONENT, reading, flashlight * Math.Clamp(flashlight / reading, 0.25, 1.0)); } + private double calculateAimDifficultyRating(double difficultyValue) => Math.Pow(difficultyValue, 0.63) * 0.02275; + + private double calculateDifficultyRating(double difficultyValue) => Math.Sqrt(difficultyValue) * 0.0675; + private double calculateStarRating(double basePerformance) { return Math.Cbrt(basePerformance * OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER); @@ -189,7 +171,7 @@ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) }; if (mods.Any(h => h is OsuModFlashlight)) - skills.Add(new Flashlight(mods)); + skills.Add(new Flashlight(mods, beatmap.HitObjects.Count)); return skills.ToArray(); } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 6ad5c98c3fb5..17788e59fd64 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -5,13 +5,15 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Game.Rulesets.Difficulty.Utils; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Utils; @@ -99,8 +101,8 @@ protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo s okHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate; mehHitWindow = hitWindows.WindowFor(HitResult.Meh) / clockRate; - approachRate = OsuDifficultyCalculator.CalculateRateAdjustedApproachRate(difficulty.ApproachRate, clockRate); - overallDifficulty = OsuDifficultyCalculator.CalculateRateAdjustedOverallDifficulty(difficulty.OverallDifficulty, clockRate); + approachRate = calculateRateAdjustedApproachRate(difficulty.ApproachRate, clockRate); + overallDifficulty = (79.5 - greatHitWindow) / 6; drainRate = difficulty.DrainRate; double comboBasedEstimatedMissCount = calculateComboBasedEstimatedMissCount(osuAttributes); @@ -536,6 +538,12 @@ private double calculateTraceableBonus(double sliderFactor = 1) private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.93 / (missCount / (4 * Math.Log(difficultStrainCount)) + 1); private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0); + private double calculateRateAdjustedApproachRate(double approachRate, double clockRate) + { + double preempt = IBeatmapDifficultyInfo.DifficultyRange(approachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN) / clockRate; + return IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN); + } + private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; private int totalImperfectHits => countOk + countMeh + countMiss; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuRatingCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuRatingCalculator.cs deleted file mode 100644 index 349677b2a8dd..000000000000 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuRatingCalculator.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; - -namespace osu.Game.Rulesets.Osu.Difficulty -{ - public class OsuRatingCalculator - { - private const double difficulty_multiplier = 0.0675; - - private readonly int totalHits; - private readonly double overallDifficulty; - - public OsuRatingCalculator(int totalHits, double overallDifficulty) - { - this.totalHits = totalHits; - this.overallDifficulty = overallDifficulty; - } - - public double ComputeAimRating(double aimDifficultyValue) - { - double aimRating = Math.Pow(aimDifficultyValue, 0.63) * 0.02275; - - double ratingMultiplier = 1.0; - - // It is important to consider accuracy difficulty when scaling with accuracy. - ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500; - - return aimRating * Math.Cbrt(ratingMultiplier); - } - - public double ComputeSpeedRating(double speedDifficultyValue) - { - return CalculateDifficultyRating(speedDifficultyValue); - } - - public double ComputeReadingRating(double readingDifficultyValue) - { - double readingRating = CalculateDifficultyRating(readingDifficultyValue); - - double ratingMultiplier = 1.0; - - ratingMultiplier *= 0.75 + Math.Pow(Math.Max(0, overallDifficulty), 2.2) / 800; - - return readingRating * Math.Cbrt(ratingMultiplier); - } - - public double ComputeFlashlightRating(double flashlightDifficultyValue) - { - double flashlightRating = CalculateDifficultyRating(flashlightDifficultyValue); - - double ratingMultiplier = 1.0; - - // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius. - ratingMultiplier *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) + - (totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0); - - // It is important to consider accuracy difficulty when scaling with accuracy. - ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500; - - return flashlightRating * Math.Sqrt(ratingMultiplier); - } - - public static double CalculateDifficultyRating(double difficultyValue) => Math.Sqrt(difficultyValue) * difficulty_multiplier; - } -} diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 413588749ab2..8e81ba531dd1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -128,6 +128,19 @@ public class OsuDifficultyHitObject : DifficultyHitObject /// public double SmallCircleBonus { get; private set; } + /// + /// Object's immediate OverallDifficulty value calculated from the hitwindow. + /// + public double OverallDifficulty + { + get + { + double hitWindowGreat = BaseObject.HitWindows.WindowFor(HitResult.Great) / ClockRate; + + return (79.5 - hitWindowGreat) / 6; + } + } + private readonly OsuDifficultyHitObject? lastLastDifficultyObject; private readonly OsuDifficultyHitObject? lastDifficultyObject; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 9ad9026ac5ee..da452ffe834a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -63,7 +63,7 @@ protected override double StrainValueAt(DifficultyHitObject current) double decay = strainDecay(((OsuDifficultyHitObject)current).AdjustedDeltaTime); currentStrain *= decay; - currentStrain += calculateModAdjustedDifficulty(current) * (1 - decay); + currentStrain += calculateAdjustedDifficulty(current) * (1 - decay); if (current.BaseObject is Slider) sliderStrains.Add(currentStrain); @@ -71,7 +71,7 @@ protected override double StrainValueAt(DifficultyHitObject current) return currentStrain; } - private double calculateModAdjustedDifficulty(DifficultyHitObject current) + private double calculateAdjustedDifficulty(DifficultyHitObject current) { double snapDifficulty = SnapAimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplierSnap; double agilityDifficulty = AgilityEvaluator.EvaluateDifficultyOf(current) * skillMultiplierAgility; @@ -85,6 +85,8 @@ private double calculateModAdjustedDifficulty(DifficultyHitObject current) totalDifficulty *= 1.0 - magnetisedStrength; } + totalDifficulty *= 0.99 + Math.Pow(Math.Max(0, ((OsuDifficultyHitObject)current).OverallDifficulty), 2) / 5500; + return totalDifficulty; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 59ea3b56eaf5..a6be21d0dad1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -8,6 +8,7 @@ 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; using osu.Game.Rulesets.Osu.Mods; namespace osu.Game.Rulesets.Osu.Difficulty.Skills @@ -17,9 +18,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Flashlight : StrainSkill { - public Flashlight(Mod[] mods) + private readonly int totalObjects; + + public Flashlight(Mod[] mods, int totalObjects) : base(mods) { + this.totalObjects = totalObjects; } private double skillMultiplier => 0.056; @@ -37,12 +41,12 @@ protected override double StrainValueAt(DifficultyHitObject current) return 0; currentStrain *= strainDecay(current.DeltaTime); - currentStrain += calculateModAdjustedDifficulty(current) * skillMultiplier; + currentStrain += calculateAdjustedDifficulty(current) * skillMultiplier; return currentStrain; } - private double calculateModAdjustedDifficulty(DifficultyHitObject current) + private double calculateAdjustedDifficulty(DifficultyHitObject current) { double difficulty = FlashlightEvaluator.EvaluateDifficultyOf(current, Mods); @@ -67,10 +71,21 @@ private double calculateModAdjustedDifficulty(DifficultyHitObject current) if (Mods.Any(m => m is OsuModAutopilot)) difficulty *= 0.4; + difficulty *= 0.99 + Math.Pow(Math.Max(0, ((OsuDifficultyHitObject)current).OverallDifficulty), 2) / 5500; + return difficulty; } - public override double DifficultyValue() => GetCurrentStrainPeaks().Sum(); + public override double DifficultyValue() + { + double sum = GetCurrentStrainPeaks().Sum(); + + // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius. + sum *= Math.Sqrt(0.7 + 0.1 * Math.Min(1.0, totalObjects / 200.0) + + (totalObjects > 200 ? 0.2 * Math.Min(1.0, (totalObjects - 200) / 200.0) : 0.0)); + + return sum; + } public static double DifficultyToPerformance(double difficulty) => 25 * Math.Pow(difficulty, 2); } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs index 2b006f4a8662..28066c217172 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs @@ -10,6 +10,7 @@ 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; using osu.Game.Rulesets.Osu.Mods; namespace osu.Game.Rulesets.Osu.Difficulty.Skills @@ -40,12 +41,12 @@ protected override double ObjectDifficultyOf(DifficultyHitObject current) double decay = strainDecay(current.DeltaTime); currentStrain *= decay; - currentStrain += calculateModAdjustedDifficulty(current) * (1 - decay) * skillMultiplier; + currentStrain += calculateAdjustedDifficulty(current) * (1 - decay) * skillMultiplier; return currentStrain; } - private double calculateModAdjustedDifficulty(DifficultyHitObject current) + private double calculateAdjustedDifficulty(DifficultyHitObject current) { double difficulty = ReadingEvaluator.EvaluateDifficultyOf(current, hasHiddenMod); @@ -117,5 +118,16 @@ public override double CountTopWeightedObjectDifficulties(double difficultyValue return ObjectDifficulties.Sum(d => DifficultyCalculationUtils.Logistic(d / consistentTopNote, 1.15, 5, 1.1)); } + + public override double DifficultyValue() + { + double difficulty = base.DifficultyValue(); + + // TODO: this should be replaced with a per-object adjustment, but it requires extra considerations + if (objectList.Count > 0) + difficulty *= 0.825 + Math.Pow(Math.Max(0, ((OsuDifficultyHitObject)objectList.First()).OverallDifficulty), 2.2) / 1125.0; + + return difficulty; + } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index d51632a7ffad..bc0383f074de 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -46,7 +46,7 @@ protected override double ObjectDifficultyOf(DifficultyHitObject current) double decay = strainDecay(((OsuDifficultyHitObject)current).AdjustedDeltaTime); currentStrain *= decay; - currentStrain += calculateModAdjustedDifficulty(current) * (1 - decay) * skillMultiplier; + currentStrain += calculateAdjustedDifficulty(current) * (1 - decay) * skillMultiplier; double currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current); @@ -58,7 +58,7 @@ protected override double ObjectDifficultyOf(DifficultyHitObject current) return totalStrain; } - private double calculateModAdjustedDifficulty(DifficultyHitObject current) + private double calculateAdjustedDifficulty(DifficultyHitObject current) { double difficulty = SpeedEvaluator.EvaluateDifficultyOf(current);