2018-04-13 17:19:50 +08:00
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
2018-05-21 09:49:23 +08:00
using System ;
2018-05-15 16:40:19 +08:00
using osu.Game.Beatmaps ;
2018-05-15 16:38:04 +08:00
using osu.Game.Rulesets.Difficulty ;
2018-05-21 09:49:23 +08:00
using osu.Game.Rulesets.Mods ;
using osu.Game.Rulesets.Catch.Objects ;
using osu.Game.Rulesets.Catch.UI ;
using osu.Game.Rulesets.Objects ;
2018-04-13 17:19:50 +08:00
2018-05-15 16:40:19 +08:00
namespace osu.Game.Rulesets.Catch.Difficulty
2018-04-13 17:19:50 +08:00
{
2018-04-19 21:04:12 +08:00
public class CatchDifficultyCalculator : DifficultyCalculator
2018-04-13 17:19:50 +08:00
{
2018-05-21 10:39:16 +08:00
private const double star_scaling_factor = 0.145 ;
private const float playfield_width = CatchPlayfield . BASE_WIDTH ;
2018-05-21 09:49:23 +08:00
private readonly List < CatchDifficultyHitObject > difficultyHitObjects = new List < CatchDifficultyHitObject > ( ) ;
public CatchDifficultyCalculator ( IBeatmap beatmap )
: base ( beatmap )
{
}
public CatchDifficultyCalculator ( IBeatmap beatmap , Mod [ ] mods )
: base ( beatmap , mods )
2018-04-13 17:19:50 +08:00
{
}
2018-05-21 09:49:23 +08:00
public override double Calculate ( Dictionary < string , double > categoryDifficulty = null )
{
difficultyHitObjects . Clear ( ) ;
float circleSize = Beatmap . BeatmapInfo . BaseDifficulty . CircleSize ;
2018-05-21 13:53:48 +08:00
float catcherWidth = ( 1.0f - 0.7f * ( circleSize - 5 ) / 5 ) * 0.62064f * CatcherArea . CATCHER_SIZE ;
2018-05-21 09:49:23 +08:00
float catcherWidthHalf = catcherWidth / 2 ;
catcherWidthHalf * = 0.8f ;
foreach ( var hitObject in Beatmap . HitObjects )
{
// We want to only consider fruits that contribute to the combo. Droplets are addressed as accuracy and spinners are not relevant for "skill" calculations.
if ( hitObject is Fruit )
{
difficultyHitObjects . Add ( new CatchDifficultyHitObject ( ( CatchHitObject ) hitObject , catcherWidthHalf ) ) ;
}
if ( hitObject is JuiceStream )
{
IEnumerator < HitObject > nestedHitObjectsEnumerator = hitObject . NestedHitObjects . GetEnumerator ( ) ;
while ( nestedHitObjectsEnumerator . MoveNext ( ) )
{
CatchHitObject objectInJuiceStream = ( CatchHitObject ) nestedHitObjectsEnumerator . Current ;
if ( ! ( objectInJuiceStream is TinyDroplet ) )
difficultyHitObjects . Add ( new CatchDifficultyHitObject ( objectInJuiceStream , catcherWidthHalf ) ) ;
}
2018-05-21 10:39:16 +08:00
// Dispose the enumerator after counting all fruits.
nestedHitObjectsEnumerator . Dispose ( ) ;
2018-05-21 09:49:23 +08:00
}
}
difficultyHitObjects . Sort ( ( a , b ) = > a . BaseHitObject . StartTime . CompareTo ( b . BaseHitObject . StartTime ) ) ;
if ( ! CalculateStrainValues ( ) ) return 0 ;
2018-05-21 10:39:16 +08:00
double starRating = Math . Sqrt ( CalculateDifficulty ( ) ) * star_scaling_factor ;
2018-05-21 09:49:23 +08:00
if ( categoryDifficulty ! = null )
{
categoryDifficulty [ "Aim" ] = starRating ;
double ar = Beatmap . BeatmapInfo . BaseDifficulty . ApproachRate ;
double preEmpt = BeatmapDifficulty . DifficultyRange ( ar , 1800 , 1200 , 450 ) / TimeRate ;
2018-05-21 13:53:48 +08:00
categoryDifficulty [ "AR" ] = preEmpt > 1200.0 ? - ( preEmpt - 1800.0 ) / 120.0 : - ( preEmpt - 1200.0 ) / 150.0 + 5.0 ;
2018-05-21 09:55:07 +08:00
categoryDifficulty [ "Max combo" ] = difficultyHitObjects . Count ;
2018-05-21 09:49:23 +08:00
}
return starRating ;
}
protected bool CalculateStrainValues ( )
{
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment.
using ( List < CatchDifficultyHitObject > . Enumerator hitObjectsEnumerator = difficultyHitObjects . GetEnumerator ( ) )
{
if ( ! hitObjectsEnumerator . MoveNext ( ) ) return false ;
CatchDifficultyHitObject currentHitObject = hitObjectsEnumerator . Current ;
// First hitObject starts at strain 1. 1 is the default for strain values, so we don't need to set it here. See DifficultyHitObject.
while ( hitObjectsEnumerator . MoveNext ( ) )
{
2018-05-21 10:39:16 +08:00
CatchDifficultyHitObject nextHitObject = hitObjectsEnumerator . Current ;
2018-05-21 13:53:48 +08:00
nextHitObject ? . CalculateStrains ( currentHitObject , TimeRate ) ;
2018-05-21 09:49:23 +08:00
currentHitObject = nextHitObject ;
}
return true ;
}
}
2018-05-21 10:16:32 +08:00
2018-05-21 09:49:23 +08:00
/// <summary>
/// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size STRAIN_STEP.
/// This is to eliminate higher influence of stream over aim by simply having more HitObjects with high strain.
/// The higher this value, the less strains there will be, indirectly giving long beatmaps an advantage.
/// </summary>
2018-05-21 10:39:16 +08:00
private const double strain_step = 750 ;
2018-05-21 09:49:23 +08:00
/// <summary>
/// The weighting of each strain value decays to this number * it's previous value
/// </summary>
2018-05-21 10:39:16 +08:00
private const double decay_weight = 0.94 ;
2018-05-21 13:35:02 +08:00
2018-05-21 09:49:23 +08:00
protected double CalculateDifficulty ( )
{
// The strain step needs to be adjusted for the algorithm to be considered equal with speed changing mods
2018-05-21 10:12:18 +08:00
double actualStrainStep = strain_step * TimeRate ;
2018-05-21 09:49:23 +08:00
// Find the highest strain value within each strain step
List < double > highestStrains = new List < double > ( ) ;
double intervalEndTime = actualStrainStep ;
double maximumStrain = 0 ; // We need to keep track of the maximum strain in the current interval
CatchDifficultyHitObject previousHitObject = null ;
foreach ( CatchDifficultyHitObject hitObject in difficultyHitObjects )
{
// While we are beyond the current interval push the currently available maximum to our strain list
while ( hitObject . BaseHitObject . StartTime > intervalEndTime )
{
highestStrains . Add ( maximumStrain ) ;
// The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay
// until the beginning of the next interval.
if ( previousHitObject = = null )
{
maximumStrain = 0 ;
}
else
{
2018-05-21 13:36:57 +08:00
double decay = Math . Pow ( CatchDifficultyHitObject . DECAY_BASE , ( intervalEndTime - previousHitObject . BaseHitObject . StartTime ) / 1000 ) ;
2018-05-21 09:49:23 +08:00
maximumStrain = previousHitObject . Strain * decay ;
}
// Go to the next time interval
intervalEndTime + = actualStrainStep ;
}
// Obtain maximum strain
maximumStrain = Math . Max ( hitObject . Strain , maximumStrain ) ;
previousHitObject = hitObject ;
}
2018-05-21 10:12:18 +08:00
// calculate maximun strain difficulty
double difficulty = StrainCalculator ( highestStrains , decay_weight ) ;
2018-05-21 09:49:23 +08:00
return difficulty ;
}
2018-04-13 17:19:50 +08:00
}
}