diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 3ff5b87fb6..b715dfc37a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -10,32 +10,8 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data; namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { - public class ColourEvaluator + public static class ColourEvaluator { - /// - /// Evaluate the difficulty of the first note of a . - /// - public static double EvaluateDifficultyOf(MonoStreak monoStreak) - { - return DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5; - } - - /// - /// Evaluate the difficulty of the first note of a . - /// - public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern) - { - return DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * EvaluateDifficultyOf(alternatingMonoPattern.Parent); - } - - /// - /// Evaluate the difficulty of the first note of a . - /// - public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern) - { - return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E)); - } - /// /// Calculates a consistency penalty based on the number of consecutive consistent intervals, /// considering the delta time between each colour sequence. @@ -58,8 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators var previousHitObject = (TaikoDifficultyHitObject)current.Previous(1); - double currentRatio = current.Rhythm.Ratio; - double previousRatio = previousHitObject.Rhythm.Ratio; + double currentRatio = current.RhythmData.Ratio; + double previousRatio = previousHitObject.RhythmData.Ratio; // A consistent interval is defined as the percentage difference between the two rhythmic ratios with the margin of error. if (Math.Abs(1 - currentRatio / previousRatio) <= threshold) @@ -85,22 +61,31 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators public static double EvaluateDifficultyOf(DifficultyHitObject hitObject) { var taikoObject = (TaikoDifficultyHitObject)hitObject; - TaikoDifficultyHitObjectColour colour = taikoObject.Colour; + TaikoColourData colourData = taikoObject.ColourData; double difficulty = 0.0d; - if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak - difficulty += EvaluateDifficultyOf(colour.MonoStreak); + if (colourData.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak + difficulty += evaluateMonoStreakDifficulty(colourData.MonoStreak); - if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern - difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern); + if (colourData.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern + difficulty += evaluateAlternatingMonoPatternDifficulty(colourData.AlternatingMonoPattern); - if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern - difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern); + if (colourData.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern + difficulty += evaluateRepeatingHitPatternsDifficulty(colourData.RepeatingHitPattern); double consistencyPenalty = consistentRatioPenalty(taikoObject); difficulty *= consistencyPenalty; return difficulty; } + + private static double evaluateMonoStreakDifficulty(MonoStreak monoStreak) => + DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * evaluateAlternatingMonoPatternDifficulty(monoStreak.Parent) * 0.5; + + private static double evaluateAlternatingMonoPatternDifficulty(AlternatingMonoPattern alternatingMonoPattern) => + DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * evaluateRepeatingHitPatternsDifficulty(alternatingMonoPattern.Parent); + + private static double evaluateRepeatingHitPatternsDifficulty(RepeatingHitPatterns repeatingHitPattern) => + 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E)); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs index 2a08f65c7b..5871979613 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs @@ -47,8 +47,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators // High density is penalised at high velocity as it is generally considered easier to read. See https://www.desmos.com/calculator/u63f3ntdsi double densityPenalty = DifficultyCalculationUtils.Logistic(objectDensity, 0.925, 15); - double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty) * DifficultyCalculationUtils.Logistic - (effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10)); + double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty) + * DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10)); return midVelocityDifficulty + highVelocityDifficulty; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index 22321a8f6e..3b3aea07f3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -18,39 +18,39 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow) { - TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm; + TaikoRhythmData rhythmData = ((TaikoDifficultyHitObject)hitObject).RhythmData; double difficulty = 0.0d; double sameRhythm = 0; double samePattern = 0; double intervalPenalty = 0; - if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects + if (rhythmData.SameRhythmGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmGroupedHitObjects { - sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); - intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow); + sameRhythm += 10.0 * evaluateDifficultyOf(rhythmData.SameRhythmGroupedHitObjects, hitWindow); + intervalPenalty = repeatedIntervalPenalty(rhythmData.SameRhythmGroupedHitObjects, hitWindow); } - if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns - samePattern += 1.15 * ratioDifficulty(rhythm.SamePatterns.IntervalRatio); + if (rhythmData.SamePatternsGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SamePatternsGroupedHitObjects + samePattern += 1.15 * ratioDifficulty(rhythmData.SamePatternsGroupedHitObjects.IntervalRatio); difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty; return difficulty; } - private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow) + private static double evaluateDifficultyOf(SameRhythmHitObjectGrouping sameRhythmGroupedHitObjects, double hitWindow) { - double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio); - double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval; + double intervalDifficulty = ratioDifficulty(sameRhythmGroupedHitObjects.HitObjectIntervalRatio); + double? previousInterval = sameRhythmGroupedHitObjects.Previous?.HitObjectInterval; - intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); + intervalDifficulty *= repeatedIntervalPenalty(sameRhythmGroupedHitObjects, hitWindow); // If a previous interval exists and there are multiple hit objects in the sequence: - if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1) + if (previousInterval != null && sameRhythmGroupedHitObjects.HitObjects.Count > 1) { - double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count; - double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious; + double expectedDurationFromPrevious = (double)previousInterval * sameRhythmGroupedHitObjects.HitObjects.Count; + double durationDifference = sameRhythmGroupedHitObjects.Duration - expectedDurationFromPrevious; if (durationDifference > 0) { @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators // Penalise patterns that can be hit within a single hit window. intervalDifficulty *= DifficultyCalculationUtils.Logistic( - sameRhythmHitObjects.Duration / hitWindow, + sameRhythmGroupedHitObjects.Duration / hitWindow, midpointOffset: 0.6, multiplier: 1, maxValue: 1); @@ -75,20 +75,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// /// Determines if the changes in hit object intervals is consistent based on a given threshold. /// - private static double repeatedIntervalPenalty(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow, double threshold = 0.1) + private static double repeatedIntervalPenalty(SameRhythmHitObjectGrouping sameRhythmGroupedHitObjects, double hitWindow, double threshold = 0.1) { - double longIntervalPenalty = sameInterval(sameRhythmHitObjects, 3); + double longIntervalPenalty = sameInterval(sameRhythmGroupedHitObjects, 3); - double shortIntervalPenalty = sameRhythmHitObjects.Children.Count < 6 - ? sameInterval(sameRhythmHitObjects, 4) + double shortIntervalPenalty = sameRhythmGroupedHitObjects.HitObjects.Count < 6 + ? sameInterval(sameRhythmGroupedHitObjects, 4) : 1.0; // Returns a non-penalty if there are 6 or more notes within an interval. // The duration penalty is based on hit object duration relative to hitWindow. - double durationPenalty = Math.Max(1 - sameRhythmHitObjects.Duration * 2 / hitWindow, 0.5); + double durationPenalty = Math.Max(1 - sameRhythmGroupedHitObjects.Duration * 2 / hitWindow, 0.5); return Math.Min(longIntervalPenalty, shortIntervalPenalty) * durationPenalty; - double sameInterval(SameRhythmHitObjects startObject, int intervalCount) + double sameInterval(SameRhythmHitObjectGrouping startObject, int intervalCount) { List intervals = new List(); var currentObject = startObject; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index b39ad953a4..32ed8ec189 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -8,43 +8,8 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { - public class StaminaEvaluator + public static class StaminaEvaluator { - /// - /// 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 finger. - private static double speedBonus(double interval) - { - // Interval is capped at a very small value to prevent infinite values. - interval = Math.Max(interval, 1); - - return 20 / 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 8; - } - /// /// 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 available fingers for each colour. @@ -70,5 +35,40 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return objectStrain; } + + /// + /// 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 finger. + private static double speedBonus(double interval) + { + // Interval is capped at a very small value to prevent infinite values. + interval = Math.Max(interval, 1); + + return 20 / 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.ColourData.PreviousColourChange; + DifficultyHitObject? nextColourChange = hitObject.ColourData.NextColourChange; + + if (previousColourChange != null && hitObject.StartTime - previousColourChange.StartTime < 300) + { + return 2; + } + + if (nextColourChange != null && nextColourChange.StartTime - hitObject.StartTime < 300) + { + return 2; + } + + return 8; + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourData.cs similarity index 96% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourData.cs index abf6fb3672..81201b6584 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourData.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// /// Stores colour compression information for a . /// - public class TaikoDifficultyHitObjectColour + public class TaikoColourData { /// /// The that encodes this note. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index 18a299ae92..3c6ef7c53c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -14,8 +14,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour public static class TaikoColourDifficultyPreprocessor { /// - /// Processes and encodes a list of s into a list of s, - /// assigning the appropriate s to each . + /// Processes and encodes a list of s into a list of s, + /// assigning the appropriate s to each . /// public static void ProcessAndAssign(List hitObjects) { @@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour foreach (var hitObject in monoStreak.HitObjects) { - hitObject.Colour.RepeatingHitPattern = repeatingHitPattern; - hitObject.Colour.AlternatingMonoPattern = monoPattern; - hitObject.Colour.MonoStreak = monoStreak; + hitObject.ColourData.RepeatingHitPattern = repeatingHitPattern; + hitObject.ColourData.AlternatingMonoPattern = monoPattern; + hitObject.ColourData.MonoStreak = monoStreak; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs deleted file mode 100644 index 17e05d5fbf..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading -{ - public class EffectiveBPMPreprocessor - { - private readonly IList noteObjects; - private readonly double globalSliderVelocity; - - public EffectiveBPMPreprocessor(IBeatmap beatmap, List noteObjects) - { - this.noteObjects = noteObjects; - globalSliderVelocity = beatmap.Difficulty.SliderMultiplier; - } - - /// - /// Calculates and sets the effective BPM and slider velocity for each note object, considering clock rate and scroll speed. - /// - public void ProcessEffectiveBPM(ControlPointInfo controlPointInfo, double clockRate) - { - foreach (var currentNoteObject in noteObjects) - { - double startTime = currentNoteObject.StartTime * clockRate; - - // Retrieve the timing point at the note's start time - TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime); - - // Calculate the slider velocity at the note's start time. - double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, startTime, clockRate); - currentNoteObject.CurrentSliderVelocity = currentSliderVelocity; - - currentNoteObject.EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; - } - } - - /// - /// Calculates the slider velocity based on control point info and clock rate. - /// - private double calculateSliderVelocity(ControlPointInfo controlPointInfo, double startTime, double clockRate) - { - var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime); - return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs deleted file mode 100644 index 50839c4561..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using System.Linq; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data -{ - /// - /// Represents grouped by their 's interval. - /// - public class SamePatterns : SameRhythm - { - public SamePatterns? Previous { get; private set; } - - /// - /// The between children within this group. - /// If there is only one child, this will have the value of the first child's . - /// - public double ChildrenInterval => Children.Count > 1 ? Children[1].Interval : Children[0].Interval; - - /// - /// The ratio of between this and the previous . In the - /// case where there is no previous , this will have a value of 1. - /// - public double IntervalRatio => ChildrenInterval / Previous?.ChildrenInterval ?? 1.0d; - - public TaikoDifficultyHitObject FirstHitObject => Children[0].FirstHitObject; - - public IEnumerable AllHitObjects => Children.SelectMany(child => child.Children); - - private SamePatterns(SamePatterns? previous, List data, ref int i) - : base(data, ref i, 5) - { - Previous = previous; - - foreach (TaikoDifficultyHitObject hitObject in AllHitObjects) - { - hitObject.Rhythm.SamePatterns = this; - } - } - - public static void GroupPatterns(List data) - { - List samePatterns = new List(); - - // Index does not need to be incremented, as it is handled within the SameRhythm constructor. - for (int i = 0; i < data.Count;) - { - SamePatterns? previous = samePatterns.Count > 0 ? samePatterns[^1] : null; - samePatterns.Add(new SamePatterns(previous, data, ref i)); - } - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs new file mode 100644 index 0000000000..938cb4670f --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data +{ + /// + /// Represents grouped by their 's interval. + /// + public class SamePatternsGroupedHitObjects + { + public IReadOnlyList Groups { get; } + + public SamePatternsGroupedHitObjects? Previous { get; } + + /// + /// The between groups . + /// If there is only one group, this will have the value of the first group's . + /// + public double GroupInterval => Groups.Count > 1 ? Groups[1].Interval : Groups[0].Interval; + + /// + /// The ratio of between this and the previous . In the + /// case where there is no previous , this will have a value of 1. + /// + public double IntervalRatio => GroupInterval / Previous?.GroupInterval ?? 1.0d; + + public TaikoDifficultyHitObject FirstHitObject => Groups[0].FirstHitObject; + + public IEnumerable AllHitObjects => Groups.SelectMany(hitObject => hitObject.HitObjects); + + public SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List groups) + { + Previous = previous; + Groups = groups; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs deleted file mode 100644 index b1ca22595b..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data -{ - /// - /// A base class for grouping s by their interval. In edges where an interval change - /// occurs, the is added to the group with the smaller interval. - /// - public abstract class SameRhythm - where ChildType : IHasInterval - { - public IReadOnlyList Children { get; private set; } - - /// - /// Determines if the intervals between two child objects are within a specified margin of error, - /// indicating that the intervals are effectively "flat" or consistent. - /// - private bool isFlat(ChildType current, ChildType previous, double marginOfError) - { - return Math.Abs(current.Interval - previous.Interval) <= marginOfError; - } - - /// - /// Create a new from a list of s, and add - /// them to the list until the end of the group. - /// - /// The list of s. - /// - /// Index in to start adding children. This will be modified and should be passed into - /// the next 's constructor. - /// - /// - /// The margin of error for the interval, within of which no interval change is considered to have occured. - /// - protected SameRhythm(List data, ref int i, double marginOfError) - { - List children = new List(); - Children = children; - children.Add(data[i]); - i++; - - for (; i < data.Count - 1; i++) - { - // An interval change occured, add the current data if the next interval is larger. - if (!isFlat(data[i], data[i + 1], marginOfError)) - { - if (data[i + 1].Interval > data[i].Interval + marginOfError) - { - children.Add(data[i]); - i++; - } - - return; - } - - // No interval change occured - children.Add(data[i]); - } - - // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error. - // If true, add the current object to the group and increment the index to process the next object. - if (data.Count > 2 && isFlat(data[^1], data[^2], marginOfError)) - { - children.Add(data[i]); - i++; - } - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs new file mode 100644 index 0000000000..9caa9b9958 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs @@ -0,0 +1,63 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Utils; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data +{ + /// + /// Represents a group of s with no rhythm variation. + /// + public class SameRhythmHitObjectGrouping : IHasInterval + { + public readonly List HitObjects; + + public TaikoDifficultyHitObject FirstHitObject => HitObjects[0]; + + public readonly SameRhythmHitObjectGrouping? Previous; + + /// + /// of the first hit object. + /// + public double StartTime => HitObjects[0].StartTime; + + /// + /// The interval between the first and final hit object within this group. + /// + public double Duration => HitObjects[^1].StartTime - HitObjects[0].StartTime; + + /// + /// The interval in ms of each hit object in this . This is only defined if there is + /// more than two hit objects in this . + /// + public readonly double? HitObjectInterval; + + /// + /// The ratio of between this and the previous . In the + /// case where one or both of the is undefined, this will have a value of 1. + /// + public readonly double HitObjectIntervalRatio; + + /// + public double Interval { get; } + + public SameRhythmHitObjectGrouping(SameRhythmHitObjectGrouping? previous, List hitObjects) + { + Previous = previous; + HitObjects = hitObjects; + + // Calculate the average interval between hitobjects, or null if there are fewer than two + HitObjectInterval = HitObjects.Count < 2 ? null : Duration / (HitObjects.Count - 1); + + // Calculate the ratio between this group's interval and the previous group's interval + HitObjectIntervalRatio = Previous?.HitObjectInterval != null && HitObjectInterval != null + ? HitObjectInterval.Value / Previous.HitObjectInterval.Value + : 1; + + // Calculate the interval from the previous group's start time + Interval = Previous != null ? StartTime - Previous.StartTime : double.PositiveInfinity; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs deleted file mode 100644 index 0ccc6da026..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Game.Rulesets.Difficulty.Preprocessing; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data -{ - /// - /// Represents a group of s with no rhythm variation. - /// - public class SameRhythmHitObjects : SameRhythm, IHasInterval - { - public TaikoDifficultyHitObject FirstHitObject => Children[0]; - - public SameRhythmHitObjects? Previous; - - /// - /// of the first hit object. - /// - public double StartTime => Children[0].StartTime; - - /// - /// The interval between the first and final hit object within this group. - /// - public double Duration => Children[^1].StartTime - Children[0].StartTime; - - /// - /// The interval in ms of each hit object in this . This is only defined if there is - /// more than two hit objects in this . - /// - public double? HitObjectInterval; - - /// - /// The ratio of between this and the previous . In the - /// case where one or both of the is undefined, this will have a value of 1. - /// - public double HitObjectIntervalRatio = 1; - - /// - /// The interval between the of this and the previous . - /// - public double Interval { get; private set; } = double.PositiveInfinity; - - public SameRhythmHitObjects(SameRhythmHitObjects? previous, List data, ref int i) - : base(data, ref i, 5) - { - Previous = previous; - - foreach (var hitObject in Children) - { - hitObject.Rhythm.SameRhythmHitObjects = this; - - // Pass the HitObjectInterval to each child. - hitObject.HitObjectInterval = HitObjectInterval; - } - - calculateIntervals(); - } - - public static List GroupHitObjects(List data) - { - List flatPatterns = new List(); - - // Index does not need to be incremented, as it is handled within SameRhythm's constructor. - for (int i = 0; i < data.Count;) - { - SameRhythmHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null; - flatPatterns.Add(new SameRhythmHitObjects(previous, data, ref i)); - } - - return flatPatterns; - } - - private void calculateIntervals() - { - // Calculate the average interval between hitobjects, or null if there are fewer than two. - HitObjectInterval = Children.Count < 2 ? null : (Children[^1].StartTime - Children[0].StartTime) / (Children.Count - 1); - - // If both the current and previous intervals are available, calculate the ratio. - if (Previous?.HitObjectInterval != null && HitObjectInterval != null) - { - HitObjectIntervalRatio = HitObjectInterval.Value / Previous.HitObjectInterval.Value; - } - - if (Previous == null) - { - return; - } - - Interval = StartTime - Previous.StartTime; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs deleted file mode 100644 index beb7bfe5f6..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) ppy Pty Ltd . 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.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm -{ - /// - /// Stores rhythm data for a . - /// - public class TaikoDifficultyHitObjectRhythm - { - /// - /// The group of hit objects with consistent rhythm that this object belongs to. - /// - public SameRhythmHitObjects? SameRhythmHitObjects; - - /// - /// The larger pattern of rhythm groups that this object is part of. - /// - public SamePatterns? SamePatterns; - - /// - /// The ratio of current - /// to previous for the rhythm change. - /// A above 1 indicates a slow-down; a below 1 indicates a speed-up. - /// - public readonly double Ratio; - - /// - /// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object. - /// - /// - /// The general guidelines for the values are: - /// - /// rhythm changes with ratio closer to 1 (that are not 1) are harder to play, - /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). - /// - /// - private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms = - { - new TaikoDifficultyHitObjectRhythm(1, 1), - new TaikoDifficultyHitObjectRhythm(2, 1), - new TaikoDifficultyHitObjectRhythm(1, 2), - new TaikoDifficultyHitObjectRhythm(3, 1), - new TaikoDifficultyHitObjectRhythm(1, 3), - new TaikoDifficultyHitObjectRhythm(3, 2), - new TaikoDifficultyHitObjectRhythm(2, 3), - new TaikoDifficultyHitObjectRhythm(5, 4), - new TaikoDifficultyHitObjectRhythm(4, 5) - }; - - /// - /// Initialises a new instance of s, - /// calculating the closest rhythm change and its associated difficulty for the current hit object. - /// - /// The current being processed. - public TaikoDifficultyHitObjectRhythm(TaikoDifficultyHitObject current) - { - var previous = current.Previous(0); - - if (previous == null) - { - Ratio = 1; - return; - } - - TaikoDifficultyHitObjectRhythm closestRhythm = getClosestRhythm(current.DeltaTime, previous.DeltaTime); - Ratio = closestRhythm.Ratio; - } - - /// - /// Creates an object representing a rhythm change. - /// - /// The numerator for . - /// The denominator for - private TaikoDifficultyHitObjectRhythm(int numerator, int denominator) - { - Ratio = numerator / (double)denominator; - } - - /// - /// Determines the closest rhythm change from that matches the timing ratio - /// between the current and previous intervals. - /// - /// The time difference between the current hit object and the previous one. - /// The time difference between the previous hit object and the one before it. - /// The closest matching rhythm from . - private TaikoDifficultyHitObjectRhythm getClosestRhythm(double currentDeltaTime, double previousDeltaTime) - { - double ratio = currentDeltaTime / previousDeltaTime; - return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); - } - } -} - diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs new file mode 100644 index 0000000000..6c4a332624 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs @@ -0,0 +1,79 @@ +// Copyright (c) ppy Pty Ltd . 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.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm +{ + /// + /// Stores rhythm data for a . + /// + public class TaikoRhythmData + { + /// + /// The group of hit objects with consistent rhythm that this object belongs to. + /// + public SameRhythmHitObjectGrouping? SameRhythmGroupedHitObjects; + + /// + /// The larger pattern of rhythm groups that this object is part of. + /// + public SamePatternsGroupedHitObjects? SamePatternsGroupedHitObjects; + + /// + /// The ratio of current + /// to previous for the rhythm change. + /// A above 1 indicates a slow-down; a below 1 indicates a speed-up. + /// + /// + /// This is snapped to the closest matching . + /// + public readonly double Ratio; + + /// + /// Initialises a new instance of s, + /// calculating the closest rhythm change and its associated difficulty for the current hit object. + /// + /// The current being processed. + public TaikoRhythmData(TaikoDifficultyHitObject current) + { + var previous = current.Previous(0); + + if (previous == null) + { + Ratio = 1; + return; + } + + double actualRatio = current.DeltaTime / previous.DeltaTime; + double closestRatio = common_ratios.OrderBy(r => Math.Abs(r - actualRatio)).First(); + + Ratio = closestRatio; + } + + /// + /// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object. + /// + /// + /// The general guidelines for the values are: + /// + /// rhythm changes with ratio closer to 1 (that are not 1) are harder to play, + /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). + /// + /// + private static readonly double[] common_ratios = new[] + { + 1.0 / 1, + 2.0 / 1, + 1.0 / 2, + 3.0 / 1, + 1.0 / 3, + 3.0 / 2, + 2.0 / 3, + 5.0 / 4, + 4.0 / 5 + }; + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs new file mode 100644 index 0000000000..5bc0fdbc03 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; +using osu.Game.Rulesets.Taiko.Difficulty.Utils; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm +{ + public static class TaikoRhythmDifficultyPreprocessor + { + public static void ProcessAndAssign(List hitObjects) + { + var rhythmGroups = createSameRhythmGroupedHitObjects(hitObjects); + + foreach (var rhythmGroup in rhythmGroups) + { + foreach (var hitObject in rhythmGroup.HitObjects) + hitObject.RhythmData.SameRhythmGroupedHitObjects = rhythmGroup; + } + + var patternGroups = createSamePatternGroupedHitObjects(rhythmGroups); + + foreach (var patternGroup in patternGroups) + { + foreach (var hitObject in patternGroup.AllHitObjects) + hitObject.RhythmData.SamePatternsGroupedHitObjects = patternGroup; + } + } + + private static List createSameRhythmGroupedHitObjects(List hitObjects) + { + var rhythmGroups = new List(); + + foreach (var grouped in IntervalGroupingUtils.GroupByInterval(hitObjects)) + rhythmGroups.Add(new SameRhythmHitObjectGrouping(rhythmGroups.LastOrDefault(), grouped)); + + return rhythmGroups; + } + + private static List createSamePatternGroupedHitObjects(List rhythmGroups) + { + var patternGroups = new List(); + + foreach (var grouped in IntervalGroupingUtils.GroupByInterval(rhythmGroups)) + patternGroups.Add(new SamePatternsGroupedHitObjects(patternGroups.LastOrDefault(), grouped)); + + return patternGroups; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index dfcd08ed94..f407e13ff1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -3,11 +3,14 @@ using System.Collections.Generic; using System.Linq; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm; +using osu.Game.Rulesets.Taiko.Difficulty.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { @@ -37,80 +40,89 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public readonly int NoteIndex; /// - /// The rhythm required to hit this hit object. + /// Rhythm data used by . + /// This is populated via . /// - public readonly TaikoDifficultyHitObjectRhythm Rhythm; + public readonly TaikoRhythmData RhythmData; /// - /// The interval between this hit object and the surrounding hit objects in its rhythm group. + /// Colour data used by and . + /// This is populated via . /// - public double? HitObjectInterval { get; set; } - - /// - /// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used - /// by other skills in the future. - /// - public readonly TaikoDifficultyHitObjectColour Colour; + public readonly TaikoColourData ColourData; /// /// The adjusted BPM of this hit object, based on its slider velocity and scroll speed. /// public double EffectiveBPM; - /// - /// The current slider velocity of this hit object. - /// - public double CurrentSliderVelocity; - - public double Interval => DeltaTime; - /// /// Creates a new difficulty hit object. /// /// The gameplay associated with this difficulty object. /// The gameplay preceding . - /// The gameplay preceding . /// The rate of the gameplay clock. Modified by speed-changing mods. /// The list of all s in the current beatmap. /// The list of centre (don) s in the current beatmap. /// The list of rim (kat) s in the current beatmap. /// The list of s that is a hit (i.e. not a drumroll or swell) in the current beatmap. /// The position of this in the list. - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, + /// The control point info of the beatmap. + /// The global slider velocity of the beatmap. + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List objects, List centreHitObjects, List rimHitObjects, - List noteObjects, int index) + List noteObjects, int index, + ControlPointInfo controlPointInfo, + double globalSliderVelocity) : base(hitObject, lastObject, clockRate, objects, index) { noteDifficultyHitObjects = noteObjects; - // Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor - Colour = new TaikoDifficultyHitObjectColour(); + ColourData = new TaikoColourData(); + RhythmData = new TaikoRhythmData(this); - // Create a Rhythm object, its properties are filled in by TaikoDifficultyHitObjectRhythm - Rhythm = new TaikoDifficultyHitObjectRhythm(this); - - switch ((hitObject as Hit)?.Type) + if (hitObject is Hit hit) { - case HitType.Centre: - MonoIndex = centreHitObjects.Count; - centreHitObjects.Add(this); - monoDifficultyHitObjects = centreHitObjects; - break; + switch (hit.Type) + { + case HitType.Centre: + MonoIndex = centreHitObjects.Count; + centreHitObjects.Add(this); + monoDifficultyHitObjects = centreHitObjects; + break; - case HitType.Rim: - MonoIndex = rimHitObjects.Count; - rimHitObjects.Add(this); - monoDifficultyHitObjects = rimHitObjects; - break; - } + case HitType.Rim: + MonoIndex = rimHitObjects.Count; + rimHitObjects.Add(this); + monoDifficultyHitObjects = rimHitObjects; + break; + } - if (hitObject is Hit) - { NoteIndex = noteObjects.Count; noteObjects.Add(this); } + + // Using `hitObject.StartTime` causes floating point error differences + double normalisedStartTime = StartTime * clockRate; + + // Retrieve the timing point at the note's start time + TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(normalisedStartTime); + + // Calculate the slider velocity at the note's start time. + double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalisedStartTime, clockRate); + + EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; + } + + /// + /// Calculates the slider velocity based on control point info and clock rate. + /// + private static double calculateSliderVelocity(ControlPointInfo controlPointInfo, double globalSliderVelocity, double startTime, double clockRate) + { + var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime); + return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate; } public TaikoDifficultyHitObject? PreviousMono(int backwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex - (backwardsIndex + 1)); @@ -120,5 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public TaikoDifficultyHitObject? PreviousNote(int backwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex - (backwardsIndex + 1)); public TaikoDifficultyHitObject? NextNote(int forwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex + (forwardsIndex + 1)); + + public double Interval => DeltaTime; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs index 885131404a..7be1107b70 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills } var taikoObject = (TaikoDifficultyHitObject)current; - int index = taikoObject.Colour.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0; + int index = taikoObject.ColourData.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0; currentStrain *= DifficultyCalculationUtils.Logistic(index, 4, -1 / 25.0, 0.5) + 0.5; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 12e1396dd7..0e1f3d41cf 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills // Safely prevents previous strains from shifting as new notes are added. var currentObject = current as TaikoDifficultyHitObject; - int index = currentObject?.Colour.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0; + int index = currentObject?.ColourData.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0; double monolengthBonus = isConvert ? 1 : 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index f3b976f970..6b9986bd68 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -13,8 +13,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm; using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Scoring; @@ -72,7 +71,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty var centreObjects = new List(); var rimObjects = new List(); var noteObjects = new List(); - EffectiveBPMPreprocessor bpmLoader = new EffectiveBPMPreprocessor(beatmap, noteObjects); // Generate TaikoDifficultyHitObjects from the beatmap's hit objects. for (int i = 2; i < beatmap.HitObjects.Count; i++) @@ -80,21 +78,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty difficultyHitObjects.Add(new TaikoDifficultyHitObject( beatmap.HitObjects[i], beatmap.HitObjects[i - 1], - beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects, centreObjects, rimObjects, noteObjects, - difficultyHitObjects.Count + difficultyHitObjects.Count, + beatmap.ControlPointInfo, + beatmap.Difficulty.SliderMultiplier )); } - var groupedHitObjects = SameRhythmHitObjects.GroupHitObjects(noteObjects); - TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); - SamePatterns.GroupPatterns(groupedHitObjects); - bpmLoader.ProcessEffectiveBPM(beatmap.ControlPointInfo, clockRate); + TaikoRhythmDifficultyPreprocessor.ProcessAndAssign(noteObjects); return difficultyHitObjects; } @@ -204,9 +200,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// Applies a final re-scaling of the star rating. /// /// The raw star rating value before re-scaling. - private double rescale(double sr) + private static double rescale(double sr) { - if (sr < 0) return sr; + if (sr < 0) + return sr; return 10.43 * Math.Log(sr / 8 + 1); } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs similarity index 51% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs index 8f3917cbde..a42940180c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs @@ -1,13 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm +namespace osu.Game.Rulesets.Taiko.Difficulty.Utils { /// - /// The interface for hitobjects that provide an interval value. + /// The interface for objects that provide an interval value. /// public interface IHasInterval { + /// + /// The interval – ie delta time – between this object and a known previous object. + /// double Interval { get; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs new file mode 100644 index 0000000000..7bd7aa7677 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs @@ -0,0 +1,59 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Utils; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Utils +{ + public static class IntervalGroupingUtils + { + public static List> GroupByInterval(IReadOnlyList objects) where T : IHasInterval + { + var groups = new List>(); + + int i = 0; + while (i < objects.Count) + groups.Add(createNextGroup(objects, ref i)); + + return groups; + } + + private static List createNextGroup(IReadOnlyList objects, ref int i) where T : IHasInterval + { + const double margin_of_error = 5; + + var groupedObjects = new List { objects[i] }; + i++; + + for (; i < objects.Count - 1; i++) + { + if (!Precision.AlmostEquals(objects[i].Interval, objects[i + 1].Interval, margin_of_error)) + { + // When an interval change occurs, include the object with the differing interval in the case it increased + // See https://github.com/ppy/osu/pull/31636#discussion_r1942368372 for rationale. + if (objects[i + 1].Interval > objects[i].Interval + margin_of_error) + { + groupedObjects.Add(objects[i]); + i++; + } + + return groupedObjects; + } + + // No interval change occurred + groupedObjects.Add(objects[i]); + } + + // Check if the last two objects in the object form a "flat" rhythm pattern within the specified margin of error. + // If true, add the current object to the group and increment the index to process the next object. + if (objects.Count > 2 && i < objects.Count && Precision.AlmostEquals(objects[^1].Interval, objects[^2].Interval, margin_of_error)) + { + groupedObjects.Add(objects[i]); + i++; + } + + return groupedObjects; + } + } +}