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 ;
2021-09-13 00:08:17 +08:00
using osu.Framework.Utils ;
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 ;
2021-09-30 03:14:54 +08:00
private const double rhythm_multiplier = 4.0 ;
2021-09-25 10:52:10 +08:00
private const int history_time_max = 5000 ; // 5 seconds of calculatingRhythmBonus max.
2021-09-30 03:14:54 +08:00
private const double min_speed_bonus = 75 ; // ~200BPM
private const double max_speed_bonus = 45 ;
private const double speed_balancing_factor = 40 ;
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 ;
2021-08-17 21:39:18 +08:00
protected override int HistoryLength = > 32 ;
2021-09-15 17:29:30 +08:00
private readonly double greatWindow ;
2018-12-18 08:51:49 +08:00
2021-09-15 17:52:50 +08:00
public Speed ( Mod [ ] mods , double hitWindowGreat )
2021-02-06 12:06:16 +08:00
: base ( mods )
{
2021-09-15 17:52:50 +08:00
greatWindow = hitWindowGreat ;
2021-02-06 12:06:16 +08:00
}
2021-08-17 21:39:18 +08:00
/// <summary>
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
/// </summary>
2021-09-25 10:52:10 +08:00
private double calculateRhythmBonus ( DifficultyHitObject current , double greatWindowFull )
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 ;
2021-09-30 03:14:54 +08:00
double startRatio = 0 ; // store the ratio of the current start of an island to buff for tighter rhythms
2021-08-17 21:39:18 +08:00
bool firstDeltaSwitch = false ;
2021-08-22 01:23:17 +08:00
for ( int i = Previous . Count - 2 ; i > 0 ; i - - )
2021-08-17 21:39:18 +08:00
{
2021-09-13 00:08:17 +08:00
DifficultyHitObject currObj = Previous [ i - 1 ] ;
DifficultyHitObject prevObj = Previous [ i ] ;
2021-09-30 03:14:54 +08:00
DifficultyHitObject lastObj = Previous [ i + 1 ] ;
2021-09-13 00:08:17 +08:00
double currHistoricalDecay = Math . Max ( 0 , ( history_time_max - ( current . StartTime - currObj . 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 23:40:34 +08:00
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-09-13 02:14:05 +08:00
double currDelta = Math . Max ( 25 , currObj . DeltaTime ) ;
double prevDelta = Math . Max ( 25 , prevObj . DeltaTime ) ;
2021-09-30 03:14:54 +08:00
double lastDelta = ( ( OsuDifficultyHitObject ) lastObj ) . StrainTime ;
2021-08-19 22:12:03 +08:00
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 )
2021-09-30 03:14:54 +08:00
effectiveRatio = 0.5 + ( effectiveRatio - 0.5 ) * 6 ; // large buff for 1/3 -> 1/4 type transitions.
else
effectiveRatio = 0.5 ;
2021-08-17 21:39:18 +08:00
2021-09-17 08:27:42 +08:00
effectiveRatio * = currHistoricalDecay ; // scale 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-09-13 00:08:17 +08:00
if ( Precision . AlmostEquals ( prevDelta , currDelta , 15 ) )
2021-08-19 22:12:03 +08:00
{
islandSize + + ; // island is still progressing, count size.
}
else
{
2021-09-30 03:14:54 +08:00
if ( islandSize > 12 )
islandSize = 12 ;
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-22 01:29:17 +08:00
if ( previousIslandSize = = islandSize ) // repeated island size (ex: triplet -> triplet)
2021-09-30 03:14:54 +08:00
effectiveRatio * = 0.35 ;
2021-08-17 21:39:18 +08:00
2021-09-30 03:14:54 +08:00
if ( previousIslandSize % 2 = = islandSize % 2 ) // repeated island polartiy (2 -> 4, 3 -> 5)
effectiveRatio * = 0.75 ;
if ( lastDelta > prevDelta + 10 & & prevDelta > currDelta + 10 ) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
2021-08-22 01:23:17 +08:00
effectiveRatio * = 0.125 ;
2021-09-30 03:14:54 +08:00
rhythmComplexitySum + = effectiveRatio * startRatio ;
startRatio = Math . Sqrt ( Math . Min ( prevDelta , currDelta ) / Math . Max ( prevDelta , currDelta ) ) ;
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-09-30 03:14:54 +08:00
startRatio = Math . Sqrt ( Math . Min ( prevDelta , currDelta ) / Math . Max ( prevDelta , currDelta ) ) ;
2021-08-17 21:39:18 +08:00
}
}
}
2021-09-30 03:14:54 +08:00
if ( greatWindowFull > 62 )
rhythmComplexitySum * = Math . Sqrt ( 62 / greatWindowFull ) ;
return Math . Sqrt ( 4 + rhythmComplexitySum * rhythm_multiplier ) / 2 ; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
2021-08-17 21:39:18 +08:00
}
2021-09-25 10:52:10 +08:00
private double tapStrainOf ( DifficultyHitObject current , double speedBonus , double strainTime )
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-09-25 10:52:10 +08:00
return speedBonus / strainTime ;
2021-08-17 21:47:45 +08:00
}
2021-08-17 21:39:18 +08:00
2021-09-25 10:52:10 +08:00
private double movementStrainOf ( DifficultyHitObject current , double speedBonus , double strainTime )
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
2021-09-30 03:14:54 +08:00
return ( speedBonus * Math . Pow ( distance / single_spacing_threshold , 3.5 ) ) / 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-09-25 10:52:10 +08:00
// derive strainTime for calculation
var osuCurrObj = ( OsuDifficultyHitObject ) current ;
var osuPrevObj = Previous . Count > 0 ? ( OsuDifficultyHitObject ) Previous [ 0 ] : null ;
double strainTime = osuCurrObj . StrainTime ;
2021-09-15 18:12:36 +08:00
double greatWindowFull = greatWindow * 2 ;
2021-09-15 18:24:48 +08:00
double speedWindowRatio = strainTime / greatWindowFull ;
2021-09-03 09:39:21 +08:00
2021-09-15 18:12:36 +08:00
// Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between)
2021-09-25 10:52:10 +08:00
if ( osuPrevObj ! = null & & strainTime < greatWindowFull & & osuPrevObj . StrainTime > strainTime )
strainTime = Interpolation . Lerp ( osuPrevObj . StrainTime , strainTime , speedWindowRatio ) ;
2021-08-30 00:19:26 +08:00
2021-09-02 23:48:34 +08:00
// Cap deltatime to the OD 300 hitwindow.
2021-09-15 19:03:47 +08:00
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
2021-09-15 19:36:15 +08:00
strainTime / = Math . Clamp ( ( strainTime / greatWindowFull ) / 0.93 , 0.92 , 1 ) ;
2021-09-02 23:48:34 +08:00
2021-09-25 10:52:10 +08:00
// derive speedBonus for calculation
2021-08-18 03:25:49 +08:00
double speedBonus = 1.0 ;
2021-09-15 18:24:48 +08:00
if ( strainTime < min_speed_bonus )
2021-09-25 10:52:10 +08:00
speedBonus = 1 + 0.75 * Math . Pow ( ( min_speed_bonus - strainTime ) / speed_balancing_factor , 2 ) ;
2021-08-18 03:25:49 +08:00
2021-09-25 10:52:10 +08:00
currentRhythm = calculateRhythmBonus ( current , greatWindowFull ) ;
2021-08-18 03:25:49 +08:00
2021-09-13 00:08:17 +08:00
double decay = strainDecay ( current . DeltaTime ) ;
currentTapStrain * = decay ;
2021-09-25 10:52:10 +08:00
currentTapStrain + = tapStrainOf ( current , speedBonus , strainTime ) * skillMultiplier ;
2021-08-17 21:39:18 +08:00
2021-09-13 00:08:17 +08:00
currentMovementStrain * = decay ;
2021-09-25 10:52:10 +08:00
currentMovementStrain + = movementStrainOf ( current , speedBonus , strainTime ) * 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
}
}
}