2023-06-12 17:33:22 +00: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.
using System ;
using System.Linq ;
using osu.Game.Beatmaps ;
2023-06-19 12:38:13 +00:00
using osu.Game.Rulesets.Judgements ;
2023-06-12 17:33:22 +00:00
using osu.Game.Rulesets.Objects ;
using osu.Game.Rulesets.Objects.Types ;
2023-06-19 12:38:13 +00:00
using osu.Game.Rulesets.Scoring ;
2023-09-04 08:43:23 +00:00
using osu.Game.Rulesets.Scoring.Legacy ;
2023-06-12 17:33:22 +00:00
using osu.Game.Rulesets.Taiko.Objects ;
namespace osu.Game.Rulesets.Taiko.Difficulty
{
2023-07-04 08:32:54 +00:00
internal class TaikoLegacyScoreSimulator : ILegacyScoreSimulator
2023-06-12 17:33:22 +00:00
{
2023-06-19 12:38:13 +00:00
private int legacyBonusScore ;
2023-09-04 08:43:23 +00:00
private int standardisedBonusScore ;
2023-06-12 17:33:22 +00:00
private int combo ;
2023-06-26 13:19:01 +00:00
private int difficultyPeppyStars ;
private IBeatmap playableBeatmap = null ! ;
2023-06-12 17:33:22 +00:00
2023-09-04 08:43:23 +00:00
public LegacyScoreAttributes Simulate ( IWorkingBeatmap workingBeatmap , IBeatmap playableBeatmap )
2023-06-12 17:33:22 +00:00
{
this . playableBeatmap = playableBeatmap ;
2023-06-26 13:19:01 +00:00
IBeatmap baseBeatmap = workingBeatmap . Beatmap ;
2023-06-12 17:33:22 +00:00
int countNormal = 0 ;
int countSlider = 0 ;
int countSpinner = 0 ;
foreach ( HitObject obj in baseBeatmap . HitObjects )
{
switch ( obj )
{
case IHasPath :
countSlider + + ;
break ;
case IHasDuration :
countSpinner + + ;
break ;
default :
countNormal + + ;
break ;
}
}
int objectCount = countNormal + countSlider + countSpinner ;
2023-06-23 15:58:45 +00:00
int drainLength = 0 ;
if ( baseBeatmap . HitObjects . Count > 0 )
{
int breakLength = baseBeatmap . Breaks . Select ( b = > ( int ) Math . Round ( b . EndTime ) - ( int ) Math . Round ( b . StartTime ) ) . Sum ( ) ;
drainLength = ( ( int ) Math . Round ( baseBeatmap . HitObjects [ ^ 1 ] . StartTime ) - ( int ) Math . Round ( baseBeatmap . HitObjects [ 0 ] . StartTime ) - breakLength ) / 1000 ;
}
2023-06-12 17:33:22 +00:00
difficultyPeppyStars = ( int ) Math . Round (
( baseBeatmap . Difficulty . DrainRate
+ baseBeatmap . Difficulty . OverallDifficulty
+ baseBeatmap . Difficulty . CircleSize
2023-06-27 07:47:42 +00:00
+ Math . Clamp ( ( float ) objectCount / drainLength * 8 , 0 , 16 ) ) / 38 * 5 ) ;
2023-06-12 17:33:22 +00:00
2023-09-04 08:43:23 +00:00
LegacyScoreAttributes attributes = new LegacyScoreAttributes ( ) ;
2023-06-12 17:33:22 +00:00
foreach ( var obj in playableBeatmap . HitObjects )
2023-09-04 08:43:23 +00:00
simulateHit ( obj , ref attributes ) ;
attributes . BonusScoreRatio = legacyBonusScore = = 0 ? 0 : ( double ) standardisedBonusScore / legacyBonusScore ;
return attributes ;
2023-06-12 17:33:22 +00:00
}
2023-09-04 08:43:23 +00:00
private void simulateHit ( HitObject hitObject , ref LegacyScoreAttributes attributes )
2023-06-12 17:33:22 +00:00
{
bool increaseCombo = true ;
bool addScoreComboMultiplier = false ;
2023-06-19 12:38:13 +00:00
2023-06-12 17:33:22 +00:00
bool isBonus = false ;
2023-06-19 12:38:13 +00:00
HitResult bonusResult = HitResult . None ;
2023-06-12 17:33:22 +00:00
int scoreIncrease = 0 ;
switch ( hitObject )
{
case SwellTick :
scoreIncrease = 300 ;
increaseCombo = false ;
break ;
case DrumRollTick :
scoreIncrease = 300 ;
increaseCombo = false ;
isBonus = true ;
2023-06-19 12:38:13 +00:00
bonusResult = HitResult . SmallBonus ;
2023-06-12 17:33:22 +00:00
break ;
case Swell swell :
// The taiko swell generally does not match the osu-stable implementation in any way.
// We'll redo the calculations to match osu-stable here...
2023-09-08 12:08:09 +00:00
// Normally, this value depends on the final overall difficulty. For simplicity, we'll only consider the worst case that maximises rotations.
const double minimum_rotations_per_second = 7.5 ;
2023-06-12 17:33:22 +00:00
2023-09-08 12:08:09 +00:00
// The amount of half spins that are required to successfully complete the spinner (i.e. get a 300).
int halfSpinsRequiredForCompletion = ( int ) ( swell . Duration / 1000 * minimum_rotations_per_second ) ;
2023-06-12 17:33:22 +00:00
halfSpinsRequiredForCompletion = ( int ) Math . Max ( 1 , halfSpinsRequiredForCompletion * 1.65f ) ;
2023-09-04 08:43:23 +00:00
//
2023-09-08 12:08:09 +00:00
// Normally, this multiplier depends on the active mods (DT = 0.75, HT = 1.5). For simplicity, we'll only consider the worst case that maximises rotations.
2023-09-04 08:43:23 +00:00
// This way, scores remain beatable at the cost of the conversion being slightly inaccurate.
// - A perfect DT/NM score will have less than 1M total score (excluding bonus).
// - A perfect HT score will have 1M total score (excluding bonus).
//
halfSpinsRequiredForCompletion = Math . Max ( 1 , ( int ) ( halfSpinsRequiredForCompletion * 1.5f ) ) ;
2023-06-12 17:33:22 +00:00
for ( int i = 0 ; i < = halfSpinsRequiredForCompletion ; i + + )
2023-09-04 08:43:23 +00:00
simulateHit ( new SwellTick ( ) , ref attributes ) ;
2023-06-12 17:33:22 +00:00
scoreIncrease = 300 ;
addScoreComboMultiplier = true ;
increaseCombo = false ;
isBonus = true ;
2023-06-19 12:38:13 +00:00
bonusResult = HitResult . LargeBonus ;
2023-06-12 17:33:22 +00:00
break ;
case Hit :
scoreIncrease = 300 ;
addScoreComboMultiplier = true ;
break ;
case DrumRoll :
foreach ( var nested in hitObject . NestedHitObjects )
2023-09-04 08:43:23 +00:00
simulateHit ( nested , ref attributes ) ;
2023-06-12 17:33:22 +00:00
return ;
}
if ( hitObject is DrumRollTick tick )
{
if ( playableBeatmap . ControlPointInfo . EffectPointAt ( tick . Parent . StartTime ) . KiaiMode )
scoreIncrease = ( int ) ( scoreIncrease * 1.2f ) ;
if ( tick . IsStrong )
scoreIncrease + = scoreIncrease / 5 ;
}
// The score increase directly contributed to by the combo-multiplied portion.
int comboScoreIncrease = 0 ;
if ( addScoreComboMultiplier )
{
int oldScoreIncrease = scoreIncrease ;
2023-09-04 08:43:23 +00:00
scoreIncrease + = ( int ) ( scoreIncrease / 35f * 2 * ( difficultyPeppyStars + 1 ) ) * ( Math . Min ( 100 , combo ) / 10 ) ;
2023-06-12 17:33:22 +00:00
if ( hitObject is Swell )
{
if ( playableBeatmap . ControlPointInfo . EffectPointAt ( hitObject . GetEndTime ( ) ) . KiaiMode )
scoreIncrease = ( int ) ( scoreIncrease * 1.2f ) ;
}
else
{
if ( playableBeatmap . ControlPointInfo . EffectPointAt ( hitObject . StartTime ) . KiaiMode )
scoreIncrease = ( int ) ( scoreIncrease * 1.2f ) ;
}
comboScoreIncrease = scoreIncrease - oldScoreIncrease ;
}
if ( hitObject is Swell | | ( hitObject is TaikoStrongableHitObject strongable & & strongable . IsStrong ) )
{
scoreIncrease * = 2 ;
comboScoreIncrease * = 2 ;
}
scoreIncrease - = comboScoreIncrease ;
if ( addScoreComboMultiplier )
2023-09-04 08:43:23 +00:00
attributes . ComboScore + = comboScoreIncrease ;
2023-06-12 17:33:22 +00:00
if ( isBonus )
2023-06-19 12:38:13 +00:00
{
legacyBonusScore + = scoreIncrease ;
2023-09-04 08:43:23 +00:00
standardisedBonusScore + = Judgement . ToNumericResult ( bonusResult ) ;
2023-06-19 12:38:13 +00:00
}
2023-06-12 17:33:22 +00:00
else
2023-09-04 08:43:23 +00:00
attributes . AccuracyScore + = scoreIncrease ;
2023-06-12 17:33:22 +00:00
if ( increaseCombo )
combo + + ;
}
}
}