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
2018-05-21 09:49:23 +08:00
using System ;
2018-06-21 11:26:15 +08:00
using System.Collections.Generic ;
2018-06-21 12:12:22 +08:00
using System.Linq ;
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 ;
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-06-21 11:26:15 +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>
private const double strain_step = 750 ;
2018-05-21 09:49:23 +08:00
2018-06-21 11:26:15 +08:00
/// <summary>
/// The weighting of each strain value decays to this number * it's previous value
/// </summary>
private const double decay_weight = 0.94 ;
2018-05-21 09:49:23 +08:00
2018-06-21 11:26:15 +08:00
private const double star_scaling_factor = 0.145 ;
public CatchDifficultyCalculator ( Ruleset ruleset , WorkingBeatmap beatmap )
: base ( ruleset , beatmap )
2018-04-13 17:19:50 +08:00
{
}
2018-06-21 11:26:15 +08:00
protected override DifficultyAttributes Calculate ( IBeatmap beatmap , Mod [ ] mods , double timeRate )
2018-05-21 09:49:23 +08:00
{
2018-06-21 11:26:15 +08:00
if ( ! beatmap . HitObjects . Any ( ) )
return new CatchDifficultyAttributes ( mods , 0 ) ;
2018-05-21 09:49:23 +08:00
2018-06-21 11:57:59 +08:00
var catcher = new CatcherArea . Catcher ( beatmap . BeatmapInfo . BaseDifficulty ) ;
float halfCatchWidth = catcher . CatchWidth * 0.5f ;
2018-05-21 09:49:23 +08:00
2018-06-21 11:26:15 +08:00
var difficultyHitObjects = new List < CatchDifficultyHitObject > ( ) ;
foreach ( var hitObject in beatmap . HitObjects )
2018-05-21 09:49:23 +08:00
{
2018-07-17 13:35:09 +08:00
switch ( hitObject )
2018-05-21 09:49:23 +08:00
{
2018-07-17 13:35:09 +08:00
// We want to only consider fruits that contribute to the combo. Droplets are addressed as accuracy and spinners are not relevant for "skill" calculations.
2018-07-17 15:33:08 +08:00
case Fruit fruit :
difficultyHitObjects . Add ( new CatchDifficultyHitObject ( fruit , halfCatchWidth ) ) ;
2018-07-17 13:35:09 +08:00
break ;
case JuiceStream _ :
difficultyHitObjects . AddRange ( hitObject . NestedHitObjects . OfType < CatchHitObject > ( ) . Where ( o = > ! ( o is TinyDroplet ) ) . Select ( o = > new CatchDifficultyHitObject ( o , halfCatchWidth ) ) ) ;
break ;
2018-05-21 09:49:23 +08:00
}
}
difficultyHitObjects . Sort ( ( a , b ) = > a . BaseHitObject . StartTime . CompareTo ( b . BaseHitObject . StartTime ) ) ;
2018-06-21 11:26:15 +08:00
if ( ! calculateStrainValues ( difficultyHitObjects , timeRate ) )
return new CatchDifficultyAttributes ( mods , 0 ) ;
2018-05-21 09:49:23 +08:00
2018-06-21 16:32:10 +08:00
// this is the same as osu!, so there's potential to share the implementation... maybe
2018-07-05 10:32:09 +08:00
double preempt = BeatmapDifficulty . DifficultyRange ( beatmap . BeatmapInfo . BaseDifficulty . ApproachRate , 1800 , 1200 , 450 ) / timeRate ;
2018-06-21 11:26:15 +08:00
double starRating = Math . Sqrt ( calculateDifficulty ( difficultyHitObjects , timeRate ) ) * star_scaling_factor ;
2018-05-21 09:49:23 +08:00
2018-06-21 11:26:15 +08:00
return new CatchDifficultyAttributes ( mods , starRating )
{
2018-07-05 10:32:09 +08:00
ApproachRate = preempt > 1200.0 ? - ( preempt - 1800.0 ) / 120.0 : - ( preempt - 1200.0 ) / 150.0 + 5.0 ,
2018-06-21 11:26:15 +08:00
MaxCombo = difficultyHitObjects . Count
} ;
2018-05-21 09:49:23 +08:00
}
2018-06-21 11:26:15 +08:00
private bool calculateStrainValues ( List < CatchDifficultyHitObject > objects , double timeRate )
2018-05-21 09:49:23 +08:00
{
2018-06-21 15:21:08 +08:00
CatchDifficultyHitObject lastObject = null ;
2018-05-21 09:49:23 +08:00
2018-06-21 15:21:08 +08:00
if ( ! objects . Any ( ) ) return false ;
2018-05-21 09:49:23 +08:00
2018-06-21 15:21:08 +08:00
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment.
foreach ( var currentObject in objects )
{
if ( lastObject ! = null )
currentObject . CalculateStrains ( lastObject , timeRate ) ;
2018-05-21 09:49:23 +08:00
2018-06-21 15:21:08 +08:00
lastObject = currentObject ;
2018-05-21 09:49:23 +08:00
}
2018-06-21 15:21:08 +08:00
return true ;
2018-05-21 09:49:23 +08:00
}
2018-05-21 10:16:32 +08:00
2018-06-21 11:26:15 +08:00
private double calculateDifficulty ( List < CatchDifficultyHitObject > objects , double timeRate )
2018-05-21 09:49:23 +08:00
{
// The strain step needs to be adjusted for the algorithm to be considered equal with speed changing mods
2018-06-21 11:26:15 +08:00
double actualStrainStep = strain_step * timeRate ;
2018-05-21 09:49:23 +08:00
// Find the highest strain value within each strain step
2018-06-21 16:49:04 +08:00
var highestStrains = new List < double > ( ) ;
2018-05-21 09:49:23 +08:00
double intervalEndTime = actualStrainStep ;
double maximumStrain = 0 ; // We need to keep track of the maximum strain in the current interval
CatchDifficultyHitObject previousHitObject = null ;
2018-06-21 11:26:15 +08:00
foreach ( CatchDifficultyHitObject hitObject in objects )
2018-05-21 09:49:23 +08:00
{
// 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-06-21 11:26:15 +08:00
// Build the weighted sum over the highest strains for each interval
double difficulty = 0 ;
double weight = 1 ;
highestStrains . Sort ( ( a , b ) = > b . CompareTo ( a ) ) ; // Sort from highest to lowest strain.
foreach ( double strain in highestStrains )
{
difficulty + = weight * strain ;
weight * = decay_weight ;
}
2018-05-21 09:49:23 +08:00
return difficulty ;
}
2018-04-13 17:19:50 +08:00
}
}