2019-01-24 16:43:03 +08:00
|
|
|
// 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;
|
2018-05-15 16:38:04 +08:00
|
|
|
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
|
|
|
|
2018-05-17 15:58:22 +08:00
|
|
|
namespace osu.Game.Rulesets.Osu.Difficulty
|
2018-04-13 17:19:50 +08:00
|
|
|
{
|
2018-04-19 21:04:12 +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;
|
2018-05-16 11:46:31 +08:00
|
|
|
private int countGreat;
|
2020-09-29 16:16:55 +08:00
|
|
|
private int countOk;
|
2018-05-16 11:46:31 +08:00
|
|
|
private int countMeh;
|
2018-04-13 17:19:50 +08:00
|
|
|
private int countMiss;
|
|
|
|
|
2020-10-03 01:24:30 +08:00
|
|
|
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;
|
2021-07-19 03:52:16 +08:00
|
|
|
countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
|
|
|
|
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
|
|
|
|
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
|
|
|
|
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
|
|
// Custom multipliers for NoFail and SpunOut.
|
2019-11-26 18:34:23 +08:00
|
|
|
double multiplier = 1.12; // 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
|
|
|
|
|
|
|
if (mods.Any(m => m is OsuModNoFail))
|
2020-12-10 16:10:29 +08:00
|
|
|
multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss);
|
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
|
|
|
|
|
|
|
double aimValue = computeAimValue();
|
|
|
|
double speedValue = computeSpeedValue();
|
|
|
|
double accuracyValue = computeAccuracyValue();
|
|
|
|
double totalValue =
|
|
|
|
Math.Pow(
|
2019-11-26 18:34:23 +08:00
|
|
|
Math.Pow(aimValue, 1.1) +
|
|
|
|
Math.Pow(speedValue, 1.1) +
|
|
|
|
Math.Pow(accuracyValue, 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);
|
2018-06-14 15:27:05 +08:00
|
|
|
categoryRatings.Add("OD", Attributes.OverallDifficulty);
|
|
|
|
categoryRatings.Add("AR", Attributes.ApproachRate);
|
2020-10-03 01:34:41 +08:00
|
|
|
categoryRatings.Add("Max Combo", Attributes.MaxCombo);
|
2018-04-13 17:19:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return totalValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
private double computeAimValue()
|
|
|
|
{
|
2019-01-31 10:51:40 +08:00
|
|
|
double rawAim = Attributes.AimStrain;
|
|
|
|
|
|
|
|
if (mods.Any(m => m is OsuModTouchDevice))
|
|
|
|
rawAim = Math.Pow(rawAim, 0.8);
|
|
|
|
|
2019-11-26 18:34:23 +08:00
|
|
|
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
|
|
|
|
|
|
|
// Longer maps are worth more
|
2019-11-26 18:34:23 +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);
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
|
|
aimValue *= lengthBonus;
|
|
|
|
|
2020-12-11 08:51:54 +08:00
|
|
|
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
2020-12-11 04:21:06 +08:00
|
|
|
if (countMiss > 0)
|
2020-12-11 22:20:56 +08:00
|
|
|
aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss);
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
|
|
// Combo scaling
|
2020-10-03 01:34:41 +08:00
|
|
|
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;
|
2019-11-26 18:34:23 +08:00
|
|
|
if (Attributes.ApproachRate > 10.33)
|
2021-07-08 16:54:58 +08:00
|
|
|
approachRateFactor = Attributes.ApproachRate - 10.33;
|
2019-11-26 18:34:23 +08:00
|
|
|
else if (Attributes.ApproachRate < 8.0)
|
2021-07-08 16:54:58 +08:00
|
|
|
approachRateFactor = 0.025 * (8.0 - Attributes.ApproachRate);
|
2018-04-13 17:19:50 +08:00
|
|
|
|
2021-07-09 05:06:05 +08:00
|
|
|
double approachRateTotalHitsFactor = 1.0 / (1.0 + Math.Exp(-(0.007 * (totalHits - 400))));
|
2018-04-13 17:19:50 +08:00
|
|
|
|
2021-07-21 01:39:41 +08:00
|
|
|
double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
2018-05-15 10:53:11 +08:00
|
|
|
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
2018-04-13 17:19:50 +08:00
|
|
|
if (mods.Any(h => h is OsuModHidden))
|
2019-11-26 18:34:23 +08:00
|
|
|
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
|
2018-04-13 17:19:50 +08:00
|
|
|
|
2021-06-13 21:18:35 +08:00
|
|
|
double flashlightBonus = 1.0;
|
2021-06-16 09:34:46 +08:00
|
|
|
|
2018-04-13 17:19:50 +08:00
|
|
|
if (mods.Any(h => h is OsuModFlashlight))
|
|
|
|
{
|
2018-12-24 13:14:28 +08:00
|
|
|
// Apply object-based bonus for flashlight.
|
2021-06-13 21:18:35 +08:00
|
|
|
flashlightBonus = 1.0 + 0.35 * Math.Min(1.0, totalHits / 200.0) +
|
2021-06-16 09:34:46 +08:00
|
|
|
(totalHits > 200
|
|
|
|
? 0.3 * Math.Min(1.0, (totalHits - 200) / 300.0) +
|
|
|
|
(totalHits > 500 ? (totalHits - 500) / 1200.0 : 0.0)
|
|
|
|
: 0.0);
|
2018-04-13 17:19:50 +08:00
|
|
|
}
|
|
|
|
|
2021-07-21 01:39:41 +08:00
|
|
|
aimValue *= Math.Max(flashlightBonus, approachRateBonus);
|
2021-06-13 21:18:35 +08:00
|
|
|
|
2018-04-13 17:19:50 +08:00
|
|
|
// Scale the aim value with accuracy _slightly_
|
2019-11-26 18:34:23 +08:00
|
|
|
aimValue *= 0.5 + accuracy / 2.0;
|
2018-04-13 17:19:50 +08:00
|
|
|
// It is important to also consider accuracy difficulty when doing that
|
2019-11-26 18:34:23 +08:00
|
|
|
aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
|
|
return aimValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
private double computeSpeedValue()
|
|
|
|
{
|
2019-11-26 18:34:23 +08:00
|
|
|
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
|
|
|
|
|
|
|
// 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
|
|
|
|
2020-12-11 08:51:54 +08:00
|
|
|
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
2020-12-11 04:21:06 +08:00
|
|
|
if (countMiss > 0)
|
2020-12-12 00:48:53 +08:00
|
|
|
speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
|
|
// Combo scaling
|
2020-10-03 01:34:41 +08:00
|
|
|
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;
|
2019-11-26 18:34:23 +08:00
|
|
|
if (Attributes.ApproachRate > 10.33)
|
2021-07-08 16:52:43 +08:00
|
|
|
approachRateFactor = Attributes.ApproachRate - 10.33;
|
2019-01-09 16:42:48 +08:00
|
|
|
|
2021-07-09 05:06:05 +08:00
|
|
|
double approachRateTotalHitsFactor = 1.0 / (1.0 + Math.Exp(-(0.007 * (totalHits - 400))));
|
2019-01-09 16:42:48 +08:00
|
|
|
|
2021-07-09 05:06:05 +08:00
|
|
|
speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
2018-05-12 14:46:25 +08:00
|
|
|
if (mods.Any(m => m is OsuModHidden))
|
2019-11-26 18:34:23 +08:00
|
|
|
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
|
2018-05-12 14:46:25 +08:00
|
|
|
|
2020-12-10 01:57:01 +08:00
|
|
|
// Scale the speed value with accuracy and OD
|
2020-12-15 12:18:41 +08:00
|
|
|
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2);
|
2021-08-19 23:27:37 +08:00
|
|
|
// Punish high speed values with low OD to prevent OD abuse on rhythmically complex songs.
|
|
|
|
if (speedValue > 100 && Attributes.OverallDifficulty < 8)
|
|
|
|
speedValue = 100 + (speedValue - 100) * Math.Max(0.5, (Attributes.OverallDifficulty - 4) / 4);
|
2020-12-10 10:07:52 +08:00
|
|
|
// Scale the speed value with # of 50s to punish doubletapping.
|
2020-12-15 02:41:24 +08:00
|
|
|
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()
|
|
|
|
{
|
2020-10-03 22:52:33 +08:00
|
|
|
int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
2021-08-19 22:49:44 +08:00
|
|
|
if (amountHitObjectsWithAccuracy == 0)
|
|
|
|
return 0;
|
|
|
|
|
2021-08-19 22:12:03 +08:00
|
|
|
// This section should be documented by Tr3, but effectively we're calculating the exact same way as before, but
|
|
|
|
// we calculate a variance based on the object count and # of 50s, 100s, etc. This prevents us from having cases
|
|
|
|
// where an SS on lower OD is actually worth more than a 95% on OD11, even though the OD11 requires a greater
|
|
|
|
// window of precision.
|
2018-04-13 17:19:50 +08:00
|
|
|
|
2021-08-19 22:12:03 +08:00
|
|
|
double p100 = (2 * (double)countOk) / amountHitObjectsWithAccuracy; // this is multiplied by two to encourage better accuracy. (scales better)
|
|
|
|
double p50 = (1 * (double)countMeh) / amountHitObjectsWithAccuracy;
|
|
|
|
double pm = (1 * (double)countMiss) / amountHitObjectsWithAccuracy;
|
|
|
|
double p300 = 1.0 - pm - p100 - p50;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
2021-08-19 22:12:03 +08:00
|
|
|
double m300 = 79.5 - 6.0 * Attributes.OverallDifficulty;
|
|
|
|
double m100 = 139.5 - 8.0 * Attributes.OverallDifficulty;
|
|
|
|
double m50 = 199.5 - 10.0 * Attributes.OverallDifficulty;
|
|
|
|
|
|
|
|
double variance = p300 * Math.Pow(m300 / 2.0, 2.0) +
|
2021-08-20 04:11:18 +08:00
|
|
|
p100 * Math.Pow((m300 + m100) / 2.0, 2.0) +
|
|
|
|
p50 * Math.Pow((m100 + m50) / 2.0, 2.0) +
|
|
|
|
pm * Math.Pow(229.5 - 11 * Attributes.OverallDifficulty, 2.0);
|
2021-08-19 22:12:03 +08:00
|
|
|
|
|
|
|
double accuracyValue = 2.83 * Math.Pow(1.52163, (79.5 - 2 * Math.Sqrt(variance)) / 6.0)
|
2021-08-20 04:11:18 +08:00
|
|
|
* Math.Pow(Math.Log(1.0 + (Math.E - 1.0) * (Math.Min(amountHitObjectsWithAccuracy, 1600) / 1000.0)), 0.5);
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
|
|
|
|
|
|
if (mods.Any(m => m is OsuModHidden))
|
2019-11-26 18:34:23 +08:00
|
|
|
accuracyValue *= 1.08;
|
2018-04-13 17:19:50 +08:00
|
|
|
if (mods.Any(m => m is OsuModFlashlight))
|
2019-11-26 18:34:23 +08:00
|
|
|
accuracyValue *= 1.02;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
|
|
return accuracyValue;
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|