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; }