2023-06-12 22:05:09 +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-12 22:05:09 +08:00
|
|
|
using System.Linq;
|
|
|
|
using osu.Game.Beatmaps;
|
2023-06-19 20:38:13 +08:00
|
|
|
using osu.Game.Rulesets.Judgements;
|
2023-10-02 14:58:31 +08:00
|
|
|
using osu.Game.Rulesets.Mods;
|
2023-06-12 22:05:09 +08:00
|
|
|
using osu.Game.Rulesets.Objects;
|
|
|
|
using osu.Game.Rulesets.Objects.Types;
|
2023-10-02 14:58:31 +08:00
|
|
|
using osu.Game.Rulesets.Osu.Mods;
|
2023-06-12 22:05:09 +08:00
|
|
|
using osu.Game.Rulesets.Osu.Objects;
|
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-06-12 22:05:09 +08:00
|
|
|
|
|
|
|
namespace osu.Game.Rulesets.Osu.Difficulty
|
|
|
|
{
|
2023-07-04 16:32:54 +08:00
|
|
|
internal class OsuLegacyScoreSimulator : ILegacyScoreSimulator
|
2023-06-12 22:05:09 +08:00
|
|
|
{
|
2023-06-19 20:38:13 +08:00
|
|
|
private int legacyBonusScore;
|
2023-09-04 16:43:23 +08:00
|
|
|
private int standardisedBonusScore;
|
2023-06-12 22:05:09 +08:00
|
|
|
private int combo;
|
|
|
|
|
2023-06-26 21:19:01 +08:00
|
|
|
private double scoreMultiplier;
|
2023-06-12 22:05:09 +08:00
|
|
|
|
2023-09-04 16:43:23 +08:00
|
|
|
public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap)
|
2023-06-12 22:05:09 +08:00
|
|
|
{
|
2023-06-26 21:19:01 +08:00
|
|
|
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
|
|
|
|
2023-06-12 22:05:09 +08:00
|
|
|
int countNormal = 0;
|
|
|
|
int countSlider = 0;
|
|
|
|
int countSpinner = 0;
|
|
|
|
|
2023-06-26 21:19:01 +08:00
|
|
|
foreach (HitObject obj in workingBeatmap.Beatmap.HitObjects)
|
2023-06-12 22:05:09 +08:00
|
|
|
{
|
|
|
|
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-12 22:05:09 +08:00
|
|
|
int 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-12 22:05:09 +08:00
|
|
|
|
2023-09-04 16:43:23 +08:00
|
|
|
scoreMultiplier = difficultyPeppyStars;
|
|
|
|
|
|
|
|
LegacyScoreAttributes attributes = new LegacyScoreAttributes();
|
2023-06-12 22:05:09 +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;
|
|
|
|
|
|
|
|
return attributes;
|
2023-06-12 22:05:09 +08:00
|
|
|
}
|
|
|
|
|
2023-09-04 16:43:23 +08:00
|
|
|
private void simulateHit(HitObject hitObject, ref LegacyScoreAttributes attributes)
|
2023-06-12 22:05:09 +08:00
|
|
|
{
|
|
|
|
bool increaseCombo = true;
|
|
|
|
bool addScoreComboMultiplier = false;
|
2023-06-19 20:38:13 +08:00
|
|
|
|
2023-06-12 22:05:09 +08:00
|
|
|
bool isBonus = false;
|
2023-06-19 20:38:13 +08:00
|
|
|
HitResult bonusResult = HitResult.None;
|
2023-06-12 22:05:09 +08:00
|
|
|
|
|
|
|
int scoreIncrease = 0;
|
|
|
|
|
|
|
|
switch (hitObject)
|
|
|
|
{
|
|
|
|
case SliderHeadCircle:
|
|
|
|
case SliderTailCircle:
|
|
|
|
case SliderRepeat:
|
|
|
|
scoreIncrease = 30;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SliderTick:
|
|
|
|
scoreIncrease = 10;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SpinnerBonusTick:
|
|
|
|
scoreIncrease = 1100;
|
|
|
|
increaseCombo = false;
|
|
|
|
isBonus = true;
|
2023-06-19 20:38:13 +08:00
|
|
|
bonusResult = HitResult.LargeBonus;
|
2023-06-12 22:05:09 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
case SpinnerTick:
|
|
|
|
scoreIncrease = 100;
|
|
|
|
increaseCombo = false;
|
|
|
|
isBonus = true;
|
2023-06-19 20:38:13 +08:00
|
|
|
bonusResult = HitResult.SmallBonus;
|
2023-06-12 22:05:09 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
case HitCircle:
|
|
|
|
scoreIncrease = 300;
|
|
|
|
addScoreComboMultiplier = true;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Slider:
|
|
|
|
foreach (var nested in hitObject.NestedHitObjects)
|
2023-09-04 16:43:23 +08:00
|
|
|
simulateHit(nested, ref attributes);
|
2023-06-12 22:05:09 +08:00
|
|
|
|
|
|
|
scoreIncrease = 300;
|
|
|
|
increaseCombo = false;
|
|
|
|
addScoreComboMultiplier = true;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Spinner spinner:
|
|
|
|
// The spinner object applies a lenience because gameplay mechanics differ from osu-stable.
|
|
|
|
// We'll redo the calculations to match osu-stable here...
|
|
|
|
const double maximum_rotations_per_second = 477.0 / 60;
|
2023-09-08 20:08:09 +08:00
|
|
|
|
2023-09-15 14:51:05 +08:00
|
|
|
// Normally, this value depends on the final overall difficulty. For simplicity, we'll only consider the worst case that maximises bonus score.
|
2023-09-15 17:35:17 +08:00
|
|
|
// As we're primarily concerned with computing the maximum theoretical final score,
|
|
|
|
// this will have the final effect of slightly underestimating bonus score achieved on stable when converting from score V1.
|
2023-09-15 14:51:05 +08:00
|
|
|
const double minimum_rotations_per_second = 3;
|
2023-09-08 20:08:09 +08:00
|
|
|
|
2023-06-12 22:05:09 +08:00
|
|
|
double secondsDuration = spinner.Duration / 1000;
|
|
|
|
|
|
|
|
// The total amount of half spins possible for the entire spinner.
|
|
|
|
int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2);
|
|
|
|
// The amount of half spins that are required to successfully complete the spinner (i.e. get a 300).
|
2023-09-08 20:08:09 +08:00
|
|
|
int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimum_rotations_per_second);
|
2023-06-12 22:05:09 +08:00
|
|
|
// To be able to receive bonus points, the spinner must be rotated another 1.5 times.
|
|
|
|
int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3;
|
|
|
|
|
|
|
|
for (int i = 0; i <= totalHalfSpinsPossible; i++)
|
|
|
|
{
|
|
|
|
if (i > halfSpinsRequiredBeforeBonus && (i - halfSpinsRequiredBeforeBonus) % 2 == 0)
|
2023-09-04 16:43:23 +08:00
|
|
|
simulateHit(new SpinnerBonusTick(), ref attributes);
|
2023-06-12 22:05:09 +08:00
|
|
|
else if (i > 1 && i % 2 == 0)
|
2023-09-04 16:43:23 +08:00
|
|
|
simulateHit(new SpinnerTick(), ref attributes);
|
2023-06-12 22:05:09 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
scoreIncrease = 300;
|
|
|
|
addScoreComboMultiplier = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (addScoreComboMultiplier)
|
|
|
|
{
|
|
|
|
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
2023-09-04 16:43:23 +08:00
|
|
|
attributes.ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier));
|
2023-06-12 22:05:09 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (isBonus)
|
2023-06-19 20:38:13 +08:00
|
|
|
{
|
|
|
|
legacyBonusScore += scoreIncrease;
|
2023-09-04 16:43:23 +08:00
|
|
|
standardisedBonusScore += Judgement.ToNumericResult(bonusResult);
|
2023-06-19 20:38:13 +08:00
|
|
|
}
|
2023-06-12 22:05:09 +08:00
|
|
|
else
|
2023-09-04 16:43:23 +08:00
|
|
|
attributes.AccuracyScore += scoreIncrease;
|
2023-06-12 22:05:09 +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 OsuModNoFail:
|
|
|
|
multiplier *= scoreV2 ? 1.0 : 0.5;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case OsuModEasy:
|
|
|
|
multiplier *= 0.5;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case OsuModHalfTime:
|
|
|
|
case OsuModDaycore:
|
|
|
|
multiplier *= 0.3;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case OsuModHidden:
|
|
|
|
multiplier *= 1.06;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case OsuModHardRock:
|
|
|
|
multiplier *= scoreV2 ? 1.10 : 1.06;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case OsuModDoubleTime:
|
|
|
|
case OsuModNightcore:
|
|
|
|
multiplier *= scoreV2 ? 1.20 : 1.12;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case OsuModFlashlight:
|
|
|
|
multiplier *= 1.12;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case OsuModSpunOut:
|
|
|
|
multiplier *= 0.9;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case OsuModRelax:
|
|
|
|
case OsuModAutopilot:
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return multiplier;
|
|
|
|
}
|
2023-06-12 22:05:09 +08:00
|
|
|
}
|
|
|
|
}
|