diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index 425f72cadc..5685ac0f60 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -16,13 +16,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
- [TestCase(3.1098944660126882d, 200, "diffcalc-test")]
- [TestCase(3.1098944660126882d, 200, "diffcalc-test-strong")]
+ [TestCase(3.0920212594351191d, 200, "diffcalc-test")]
+ [TestCase(3.0920212594351191d, 200, "diffcalc-test-strong")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(4.0974106752474251d, 200, "diffcalc-test")]
- [TestCase(4.0974106752474251d, 200, "diffcalc-test-strong")]
+ [TestCase(4.0789820318081444d, 200, "diffcalc-test")]
+ [TestCase(4.0789820318081444d, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
index 7d88be2f70..9f63e84867 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
@@ -54,11 +54,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour;
double difficulty = 0.0d;
- if (colour.MonoStreak != null) // Difficulty for MonoStreak
+ if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak
difficulty += EvaluateDifficultyOf(colour.MonoStreak);
- if (colour.AlternatingMonoPattern != null) // Difficulty for AlternatingMonoPattern
+ if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern
difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern);
- if (colour.RepeatingHitPattern != null) // Difficulty for RepeatingHitPattern
+ if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern);
return difficulty;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
index 49b3ae2e19..84d5de4c63 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
@@ -11,22 +11,43 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
public class StaminaEvaluator
{
///
- /// Applies a speed bonus dependent on the time since the last hit performed using this key.
+ /// Applies a speed bonus dependent on the time since the last hit performed using this finger.
///
- /// The interval between the current and previous note hit using the same key.
+ /// The interval between the current and previous note hit using the same finger.
private static double speedBonus(double interval)
{
- // Cap to 600bpm 1/4, 25ms note interval, 50ms key interval
- // Interval will be capped at a very small value to avoid infinite/negative speed bonuses.
- // TODO - This is a temporary measure as we need to implement methods of detecting playstyle-abuse of SpeedBonus.
- interval = Math.Max(interval, 50);
+ // Interval is capped at a very small value to prevent infinite values.
+ interval = Math.Max(interval, 1);
return 30 / interval;
}
+ ///
+ /// Determines the number of fingers available to hit the current .
+ /// Any mono notes that is more than 300ms apart from a colour change will be considered to have more than 2
+ /// fingers available, since players can hit the same key with multiple fingers.
+ ///
+ private static int availableFingersFor(TaikoDifficultyHitObject hitObject)
+ {
+ DifficultyHitObject? previousColourChange = hitObject.Colour.PreviousColourChange;
+ DifficultyHitObject? nextColourChange = hitObject.Colour.NextColourChange;
+
+ if (previousColourChange != null && hitObject.StartTime - previousColourChange.StartTime < 300)
+ {
+ return 2;
+ }
+
+ if (nextColourChange != null && nextColourChange.StartTime - hitObject.StartTime < 300)
+ {
+ return 2;
+ }
+
+ return 4;
+ }
+
///
/// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the
- /// maximum possible interval between two hits using the same key, by alternating 2 keys for each colour.
+ /// maximum possible interval between two hits using the same key, by alternating available fingers for each colour.
///
public static double EvaluateDifficultyOf(DifficultyHitObject current)
{
@@ -35,13 +56,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
return 0.0;
}
- // Find the previous hit object hit by the current key, which is two notes of the same colour prior.
+ // Find the previous hit object hit by the current finger, which is n notes prior, n being the number of
+ // available fingers.
TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current;
- TaikoDifficultyHitObject? keyPrevious = taikoCurrent.PreviousMono(1);
+ TaikoDifficultyHitObject? keyPrevious = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1);
if (keyPrevious == null)
{
- // There is no previous hit object hit by the current key
+ // There is no previous hit object hit by the current finger
return 0.0;
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs
index 174988bed7..c01a0f6686 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs
@@ -33,6 +33,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data
///
public TaikoDifficultyHitObject FirstHitObject => HitObjects[0];
+ ///
+ /// The last in this .
+ ///
+ public TaikoDifficultyHitObject LastHitObject => HitObjects[^1];
+
///
/// The hit type of all objects encoded within this
///
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPatterns.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPatterns.cs
index fe0dc6dd9a..468a9ab876 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPatterns.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPatterns.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data
public readonly List AlternatingMonoPatterns = new List();
///
- /// The parent in this
+ /// The first in this
///
public TaikoDifficultyHitObject FirstHitObject => AlternatingMonoPatterns[0].FirstHitObject;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs
index d19e05f4e0..18a299ae92 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs
@@ -15,19 +15,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
{
///
/// Processes and encodes a list of s into a list of s,
- /// assigning the appropriate s to each ,
- /// and pre-evaluating colour difficulty of each .
+ /// assigning the appropriate s to each .
///
public static void ProcessAndAssign(List hitObjects)
{
List hitPatterns = encode(hitObjects);
- // Assign indexing and encoding data to all relevant objects. Only the first note of each encoding type is
- // assigned with the relevant encodings.
+ // Assign indexing and encoding data to all relevant objects.
foreach (var repeatingHitPattern in hitPatterns)
{
- repeatingHitPattern.FirstHitObject.Colour.RepeatingHitPattern = repeatingHitPattern;
-
// The outermost loop is kept a ForEach loop since it doesn't need index information, and we want to
// keep i and j for AlternatingMonoPattern's and MonoStreak's index respectively, to keep it in line with
// documentation.
@@ -36,14 +32,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
AlternatingMonoPattern monoPattern = repeatingHitPattern.AlternatingMonoPatterns[i];
monoPattern.Parent = repeatingHitPattern;
monoPattern.Index = i;
- monoPattern.FirstHitObject.Colour.AlternatingMonoPattern = monoPattern;
for (int j = 0; j < monoPattern.MonoStreaks.Count; ++j)
{
MonoStreak monoStreak = monoPattern.MonoStreaks[j];
monoStreak.Parent = monoPattern;
monoStreak.Index = j;
- monoStreak.FirstHitObject.Colour.MonoStreak = monoStreak;
+
+ foreach (var hitObject in monoStreak.HitObjects)
+ {
+ hitObject.Colour.RepeatingHitPattern = repeatingHitPattern;
+ hitObject.Colour.AlternatingMonoPattern = monoPattern;
+ hitObject.Colour.MonoStreak = monoStreak;
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs
index 9c147eee9c..abf6fb3672 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs
@@ -11,18 +11,28 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
public class TaikoDifficultyHitObjectColour
{
///
- /// The that encodes this note, only present if this is the first note within a
+ /// The that encodes this note.
///
public MonoStreak? MonoStreak;
///
- /// The that encodes this note, only present if this is the first note within a
+ /// The that encodes this note.
///
public AlternatingMonoPattern? AlternatingMonoPattern;
///
- /// The that encodes this note, only present if this is the first note within a
+ /// The that encodes this note.
///
public RepeatingHitPatterns? RepeatingHitPattern;
+
+ ///
+ /// The closest past that's not the same colour.
+ ///
+ public TaikoDifficultyHitObject? PreviousColourChange => MonoStreak?.FirstHitObject.PreviousNote(0);
+
+ ///
+ /// The closest future that's not the same colour.
+ ///
+ public TaikoDifficultyHitObject? NextColourChange => MonoStreak?.LastHitObject.NextNote(0);
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
index 344004bcf6..d04c028fec 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
@@ -13,9 +13,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
/// Calculates the stamina coefficient of taiko difficulty.
///
- ///
- /// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit).
- ///
public class Stamina : StrainDecaySkill
{
protected override double SkillMultiplier => 1.1;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index 2b0b563323..24b5f5939a 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -83,15 +83,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double combinedRating = combined.DifficultyValue() * difficulty_multiplier;
double starRating = rescale(combinedRating * 1.4);
- // TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system.
- if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0)
- {
- starRating *= 0.925;
- // For maps with low colour variance and high stamina requirement, multiple inputs are more likely to be abused.
- if (colourRating < 2 && staminaRating > 8)
- starRating *= 0.80;
- }
-
HitWindows hitWindows = new TaikoHitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index dc7bad2f75..2d1b2903c9 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -43,6 +43,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (totalSuccessfulHits > 0)
effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss;
+ // TODO: The detection of rulesets is temporary until the leftover old skills have been reworked.
+ bool isConvert = score.BeatmapInfo.Ruleset.OnlineID != 1;
+
double multiplier = 1.13;
if (score.Mods.Any(m => m is ModHidden))
@@ -51,8 +54,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (score.Mods.Any(m => m is ModEasy))
multiplier *= 0.975;
- double difficultyValue = computeDifficultyValue(score, taikoAttributes);
- double accuracyValue = computeAccuracyValue(score, taikoAttributes);
+ double difficultyValue = computeDifficultyValue(score, taikoAttributes, isConvert);
+ double accuracyValue = computeAccuracyValue(score, taikoAttributes, isConvert);
double totalValue =
Math.Pow(
Math.Pow(difficultyValue, 1.1) +
@@ -68,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
};
}
- private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
+ private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
{
double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0, 2.25) / 1150.0;
@@ -80,7 +83,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (score.Mods.Any(m => m is ModEasy))
difficultyValue *= 0.985;
- if (score.Mods.Any(m => m is ModHidden))
+ if (score.Mods.Any(m => m is ModHidden) && !isConvert)
difficultyValue *= 1.025;
if (score.Mods.Any(m => m is ModHardRock))
@@ -92,7 +95,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
return difficultyValue * Math.Pow(accuracy, 2.0);
}
- private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
+ private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
{
if (attributes.GreatHitWindow <= 0)
return 0;
@@ -102,9 +105,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
accuracyValue *= lengthBonus;
- // Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values
- if (score.Mods.Any(m => m is ModFlashlight) && score.Mods.Any(m => m is ModHidden))
- accuracyValue *= Math.Max(1.050, 1.075 * lengthBonus);
+ // Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values.
+ if (score.Mods.Any(m => m is ModFlashlight) && score.Mods.Any(m => m is ModHidden) && !isConvert)
+ accuracyValue *= Math.Max(1.0, 1.1 * lengthBonus);
return accuracyValue;
}