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-10-06 07:39:01 +08:00
private const double rhythm_multiplier = 0.75 ;
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 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 ;
2021-10-04 01:06:59 +08:00
private double currentStrain = 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-10-03 12:35:37 +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-10-03 12:35:37 +08:00
int previousIslandSize = 0 ;
2021-08-19 22:12:03 +08:00
double rhythmComplexitySum = 0 ;
2021-10-03 12:35:37 +08:00
int islandSize = 1 ;
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-10-04 01:06:59 +08:00
OsuDifficultyHitObject currObj = ( OsuDifficultyHitObject ) Previous [ i - 1 ] ;
OsuDifficultyHitObject prevObj = ( OsuDifficultyHitObject ) Previous [ i ] ;
OsuDifficultyHitObject lastObj = ( OsuDifficultyHitObject ) 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-10-03 12:35:37 +08:00
currHistoricalDecay = Math . Min ( ( double ) ( Previous . Count - i ) / Previous . Count , currHistoricalDecay ) ; // either we're limited by time or limited by object count.
2021-08-17 21:39:18 +08:00
2021-10-04 01:06:59 +08:00
double currDelta = currObj . StrainTime ;
double prevDelta = prevObj . StrainTime ;
double lastDelta = lastObj . StrainTime ;
2021-10-06 07:39:01 +08:00
double currRatio = 1.0 + 6.0 * Math . Min ( 0.5 , Math . Pow ( Math . Sin ( Math . PI / ( Math . Min ( prevDelta , currDelta ) / Math . Max ( prevDelta , currDelta ) ) ) , 2 ) ) ; // fancy function to calculate rhythmbonuses.
2021-08-17 21:39:18 +08:00
2021-10-06 07:39:01 +08:00
double windowPenalty = Math . Min ( 1 , Math . Max ( 0 , Math . Abs ( prevDelta - currDelta ) - greatWindow * 0.6 ) / ( greatWindow * 0.6 ) ) ;
2021-08-17 21:39:18 +08:00
2021-10-06 07:39:01 +08:00
windowPenalty = Math . Min ( 1 , windowPenalty ) ;
double effectiveRatio = windowPenalty * currRatio ;
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-10-03 12:35:37 +08:00
if ( ! ( prevDelta > 1.25 * currDelta | | prevDelta * 1.25 < currDelta ) )
2021-08-19 22:12:03 +08:00
{
2021-10-04 01:06:59 +08:00
if ( islandSize < 7 )
islandSize + + ; // island is still progressing, count size.
2021-08-19 22:12:03 +08:00
}
else
{
if ( Previous [ i - 1 ] . BaseObject is Slider ) // bpm change is into slider, this is easy acc window
2021-10-03 12:35:37 +08:00
effectiveRatio * = 0.125 ;
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
2021-10-03 12:35:37 +08:00
effectiveRatio * = 0.25 ;
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-10-03 12:35:37 +08:00
effectiveRatio * = 0.25 ;
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)
2021-10-03 12:35:37 +08:00
effectiveRatio * = 0.50 ;
2021-09-30 03:14:54 +08:00
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-10-03 12:35:37 +08:00
rhythmComplexitySum + = Math . Sqrt ( effectiveRatio * startRatio ) * currHistoricalDecay * Math . Sqrt ( 4 + islandSize ) / 2 * Math . Sqrt ( 4 + previousIslandSize ) / 2 ;
2021-09-30 03:14:54 +08:00
2021-10-06 03:19:00 +08:00
startRatio = 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-10-03 12:35:37 +08:00
islandSize = 1 ;
2021-08-19 22:12:03 +08:00
}
}
else if ( prevDelta > 1.25 * currDelta ) // we want to be speeding up.
{
// Begin counting island until we change speed again.
firstDeltaSwitch = true ;
2021-10-06 07:39:01 +08:00
startRatio = effectiveRatio ;
2021-10-03 12:35:37 +08:00
islandSize = 1 ;
2021-08-17 21:39:18 +08:00
}
}
}
2021-09-30 03:14:54 +08:00
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-10-04 01:06:59 +08:00
private double strainValueOf ( DifficultyHitObject current )
2021-08-17 21:47:45 +08:00
{
if ( current . BaseObject is Spinner )
return 0 ;
2021-08-17 21:39:18 +08:00
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-10-04 01:06:59 +08:00
double distance = Math . Min ( single_spacing_threshold , osuCurrObj . TravelDistance + osuCurrObj . JumpDistance ) ;
2021-08-18 03:25:49 +08:00
2021-10-04 01:06:59 +08:00
return ( speedBonus + speedBonus * Math . Pow ( distance / single_spacing_threshold , 3.5 ) ) / strainTime ;
}
2021-09-13 00:08:17 +08:00
2021-10-04 01:06:59 +08:00
private double strainDecay ( double ms ) = > Math . Pow ( strainDecayBase , ms / 1000 ) ;
protected override double CalculateInitialStrain ( double time ) = > ( currentStrain * currentRhythm ) * strainDecay ( time - Previous [ 0 ] . StartTime ) ;
protected override double StrainValueAt ( DifficultyHitObject current )
{
currentStrain * = strainDecay ( current . DeltaTime ) ;
currentStrain + = strainValueOf ( current ) * skillMultiplier ;
2021-08-17 21:39:18 +08:00
2021-10-04 01:06:59 +08:00
currentRhythm = calculateRhythmBonus ( current ) ;
2021-08-17 21:39:18 +08:00
2021-10-04 01:06:59 +08:00
return currentStrain * currentRhythm ;
2018-04-13 17:19:50 +08:00
}
}
}