diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingEvaluator.cs index e8fb4a8469..ed7c60ccf6 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingEvaluator.cs @@ -165,94 +165,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators return Math.Max(0, Math.Pow(difficulty, 1.5) - 1); } - // Returns value from 0 to 1, where 0 is very predictable and 1 is very unpredictable - public static double EvaluateInpredictabilityOf(DifficultyHitObject current) - { - // make the sum equal to 1 - const double velocity_change_part = 0.8; - const double angle_change_part = 0.1; - const double rhythm_change_part = 0.1; - - if (current.BaseObject is Spinner || current.Index == 0 || current.Previous(0).BaseObject is Spinner) - return 0; - - var osuCurrObj = (OsuDifficultyHitObject)current; - var osuLastObj = (OsuDifficultyHitObject)current.Previous(0); - - // Rhythm difference punishment for velocity and angle bonuses - double rhythmSimilarity = 1 - getRhythmDifference(osuCurrObj.StrainTime, osuLastObj.StrainTime); - - // Make differentiation going from 1/4 to 1/2 and bigger difference - // To 1/3 to 1/2 and smaller difference - rhythmSimilarity = Math.Clamp(rhythmSimilarity, 0.5, 0.75); - rhythmSimilarity = 4 * (rhythmSimilarity - 0.5); - - double velocityChangeBonus = getVelocityChangeFactor(osuCurrObj, osuLastObj) * rhythmSimilarity; - - double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime; - double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime; - - double angleChangeBonus = 0; - - if (osuCurrObj.Angle != null && osuLastObj.Angle != null && currVelocity > 0 && prevVelocity > 0) - { - angleChangeBonus = 1 - osuCurrObj.AnglePredictability; - angleChangeBonus *= Math.Min(currVelocity, prevVelocity) / Math.Max(currVelocity, prevVelocity); // Prevent cheesing - } - - angleChangeBonus *= rhythmSimilarity; - - // This bonus only awards rhythm changes if they're not filled with sliderends - double rhythmChangeBonus = 0; - - if (current.Index > 1) - { - var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1); - - double currDelta = osuCurrObj.StrainTime; - double lastDelta = osuLastObj.StrainTime; - - if (osuLastObj.BaseObject is Slider sliderCurr) - { - currDelta -= sliderCurr.Duration / osuCurrObj.ClockRate; - currDelta = Math.Max(0, currDelta); - } - - if (osuLastLastObj.BaseObject is Slider sliderLast) - { - lastDelta -= sliderLast.Duration / osuLastObj.ClockRate; - lastDelta = Math.Max(0, lastDelta); - } - - rhythmChangeBonus = getRhythmDifference(currDelta, lastDelta); - } - - double result = velocity_change_part * velocityChangeBonus + angle_change_part * angleChangeBonus + rhythm_change_part * rhythmChangeBonus; - return result; - } - - private static double getVelocityChangeFactor(OsuDifficultyHitObject osuCurrObj, OsuDifficultyHitObject osuLastObj) - { - double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime; - double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime; - - double velocityChangeFactor = 0; - - // https://www.desmos.com/calculator/kqxmqc8pkg - if (currVelocity > 0 || prevVelocity > 0) - { - double velocityChange = Math.Max(0, - Math.Min( - Math.Abs(prevVelocity - currVelocity) - 0.5 * Math.Min(currVelocity, prevVelocity), - Math.Max(((OsuHitObject)osuCurrObj.BaseObject).Radius / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Min(currVelocity, prevVelocity)) - )); // Stealed from xexxar - velocityChangeFactor = velocityChange / Math.Max(currVelocity, prevVelocity); // maxiumum is 0.4 - velocityChangeFactor /= 0.4; - } - - return velocityChangeFactor; - } - private static double getTimeNerfFactor(double deltaTime) => Math.Clamp(2 - deltaTime / (reading_window_size / 2), 0, 1); private static double getRhythmDifference(double t1, double t2) => 1 - Math.Min(t1, t2) / Math.Max(t1, t2); private static double logistic(double x) => 1 / (1 + Math.Exp(-x)); @@ -282,73 +194,4 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators return arr[result].Overlapness; } } - - public static class ReadingHiddenEvaluator - { - public static double EvaluateDifficultyOf(DifficultyHitObject current) - { - var currObj = (OsuDifficultyHitObject)current; - - double density = ReadingEvaluator.EvaluateDensityOf(current, false); - double preempt = currObj.Preempt / 1000; - - double densityFactor = Math.Pow(density / 6.2, 1.5); - - double invisibilityFactor; - - // AR11+DT and faster = 0 HD pp unless density is big - if (preempt < 0.2) invisibilityFactor = 0; - - // Else accelerating growth until around ART0, then linear, and starting from AR5 is 3 times faster again to buff AR0 +HD - else invisibilityFactor = Math.Min(Math.Pow(preempt * 2.4 - 0.2, 5), Math.Max(preempt, preempt * 3 - 2.4)); - - - double hdDifficulty = invisibilityFactor + densityFactor; - - // Scale by inpredictability slightly - hdDifficulty *= 0.96 + 0.1 * ReadingEvaluator.EvaluateInpredictabilityOf(current); // Max multiplier is 1.1 - - return hdDifficulty; - } - } - - public static class ReadingHighAREvaluator - { - public static double EvaluateDifficultyOf(DifficultyHitObject current, bool applyAdjust = false) - { - var currObj = (OsuDifficultyHitObject)current; - - double result = GetDifficulty(currObj.Preempt); - - if (applyAdjust) - { - double inpredictability = ReadingEvaluator.EvaluateInpredictabilityOf(current); - - // follow lines make high AR easier, so apply nerf if object isn't new combo - inpredictability *= 1 + 0.1 * (800 - currObj.FollowLineTime) / 800; - - result *= 0.98 + 0.6 * inpredictability; - } - - return result; - } - - // High AR curve - // https://www.desmos.com/calculator/srzbeumngi - public static double GetDifficulty(double preempt) - { - // Get preempt in seconds - preempt /= 1000; - double value; - - if (preempt < 0.375) // We have stop in the point of AR10.5, the value here = 0.396875, derivative = -10.5833, - value = 0.63 * Math.Pow(8 - 20 * preempt, 2.0 / 3); // This function is matching live high AR bonus - else - value = Math.Exp(9.07583 - 80.0 * preempt / 3); - - // The power is 2 times higher to compensate sqrt in high AR skill - // EDIT: looks like AR11 getting a bit overnerfed in comparison to other ARs, so i will increase the difference - return Math.Pow(value, 2.2); - } - } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 4ee92c7b8f..2a2a0e1dc2 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -38,24 +38,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("reading_low_ar_difficulty")] public double ReadingDifficultyLowAR { get; set; } - /// - /// The difficulty corresponding to the reading skill. High AR branch. - /// - [JsonProperty("reading_high_ar_difficulty")] - public double ReadingDifficultyHighAR { get; set; } - - /// - /// The difficulty corresponding to the reading skill. Sliders branch. - /// - [JsonProperty("reading_sliders_difficulty")] - public double ReadingDifficultySliders { get; set; } - - /// - /// The difficulty corresponding to the reading skill. Hidden mod branch. - /// - [JsonProperty("reading_hidden_difficulty")] - public double HiddenDifficulty { get; set; } - /// /// The difficulty corresponding to the flashlight skill. /// diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index dde58f59c7..1cc31cf2e4 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -44,17 +44,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty double hiddenFlashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * DIFFICULTY_MULTIPLIER; double readingLowARRating = Math.Sqrt(skills[4].DifficultyValue()) * DIFFICULTY_MULTIPLIER; - double readingHighARRating = Math.Sqrt(skills[5].DifficultyValue()) * DIFFICULTY_MULTIPLIER; - - double hiddenRating = 0; - double flashlightRating = 0; double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; + double flashlightRating = 0; + double baseFlashlightPerformance = 0.0; + if (mods.Any(h => h is OsuModFlashlight)) + { + flashlightRating = Math.Sqrt(skills[5].DifficultyValue()) * DIFFICULTY_MULTIPLIER; + baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating); + } + if (mods.Any(m => m is OsuModTouchDevice)) { aimRating = Math.Pow(aimRating, 0.8); flashlightRating = Math.Pow(flashlightRating, 0.8); + readingLowARRating = Math.Pow(readingLowARRating, 0.9); } if (mods.Any(h => h is OsuModRelax)) @@ -62,32 +67,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimRating *= 0.9; speedRating = 0.0; flashlightRating *= 0.7; + readingLowARRating *= 0.95; } double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating); double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating); // Cognition - int flIndex = 6; - - double baseReadingHiddenPerformance = 0; - if (mods.Any(h => h is OsuModHidden)) - { - hiddenRating = Math.Sqrt(skills[6].DifficultyValue()) * DIFFICULTY_MULTIPLIER; - baseReadingHiddenPerformance = ReadingHidden.DifficultyToPerformance(hiddenRating); - flIndex++; - } - - double baseFlashlightPerformance = 0.0; - if (mods.Any(h => h is OsuModFlashlight)) - { - flashlightRating = Math.Sqrt(skills[flIndex].DifficultyValue()) * DIFFICULTY_MULTIPLIER; - baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating); - } - double baseReadingLowARPerformance = ReadingLowAR.DifficultyToPerformance(readingLowARRating); - double baseReadingHighARPerformance = OsuStrainSkill.DifficultyToPerformance(readingHighARRating); - double baseReadingARPerformance = Math.Pow(Math.Pow(baseReadingLowARPerformance, SUM_POWER) + Math.Pow(baseReadingHighARPerformance, SUM_POWER), 1.0 / SUM_POWER); + double baseReadingARPerformance = baseReadingLowARPerformance; double baseFlashlightARPerformance = Math.Pow(Math.Pow(baseFlashlightPerformance, FL_SUM_POWER) + Math.Pow(baseReadingARPerformance, FL_SUM_POWER), 1.0 / FL_SUM_POWER); @@ -100,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty int sliderCount = beatmap.HitObjects.Count(h => h is Slider); int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner); - double cognitionPerformance = baseFlashlightARPerformance + baseReadingHiddenPerformance; + double cognitionPerformance = baseFlashlightARPerformance; double mechanicalPerformance = Math.Pow(Math.Pow(baseAimPerformance, SUM_POWER) + Math.Pow(baseSpeedPerformance, SUM_POWER), 1.0 / SUM_POWER); // Limit cognition by full memorisation difficulty @@ -126,8 +114,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty SpeedDifficulty = speedRating, SpeedNoteCount = speedNotes, ReadingDifficultyLowAR = readingLowARRating, - ReadingDifficultyHighAR = readingHighARRating, - HiddenDifficulty = hiddenRating, FlashlightDifficulty = flashlightRating, HiddenFlashlightDifficulty = hiddenFlashlightRating, SliderFactor = sliderFactor, @@ -167,12 +153,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty new Speed(mods), new HiddenFlashlight(mods), new ReadingLowAR(mods), - new ReadingHighAR(mods), }; - if (mods.Any(h => h is OsuModHidden)) - skills.Add(new ReadingHidden(mods)); - if (mods.Any(h => h is OsuModFlashlight)) skills.Add(new Flashlight(mods)); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index fe4cea02c2..b708d35db6 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -77,23 +77,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty double potentialHiddenFlashlightValue = computeFlashlightValue(score, osuAttributes, true); double lowARValue = computeReadingLowARValue(score, osuAttributes); - double highARValue = computeReadingHighARValue(score, osuAttributes); - double readingARValue = Math.Pow(Math.Pow(lowARValue, power) + Math.Pow(highARValue, power), 1.0 / power); + double readingARValue = lowARValue; double flashlightValue = 0; if (score.Mods.Any(h => h is OsuModFlashlight)) flashlightValue = computeFlashlightValue(score, osuAttributes); - double readingHDValue = 0; - if (score.Mods.Any(h => h is OsuModHidden)) - readingHDValue = computeReadingHiddenValue(score, osuAttributes); - // Reduce AR reading bonus if FL is present double flPower = OsuDifficultyCalculator.FL_SUM_POWER; double flashlightARValue = Math.Pow(Math.Pow(flashlightValue, flPower) + Math.Pow(readingARValue, flPower), 1.0 / flPower); - double cognitionValue = flashlightARValue + readingHDValue; + double cognitionValue = flashlightARValue; cognitionValue = AdjustCognitionPerformance(cognitionValue, mechanicalValue, potentialHiddenFlashlightValue); double accuracyValue = computeAccuracyValue(score, osuAttributes); @@ -129,9 +124,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= getComboScalingFactor(attributes); + double approachRateFactor = 0.0; + if (attributes.ApproachRate > 10.33) + approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); + + if (score.Mods.Any(h => h is OsuModRelax)) + approachRateFactor = 0.0; + + aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. + if (score.Mods.Any(m => m is OsuModBlinds)) 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)) + else if (score.Mods.Any(m => m is OsuModHidden || 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 - attributes.ApproachRate); @@ -170,12 +174,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= getComboScalingFactor(attributes); + double approachRateFactor = 0.0; + if (attributes.ApproachRate > 10.33) + approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); + + speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. + if (score.Mods.Any(m => m is OsuModBlinds)) { // Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given. speedValue *= 1.12; } - else if (score.Mods.Any(m => m is OsuModTraceable)) + else if (score.Mods.Any(m => m is OsuModHidden || 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 - attributes.ApproachRate); @@ -225,6 +235,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. 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; if (score.Mods.Any(m => m is OsuModFlashlight)) accuracyValue *= 1.02; @@ -239,8 +251,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty double deltaBonus = (1 - Math.Pow(0.95, Math.Pow(ARODDelta, 4))); accuracyValue *= 1 + visualBonus * (1 + 2 * deltaBonus); - if (score.Mods.Any(h => h is OsuModHidden || h is OsuModTraceable)) - accuracyValue *= 1 + visualBonus * (1 + deltaBonus); return accuracyValue; } @@ -299,86 +309,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty return readingValue; } - - private double computeReadingHighARValue(ScoreInfo score, OsuDifficultyAttributes attributes) - { - double highARValue = OsuStrainSkill.DifficultyToPerformance(attributes.ReadingDifficultyHighAR); - - // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. - if (effectiveMissCount > 0) - highARValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount); - - highARValue *= getComboScalingFactor(attributes); - - // Approximate how much of high AR difficulty is aim - double aimPerformance = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty); - double speedPerformance = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty); - - double aimRatio = aimPerformance / (aimPerformance + speedPerformance); - - // Aim part calculation - double aimPartValue = highARValue * aimRatio; - { - // We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator. - double estimateDifficultSliders = attributes.SliderCount * 0.15; - - if (attributes.SliderCount > 0) - { - double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders); - double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + attributes.SliderFactor; - aimPartValue *= sliderNerfFactor; - } - - aimPartValue *= accuracy; - // It is important to consider accuracy difficulty when scaling with accuracy. - aimPartValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; - } - - // Speed part calculation - double speedPartValue = highARValue * (1 - aimRatio); - { - // Calculate accuracy assuming the worst case scenario - double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; - double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); - double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat)); - double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk)); - double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0); - - // Scale the speed value with accuracy and OD. - speedPartValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2); - - // Scale the speed value with # of 50s to punish doubletapping. - speedPartValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); - } - - return aimPartValue + speedPartValue; - } - - private double computeReadingHiddenValue(ScoreInfo score, OsuDifficultyAttributes attributes) - { - if (!score.Mods.Any(h => h is OsuModHidden)) - return 0.0; - - double rawReading = attributes.HiddenDifficulty; - double hiddenValue = ReadingHidden.DifficultyToPerformance(attributes.HiddenDifficulty); - - double lengthBonus = CalculateDefaultLengthBonus(totalHits); - hiddenValue *= lengthBonus; - - // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. - if (effectiveMissCount > 0) - hiddenValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); - - hiddenValue *= getComboScalingFactor(attributes); - - // Scale the reading value with accuracy _harshly_. Additional note: it would have it's own curve in Statistical Accuracy rework. - hiddenValue *= accuracy * accuracy; - // It is important to also consider accuracy difficulty when doing that. - hiddenValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; - - return hiddenValue; - } - private double calculateEffectiveMissCount(OsuDifficultyAttributes attributes) { // Guess the number of misses + slider breaks from combo diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index bf968e2145..64d12c6d9f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -98,11 +98,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing /// public double HitWindowGreat { get; private set; } - /// - /// Rhythm difficulty of the object. Saved for optimization, rhythm calculation is very expensive. - /// - public double RhythmDifficulty { get; private set; } - /// /// Density of the object for given preempt. Saved for optimization, density calculation is expensive. /// @@ -128,18 +123,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing /// public readonly double Preempt; - /// - /// Preempt of follow line for this adjusted by clockrate. - /// Will be equal to 0 if object is New Combo. - /// - public readonly double FollowLineTime; - - /// - /// Playback rate of beatmap. - /// Will be equal 1.5 on DT and 0.75 on HT. - /// - public readonly double ClockRate; - private readonly OsuHitObject? lastLastObject; private readonly OsuHitObject lastObject; @@ -152,9 +135,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing StackedPosition = currObject.StackedPosition; Preempt = BaseObject.TimePreempt / clockRate; - FollowLineTime = 800 / clockRate; // 800ms is follow line appear time - FollowLineTime *= (currObject.NewCombo ? 0 : 1); // no follow lines when NC - ClockRate = clockRate; // Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects. StrainTime = Math.Max(DeltaTime, min_delta_time); @@ -174,7 +154,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing (ReadingObjects, OverlapValues) = getReadingObjects(); - RhythmDifficulty = RhythmEvaluator.EvaluateDifficultyOf(this); Density = ReadingEvaluator.EvaluateDensityOf(this); } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 6929095f21..3f6b22bbb1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -21,17 +21,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private readonly bool withSliders; - protected double CurrentStrain; - protected double SkillMultiplier => 23.55; + private double currentStrain; - protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => CurrentStrain * StrainDecay(time - current.Previous(0).StartTime); + private double skillMultiplier => 23.55; + private double strainDecayBase => 0.15; + + private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); + + protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime); protected override double StrainValueAt(DifficultyHitObject current) { - CurrentStrain *= StrainDecay(current.DeltaTime); - CurrentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * SkillMultiplier; + currentStrain *= strainDecay(current.DeltaTime); + currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier; - return CurrentStrain; + return currentStrain; } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 9d2719449a..9fafeacb9c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills hasHiddenMod = mods.Any(m => m is OsuModHidden); } - private double skillMultiplier => 0.05; + private double skillMultiplier => 0.052; private double strainDecayBase => 0.15; private double currentStrain; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 103ca5571f..93c21d33ad 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -34,10 +34,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// protected virtual double DifficultyMultiplier => DEFAULT_DIFFICULTY_MULTIPLIER; - protected virtual double StrainDecayBase => 0.15; - - protected double StrainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000); - protected OsuStrainSkill(Mod[] mods) : base(mods) { @@ -76,10 +72,5 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// Converts difficulty value from to base performance. /// public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0; - - /// - /// Converts base performance to difficulty value.s - /// - public static double PerformanceToDifficulty(double performance) => (Math.Pow(100000.0 * performance, 1.0 / 3.0) + 4.0) / 5.0 * 0.0675; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs index ac3e9e64e9..4835d5d817 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs @@ -9,13 +9,11 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Evaluators; -using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; -using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Difficulty.Skills { - public class ReadingLowAR : GraphSkill + public class ReadingLowAR : Skill { private readonly List difficulties = new List(); private double skillMultiplier => 1.26; @@ -42,18 +40,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills double totalDensityDifficulty = (currentDensityAimStrain + densityReadingDifficulty) * skillMultiplier; difficulties.Add(totalDensityDifficulty); - - if (current.Index == 0) - CurrentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength; - - while (current.StartTime > CurrentSectionEnd) - { - StrainPeaks.Add(CurrentSectionPeak); - CurrentSectionPeak = 0; - CurrentSectionEnd += SectionLength; - } - - CurrentSectionPeak = Math.Max(totalDensityDifficulty, CurrentSectionPeak); } private double reducedNoteCount => 5; @@ -89,145 +75,4 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills Math.Max(Math.Pow(difficulty, 1.5) * 20, Math.Pow(difficulty, 2) * 17.0), Math.Max(Math.Pow(difficulty, 3) * 10.5, Math.Pow(difficulty, 4) * 6.00)); } - - public class ReadingHidden : Aim - { - public ReadingHidden(Mod[] mods) - : base(mods, false) - { - } - protected new double SkillMultiplier => 7.2; - - protected override double StrainValueAt(DifficultyHitObject current) - { - CurrentStrain *= StrainDecay(current.DeltaTime); - - // We're not using slider aim because we assuming that HD doesn't makes sliders harder (what is not true, but we will ignore this for now) - double hiddenDifficulty = AimEvaluator.EvaluateDifficultyOf(current, false); - hiddenDifficulty *= ReadingHiddenEvaluator.EvaluateDifficultyOf(current); - hiddenDifficulty *= SkillMultiplier; - - CurrentStrain += hiddenDifficulty; - - return CurrentStrain; - } - - public new static double DifficultyToPerformance(double difficulty) => Math.Max( - Math.Max(difficulty * 16, Math.Pow(difficulty, 2) * 10), Math.Pow(difficulty, 3) * 4); - } - - public class ReadingHighAR : GraphSkill - { - - private const double component_multiplier = 0.135; - private const double component_default_value_multiplier = 60; - public ReadingHighAR(Mod[] mods) - : base(mods) - { - aimComponent = new HighARAimComponent(mods); - speedComponent = new HighARSpeedComponent(mods); - } - - private HighARAimComponent aimComponent; - private HighARSpeedComponent speedComponent; - - private readonly List difficulties = new List(); - private int objectsCount = 0; - - public override void Process(DifficultyHitObject current) - { - aimComponent.Process(current); - speedComponent.Process(current); - - if (current.BaseObject is not Spinner) - objectsCount++; - - double power = OsuDifficultyCalculator.SUM_POWER; - double mergedDifficulty = Math.Pow( - Math.Pow(aimComponent.CurrentSectionPeak, power) + - Math.Pow(speedComponent.CurrentSectionPeak, power), 1.0 / power); - - difficulties.Add(mergedDifficulty); - - if (current.Index == 0) - CurrentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength; - - while (current.StartTime > CurrentSectionEnd) - { - StrainPeaks.Add(CurrentSectionPeak); - CurrentSectionPeak = 0; - CurrentSectionEnd += SectionLength; - } - - CurrentSectionPeak = Math.Max(mergedDifficulty, CurrentSectionPeak); - } - public override double DifficultyValue() - { - // Simulating summing to get the most correct value possible - double aimValue = Math.Sqrt(aimComponent.DifficultyValue()) * OsuDifficultyCalculator.DIFFICULTY_MULTIPLIER; - double speedValue = Math.Sqrt(speedComponent.DifficultyValue()) * OsuDifficultyCalculator.DIFFICULTY_MULTIPLIER; - - double aimPerformance = OsuStrainSkill.DifficultyToPerformance(aimValue); - double speedPerformance = OsuStrainSkill.DifficultyToPerformance(speedValue); - - double power = OsuDifficultyCalculator.SUM_POWER; - double totalPerformance = Math.Pow(Math.Pow(aimPerformance, power) + Math.Pow(speedPerformance, power), 1.0 / power); - - // Length bonus is in SR to not inflate Star Rating short AR11 maps - double lengthBonus = OsuPerformanceCalculator.CalculateDefaultLengthBonus(objectsCount); - totalPerformance *= Math.Pow(lengthBonus, 4); // make it bypass sqrt - - double adjustedDifficulty = OsuStrainSkill.PerformanceToDifficulty(totalPerformance); - double difficultyValue = Math.Pow(adjustedDifficulty / OsuDifficultyCalculator.DIFFICULTY_MULTIPLIER, 2.0); - - return 53.2 * Math.Sqrt(difficultyValue); - } - - public class HighARAimComponent : Aim - { - public HighARAimComponent(Mod[] mods) - : base(mods, true) - { - } - - protected new double SkillMultiplier => base.SkillMultiplier * component_multiplier; - - protected override double StrainValueAt(DifficultyHitObject current) - { - CurrentStrain *= StrainDecay(current.DeltaTime); - - double aimDifficulty = AimEvaluator.EvaluateDifficultyOf(current, true) * SkillMultiplier; - aimDifficulty *= ReadingHighAREvaluator.EvaluateDifficultyOf(current, true); - - CurrentStrain += aimDifficulty; - - return CurrentStrain + component_default_value_multiplier * ReadingHighAREvaluator.EvaluateDifficultyOf(current, true); - } - } - - public class HighARSpeedComponent : Speed - { - public HighARSpeedComponent(Mod[] mods) - : base(mods) - { - } - - protected new double SkillMultiplier => base.SkillMultiplier * component_multiplier; - - protected override double StrainValueAt(DifficultyHitObject current) - { - OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current; - - CurrentStrain *= StrainDecay(currObj.StrainTime); - - double speedDifficulty = SpeedEvaluator.EvaluateDifficultyOf(current) * SkillMultiplier; - speedDifficulty *= ReadingHighAREvaluator.EvaluateDifficultyOf(current); - CurrentStrain += speedDifficulty; - - CurrentRhythm = currObj.RhythmDifficulty; - double totalStrain = CurrentStrain * CurrentRhythm; - return totalStrain + component_default_value_multiplier * ReadingHighAREvaluator.EvaluateDifficultyOf(current); - } - } - } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index c360d21bb7..40aac013ab 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -16,11 +16,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - protected double SkillMultiplier => 1375; - protected override double StrainDecayBase => 0.3; + private double skillMultiplier => 1375; + private double strainDecayBase => 0.3; - protected double CurrentStrain; - protected double CurrentRhythm; + private double currentStrain; + private double currentRhythm; protected override int ReducedSectionCount => 5; protected override double DifficultyMultiplier => 1.04; @@ -32,17 +32,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { } - protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => (CurrentStrain * CurrentRhythm) * StrainDecay(time - current.Previous(0).StartTime); + private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); + + protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => (currentStrain * currentRhythm) * strainDecay(time - current.Previous(0).StartTime); protected override double StrainValueAt(DifficultyHitObject current) { - OsuDifficultyHitObject currODHO = (OsuDifficultyHitObject)current; + currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime); + currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier; - CurrentStrain *= StrainDecay(currODHO.StrainTime); - CurrentStrain += SpeedEvaluator.EvaluateDifficultyOf(current) * SkillMultiplier; + currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current); - CurrentRhythm = currODHO.RhythmDifficulty; - double totalStrain = CurrentStrain * CurrentRhythm; + double totalStrain = currentStrain * currentRhythm; objectStrains.Add(totalStrain); diff --git a/osu.Game/Rulesets/Difficulty/Skills/GraphSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/GraphSkill.cs deleted file mode 100644 index 953abd61fe..0000000000 --- a/osu.Game/Rulesets/Difficulty/Skills/GraphSkill.cs +++ /dev/null @@ -1,39 +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.Collections.Generic; -using System.Linq; -using osu.Game.Rulesets.Mods; - -namespace osu.Game.Rulesets.Difficulty.Skills -{ - /// - /// A abstract skill with available per objet difficulty. - /// - /// - /// This class should be considered a "processing" class and not persisted. - /// - public abstract class GraphSkill : Skill - { - protected GraphSkill(Mod[] mods) - : base(mods) - { - } - - /// - /// The length of each section. - /// - protected virtual int SectionLength => 400; - - public double CurrentSectionPeak { get; protected set; } // We also keep track of the peak level in the current section. - - protected double CurrentSectionEnd; - - protected readonly List StrainPeaks = new List(); - - /// - /// Returns a live enumerable of the difficulties - /// - public virtual IEnumerable GetCurrentStrainPeaks() => StrainPeaks.Append(CurrentSectionPeak); - } -} diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index ace40b07b2..b07e8399c0 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -2,6 +2,7 @@ // 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.Mods; @@ -12,13 +13,24 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// Used to processes strain values of s, keep track of strain levels caused by the processed objects /// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects. /// - public abstract class StrainSkill : GraphSkill + public abstract class StrainSkill : Skill { /// /// The weight by which each strain value decays. /// protected virtual double DecayWeight => 0.9; + /// + /// The length of each strain section. + /// + protected virtual int SectionLength => 400; + + private double currentSectionPeak; // We also keep track of the peak strain level in the current section. + + private double currentSectionEnd; + + private readonly List strainPeaks = new List(); + protected StrainSkill(Mod[] mods) : base(mods) { @@ -36,16 +48,16 @@ namespace osu.Game.Rulesets.Difficulty.Skills { // The first object doesn't generate a strain, so we begin with an incremented section end if (current.Index == 0) - CurrentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength; + currentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength; - while (current.StartTime > CurrentSectionEnd) + while (current.StartTime > currentSectionEnd) { saveCurrentPeak(); - startNewSectionFrom(CurrentSectionEnd, current); - CurrentSectionEnd += SectionLength; + startNewSectionFrom(currentSectionEnd, current); + currentSectionEnd += SectionLength; } - CurrentSectionPeak = Math.Max(StrainValueAt(current), CurrentSectionPeak); + currentSectionPeak = Math.Max(StrainValueAt(current), currentSectionPeak); } /// @@ -53,7 +65,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// private void saveCurrentPeak() { - StrainPeaks.Add(CurrentSectionPeak); + strainPeaks.Add(currentSectionPeak); } /// @@ -65,7 +77,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills { // The maximum strain of the new section is not zero by default // This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level. - CurrentSectionPeak = CalculateInitialStrain(time, current); + currentSectionPeak = CalculateInitialStrain(time, current); } /// @@ -76,6 +88,12 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// The peak strain. protected abstract double CalculateInitialStrain(double time, DifficultyHitObject current); + /// + /// Returns a live enumerable of the peak strains for each section of the beatmap, + /// including the peak of the current section. + /// + public IEnumerable GetCurrentStrainPeaks() => strainPeaks.Append(currentSectionPeak); + /// /// Returns the calculated difficulty value representing all s that have been processed up to this point. ///