1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 19:27:26 +08:00
osu-lazer/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs

272 lines
12 KiB
C#
Raw Normal View History

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
2018-11-28 15:12:57 +08:00
using osu.Game.Scoring;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Rulesets.Osu.Difficulty
2018-04-13 17:19:50 +08:00
{
public class OsuPerformanceCalculator : PerformanceCalculator
2018-04-13 17:19:50 +08:00
{
2018-06-14 15:04:48 +08:00
public new OsuDifficultyAttributes Attributes => (OsuDifficultyAttributes)base.Attributes;
2018-04-13 17:19:50 +08:00
private Mod[] mods;
2018-05-14 10:52:22 +08:00
2018-04-13 17:19:50 +08:00
private double accuracy;
private int scoreMaxCombo;
private int countGreat;
2020-09-29 16:16:55 +08:00
private int countOk;
private int countMeh;
2018-04-13 17:19:50 +08:00
private int countMiss;
2021-10-22 02:37:06 +08:00
private int effectiveMissCount;
public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
: base(ruleset, attributes, score)
2018-04-13 17:19:50 +08:00
{
}
public override double Calculate(Dictionary<string, double> categoryRatings = null)
{
2018-11-30 14:18:52 +08:00
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);
2021-10-22 02:37:06 +08:00
effectiveMissCount = calculateEffectiveMissCount();
2018-04-13 17:19:50 +08:00
double multiplier = 1.08; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
2018-04-13 17:19:50 +08:00
2021-10-22 02:37:06 +08:00
// Custom multipliers for NoFail and SpunOut.
2018-04-13 17:19:50 +08:00
if (mods.Any(m => m is OsuModNoFail))
2021-10-22 02:37:06 +08:00
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
2018-04-13 17:19:50 +08:00
if (mods.Any(m => m is OsuModSpunOut))
2020-12-08 21:09:48 +08:00
multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85);
2018-04-13 17:19:50 +08:00
2021-10-22 02:37:06 +08:00
if (mods.Any(h => h is OsuModRelax))
{
effectiveMissCount += countOk + countMeh;
multiplier *= 0.6;
}
2018-04-13 17:19:50 +08:00
double aimValue = computeAimValue();
double speedValue = computeSpeedValue();
double accuracyValue = computeAccuracyValue();
2021-08-08 21:56:03 +08:00
double flashlightValue = computeFlashlightValue();
2018-04-13 17:19:50 +08:00
double totalValue =
Math.Pow(
Math.Pow(aimValue, 1.1) +
Math.Pow(speedValue, 1.1) +
2021-08-09 06:31:28 +08:00
Math.Pow(accuracyValue, 1.1) +
2021-08-08 21:56:03 +08:00
Math.Pow(flashlightValue, 1.1), 1.0 / 1.1
2018-04-13 17:19:50 +08:00
) * multiplier;
if (categoryRatings != null)
{
categoryRatings.Add("Aim", aimValue);
categoryRatings.Add("Speed", speedValue);
categoryRatings.Add("Accuracy", accuracyValue);
2021-08-08 21:56:03 +08:00
categoryRatings.Add("Flashlight", flashlightValue);
categoryRatings.Add("OD", Attributes.OverallDifficulty);
categoryRatings.Add("AR", Attributes.ApproachRate);
categoryRatings.Add("Max Combo", Attributes.MaxCombo);
2018-04-13 17:19:50 +08:00
}
return totalValue;
}
private double computeAimValue()
{
double rawAim = Attributes.AimStrain;
if (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;
2018-04-13 17:19:50 +08:00
2021-08-12 07:54:25 +08:00
// Longer maps are worth more.
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
2018-04-13 17:19:50 +08:00
aimValue *= lengthBonus;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
2021-10-22 02:37:06 +08:00
if (effectiveMissCount > 0)
aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), effectiveMissCount);
2018-04-13 17:19:50 +08:00
2021-08-12 07:54:25 +08:00
// Combo scaling.
if (Attributes.MaxCombo > 0)
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
2018-04-13 17:19:50 +08:00
2020-12-07 08:06:36 +08:00
double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33)
2021-10-22 01:18:24 +08:00
approachRateFactor = 0.3 * (Attributes.ApproachRate - 10.33);
else if (Attributes.ApproachRate < 8.0)
2021-10-22 01:18:24 +08:00
approachRateFactor = 0.1 * (8.0 - Attributes.ApproachRate);
2018-04-13 17:19:50 +08:00
2021-10-22 01:18:24 +08:00
aimValue *= 1.0 + approachRateFactor;
2018-04-13 17:19:50 +08:00
2021-10-22 02:37:06 +08:00
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))
{
// 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);
2021-10-22 02:37:06 +08:00
}
2018-04-13 17:19:50 +08:00
2021-10-22 01:18:24 +08:00
// Scale the aim value with accuracy
aimValue *= accuracy;
2021-08-12 07:54:25 +08:00
// It is important to also consider accuracy difficulty when doing that.
aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
2018-04-13 17:19:50 +08:00
return aimValue;
}
private double computeSpeedValue()
{
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedStrain / 0.0675) - 4.0, 3.0) / 100000.0;
2018-04-13 17:19:50 +08:00
2021-08-12 07:54:25 +08:00
// Longer maps are worth more.
2020-12-07 08:06:36 +08:00
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
speedValue *= lengthBonus;
2018-04-13 17:19:50 +08:00
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
2021-10-22 02:37:06 +08:00
if (effectiveMissCount > 0)
speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
2018-04-13 17:19:50 +08:00
2021-08-12 07:54:25 +08:00
// Combo scaling.
if (Attributes.MaxCombo > 0)
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
2019-01-09 16:47:39 +08:00
2020-12-07 08:06:36 +08:00
double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33)
2021-10-22 01:18:24 +08:00
approachRateFactor = 0.3 * (Attributes.ApproachRate - 10.33);
2021-10-22 01:18:24 +08:00
speedValue *= 1.0 + approachRateFactor;
2018-04-13 17:19:50 +08:00
2021-10-22 02:37:06 +08:00
if (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))
{
// 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);
2021-10-22 02:37:06 +08:00
}
2021-08-12 07:54:25 +08:00
// 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);
2021-10-22 02:37:06 +08:00
2020-12-10 10:07:52 +08:00
// Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
2018-04-13 17:19:50 +08:00
return speedValue;
}
private double computeAccuracyValue()
{
2021-10-22 02:37:06 +08:00
if (mods.Any(h => h is OsuModRelax))
return 0.0;
2021-08-12 07:54:25 +08:00
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
2018-04-13 17:19:50 +08:00
double betterAccuracyPercentage;
int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;
2018-04-13 17:19:50 +08:00
if (amountHitObjectsWithAccuracy > 0)
2020-09-29 16:16:55 +08:00
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
2018-04-13 17:19:50 +08:00
else
betterAccuracyPercentage = 0;
2021-08-12 07:54:25 +08:00
// It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points.
2018-04-13 17:19:50 +08:00
if (betterAccuracyPercentage < 0)
betterAccuracyPercentage = 0;
// Lots of arbitrary values from testing.
2021-08-12 07:54:25 +08:00
// 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;
2018-04-13 17:19:50 +08:00
2021-08-12 07:54:25 +08:00
// 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));
2018-04-13 17:19:50 +08:00
2021-10-22 02:37:06 +08:00
// 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))
accuracyValue *= 1.14;
else if (mods.Any(m => m is OsuModHidden))
accuracyValue *= 1.08;
2021-10-22 02:37:06 +08:00
2018-04-13 17:19:50 +08:00
if (mods.Any(m => m is OsuModFlashlight))
accuracyValue *= 1.02;
2018-04-13 17:19:50 +08:00
return accuracyValue;
}
2021-08-08 21:56:03 +08:00
private double computeFlashlightValue()
{
2021-09-13 15:39:05 +08:00
if (!mods.Any(h => h is OsuModFlashlight))
return 0.0;
2021-08-08 21:56:03 +08:00
double rawFlashlight = Attributes.FlashlightRating;
2021-08-11 04:14:38 +08:00
2021-09-13 15:39:05 +08:00
if (mods.Any(m => m is OsuModTouchDevice))
rawFlashlight = Math.Pow(rawFlashlight, 0.8);
2021-08-11 04:14:38 +08:00
2021-09-13 15:39:05 +08:00
double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0;
2021-08-09 06:31:28 +08:00
2021-09-13 15:39:05 +08:00
// Add an additional bonus for HDFL.
if (mods.Any(h => h is OsuModHidden))
flashlightValue *= 1.3;
2021-08-08 21:56:03 +08:00
2021-09-13 15:39:05 +08:00
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
2021-10-22 02:37:06 +08:00
if (effectiveMissCount > 0)
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
2021-08-08 21:56:03 +08:00
2021-09-13 15:39:05 +08:00
// Combo scaling.
if (Attributes.MaxCombo > 0)
flashlightValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
2021-08-08 21:56:03 +08:00
2021-09-13 15:39:05 +08:00
// 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) +
(totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0);
2021-08-08 21:56:03 +08:00
2021-09-13 15:39:05 +08:00
// 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;
2021-08-08 21:56:03 +08:00
return flashlightValue;
}
2021-10-22 02:37:06 +08:00
private int calculateEffectiveMissCount()
{
// guess the number of misses + slider breaks from combo
double comboBasedMissCount = 0.0;
if (Attributes.SliderCount > 0)
{
double fullComboThreshold = Attributes.MaxCombo - 0.1 * Attributes.SliderCount;
if (scoreMaxCombo < fullComboThreshold)
comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
}
// we're clamping misscount because since its derived from combo it can be higher than total hits and that breaks some calculations
comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits);
return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount));
}
2020-09-29 16:16:55 +08:00
private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
2018-04-13 17:19:50 +08:00
}
}