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-12-18 08:38:02 +08:00
using System ;
2019-02-12 15:03:28 +08:00
using osu.Game.Rulesets.Difficulty.Preprocessing ;
2021-02-06 12:06:16 +08:00
using osu.Game.Rulesets.Mods ;
2018-05-15 16:36:29 +08:00
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing ;
2019-02-18 13:58:33 +08:00
using osu.Game.Rulesets.Osu.Objects ;
2018-04-13 17:19:50 +08:00
2018-05-15 16:36:29 +08:00
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
2018-04-13 17:19:50 +08:00
{
/// <summary>
/// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit.
/// </summary>
2021-06-15 01:18:49 +08:00
public class Speed : OsuStrainSkill
2018-04-13 17:19:50 +08:00
{
2019-02-12 15:03:28 +08:00
private const double single_spacing_threshold = 125 ;
2018-12-24 11:41:04 +08:00
private const double angle_bonus_begin = 5 * Math . PI / 6 ;
2018-12-08 14:52:12 +08:00
private const double pi_over_4 = Math . PI / 4 ;
2018-12-22 08:56:33 +08:00
private const double pi_over_2 = Math . PI / 2 ;
2018-12-08 14:52:12 +08:00
2021-08-20 04:11:18 +08:00
private const double rhythm_multiplier = 2.0 ;
private const int history_time_max = 3000 ; // 3 seconds of calculatingRhythmBonus max.
2021-08-17 21:39:18 +08:00
2021-08-19 22:12:03 +08:00
private double skillMultiplier = > 1375 ;
2021-08-17 21:39:18 +08:00
private double strainDecayBase = > 0.3 ;
private double currentTapStrain = 1 ;
private double currentMovementStrain = 1 ;
2021-08-18 03:25:49 +08:00
private double currentRhythm = 1 ;
2021-08-17 21:39:18 +08:00
2021-06-16 21:13:46 +08:00
protected override int ReducedSectionCount = > 5 ;
2021-08-17 22:39:43 +08:00
protected override double DifficultyMultiplier = > 1.04 ;
2018-04-13 17:19:50 +08:00
2018-12-18 08:51:49 +08:00
private const double min_speed_bonus = 75 ; // ~200BPM
private const double max_speed_bonus = 45 ; // ~330BPM
private const double speed_balancing_factor = 40 ;
2021-08-17 21:39:18 +08:00
protected override int HistoryLength = > 32 ;
2021-02-06 12:06:16 +08:00
public Speed ( Mod [ ] mods )
: base ( mods )
{
}
2021-08-17 21:39:18 +08:00
private bool isRatioEqual ( double ratio , double a , double b )
{
return a + 15 > ratio * b & & a - 15 < ratio * b ;
}
/// <summary>
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
/// </summary>
2021-08-18 03:25:49 +08:00
private double calculateRhythmBonus ( DifficultyHitObject current )
2018-04-13 17:19:50 +08:00
{
2021-08-18 03:25:49 +08:00
if ( current . BaseObject is Spinner )
return 0 ;
2021-08-17 21:39:18 +08:00
int previousIslandSize = - 1 ;
2021-08-19 22:12:03 +08:00
double rhythmComplexitySum = 0 ;
2021-08-17 21:39:18 +08:00
int islandSize = 0 ;
bool firstDeltaSwitch = false ;
for ( int i = Previous . Count - 1 ; i > 0 ; i - - )
{
2021-08-20 04:11:18 +08:00
double currHistoricalDecay = Math . Max ( 0 , ( history_time_max - ( current . StartTime - Previous [ i - 1 ] . StartTime ) ) ) / history_time_max ; // scales note 0 to 1 from history to now
2021-08-17 21:39:18 +08:00
2021-08-19 22:12:03 +08:00
if ( currHistoricalDecay ! = 0 )
{
2021-08-20 04:11:18 +08:00
// below was bugged in initial version. fixed now, but will change values, will do more testing
// currHistoricalDecay = Math.Min(currHistoricalDecay, (double)(Previous.Count - i) / Previous.Count); // either we're limited by time or limited by object count.
2021-08-17 21:39:18 +08:00
2021-08-19 22:12:03 +08:00
double currDelta = ( ( OsuDifficultyHitObject ) Previous [ i - 1 ] ) . StrainTime ;
double prevDelta = ( ( OsuDifficultyHitObject ) Previous [ i ] ) . StrainTime ;
double effectiveRatio = Math . Min ( prevDelta , currDelta ) / Math . Max ( prevDelta , currDelta ) ;
2021-08-17 21:39:18 +08:00
2021-08-19 22:12:03 +08:00
if ( effectiveRatio > 0.5 )
effectiveRatio = 0.5 + ( effectiveRatio - 0.5 ) * 10 ; // large buff for 1/3 -> 1/4 type transitions.
2021-08-17 21:39:18 +08:00
2021-08-19 22:12:03 +08:00
effectiveRatio * = Math . Sqrt ( 100 / ( ( currDelta + prevDelta ) / 2 ) ) * currHistoricalDecay ; // scale with bpm slightly and with time
2021-08-17 21:39:18 +08:00
2021-08-19 22:12:03 +08:00
if ( firstDeltaSwitch )
2021-08-17 21:39:18 +08:00
{
2021-08-19 22:12:03 +08:00
if ( isRatioEqual ( 1.0 , prevDelta , currDelta ) )
{
islandSize + + ; // island is still progressing, count size.
}
else
{
if ( islandSize > 6 )
islandSize = 6 ;
2021-08-17 21:39:18 +08:00
2021-08-19 22:12:03 +08:00
if ( Previous [ i - 1 ] . BaseObject is Slider ) // bpm change is into slider, this is easy acc window
effectiveRatio * = 0.25 ;
2021-08-17 21:39:18 +08:00
2021-08-19 22:12:03 +08:00
if ( Previous [ i ] . BaseObject is Slider ) // bpm change was from a slider, this is easier typically than circle -> circle
effectiveRatio * = 0.5 ;
2021-08-17 21:39:18 +08:00
2021-08-19 22:12:03 +08:00
if ( previousIslandSize = = islandSize ) // repeated island size (ex: triplet -> triplet)
effectiveRatio * = 0.25 ;
2021-08-17 21:39:18 +08:00
2021-08-19 23:05:39 +08:00
rhythmComplexitySum + = effectiveRatio ;
2021-08-17 21:39:18 +08:00
2021-08-19 22:12:03 +08:00
previousIslandSize = islandSize ; // log the last island size.
2021-08-17 21:39:18 +08:00
2021-08-19 22:12:03 +08:00
if ( prevDelta * 1.25 < currDelta ) // we're slowing down, stop counting
firstDeltaSwitch = false ; // if we're speeding up, this stays true and we keep counting island size.
2021-08-17 21:39:18 +08:00
2021-08-19 22:12:03 +08:00
islandSize = 0 ;
}
}
else if ( prevDelta > 1.25 * currDelta ) // we want to be speeding up.
{
// Begin counting island until we change speed again.
firstDeltaSwitch = true ;
2021-08-17 21:39:18 +08:00
islandSize = 0 ;
}
}
}
2021-08-20 04:11:18 +08:00
return Math . Sqrt ( 4 + rhythmComplexitySum * rhythm_multiplier ) / 2 ; //produces multiplier that can be applied to strain. range [1, infinity)
2021-08-17 21:39:18 +08:00
}
2021-08-18 03:25:49 +08:00
private double tapStrainOf ( DifficultyHitObject current , double speedBonus )
2021-08-17 21:39:18 +08:00
{
2021-08-17 21:47:45 +08:00
if ( current . BaseObject is Spinner )
return 0 ;
2019-02-18 13:58:33 +08:00
2021-08-19 22:12:03 +08:00
var osuCurrObj = ( OsuDifficultyHitObject ) current ;
2019-02-12 15:03:28 +08:00
2021-08-19 22:12:03 +08:00
return speedBonus / osuCurrObj . StrainTime ;
2021-08-17 21:47:45 +08:00
}
2021-08-17 21:39:18 +08:00
2021-08-18 03:25:49 +08:00
private double movementStrainOf ( DifficultyHitObject current , double speedBonus )
2021-08-17 21:47:45 +08:00
{
if ( current . BaseObject is Spinner )
return 0 ;
2021-08-17 21:39:18 +08:00
2021-08-19 22:12:03 +08:00
var osuCurrObj = ( OsuDifficultyHitObject ) current ;
2021-08-17 21:39:18 +08:00
2021-08-19 22:12:03 +08:00
double distance = Math . Min ( single_spacing_threshold , osuCurrObj . TravelDistance + osuCurrObj . JumpDistance ) ;
2021-08-17 21:47:45 +08:00
double angleBonus = 1.0 ;
2021-08-19 22:12:03 +08:00
if ( osuCurrObj . Angle ! = null )
2021-08-17 21:47:45 +08:00
{
2021-08-19 22:12:03 +08:00
double angle = osuCurrObj . Angle . Value ;
if ( angle < pi_over_2 )
angleBonus = 1.25 ;
else if ( angle < angle_bonus_begin )
2021-08-20 04:11:18 +08:00
angleBonus = 1 + Math . Pow ( Math . Sin ( 1.5 * ( angle_bonus_begin - angle ) ) , 2 ) / 4 ;
2021-08-17 21:47:45 +08:00
}
2021-08-19 22:12:03 +08:00
return ( angleBonus * speedBonus * Math . Pow ( distance / single_spacing_threshold , 3.5 ) ) / osuCurrObj . StrainTime ;
2021-08-17 21:39:18 +08:00
}
private double strainDecay ( double ms ) = > Math . Pow ( strainDecayBase , ms / 1000 ) ;
2021-08-18 03:25:49 +08:00
protected override double CalculateInitialStrain ( double time ) = > ( currentMovementStrain + currentTapStrain * currentRhythm ) * strainDecay ( time - Previous [ 0 ] . StartTime ) ;
2021-08-17 21:39:18 +08:00
protected override double StrainValueAt ( DifficultyHitObject current )
{
2021-08-18 03:25:49 +08:00
double speedBonus = 1.0 ;
double deltaTime = Math . Max ( max_speed_bonus , current . DeltaTime ) ;
if ( deltaTime < min_speed_bonus )
2021-08-19 22:12:03 +08:00
speedBonus = 1 + 0.75 * Math . Pow ( ( min_speed_bonus - deltaTime ) / speed_balancing_factor , 2 ) ;
2021-08-18 03:25:49 +08:00
currentRhythm = calculateRhythmBonus ( current ) ;
2021-08-17 21:47:45 +08:00
currentTapStrain * = strainDecay ( current . DeltaTime ) ;
2021-08-18 03:25:49 +08:00
currentTapStrain + = tapStrainOf ( current , speedBonus ) * skillMultiplier ;
2021-08-17 21:39:18 +08:00
2021-08-17 21:47:45 +08:00
currentMovementStrain * = strainDecay ( current . DeltaTime ) ;
2021-08-18 03:25:49 +08:00
currentMovementStrain + = movementStrainOf ( current , speedBonus ) * skillMultiplier ;
2021-08-17 21:39:18 +08:00
2021-08-18 03:25:49 +08:00
return currentMovementStrain + currentTapStrain * currentRhythm ;
2018-04-13 17:19:50 +08:00
}
}
}