osu/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

199 lines
7.4 KiB
C#
Raw Normal View History

// 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;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
2023-06-19 12:38:13 +00:00
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty
{
internal class TaikoLegacyScoreSimulator : ILegacyScoreSimulator
{
2023-06-19 12:38:13 +00:00
private int legacyBonusScore;
private int standardisedBonusScore;
private int combo;
2023-06-26 13:19:01 +00:00
private int difficultyPeppyStars;
private IBeatmap playableBeatmap = null!;
public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap)
{
this.playableBeatmap = playableBeatmap;
2023-06-26 13:19:01 +00:00
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
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;
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;
}
difficultyPeppyStars = (int)Math.Round(
(baseBeatmap.Difficulty.DrainRate
+ baseBeatmap.Difficulty.OverallDifficulty
+ baseBeatmap.Difficulty.CircleSize
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
LegacyScoreAttributes attributes = new LegacyScoreAttributes();
foreach (var obj in playableBeatmap.HitObjects)
simulateHit(obj, ref attributes);
attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore;
return attributes;
}
private void simulateHit(HitObject hitObject, ref LegacyScoreAttributes attributes)
{
bool increaseCombo = true;
bool addScoreComboMultiplier = false;
2023-06-19 12:38:13 +00:00
bool isBonus = false;
2023-06-19 12:38:13 +00:00
HitResult bonusResult = HitResult.None;
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;
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-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);
halfSpinsRequiredForCompletion = (int)Math.Max(1, halfSpinsRequiredForCompletion * 1.65f);
//
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.
// 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));
for (int i = 0; i <= halfSpinsRequiredForCompletion; i++)
simulateHit(new SwellTick(), ref attributes);
scoreIncrease = 300;
addScoreComboMultiplier = true;
increaseCombo = false;
isBonus = true;
2023-06-19 12:38:13 +00:00
bonusResult = HitResult.LargeBonus;
break;
case Hit:
scoreIncrease = 300;
addScoreComboMultiplier = true;
break;
case DrumRoll:
foreach (var nested in hitObject.NestedHitObjects)
simulateHit(nested, ref attributes);
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;
scoreIncrease += (int)(scoreIncrease / 35f * 2 * (difficultyPeppyStars + 1)) * (Math.Min(100, combo) / 10);
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)
attributes.ComboScore += comboScoreIncrease;
if (isBonus)
2023-06-19 12:38:13 +00:00
{
legacyBonusScore += scoreIncrease;
standardisedBonusScore += Judgement.ToNumericResult(bonusResult);
2023-06-19 12:38:13 +00:00
}
else
attributes.AccuracyScore += scoreIncrease;
if (increaseCombo)
combo++;
}
}
}