diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 70d11c42e5..80b9436b2c 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -19,7 +19,6 @@ using osu.Game.Rulesets.Catch.Difficulty; using osu.Game.Rulesets.Catch.Scoring; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; using System; using osu.Framework.Extensions.EnumExtensions; using osu.Game.Rulesets.Catch.Edit; @@ -182,7 +181,7 @@ namespace osu.Game.Rulesets.Catch public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new CatchLegacySkinTransformer(skin); - public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score); + public override PerformanceCalculator CreatePerformanceCalculator() => new CatchPerformanceCalculator(); public int LegacyID => 2; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index 8cdbe500f0..b30b85be2d 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -13,33 +13,29 @@ namespace osu.Game.Rulesets.Catch.Difficulty { public class CatchPerformanceCalculator : PerformanceCalculator { - protected new CatchDifficultyAttributes Attributes => (CatchDifficultyAttributes)base.Attributes; - - private Mod[] mods; - private int fruitsHit; private int ticksHit; private int tinyTicksHit; private int tinyTicksMissed; private int misses; - public CatchPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) - : base(ruleset, attributes, score) + public CatchPerformanceCalculator() + : base(new CatchRuleset()) { } - public override PerformanceAttributes Calculate() + protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes) { - mods = Score.Mods; + var catchAttributes = (CatchDifficultyAttributes)attributes; - fruitsHit = Score.Statistics.GetValueOrDefault(HitResult.Great); - ticksHit = Score.Statistics.GetValueOrDefault(HitResult.LargeTickHit); - tinyTicksHit = Score.Statistics.GetValueOrDefault(HitResult.SmallTickHit); - tinyTicksMissed = Score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss); - misses = Score.Statistics.GetValueOrDefault(HitResult.Miss); + fruitsHit = score.Statistics.GetValueOrDefault(HitResult.Great); + ticksHit = score.Statistics.GetValueOrDefault(HitResult.LargeTickHit); + tinyTicksHit = score.Statistics.GetValueOrDefault(HitResult.SmallTickHit); + tinyTicksMissed = score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss); + misses = score.Statistics.GetValueOrDefault(HitResult.Miss); // We are heavily relying on aim in catch the beat - double value = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0; + double value = Math.Pow(5.0 * Math.Max(1.0, catchAttributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0; // Longer maps are worth more. "Longer" means how many hits there are which can contribute to combo int numTotalHits = totalComboHits(); @@ -52,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty value *= Math.Pow(0.97, misses); // Combo scaling - if (Attributes.MaxCombo > 0) - value *= Math.Min(Math.Pow(Score.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); + if (catchAttributes.MaxCombo > 0) + value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0); - double approachRate = Attributes.ApproachRate; + double approachRate = catchAttributes.ApproachRate; double approachRateFactor = 1.0; if (approachRate > 9.0) approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9 @@ -66,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty value *= approachRateFactor; - if (mods.Any(m => m is ModHidden)) + if (score.Mods.Any(m => m is ModHidden)) { // Hiddens gives almost nothing on max approach rate, and more the lower it is if (approachRate <= 10.0) @@ -75,12 +71,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty value *= 1.01 + 0.04 * (11.0 - Math.Min(11.0, approachRate)); // 5% at AR 10, 1% at AR 11 } - if (mods.Any(m => m is ModFlashlight)) + if (score.Mods.Any(m => m is ModFlashlight)) value *= 1.35 * lengthBonus; value *= Math.Pow(accuracy(), 5.5); - if (mods.Any(m => m is ModNoFail)) + if (score.Mods.Any(m => m is ModNoFail)) value *= 0.90; return new CatchPerformanceAttributes diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 722cb55036..b347cc9ae2 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -13,10 +13,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty { public class ManiaPerformanceCalculator : PerformanceCalculator { - protected new ManiaDifficultyAttributes Attributes => (ManiaDifficultyAttributes)base.Attributes; - - private Mod[] mods; - // Score after being scaled by non-difficulty-increasing mods private double scaledScore; @@ -27,39 +23,40 @@ namespace osu.Game.Rulesets.Mania.Difficulty private int countMeh; private int countMiss; - public ManiaPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) - : base(ruleset, attributes, score) + public ManiaPerformanceCalculator() + : base(new ManiaRuleset()) { } - public override PerformanceAttributes Calculate() + protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes) { - mods = Score.Mods; - scaledScore = Score.TotalScore; - countPerfect = Score.Statistics.GetValueOrDefault(HitResult.Perfect); - countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great); - countGood = Score.Statistics.GetValueOrDefault(HitResult.Good); - countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok); - countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); - countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); + var maniaAttributes = (ManiaDifficultyAttributes)attributes; - if (Attributes.ScoreMultiplier > 0) + scaledScore = score.TotalScore; + countPerfect = score.Statistics.GetValueOrDefault(HitResult.Perfect); + countGreat = score.Statistics.GetValueOrDefault(HitResult.Great); + countGood = score.Statistics.GetValueOrDefault(HitResult.Good); + countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); + countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); + countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); + + if (maniaAttributes.ScoreMultiplier > 0) { // Scale score up, so it's comparable to other keymods - scaledScore *= 1.0 / Attributes.ScoreMultiplier; + scaledScore *= 1.0 / maniaAttributes.ScoreMultiplier; } // Arbitrary initial value for scaling pp in order to standardize distributions across game modes. // The specific number has no intrinsic meaning and can be adjusted as needed. double multiplier = 0.8; - if (mods.Any(m => m is ModNoFail)) + if (score.Mods.Any(m => m is ModNoFail)) multiplier *= 0.9; - if (mods.Any(m => m is ModEasy)) + if (score.Mods.Any(m => m is ModEasy)) multiplier *= 0.5; - double difficultyValue = computeDifficultyValue(); - double accValue = computeAccuracyValue(difficultyValue); + double difficultyValue = computeDifficultyValue(maniaAttributes); + double accValue = computeAccuracyValue(difficultyValue, maniaAttributes); double totalValue = Math.Pow( Math.Pow(difficultyValue, 1.1) + @@ -75,9 +72,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty }; } - private double computeDifficultyValue() + private double computeDifficultyValue(ManiaDifficultyAttributes attributes) { - double difficultyValue = Math.Pow(5 * Math.Max(1, Attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0; + double difficultyValue = Math.Pow(5 * Math.Max(1, attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0; difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0); @@ -97,14 +94,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty return difficultyValue; } - private double computeAccuracyValue(double difficultyValue) + private double computeAccuracyValue(double difficultyValue, ManiaDifficultyAttributes attributes) { - if (Attributes.GreatHitWindow <= 0) + if (attributes.GreatHitWindow <= 0) return 0; // Lots of arbitrary values from testing. // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution - double accuracyValue = Math.Max(0.0, 0.2 - (Attributes.GreatHitWindow - 34) * 0.006667) + double accuracyValue = Math.Max(0.0, 0.2 - (attributes.GreatHitWindow - 34) * 0.006667) * difficultyValue * Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1); diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index f139a88f50..bd6a67bf67 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this); - public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new ManiaPerformanceCalculator(this, attributes, score); + public override PerformanceCalculator CreatePerformanceCalculator() => new ManiaPerformanceCalculator(); public const string SHORT_NAME = "mania"; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 604ab73454..a93a1641a1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -14,10 +13,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public class OsuPerformanceCalculator : PerformanceCalculator { - public new OsuDifficultyAttributes Attributes => (OsuDifficultyAttributes)base.Attributes; - - private Mod[] mods; - private double accuracy; private int scoreMaxCombo; private int countGreat; @@ -27,31 +22,32 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double effectiveMissCount; - public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) - : base(ruleset, attributes, score) + public OsuPerformanceCalculator() + : base(new OsuRuleset()) { } - public override PerformanceAttributes Calculate() + protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes) { - mods = Score.Mods; - accuracy = Score.Accuracy; - scoreMaxCombo = Score.MaxCombo; - countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great); - countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok); - countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); - countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); - effectiveMissCount = calculateEffectiveMissCount(); + var osuAttributes = (OsuDifficultyAttributes)attributes; + + accuracy = score.Accuracy; + scoreMaxCombo = score.MaxCombo; + countGreat = score.Statistics.GetValueOrDefault(HitResult.Great); + countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); + countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); + countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); + effectiveMissCount = calculateEffectiveMissCount(osuAttributes); double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. - if (mods.Any(m => m is OsuModNoFail)) + if (score.Mods.Any(m => m is OsuModNoFail)) multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount); - if (mods.Any(m => m is OsuModSpunOut) && totalHits > 0) - multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); + if (score.Mods.Any(m => m is OsuModSpunOut) && totalHits > 0) + multiplier *= 1.0 - Math.Pow((double)osuAttributes.SpinnerCount / totalHits, 0.85); - if (mods.Any(h => h is OsuModRelax)) + if (score.Mods.Any(h => h is OsuModRelax)) { // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it. effectiveMissCount = Math.Min(effectiveMissCount + countOk + countMeh, totalHits); @@ -59,10 +55,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty multiplier *= 0.6; } - double aimValue = computeAimValue(); - double speedValue = computeSpeedValue(); - double accuracyValue = computeAccuracyValue(); - double flashlightValue = computeFlashlightValue(); + double aimValue = computeAimValue(score, osuAttributes); + double speedValue = computeSpeedValue(score, osuAttributes); + double accuracyValue = computeAccuracyValue(score, osuAttributes); + double flashlightValue = computeFlashlightValue(score, osuAttributes); double totalValue = Math.Pow( Math.Pow(aimValue, 1.1) + @@ -82,11 +78,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty }; } - private double computeAimValue() + private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - double rawAim = Attributes.AimDifficulty; + double rawAim = attributes.AimDifficulty; - if (mods.Any(m => m is OsuModTouchDevice)) + if (score.Mods.Any(m => m is OsuModTouchDevice)) rawAim = Math.Pow(rawAim, 0.8); double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0; @@ -99,44 +95,44 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (effectiveMissCount > 0) aimValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount); - aimValue *= getComboScalingFactor(); + aimValue *= getComboScalingFactor(attributes); double approachRateFactor = 0.0; - if (Attributes.ApproachRate > 10.33) - approachRateFactor = 0.3 * (Attributes.ApproachRate - 10.33); - else if (Attributes.ApproachRate < 8.0) - approachRateFactor = 0.1 * (8.0 - Attributes.ApproachRate); + if (attributes.ApproachRate > 10.33) + approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); + else if (attributes.ApproachRate < 8.0) + approachRateFactor = 0.1 * (8.0 - attributes.ApproachRate); aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. - if (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 (mods.Any(h => h is OsuModHidden)) + 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(h => h 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. - aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); + aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } // 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; + double estimateDifficultSliders = attributes.SliderCount * 0.15; - if (Attributes.SliderCount > 0) + 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; + 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; aimValue *= sliderNerfFactor; } aimValue *= accuracy; // It is important to consider accuracy difficulty when scaling with accuracy. - aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500; + aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; return aimValue; } - private double computeSpeedValue() + private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; + double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); @@ -146,27 +142,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (effectiveMissCount > 0) speedValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); - speedValue *= getComboScalingFactor(); + speedValue *= getComboScalingFactor(attributes); double approachRateFactor = 0.0; - if (Attributes.ApproachRate > 10.33) - approachRateFactor = 0.3 * (Attributes.ApproachRate - 10.33); + 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 (mods.Any(m => m is OsuModBlinds)) + 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 (mods.Any(m => m is OsuModHidden)) + else if (score.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. - speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); + speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } // Scale the speed value with accuracy and OD. - speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2); + speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2); // Scale the speed value with # of 50s to punish doubletapping. speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); @@ -174,14 +170,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty return speedValue; } - private double computeAccuracyValue() + private double computeAccuracyValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - if (mods.Any(h => h is OsuModRelax)) + if (score.Mods.Any(h => h is OsuModRelax)) return 0.0; // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window. double betterAccuracyPercentage; - int amountHitObjectsWithAccuracy = Attributes.HitCircleCount; + int amountHitObjectsWithAccuracy = attributes.HitCircleCount; if (amountHitObjectsWithAccuracy > 0) betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); @@ -194,43 +190,43 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Lots of arbitrary values from testing. // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution. - double accuracyValue = Math.Pow(1.52163, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83; + double accuracyValue = Math.Pow(1.52163, attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83; // Bonus for many hitcircles - it's harder to keep good accuracy up for longer. accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)); // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. - if (mods.Any(m => m is OsuModBlinds)) + if (score.Mods.Any(m => m is OsuModBlinds)) accuracyValue *= 1.14; - else if (mods.Any(m => m is OsuModHidden)) + else if (score.Mods.Any(m => m is OsuModHidden)) accuracyValue *= 1.08; - if (mods.Any(m => m is OsuModFlashlight)) + if (score.Mods.Any(m => m is OsuModFlashlight)) accuracyValue *= 1.02; return accuracyValue; } - private double computeFlashlightValue() + private double computeFlashlightValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - if (!mods.Any(h => h is OsuModFlashlight)) + if (!score.Mods.Any(h => h is OsuModFlashlight)) return 0.0; - double rawFlashlight = Attributes.FlashlightDifficulty; + double rawFlashlight = attributes.FlashlightDifficulty; - if (mods.Any(m => m is OsuModTouchDevice)) + if (score.Mods.Any(m => m is OsuModTouchDevice)) rawFlashlight = Math.Pow(rawFlashlight, 0.8); double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0; - if (mods.Any(h => h is OsuModHidden)) + if (score.Mods.Any(h => h is OsuModHidden)) flashlightValue *= 1.3; // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (effectiveMissCount > 0) flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); - flashlightValue *= getComboScalingFactor(); + flashlightValue *= getComboScalingFactor(attributes); // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius. flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) + @@ -239,19 +235,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the flashlight value with accuracy _slightly_. flashlightValue *= 0.5 + accuracy / 2.0; // It is important to also consider accuracy difficulty when doing that. - flashlightValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500; + flashlightValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; return flashlightValue; } - private double calculateEffectiveMissCount() + private double calculateEffectiveMissCount(OsuDifficultyAttributes attributes) { // Guess the number of misses + slider breaks from combo double comboBasedMissCount = 0.0; - if (Attributes.SliderCount > 0) + if (attributes.SliderCount > 0) { - double fullComboThreshold = Attributes.MaxCombo - 0.1 * Attributes.SliderCount; + double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount; if (scoreMaxCombo < fullComboThreshold) comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); } @@ -262,7 +258,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty return Math.Max(countMiss, comboBasedMissCount); } - private double getComboScalingFactor() => Attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); + 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 int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 5b936b1bf1..2fdf42fca1 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -213,7 +213,7 @@ namespace osu.Game.Rulesets.Osu public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(RulesetInfo, beatmap); - public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new OsuPerformanceCalculator(this, attributes, score); + public override PerformanceCalculator CreatePerformanceCalculator() => new OsuPerformanceCalculator(); public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index bcd55f8fae..a8122551ff 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -14,37 +14,35 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoPerformanceCalculator : PerformanceCalculator { - protected new TaikoDifficultyAttributes Attributes => (TaikoDifficultyAttributes)base.Attributes; - - private Mod[] mods; private int countGreat; private int countOk; private int countMeh; private int countMiss; - public TaikoPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) - : base(ruleset, attributes, score) + public TaikoPerformanceCalculator() + : base(new TaikoRuleset()) { } - public override PerformanceAttributes Calculate() + protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes) { - mods = Score.Mods; - countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great); - countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok); - countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); - countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); + var taikoAttributes = (TaikoDifficultyAttributes)attributes; + + countGreat = score.Statistics.GetValueOrDefault(HitResult.Great); + countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); + countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); + countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things - if (mods.Any(m => m is ModNoFail)) + if (score.Mods.Any(m => m is ModNoFail)) multiplier *= 0.90; - if (mods.Any(m => m is ModHidden)) + if (score.Mods.Any(m => m is ModHidden)) multiplier *= 1.10; - double difficultyValue = computeDifficultyValue(); - double accuracyValue = computeAccuracyValue(); + double difficultyValue = computeDifficultyValue(score, taikoAttributes); + double accuracyValue = computeAccuracyValue(score, taikoAttributes); double totalValue = Math.Pow( Math.Pow(difficultyValue, 1.1) + @@ -59,30 +57,30 @@ namespace osu.Game.Rulesets.Taiko.Difficulty }; } - private double computeDifficultyValue() + private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) { - double difficultyValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0; + double difficultyValue = Math.Pow(5.0 * Math.Max(1.0, attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0; double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); difficultyValue *= lengthBonus; difficultyValue *= Math.Pow(0.985, countMiss); - if (mods.Any(m => m is ModHidden)) + if (score.Mods.Any(m => m is ModHidden)) difficultyValue *= 1.025; - if (mods.Any(m => m is ModFlashlight)) + if (score.Mods.Any(m => m is ModFlashlight)) difficultyValue *= 1.05 * lengthBonus; - return difficultyValue * Score.Accuracy; + return difficultyValue * score.Accuracy; } - private double computeAccuracyValue() + private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) { - if (Attributes.GreatHitWindow <= 0) + if (attributes.GreatHitWindow <= 0) return 0; - double accValue = Math.Pow(150.0 / Attributes.GreatHitWindow, 1.1) * Math.Pow(Score.Accuracy, 15) * 22.0; + double accValue = Math.Pow(150.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 15) * 22.0; // Bonus for many objects - it's harder to keep good accuracy up for longer return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index dc90845d92..615fbf093f 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Taiko public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(RulesetInfo, beatmap); - public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new TaikoPerformanceCalculator(this, attributes, score); + public override PerformanceCalculator CreatePerformanceCalculator() => new TaikoPerformanceCalculator(); public int LegacyID => 1; diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index cc380df183..4eed2a25f5 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -390,7 +390,7 @@ namespace osu.Game.Tests.Visual.Ranking private class RulesetWithNoPerformanceCalculator : OsuRuleset { - public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => null; + public override PerformanceCalculator CreatePerformanceCalculator() => null; } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index fc38ed0298..1e5dda253f 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Difficulty ).ConfigureAwait(false); // ScorePerformanceCache is not used to avoid caching multiple copies of essentially identical perfect performance attributes - return difficulty == null ? null : ruleset.CreatePerformanceCalculator(difficulty.Value.Attributes, perfectPlay)?.Calculate(); + return difficulty == null ? null : ruleset.CreatePerformanceCalculator()?.Calculate(perfectPlay, difficulty.Value.Attributes); }, cancellationToken); } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs index 6358ec18b7..4c55249661 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs @@ -1,41 +1,31 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Linq; -using osu.Framework.Audio.Track; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Game.Rulesets.Mods; +using osu.Game.Beatmaps; using osu.Game.Scoring; namespace osu.Game.Rulesets.Difficulty { public abstract class PerformanceCalculator { - protected readonly DifficultyAttributes Attributes; - protected readonly Ruleset Ruleset; - protected readonly ScoreInfo Score; - protected double TimeRate { get; private set; } = 1; - - protected PerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) + protected PerformanceCalculator(Ruleset ruleset) { Ruleset = ruleset; - Score = score; - - Attributes = attributes ?? throw new ArgumentNullException(nameof(attributes)); - - ApplyMods(score.Mods); } - protected virtual void ApplyMods(Mod[] mods) - { - var track = new TrackVirtual(10000); - mods.OfType().ForEach(m => m.ApplyToTrack(track)); - TimeRate = track.Rate; - } + public PerformanceAttributes Calculate(ScoreInfo score, DifficultyAttributes attributes) + => CreatePerformanceAttributes(score, attributes); - public abstract PerformanceAttributes Calculate(); + public PerformanceAttributes Calculate(ScoreInfo score, IWorkingBeatmap beatmap) + => Calculate(score, Ruleset.CreateDifficultyCalculator(beatmap).Calculate(score.Mods)); + + /// + /// Creates to describe a score's performance. + /// + /// The score to create the attributes for. + /// The difficulty attributes for the beatmap relating to the score. + protected abstract PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes); } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 616540b59c..d5926386e8 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -228,25 +228,9 @@ namespace osu.Game.Rulesets /// /// Optionally creates a to generate performance data from the provided score. /// - /// Difficulty attributes for the beatmap related to the provided score. - /// The score to be processed. /// A performance calculator instance for the provided score. [CanBeNull] - public virtual PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => null; - - /// - /// Optionally creates a to generate performance data from the provided score. - /// - /// The beatmap to use as a source for generating . - /// The score to be processed. - /// A performance calculator instance for the provided score. - [CanBeNull] - public PerformanceCalculator CreatePerformanceCalculator(IWorkingBeatmap beatmap, ScoreInfo score) - { - var difficultyCalculator = CreateDifficultyCalculator(beatmap); - var difficultyAttributes = difficultyCalculator.Calculate(score.Mods); - return CreatePerformanceCalculator(difficultyAttributes, score); - } + public virtual PerformanceCalculator CreatePerformanceCalculator() => null; public virtual HitObjectComposer CreateHitObjectComposer() => null; diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs index a428a66aae..e15d59e648 100644 --- a/osu.Game/Scoring/ScorePerformanceCache.cs +++ b/osu.Game/Scoring/ScorePerformanceCache.cs @@ -43,9 +43,7 @@ namespace osu.Game.Scoring token.ThrowIfCancellationRequested(); - var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(attributes.Value.Attributes, score); - - return calculator?.Calculate(); + return score.Ruleset.CreateInstance().CreatePerformanceCalculator()?.Calculate(score, attributes.Value.Attributes); } public readonly struct PerformanceCacheLookup diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 21a7698248..a9b5544921 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -55,6 +55,7 @@ namespace osu.Game.Screens.Play.HUD private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource(); private JudgementResult lastJudgement; + private PerformanceCalculator performanceCalculator; public PerformancePointsCounter() { @@ -70,6 +71,8 @@ namespace osu.Game.Screens.Play.HUD if (gameplayState != null) { + performanceCalculator = gameplayState.Ruleset.CreatePerformanceCalculator(); + clonedMods = gameplayState.Mods.Select(m => m.DeepClone()).ToArray(); var gameplayWorkingBeatmap = new GameplayWorkingBeatmap(gameplayState.Beatmap); @@ -130,9 +133,7 @@ namespace osu.Game.Screens.Play.HUD var scoreInfo = gameplayState.Score.ScoreInfo.DeepClone(); scoreInfo.Mods = clonedMods; - var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(attrib, scoreInfo); - - Current.Value = (int)Math.Round(calculator?.Calculate().Total ?? 0, MidpointRounding.AwayFromZero); + Current.Value = (int)Math.Round(performanceCalculator?.Calculate(scoreInfo, attrib).Total ?? 0, MidpointRounding.AwayFromZero); IsValid = true; }