2023-06-13 01:33:22 +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.
using System ;
2023-10-02 14:58:31 +08:00
using System.Collections.Generic ;
2023-06-13 01:33:22 +08:00
using System.Linq ;
using osu.Game.Beatmaps ;
2023-10-02 14:58:31 +08:00
using osu.Game.Rulesets.Mods ;
2023-06-13 01:33:22 +08:00
using osu.Game.Rulesets.Objects ;
using osu.Game.Rulesets.Objects.Types ;
2023-06-19 20:38:13 +08:00
using osu.Game.Rulesets.Scoring ;
2023-09-04 16:43:23 +08:00
using osu.Game.Rulesets.Scoring.Legacy ;
2023-10-02 14:58:31 +08:00
using osu.Game.Rulesets.Taiko.Mods ;
2023-06-13 01:33:22 +08:00
using osu.Game.Rulesets.Taiko.Objects ;
2023-12-20 19:23:43 +08:00
using osu.Game.Rulesets.Taiko.Scoring ;
2023-06-13 01:33:22 +08:00
namespace osu.Game.Rulesets.Taiko.Difficulty
{
2023-07-04 16:32:54 +08:00
internal class TaikoLegacyScoreSimulator : ILegacyScoreSimulator
2023-06-13 01:33:22 +08:00
{
2023-12-20 19:23:43 +08:00
private readonly ScoreProcessor scoreProcessor = new TaikoScoreProcessor ( ) ;
2023-06-19 20:38:13 +08:00
private int legacyBonusScore ;
2023-09-04 16:43:23 +08:00
private int standardisedBonusScore ;
2023-06-13 01:33:22 +08:00
private int combo ;
2023-06-26 21:19:01 +08:00
private int difficultyPeppyStars ;
private IBeatmap playableBeatmap = null ! ;
2023-06-13 01:33:22 +08:00
2023-09-04 16:43:23 +08:00
public LegacyScoreAttributes Simulate ( IWorkingBeatmap workingBeatmap , IBeatmap playableBeatmap )
2023-06-13 01:33:22 +08:00
{
this . playableBeatmap = playableBeatmap ;
2023-06-26 21:19:01 +08:00
IBeatmap baseBeatmap = workingBeatmap . Beatmap ;
2023-06-13 01:33:22 +08: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 23:58:45 +08: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-13 01:33:22 +08:00
difficultyPeppyStars = ( int ) Math . Round (
( baseBeatmap . Difficulty . DrainRate
+ baseBeatmap . Difficulty . OverallDifficulty
+ baseBeatmap . Difficulty . CircleSize
2023-06-27 15:47:42 +08:00
+ Math . Clamp ( ( float ) objectCount / drainLength * 8 , 0 , 16 ) ) / 38 * 5 ) ;
2023-06-13 01:33:22 +08:00
2023-09-04 16:43:23 +08:00
LegacyScoreAttributes attributes = new LegacyScoreAttributes ( ) ;
2023-06-13 01:33:22 +08:00
foreach ( var obj in playableBeatmap . HitObjects )
2023-09-04 16:43:23 +08:00
simulateHit ( obj , ref attributes ) ;
attributes . BonusScoreRatio = legacyBonusScore = = 0 ? 0 : ( double ) standardisedBonusScore / legacyBonusScore ;
2023-11-25 06:07:27 +08:00
attributes . BonusScore = legacyBonusScore ;
2023-12-18 11:01:51 +08:00
attributes . MaxCombo = combo ;
2023-09-04 16:43:23 +08:00
return attributes ;
2023-06-13 01:33:22 +08:00
}
2023-09-04 16:43:23 +08:00
private void simulateHit ( HitObject hitObject , ref LegacyScoreAttributes attributes )
2023-06-13 01:33:22 +08:00
{
bool increaseCombo = true ;
bool addScoreComboMultiplier = false ;
2023-06-19 20:38:13 +08:00
2023-06-13 01:33:22 +08:00
bool isBonus = false ;
2023-06-19 20:38:13 +08:00
HitResult bonusResult = HitResult . None ;
2023-06-13 01:33:22 +08:00
int scoreIncrease = 0 ;
switch ( hitObject )
{
case SwellTick :
scoreIncrease = 300 ;
increaseCombo = false ;
break ;
case DrumRollTick :
scoreIncrease = 300 ;
increaseCombo = false ;
isBonus = true ;
2023-06-19 20:38:13 +08:00
bonusResult = HitResult . SmallBonus ;
2023-06-13 01:33:22 +08: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 20:08:09 +08: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-13 01:33:22 +08:00
2023-09-08 20:08:09 +08: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-13 01:33:22 +08:00
halfSpinsRequiredForCompletion = ( int ) Math . Max ( 1 , halfSpinsRequiredForCompletion * 1.65f ) ;
2023-09-04 16:43:23 +08:00
//
2023-09-08 20:08:09 +08: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 16:43:23 +08: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-13 01:33:22 +08:00
for ( int i = 0 ; i < = halfSpinsRequiredForCompletion ; i + + )
2023-09-04 16:43:23 +08:00
simulateHit ( new SwellTick ( ) , ref attributes ) ;
2023-06-13 01:33:22 +08:00
scoreIncrease = 300 ;
addScoreComboMultiplier = true ;
increaseCombo = false ;
isBonus = true ;
2023-06-19 20:38:13 +08:00
bonusResult = HitResult . LargeBonus ;
2023-06-13 01:33:22 +08:00
break ;
case Hit :
scoreIncrease = 300 ;
addScoreComboMultiplier = true ;
break ;
case DrumRoll :
foreach ( var nested in hitObject . NestedHitObjects )
2023-09-04 16:43:23 +08:00
simulateHit ( nested , ref attributes ) ;
2023-06-13 01:33:22 +08: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-26 16:46:03 +08:00
scoreIncrease + = scoreIncrease / 35 * 2 * ( difficultyPeppyStars + 1 ) * ( Math . Min ( 100 , combo ) / 10 ) ;
2023-06-13 01:33:22 +08: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 16:43:23 +08:00
attributes . ComboScore + = comboScoreIncrease ;
2023-06-13 01:33:22 +08:00
if ( isBonus )
2023-06-19 20:38:13 +08:00
{
legacyBonusScore + = scoreIncrease ;
2023-12-21 13:58:23 +08:00
standardisedBonusScore + = scoreProcessor . GetBaseScoreForResult ( bonusResult ) ;
2023-06-19 20:38:13 +08:00
}
2023-06-13 01:33:22 +08:00
else
2023-09-04 16:43:23 +08:00
attributes . AccuracyScore + = scoreIncrease ;
2023-06-13 01:33:22 +08:00
if ( increaseCombo )
combo + + ;
}
2023-10-02 14:58:31 +08:00
public double GetLegacyScoreMultiplier ( IReadOnlyList < Mod > mods , LegacyBeatmapConversionDifficultyInfo difficulty )
{
bool scoreV2 = mods . Any ( m = > m is ModScoreV2 ) ;
double multiplier = 1.0 ;
foreach ( var mod in mods )
{
switch ( mod )
{
case TaikoModNoFail :
multiplier * = scoreV2 ? 1.0 : 0.5 ;
break ;
case TaikoModEasy :
multiplier * = 0.5 ;
break ;
case TaikoModHalfTime :
case TaikoModDaycore :
multiplier * = 0.3 ;
break ;
case TaikoModHidden :
case TaikoModHardRock :
multiplier * = 1.06 ;
break ;
case TaikoModDoubleTime :
case TaikoModNightcore :
case TaikoModFlashlight :
multiplier * = 1.12 ;
break ;
case TaikoModRelax :
return 0 ;
}
}
return multiplier ;
}
2023-06-13 01:33:22 +08:00
}
}