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-05-17 16:40:46 +08:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using osu.Game.Rulesets.Difficulty ;
using osu.Game.Rulesets.Mods ;
using osu.Game.Rulesets.Scoring ;
2018-11-12 01:38:12 +08:00
using osu.Game.Rulesets.Taiko.Objects ;
2018-11-28 15:12:57 +08:00
using osu.Game.Scoring ;
2024-03-10 13:37:28 +08:00
using osu.Game.Utils ;
2018-05-17 16:40:46 +08:00
namespace osu.Game.Rulesets.Taiko.Difficulty
{
public class TaikoPerformanceCalculator : PerformanceCalculator
{
private int countGreat ;
2020-09-29 16:16:55 +08:00
private int countOk ;
2018-05-17 16:40:46 +08:00
private int countMeh ;
private int countMiss ;
2024-05-29 21:40:59 +08:00
private double? estimatedUnstableRate ;
2018-05-17 16:40:46 +08:00
2022-08-19 20:57:28 +08:00
private double effectiveMissCount ;
2022-03-15 11:37:39 +08:00
public TaikoPerformanceCalculator ( )
: base ( new TaikoRuleset ( ) )
2018-05-17 16:40:46 +08:00
{
}
2022-03-14 13:25:26 +08:00
protected override PerformanceAttributes CreatePerformanceAttributes ( ScoreInfo score , DifficultyAttributes attributes )
2018-05-17 16:40:46 +08:00
{
2022-03-14 13:25:26 +08:00
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 ) ;
2024-05-29 21:40:59 +08:00
estimatedUnstableRate = computeDeviationUpperBound ( taikoAttributes ) * 10 ;
2018-05-17 16:40:46 +08:00
2022-08-19 21:15:38 +08:00
// The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000.
2022-08-25 13:02:10 +08:00
if ( totalSuccessfulHits > 0 )
effectiveMissCount = Math . Max ( 1.0 , 1000.0 / totalSuccessfulHits ) * countMiss ;
2022-08-19 20:57:28 +08:00
2024-11-02 22:47:53 +08:00
// Converts are detected and omitted from mod-specific bonuses due to the scope of current difficulty calculation.
2023-07-04 13:50:34 +08:00
bool isConvert = score . BeatmapInfo ! . Ruleset . OnlineID ! = 1 ;
2022-09-30 20:56:16 +08:00
2022-08-19 20:57:28 +08:00
double multiplier = 1.13 ;
2018-05-17 16:40:46 +08:00
2024-10-31 08:15:29 +08:00
if ( score . Mods . Any ( m = > m is ModHidden ) & & ! isConvert )
2022-07-17 12:10:49 +08:00
multiplier * = 1.075 ;
if ( score . Mods . Any ( m = > m is ModEasy ) )
2024-10-31 08:15:29 +08:00
multiplier * = 0.950 ;
2018-05-17 16:40:46 +08:00
2024-10-31 10:08:12 +08:00
double difficultyValue = computeDifficultyValue ( score , taikoAttributes ) ;
2022-11-04 15:17:49 +08:00
double accuracyValue = computeAccuracyValue ( score , taikoAttributes , isConvert ) ;
2018-05-17 16:40:46 +08:00
double totalValue =
Math . Pow (
2021-12-18 04:39:03 +08:00
Math . Pow ( difficultyValue , 1.1 ) +
2018-05-17 16:40:46 +08:00
Math . Pow ( accuracyValue , 1.1 ) , 1.0 / 1.1
) * multiplier ;
2021-12-21 18:08:31 +08:00
return new TaikoPerformanceAttributes
2018-05-17 16:40:46 +08:00
{
2021-12-21 18:08:31 +08:00
Difficulty = difficultyValue ,
Accuracy = accuracyValue ,
2022-08-19 20:57:28 +08:00
EffectiveMissCount = effectiveMissCount ,
2024-05-29 21:40:59 +08:00
EstimatedUnstableRate = estimatedUnstableRate ,
2022-11-25 08:28:47 +08:00
Total = totalValue
2021-12-21 18:08:31 +08:00
} ;
2018-05-17 16:40:46 +08:00
}
2024-10-31 10:08:12 +08:00
private double computeDifficultyValue ( ScoreInfo score , TaikoDifficultyAttributes attributes )
2018-05-17 16:40:46 +08:00
{
2022-07-17 12:10:49 +08:00
double difficultyValue = Math . Pow ( 5 * Math . Max ( 1.0 , attributes . StarRating / 0.115 ) - 4.0 , 2.25 ) / 1150.0 ;
2018-05-17 16:40:46 +08:00
2020-07-29 19:53:14 +08:00
double lengthBonus = 1 + 0.1 * Math . Min ( 1.0 , totalHits / 1500.0 ) ;
2021-12-18 04:39:03 +08:00
difficultyValue * = lengthBonus ;
2018-05-17 16:40:46 +08:00
2022-08-19 20:57:28 +08:00
difficultyValue * = Math . Pow ( 0.986 , effectiveMissCount ) ;
2022-07-17 12:10:49 +08:00
if ( score . Mods . Any ( m = > m is ModEasy ) )
2024-10-31 08:15:29 +08:00
difficultyValue * = 0.90 ;
2018-05-17 16:40:46 +08:00
2024-10-31 08:15:29 +08:00
if ( score . Mods . Any ( m = > m is ModHidden ) )
2021-12-18 04:39:03 +08:00
difficultyValue * = 1.025 ;
2018-05-17 16:40:46 +08:00
2022-08-19 20:57:28 +08:00
if ( score . Mods . Any ( m = > m is ModHardRock ) )
2022-10-27 03:58:20 +08:00
difficultyValue * = 1.10 ;
2022-08-19 20:57:28 +08:00
2022-03-14 13:25:26 +08:00
if ( score . Mods . Any ( m = > m is ModFlashlight < TaikoHitObject > ) )
2024-10-31 08:15:29 +08:00
difficultyValue * = Math . Max ( 1 , 1.050 - Math . Min ( attributes . MonoStaminaFactor / 50 , 1 ) * lengthBonus ) ;
2018-05-17 16:40:46 +08:00
2024-05-29 21:40:59 +08:00
if ( estimatedUnstableRate = = null )
2022-10-29 04:18:17 +08:00
return 0 ;
2024-10-31 10:08:12 +08:00
// Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps.
2024-10-31 06:57:47 +08:00
double accScalingExponent = 2 + attributes . MonoStaminaFactor ;
double accScalingShift = 300 - 100 * attributes . MonoStaminaFactor ;
return difficultyValue * Math . Pow ( SpecialFunctions . Erf ( accScalingShift / ( Math . Sqrt ( 2 ) * estimatedUnstableRate . Value ) ) , accScalingExponent ) ;
2018-05-17 16:40:46 +08:00
}
2022-11-04 15:17:49 +08:00
private double computeAccuracyValue ( ScoreInfo score , TaikoDifficultyAttributes attributes , bool isConvert )
2018-05-17 16:40:46 +08:00
{
2024-05-29 21:40:59 +08:00
if ( attributes . GreatHitWindow < = 0 | | estimatedUnstableRate = = null )
2018-05-17 16:40:46 +08:00
return 0 ;
2024-05-29 21:40:59 +08:00
double accuracyValue = Math . Pow ( 70 / estimatedUnstableRate . Value , 1.1 ) * Math . Pow ( attributes . StarRating , 0.4 ) * 100.0 ;
2022-07-17 12:10:49 +08:00
double lengthBonus = Math . Min ( 1.15 , Math . Pow ( totalHits / 1500.0 , 0.3 ) ) ;
2022-10-02 07:05:58 +08:00
// Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values.
2022-11-04 15:17:49 +08:00
if ( score . Mods . Any ( m = > m is ModFlashlight < TaikoHitObject > ) & & score . Mods . Any ( m = > m is ModHidden ) & & ! isConvert )
2022-10-27 03:58:20 +08:00
accuracyValue * = Math . Max ( 1.0 , 1.05 * lengthBonus ) ;
2018-05-17 16:40:46 +08:00
2022-07-17 12:10:49 +08:00
return accuracyValue ;
2018-05-17 16:40:46 +08:00
}
2022-11-28 04:24:54 +08:00
/// <summary>
2024-03-10 12:10:53 +08:00
/// Computes an upper bound on the player's tap deviation based on the OD, number of circles and sliders,
/// and the hit judgements, assuming the player's mean hit error is 0. The estimation is consistent in that
/// two SS scores on the same map with the same settings will always return the same deviation.
2022-11-28 04:24:54 +08:00
/// </summary>
2024-03-10 12:10:53 +08:00
private double? computeDeviationUpperBound ( TaikoDifficultyAttributes attributes )
2022-10-26 05:41:20 +08:00
{
2023-03-21 10:00:33 +08:00
if ( totalSuccessfulHits = = 0 | | attributes . GreatHitWindow < = 0 )
2022-10-29 04:18:17 +08:00
return null ;
2022-10-27 12:07:32 +08:00
2023-02-23 03:55:48 +08:00
double h300 = attributes . GreatHitWindow ;
2023-03-21 10:00:33 +08:00
double h100 = attributes . OkHitWindow ;
2022-11-29 09:48:24 +08:00
2024-03-10 12:10:53 +08:00
const double z = 2.32634787404 ; // 99% critical value for the normal distribution (one-tailed).
2024-03-10 13:19:04 +08:00
// The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window.
2024-03-10 12:10:53 +08:00
double? calcDeviationGreatWindow ( )
2022-10-26 05:41:20 +08:00
{
2024-03-10 12:10:53 +08:00
if ( countGreat = = 0 ) return null ;
2024-03-10 13:19:04 +08:00
double n = totalHits ;
2022-11-25 08:09:30 +08:00
2024-03-10 13:20:06 +08:00
// Proportion of greats hit.
2024-03-10 12:10:53 +08:00
double p = countGreat / n ;
2022-11-25 08:09:30 +08:00
2024-03-10 12:10:53 +08:00
// We can be 99% confident that p is at least this value.
double pLowerBound = ( n * p + z * z / 2 ) / ( n + z * z ) - z / ( n + z * z ) * Math . Sqrt ( n * p * ( 1 - p ) + z * z / 4 ) ;
2022-11-25 08:09:30 +08:00
2024-03-10 12:10:53 +08:00
// We can be 99% confident that the deviation is not higher than:
return h300 / ( Math . Sqrt ( 2 ) * SpecialFunctions . ErfInv ( pLowerBound ) ) ;
2022-10-26 05:41:20 +08:00
}
2024-03-10 13:19:04 +08:00
// The upper bound on deviation, calculated with the ratio of 300s + 100s to objects, and the good hit window.
2024-03-10 12:10:53 +08:00
// This will return a lower value than the first method when the number of 100s is high, but the miss count is low.
double? calcDeviationGoodWindow ( )
{
if ( totalSuccessfulHits = = 0 ) return null ;
2022-10-26 05:41:20 +08:00
2024-03-10 12:10:53 +08:00
double n = totalHits ;
2022-10-26 05:41:20 +08:00
2024-03-10 12:10:53 +08:00
// Proportion of greats + goods hit.
double p = totalSuccessfulHits / n ;
2022-08-19 20:57:28 +08:00
2024-03-10 12:10:53 +08:00
// We can be 99% confident that p is at least this value.
double pLowerBound = ( n * p + z * z / 2 ) / ( n + z * z ) - z / ( n + z * z ) * Math . Sqrt ( n * p * ( 1 - p ) + z * z / 4 ) ;
2022-11-25 08:09:30 +08:00
2024-03-10 12:10:53 +08:00
// We can be 99% confident that the deviation is not higher than:
return h100 / ( Math . Sqrt ( 2 ) * SpecialFunctions . ErfInv ( pLowerBound ) ) ;
}
2024-06-24 07:17:19 +08:00
double? deviationGreatWindow = calcDeviationGreatWindow ( ) ;
double? deviationGoodWindow = calcDeviationGoodWindow ( ) ;
if ( deviationGreatWindow is null )
return deviationGoodWindow ;
return Math . Min ( deviationGreatWindow . Value , deviationGoodWindow ! . Value ) ;
2024-03-10 12:10:53 +08:00
}
2022-11-29 09:48:24 +08:00
2024-03-10 12:10:53 +08:00
private int totalHits = > countGreat + countOk + countMeh + countMiss ;
2022-11-25 08:09:30 +08:00
2024-03-10 12:10:53 +08:00
private int totalSuccessfulHits = > countGreat + countOk + countMeh ;
2018-05-17 16:40:46 +08:00
}
}