2019-01-24 17:43:03 +09: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 17:40:46 +09:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
2025-02-07 10:21:12 +00:00
using osu.Framework.Audio.Track ;
using osu.Framework.Extensions.IEnumerableExtensions ;
2018-05-17 17:40:46 +09:00
using osu.Game.Rulesets.Difficulty ;
2025-01-17 14:37:34 -05:00
using osu.Game.Rulesets.Difficulty.Utils ;
2018-05-17 17:40:46 +09:00
using osu.Game.Rulesets.Mods ;
using osu.Game.Rulesets.Scoring ;
2018-11-11 18:38:12 +01:00
using osu.Game.Rulesets.Taiko.Objects ;
2025-02-07 10:21:12 +00:00
using osu.Game.Rulesets.Taiko.Scoring ;
2018-11-28 16:12:57 +09:00
using osu.Game.Scoring ;
2018-05-17 17:40:46 +09:00
namespace osu.Game.Rulesets.Taiko.Difficulty
{
public class TaikoPerformanceCalculator : PerformanceCalculator
{
private int countGreat ;
2020-09-29 17:16:55 +09:00
private int countOk ;
2018-05-17 17:40:46 +09:00
private int countMeh ;
private int countMiss ;
2024-05-29 09:40:59 -04:00
private double? estimatedUnstableRate ;
2018-05-17 17:40:46 +09:00
2025-02-07 10:21:12 +00:00
private double clockRate ;
private double greatHitWindow ;
2022-08-19 22:57:28 +10:00
private double effectiveMissCount ;
2022-03-15 12:37:39 +09:00
public TaikoPerformanceCalculator ( )
: base ( new TaikoRuleset ( ) )
2018-05-17 17:40:46 +09:00
{
}
2022-03-14 14:25:26 +09:00
protected override PerformanceAttributes CreatePerformanceAttributes ( ScoreInfo score , DifficultyAttributes attributes )
2018-05-17 17:40:46 +09:00
{
2022-03-14 14:25:26 +09: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 ) ;
2025-02-07 10:21:12 +00:00
var track = new TrackVirtual ( 10000 ) ;
score . Mods . OfType < IApplicableToTrack > ( ) . ForEach ( m = > m . ApplyToTrack ( track ) ) ;
clockRate = track . Rate ;
var difficulty = score . BeatmapInfo ! . Difficulty . Clone ( ) ;
score . Mods . OfType < IApplicableToDifficulty > ( ) . ForEach ( m = > m . ApplyToDifficulty ( difficulty ) ) ;
HitWindows hitWindows = new TaikoHitWindows ( ) ;
hitWindows . SetDifficulty ( difficulty . OverallDifficulty ) ;
greatHitWindow = hitWindows . WindowFor ( HitResult . Great ) / clockRate ;
estimatedUnstableRate = computeDeviationUpperBound ( ) * 10 ;
2018-05-17 17:40:46 +09:00
2022-08-19 23:15:38 +10: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 14:02:10 +09:00
if ( totalSuccessfulHits > 0 )
effectiveMissCount = Math . Max ( 1.0 , 1000.0 / totalSuccessfulHits ) * countMiss ;
2022-08-19 22:57:28 +10:00
2024-11-03 00:47:53 +10:00
// Converts are detected and omitted from mod-specific bonuses due to the scope of current difficulty calculation.
2023-07-04 14:50:34 +09:00
bool isConvert = score . BeatmapInfo ! . Ruleset . OnlineID ! = 1 ;
2022-09-30 20:56:16 +08:00
2022-08-19 22:57:28 +10:00
double multiplier = 1.13 ;
2018-05-17 17:40:46 +09:00
2024-10-31 10:15:29 +10:00
if ( score . Mods . Any ( m = > m is ModHidden ) & & ! isConvert )
2022-07-17 14:10:49 +10:00
multiplier * = 1.075 ;
if ( score . Mods . Any ( m = > m is ModEasy ) )
2024-10-31 10:15:29 +10:00
multiplier * = 0.950 ;
2018-05-17 17:40:46 +09:00
2024-10-31 12:08:12 +10:00
double difficultyValue = computeDifficultyValue ( score , taikoAttributes ) ;
2022-11-04 16:17:49 +09:00
double accuracyValue = computeAccuracyValue ( score , taikoAttributes , isConvert ) ;
2018-05-17 17:40:46 +09:00
double totalValue =
Math . Pow (
2021-12-17 23:39:03 +03:00
Math . Pow ( difficultyValue , 1.1 ) +
2018-05-17 17:40:46 +09:00
Math . Pow ( accuracyValue , 1.1 ) , 1.0 / 1.1
) * multiplier ;
2021-12-21 13:08:31 +03:00
return new TaikoPerformanceAttributes
2018-05-17 17:40:46 +09:00
{
2021-12-21 13:08:31 +03:00
Difficulty = difficultyValue ,
Accuracy = accuracyValue ,
2022-08-19 22:57:28 +10:00
EffectiveMissCount = effectiveMissCount ,
2024-05-29 09:40:59 -04:00
EstimatedUnstableRate = estimatedUnstableRate ,
2022-11-24 19:28:47 -05:00
Total = totalValue
2021-12-21 13:08:31 +03:00
} ;
2018-05-17 17:40:46 +09:00
}
2024-10-31 12:08:12 +10:00
private double computeDifficultyValue ( ScoreInfo score , TaikoDifficultyAttributes attributes )
2018-05-17 17:40:46 +09:00
{
2025-01-17 07:14:05 +10:00
double baseDifficulty = 5 * Math . Max ( 1.0 , attributes . StarRating / 0.110 ) - 4.0 ;
double difficultyValue = Math . Min ( Math . Pow ( baseDifficulty , 3 ) / 69052.51 , Math . Pow ( baseDifficulty , 2.25 ) / 1250.0 ) ;
difficultyValue * = 1 + 0.10 * Math . Max ( 0 , attributes . StarRating - 10 ) ;
2018-05-17 17:40:46 +09:00
2020-07-29 20:53:14 +09:00
double lengthBonus = 1 + 0.1 * Math . Min ( 1.0 , totalHits / 1500.0 ) ;
2021-12-17 23:39:03 +03:00
difficultyValue * = lengthBonus ;
2018-05-17 17:40:46 +09:00
2022-08-19 22:57:28 +10:00
difficultyValue * = Math . Pow ( 0.986 , effectiveMissCount ) ;
2022-07-17 14:10:49 +10:00
if ( score . Mods . Any ( m = > m is ModEasy ) )
2024-10-31 10:15:29 +10:00
difficultyValue * = 0.90 ;
2018-05-17 17:40:46 +09:00
2024-10-31 10:15:29 +10:00
if ( score . Mods . Any ( m = > m is ModHidden ) )
2021-12-17 23:39:03 +03:00
difficultyValue * = 1.025 ;
2018-05-17 17:40:46 +09:00
2022-03-14 14:25:26 +09:00
if ( score . Mods . Any ( m = > m is ModFlashlight < TaikoHitObject > ) )
2024-10-31 10:15:29 +10:00
difficultyValue * = Math . Max ( 1 , 1.050 - Math . Min ( attributes . MonoStaminaFactor / 50 , 1 ) * lengthBonus ) ;
2018-05-17 17:40:46 +09:00
2024-05-29 09:40:59 -04:00
if ( estimatedUnstableRate = = null )
2022-10-28 16:18:17 -04:00
return 0 ;
2024-10-31 12:08:12 +10:00
// Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps.
2024-10-30 18:57:47 -04:00
double accScalingExponent = 2 + attributes . MonoStaminaFactor ;
2025-01-20 07:55:34 +10:00
double accScalingShift = 500 - 100 * ( attributes . MonoStaminaFactor * 3 ) ;
2024-10-30 18:57:47 -04:00
2025-01-17 14:37:34 -05:00
return difficultyValue * Math . Pow ( DifficultyCalculationUtils . Erf ( accScalingShift / ( Math . Sqrt ( 2 ) * estimatedUnstableRate . Value ) ) , accScalingExponent ) ;
2018-05-17 17:40:46 +09:00
}
2022-11-04 16:17:49 +09:00
private double computeAccuracyValue ( ScoreInfo score , TaikoDifficultyAttributes attributes , bool isConvert )
2018-05-17 17:40:46 +09:00
{
2025-02-07 10:21:12 +00:00
if ( greatHitWindow < = 0 | | estimatedUnstableRate = = null )
2018-05-17 17:40:46 +09:00
return 0 ;
2024-05-29 09:40:59 -04:00
double accuracyValue = Math . Pow ( 70 / estimatedUnstableRate . Value , 1.1 ) * Math . Pow ( attributes . StarRating , 0.4 ) * 100.0 ;
2022-07-17 14:10:49 +10:00
double lengthBonus = Math . Min ( 1.15 , Math . Pow ( totalHits / 1500.0 , 0.3 ) ) ;
2022-10-02 09:05:58 +10:00
// Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values.
2022-11-04 16:17:49 +09:00
if ( score . Mods . Any ( m = > m is ModFlashlight < TaikoHitObject > ) & & score . Mods . Any ( m = > m is ModHidden ) & & ! isConvert )
2022-10-26 15:58:20 -04:00
accuracyValue * = Math . Max ( 1.0 , 1.05 * lengthBonus ) ;
2018-05-17 17:40:46 +09:00
2022-07-17 14:10:49 +10:00
return accuracyValue ;
2018-05-17 17:40:46 +09:00
}
2022-11-27 15:24:54 -05:00
/// <summary>
2024-03-09 23:10:53 -05: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-27 15:24:54 -05:00
/// </summary>
2025-02-07 10:21:12 +00:00
private double? computeDeviationUpperBound ( )
2022-10-25 17:41:20 -04:00
{
2025-02-07 10:21:12 +00:00
if ( countGreat = = 0 | | greatHitWindow < = 0 )
2022-10-28 16:18:17 -04:00
return null ;
2022-10-27 00:07:32 -04:00
2024-03-09 23:10:53 -05:00
const double z = 2.32634787404 ; // 99% critical value for the normal distribution (one-tailed).
2025-01-14 14:12:08 -05:00
double n = totalHits ;
2022-10-25 17:41:20 -04:00
2025-01-14 14:12:08 -05:00
// Proportion of greats hit.
double p = countGreat / n ;
2022-08-19 22:57:28 +10:00
2025-01-14 14:12:08 -05: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-24 19:09:30 -05:00
2025-01-14 14:12:08 -05:00
// We can be 99% confident that the deviation is not higher than:
2025-02-07 10:21:12 +00:00
return greatHitWindow / ( Math . Sqrt ( 2 ) * DifficultyCalculationUtils . ErfInv ( pLowerBound ) ) ;
2024-03-09 23:10:53 -05:00
}
2022-11-29 10:48:24 +09:00
2024-03-09 23:10:53 -05:00
private int totalHits = > countGreat + countOk + countMeh + countMiss ;
2022-11-24 19:09:30 -05:00
2024-03-09 23:10:53 -05:00
private int totalSuccessfulHits = > countGreat + countOk + countMeh ;
2018-05-17 17:40:46 +09:00
}
}