diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs new file mode 100644 index 0000000000..c1340d70b9 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators +{ + public static class ReadingEvaluator + { + private const double high_sv_multiplier = 1.0; + + /// + /// Calculates the influence of higher slider velocities on hitobject difficulty. + /// The bonus is determined based on the EffectiveBPM, shifting within a defined range + /// between the upper and lower boundaries to reflect how increased slider velocity impacts difficulty. + /// + /// The hit object to evaluate. + /// The reading difficulty value for the given hit object. + public static double EvaluateDifficultyOf(TaikoDifficultyHitObject noteObject) + { + double effectiveBPM = noteObject.EffectiveBPM; + + const double velocity_max = 640; + const double velocity_min = 480; + + const double center = (velocity_max + velocity_min) / 2; + const double range = velocity_max - velocity_min; + + return high_sv_multiplier * DifficultyCalculationUtils.Logistic(effectiveBPM, center, 1.0 / (range / 10)); + } + + /// + /// Calculates the object density based on the DeltaTime, EffectiveBPM, and CurrentSliderVelocity. + /// + /// The current noteObject to evaluate. + /// The calculated object density. + public static double CalculateObjectDensity(TaikoDifficultyHitObject noteObject) + { + double objectDensity = 50 * DifficultyCalculationUtils.Logistic(noteObject.DeltaTime, 200, 1.0 / 300); + + return 1 - DifficultyCalculationUtils.Logistic(noteObject.EffectiveBPM, objectDensity, 1.0 / 240); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs new file mode 100644 index 0000000000..34893879a0 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -0,0 +1,145 @@ +// 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; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators +{ + public class RhythmEvaluator + { + /// + /// Multiplier for a given denominator term. + /// + private static double termPenalty(double ratio, int denominator, double power, double multiplier) + { + return -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power); + } + + /// + /// Gives a bonus for target ratio using a bell-shaped function. + /// + private static double bellCurve(double ratio, double targetRatio, double width, double multiplier) + { + return multiplier * Math.Exp(Math.E * -(Math.Pow(ratio - targetRatio, 2) / Math.Pow(width, 2))); + } + + /// + /// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses. + /// + private static double ratioDifficulty(double ratio, int terms = 8) + { + // Sum of n = 8 terms of periodic penalty. + double difficulty = 0; + + for (int i = 1; i <= terms; ++i) + { + difficulty += termPenalty(ratio, i, 2, 1); + } + + difficulty += terms; + + // Give bonus to near-1 ratios + difficulty += bellCurve(ratio, 1, 0.5, 1); + + // Penalise ratios that are VERY near 1 + difficulty -= bellCurve(ratio, 1, 0.3, 1); + + return difficulty / Math.Sqrt(8); + } + + /// + /// Determines if the pattern of hit object intervals is consistent based on a given threshold. + /// + private static bool isConsistentPattern(EvenHitObjects evenHitObjects, double threshold = 0.1) + { + // Collect the last 4 intervals (current and the last 3 previous). + List intervals = new List(); + var currentObject = evenHitObjects; + const int interval_count = 4; + + for (int i = 0; i < interval_count && currentObject != null; i++) + { + intervals.Add(currentObject.HitObjectInterval); + currentObject = currentObject.Previous; + } + + intervals.RemoveAll(interval => interval == null); + + // If there are fewer than 4 valid intervals, skip the consistency check. + if (intervals.Count < interval_count) + return false; + + for (int i = 0; i < intervals.Count; i++) + { + for (int j = i + 1; j < intervals.Count; j++) + { + double ratio = intervals[i]!.Value / intervals[j]!.Value; + if (Math.Abs(1 - ratio) <= threshold) // If any two intervals are similar, return true. + return true; + } + } + + // No similar intervals were found. + return false; + } + + private static double evaluateDifficultyOf(EvenHitObjects evenHitObjects, double hitWindow) + { + double intervalDifficulty = ratioDifficulty(evenHitObjects.HitObjectIntervalRatio); + double? previousInterval = evenHitObjects.Previous?.HitObjectInterval; + + // If a previous interval exists and there are multiple hit objects in the sequence: + if (previousInterval != null && evenHitObjects.Children.Count > 1) + { + double expectedDurationFromPrevious = (double)previousInterval * evenHitObjects.Children.Count; + double durationDifference = Math.Abs(evenHitObjects.Duration - expectedDurationFromPrevious); + + intervalDifficulty *= DifficultyCalculationUtils.Logistic( + durationDifference / hitWindow, + midpointOffset: 0.5, + multiplier: 1.5, + maxValue: 1); + } + + // Penalise regular intervals within the last four intervals. + if (isConsistentPattern(evenHitObjects)) + { + intervalDifficulty *= 0.4; + } + + // Penalise patterns that can be hit within a single hit window. + intervalDifficulty *= DifficultyCalculationUtils.Logistic( + evenHitObjects.Duration / hitWindow, + midpointOffset: 0.5, + multiplier: 1, + maxValue: 1); + + return intervalDifficulty; + } + + private static double evaluateDifficultyOf(EvenPatterns evenPatterns) + { + return ratioDifficulty(evenPatterns.IntervalRatio); + } + + public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow) + { + TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm; + double difficulty = 0.0d; + + if (rhythm.EvenHitObjects?.FirstHitObject == hitObject) // Difficulty for EvenHitObjects + difficulty += evaluateDifficultyOf(rhythm.EvenHitObjects, hitWindow); + + if (rhythm.EvenPatterns?.FirstHitObject == hitObject) // Difficulty for EvenPatterns + difficulty += evaluateDifficultyOf(rhythm.EvenPatterns) * rhythm.Difficulty; + + return difficulty; + } + } +} 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 c01a0f6686..014461c10b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs @@ -1,9 +1,9 @@ // 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.Objects; -using System.Collections.Generic; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPMPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPMPreprocessor.cs new file mode 100644 index 0000000000..17e05d5fbf --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPMPreprocessor.cs @@ -0,0 +1,50 @@ +// 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/EvenHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/EvenHitObjects.cs new file mode 100644 index 0000000000..48af379c0c --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/EvenHitObjects.cs @@ -0,0 +1,94 @@ +// 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 EvenHitObjects : EvenRhythm, IHasInterval + { + public TaikoDifficultyHitObject FirstHitObject => Children[0]; + + public EvenHitObjects? 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 EvenHitObjects(EvenHitObjects? previous, List data, ref int i) + : base(data, ref i, 5) + { + Previous = previous; + + foreach (var hitObject in Children) + { + hitObject.Rhythm.EvenHitObjects = 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 EvenRhythm's constructor. + for (int i = 0; i < data.Count;) + { + EvenHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null; + flatPatterns.Add(new EvenHitObjects(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/Data/EvenPatterns.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/EvenPatterns.cs new file mode 100644 index 0000000000..fc2ec71910 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/EvenPatterns.cs @@ -0,0 +1,55 @@ +// 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 EvenPatterns : EvenRhythm + { + public EvenPatterns? 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 EvenPatterns(EvenPatterns? previous, List data, ref int i) + : base(data, ref i, 5) + { + Previous = previous; + + foreach (TaikoDifficultyHitObject hitObject in AllHitObjects) + { + hitObject.Rhythm.EvenPatterns = this; + } + } + + public static void GroupPatterns(List data) + { + List evenPatterns = new List(); + + // Index does not need to be incremented, as it is handled within the EvenRhythm constructor. + for (int i = 0; i < data.Count;) + { + EvenPatterns? previous = evenPatterns.Count > 0 ? evenPatterns[^1] : null; + evenPatterns.Add(new EvenPatterns(previous, data, ref i)); + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/EvenRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/EvenRhythm.cs new file mode 100644 index 0000000000..4b0ecd2bfa --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/EvenRhythm.cs @@ -0,0 +1,73 @@ +// 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 EvenRhythm + 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 EvenRhythm(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/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs new file mode 100644 index 0000000000..fea9439fc0 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs @@ -0,0 +1,13 @@ +// 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 +{ + /// + /// The interface for hitobjects that provide an interval value, + /// + public interface IHasInterval + { + double Interval { get; } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs index a273d7e2ea..d35004b0a4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs @@ -1,24 +1,82 @@ // 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 { /// - /// Represents a rhythm change in a taiko map. + /// Stores rhythm data for a . /// public class TaikoDifficultyHitObjectRhythm { + /// + /// The group of hit objects with consistent rhythm that this object belongs to. + /// + public EvenHitObjects? EvenHitObjects; + + /// + /// The larger pattern of rhythm groups that this object is part of. + /// + public EvenPatterns? EvenPatterns; + + /// + /// 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; + /// /// The difficulty multiplier associated with this rhythm change. /// public readonly double Difficulty; /// - /// The ratio of current - /// to previous for the rhythm change. - /// A above 1 indicates a slow-down; a below 1 indicates a speed-up. + /// List of most common rhythm changes in taiko maps. This is used as a display value. /// - public readonly double Ratio; + /// + /// 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, 0.0), + new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), + new TaikoDifficultyHitObjectRhythm(1, 2, 0.5), + new TaikoDifficultyHitObjectRhythm(3, 1, 0.3), + new TaikoDifficultyHitObjectRhythm(1, 3, 0.35), + new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), + new TaikoDifficultyHitObjectRhythm(2, 3, 0.4), + new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), + new TaikoDifficultyHitObjectRhythm(4, 5, 0.7) + }; + + /// + /// 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; + Difficulty = 0.0; + return; + } + + TaikoDifficultyHitObjectRhythm closestRhythm = getClosestRhythm(current.DeltaTime, previous.DeltaTime); + Ratio = closestRhythm.Ratio; + Difficulty = closestRhythm.Difficulty; + } /// /// Creates an object representing a rhythm change. @@ -26,10 +84,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// The numerator for . /// The denominator for /// The difficulty multiplier associated with this rhythm change. - public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty) + private TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty) { Ratio = numerator / (double)denominator; Difficulty = difficulty; } + + /// + /// 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/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 4aaee50c18..a56c217f82 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -1,27 +1,36 @@ // 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; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; -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.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { /// /// Represents a single hit object in taiko difficulty calculation. /// - public class TaikoDifficultyHitObject : DifficultyHitObject + public class TaikoDifficultyHitObject : DifficultyHitObject, IHasInterval { /// /// The list of all of the same colour as this in the beatmap. /// private readonly IReadOnlyList? monoDifficultyHitObjects; + /// + /// 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; + /// /// The index of this in . /// @@ -42,12 +51,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public readonly TaikoDifficultyHitObjectRhythm Rhythm; + /// + /// The interval between this hit object and the surrounding hit objects in its rhythm group. + /// + 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 double Interval => DeltaTime; + /// /// Creates a new difficulty hit object. /// @@ -71,7 +87,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing // Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor Colour = new TaikoDifficultyHitObjectColour(); - Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate); + Rhythm = new TaikoDifficultyHitObjectRhythm(this); switch ((hitObject as Hit)?.Type) { @@ -95,43 +111,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } } - /// - /// List of most common rhythm changes in taiko maps. - /// - /// - /// 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, 0.0), - new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), - new TaikoDifficultyHitObjectRhythm(1, 2, 0.5), - new TaikoDifficultyHitObjectRhythm(3, 1, 0.3), - new TaikoDifficultyHitObjectRhythm(1, 3, 0.35), - new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), // purposefully higher (requires hand switch in full alternating gameplay style) - new TaikoDifficultyHitObjectRhythm(2, 3, 0.4), - new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), - new TaikoDifficultyHitObjectRhythm(4, 5, 0.7) - }; - - /// - /// Returns the closest rhythm change from required to hit this object. - /// - /// The gameplay preceding this one. - /// The gameplay preceding . - /// The rate of the gameplay clock. - private TaikoDifficultyHitObjectRhythm getClosestRhythm(HitObject lastObject, HitObject lastLastObject, double clockRate) - { - double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; - double ratio = DeltaTime / prevLength; - - return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); - } - public TaikoDifficultyHitObject? PreviousMono(int backwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex - (backwardsIndex + 1)); public TaikoDifficultyHitObject? NextMono(int forwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex + (forwardsIndex + 1)); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 2d45b5eed0..c2814df6c3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double SkillMultiplier => 0.12; // This is set to decay slower than other skills, due to the fact that only the first note of each encoding class - // having any difficulty values, and we want to allow colour difficulty to be able to build up even on + // having any difficulty values, and we want to allow colour difficulty to be able to build up even on // slower maps. protected override double StrainDecayBase => 0.8; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs new file mode 100644 index 0000000000..757056ba57 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Skills +{ + /// + /// Calculates the reading coefficient of taiko difficulty. + /// + public class Reading : StrainDecaySkill + { + protected override double SkillMultiplier => 1.0; + protected override double StrainDecayBase => 0.4; + + private double currentStrain; + + public double ObjectDensity { get; private set; } + + public Reading(Mod[] mods) + : base(mods) + { + } + + protected override double StrainValueOf(DifficultyHitObject current) + { + if (current is not TaikoDifficultyHitObject taikoObject) + return 0.0; + + // Drum Rolls and Swells are exempt. + if (current.BaseObject is not Hit) + { + return 0.0; + } + + ObjectDensity = ReadingEvaluator.CalculateObjectDensity(taikoObject) * 10; + + currentStrain *= StrainDecayBase; + currentStrain += ReadingEvaluator.EvaluateDifficultyOf(taikoObject) * SkillMultiplier; + + return currentStrain; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index e76af13686..72c6a5e058 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -1,13 +1,10 @@ // 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 osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; -using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Utils; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { @@ -16,158 +13,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// public class Rhythm : StrainDecaySkill { - protected override double SkillMultiplier => 10; - protected override double StrainDecayBase => 0; + protected override double SkillMultiplier => 1.6; + protected override double StrainDecayBase => 0.4; - /// - /// The note-based decay for rhythm strain. - /// - /// - /// is not used here, as it's time- and not note-based. - /// - private const double strain_decay = 0.96; + private readonly double greatHitWindow; - /// - /// Maximum number of entries in . - /// - private const int rhythm_history_max_length = 8; - - /// - /// Contains the last changes in note sequence rhythms. - /// - private readonly LimitedCapacityQueue rhythmHistory = new LimitedCapacityQueue(rhythm_history_max_length); - - /// - /// Contains the rolling rhythm strain. - /// Used to apply per-note decay. - /// - private double currentStrain; - - /// - /// Number of notes since the last rhythm change has taken place. - /// - private int notesSinceRhythmChange; - - public Rhythm(Mod[] mods) + public Rhythm(Mod[] mods, double greatHitWindow) : base(mods) { + this.greatHitWindow = greatHitWindow; } protected override double StrainValueOf(DifficultyHitObject current) { - // drum rolls and swells are exempt. - if (!(current.BaseObject is Hit)) - { - resetRhythmAndStrain(); - return 0.0; - } + double difficulty = RhythmEvaluator.EvaluateDifficultyOf(current, greatHitWindow); - currentStrain *= strain_decay; + // To prevent abuse of exceedingly long intervals between awkward rhythms, we penalise its difficulty. + if (current.DeltaTime > 400) + difficulty *= 0.5; - TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current; - notesSinceRhythmChange += 1; - - // rhythm difficulty zero (due to rhythm not changing) => no rhythm strain. - if (hitObject.Rhythm.Difficulty == 0.0) - { - return 0.0; - } - - double objectStrain = hitObject.Rhythm.Difficulty; - - objectStrain *= repetitionPenalties(hitObject); - objectStrain *= patternLengthPenalty(notesSinceRhythmChange); - objectStrain *= speedPenalty(hitObject.DeltaTime); - - // careful - needs to be done here since calls above read this value - notesSinceRhythmChange = 0; - - currentStrain += objectStrain; - return currentStrain; - } - - /// - /// Returns a penalty to apply to the current hit object caused by repeating rhythm changes. - /// - /// - /// Repetitions of more recent patterns are associated with a higher penalty. - /// - /// The current hit object being considered. - private double repetitionPenalties(TaikoDifficultyHitObject hitObject) - { - double penalty = 1; - - rhythmHistory.Enqueue(hitObject); - - for (int mostRecentPatternsToCompare = 2; mostRecentPatternsToCompare <= rhythm_history_max_length / 2; mostRecentPatternsToCompare++) - { - for (int start = rhythmHistory.Count - mostRecentPatternsToCompare - 1; start >= 0; start--) - { - if (!samePattern(start, mostRecentPatternsToCompare)) - continue; - - int notesSince = hitObject.Index - rhythmHistory[start].Index; - penalty *= repetitionPenalty(notesSince); - break; - } - } - - return penalty; - } - - /// - /// Determines whether the rhythm change pattern starting at is a repeat of any of the - /// . - /// - private bool samePattern(int start, int mostRecentPatternsToCompare) - { - for (int i = 0; i < mostRecentPatternsToCompare; i++) - { - if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - mostRecentPatternsToCompare + i].Rhythm) - return false; - } - - return true; - } - - /// - /// Calculates a single rhythm repetition penalty. - /// - /// Number of notes since the last repetition of a rhythm change. - private static double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince); - - /// - /// Calculates a penalty based on the number of notes since the last rhythm change. - /// Both rare and frequent rhythm changes are penalised. - /// - /// Number of notes since the last rhythm change. - private static double patternLengthPenalty(int patternLength) - { - double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); - double longPatternPenalty = Math.Clamp(2.5 - 0.15 * patternLength, 0.0, 1.0); - return Math.Min(shortPatternPenalty, longPatternPenalty); - } - - /// - /// Calculates a penalty for objects that do not require alternating hands. - /// - /// Time (in milliseconds) since the last hit object. - private double speedPenalty(double deltaTime) - { - if (deltaTime < 80) return 1; - if (deltaTime < 210) return Math.Max(0, 1.4 - 0.005 * deltaTime); - - resetRhythmAndStrain(); - return 0.0; - } - - /// - /// Resets the rolling strain value and counter. - /// - private void resetRhythmAndStrain() - { - currentStrain = 0.0; - notesSinceRhythmChange = 0; + return difficulty; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index c8f0448767..8de9a5c845 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -28,6 +28,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("rhythm_difficulty")] public double RhythmDifficulty { get; set; } + /// + /// The difficulty corresponding to the reading skill. + /// + [JsonProperty("reading_difficulty")] + public double ReadingDifficulty { get; set; } + + /// + /// Determines the object density of a beatmap, based on DeltaTime. + /// + [JsonProperty("object_density")] + public double ObjectDensity { get; set; } + /// /// The difficulty corresponding to the colour skill. /// @@ -35,10 +47,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public double ColourDifficulty { get; set; } /// - /// The difficulty corresponding to the hardest parts of the map. + /// The penalty factor corresponding to patterns based on their correlation between skills. /// - [JsonProperty("peak_difficulty")] - public double PeakDifficulty { get; set; } + [JsonProperty("simple_pattern_factor")] + public double SimplePattern { get; set; } + + /// + /// Determines the amount of difficult strain peaks present within each skill. + /// + [JsonProperty("stamina_difficult_strains")] + public double StaminaTopStrains { get; set; } + + [JsonProperty("rhythm_difficult_strains")] + public double RhythmTopStrains { get; set; } + + [JsonProperty("reading_difficult_strains")] + public double ReadingTopStrains { get; set; } + + [JsonProperty("colour_difficult_strains")] + public double ColourTopStrains { get; set; } /// /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 7f2558c406..af782a922f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -12,6 +12,8 @@ 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.Skills; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Scoring; @@ -21,11 +23,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public class TaikoDifficultyCalculator : DifficultyCalculator { private const double difficulty_multiplier = 0.084375; - private const double rhythm_skill_multiplier = 0.2 * difficulty_multiplier; - private const double colour_skill_multiplier = 0.375 * difficulty_multiplier; + private const double rhythm_skill_multiplier = 0.070 * difficulty_multiplier; + private const double reading_skill_multiplier = 0.100 * difficulty_multiplier; + private const double colour_skill_multiplier = 0.425 * difficulty_multiplier; private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier; - public override int Version => 20241007; + private double simpleRhythmPenalty = 1; + private double simpleColourPenalty = 1; + + private double colourDifficultStrains; + private double rhythmDifficultStrains; + private double readingDifficultStrains; + private double staminaDifficultStrains; + + public override int Version => 20241115; public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) @@ -34,9 +45,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) { + HitWindows hitWindows = new HitWindows(); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + return new Skill[] { - new Rhythm(mods), + new Rhythm(mods, hitWindows.WindowFor(HitResult.Great) / clockRate), + new Reading(mods), new Colour(mods), new Stamina(mods, false), new Stamina(mods, true) @@ -53,25 +68,67 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - List difficultyHitObjects = new List(); - List centreObjects = new List(); - List rimObjects = new List(); - List noteObjects = new List(); + var hitWindows = new HitWindows(); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + var difficultyHitObjects = new List(); + 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++) { - difficultyHitObjects.Add( - new TaikoDifficultyHitObject( - beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects, - centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count) - ); + difficultyHitObjects.Add(new TaikoDifficultyHitObject( + beatmap.HitObjects[i], + beatmap.HitObjects[i - 1], + beatmap.HitObjects[i - 2], + clockRate, + difficultyHitObjects, + centreObjects, + rimObjects, + noteObjects, + difficultyHitObjects.Count + )); } + var groupedHitObjects = EvenHitObjects.GroupHitObjects(noteObjects); + TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); + EvenPatterns.GroupPatterns(groupedHitObjects); + bpmLoader.ProcessEffectiveBPM(beatmap.ControlPointInfo, clockRate); return difficultyHitObjects; } + /// + /// Calculates the combined penalty based on the relationship between skills. + /// Lower skill values are penalised more heavily relative to predefined thresholds and their + /// interaction with the opposing skill rating. + /// + private double simplePatternPenalty(double rhythmRating, double colourRating, double clockRate) + { + double colourThreshold = 1.25 * clockRate; // Threshold changes based on rate + const double stamina_threshold = 1250.0; + + // We count difficult stamina strains to ensure that even if there's no rhythm, very heavy stamina maps still give their respective difficulty. + double staminaTransition = Math.Clamp((staminaDifficultStrains - stamina_threshold) / (1350 - stamina_threshold), 0, 1); + double staminaFactor = (1 - staminaTransition) * 1.0 + staminaTransition * 0.85; + staminaFactor *= Math.Min(1, stamina_threshold / Math.Min(2000, staminaDifficultStrains)); + + simpleRhythmPenalty = patternRating(rhythmRating, 2.5, 5, colourRating); + simpleRhythmPenalty *= staminaFactor; + simpleRhythmPenalty = Math.Max(0, simpleRhythmPenalty); + + // For awkwardly snapped maps with low rhythm strain count, we add a penalty. + double rhythmTransition = 1 - Math.Max(0, (50 - rhythmDifficultStrains) / 50.0); + double colourFactor = Math.Max(1, 0.50 * ((colourThreshold - colourRating) / colourThreshold)); + simpleColourPenalty = Math.Max(0, rhythmTransition * colourFactor); + + return simpleRhythmPenalty; + } + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { if (beatmap.HitObjects.Count == 0) @@ -79,25 +136,32 @@ namespace osu.Game.Rulesets.Taiko.Difficulty Colour colour = (Colour)skills.First(x => x is Colour); Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); + Reading reading = (Reading)skills.First(x => x is Reading); Stamina stamina = (Stamina)skills.First(x => x is Stamina); Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina); double colourRating = colour.DifficultyValue() * colour_skill_multiplier; double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; + double readingRating = reading.DifficultyValue() * reading_skill_multiplier; + double objectDensity = reading.ObjectDensity; double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5); - double combinedRating = combinedDifficultyValue(rhythm, colour, stamina); - double starRating = rescale(combinedRating * 1.4); + colourDifficultStrains = colour.CountTopWeightedStrains(); + rhythmDifficultStrains = rhythm.CountTopWeightedStrains(); + readingDifficultStrains = reading.CountTopWeightedStrains(); + staminaDifficultStrains = stamina.CountTopWeightedStrains() * Math.Min(clockRate, 1.25); // Bonus is capped past 1.25x rate - // TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system. + double patternPenalty = simplePatternPenalty(rhythmRating, colourRating, clockRate); + + double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina); + double starRating = rescale(combinedRating * 1.8); + + // Converts are penalised outside of the scope of difficulty calculation, as our assumptions surrounding playstyle becomes out-of-scope. 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; + starRating *= 0.80; } HitWindows hitWindows = new TaikoHitWindows(); @@ -109,9 +173,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty Mods = mods, StaminaDifficulty = staminaRating, MonoStaminaFactor = monoStaminaFactor, - RhythmDifficulty = rhythmRating, + SimplePattern = patternPenalty, + RhythmDifficulty = rhythmRating * 8, + ReadingDifficulty = readingRating * 1.5, + ObjectDensity = objectDensity, ColourDifficulty = colourRating, - PeakDifficulty = combinedRating, + StaminaTopStrains = staminaDifficultStrains, + RhythmTopStrains = rhythmDifficultStrains, + ReadingTopStrains = readingDifficultStrains, + ColourTopStrains = colourDifficultStrains, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate, MaxCombo = beatmap.GetMaxCombo(), @@ -120,17 +190,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return attributes; } - /// - /// Applies a final re-scaling of the star rating. - /// - /// The raw star rating value before re-scaling. - private double rescale(double sr) - { - if (sr < 0) return sr; - - return 10.43 * Math.Log(sr / 8 + 1); - } - /// /// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map. /// @@ -138,22 +197,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). /// - private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina) + private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina) { List peaks = new List(); var colourPeaks = colour.GetCurrentStrainPeaks().ToList(); var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList(); + var readingPeaks = reading.GetCurrentStrainPeaks().ToList(); var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList(); for (int i = 0; i < colourPeaks.Count; i++) { - double colourPeak = colourPeaks[i] * colour_skill_multiplier; - double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; - double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; + // Peaks uses separate constants due to strain pertaining differently to display values. + double baseColourPeak = colourPeaks[i] * 0.0359; + double colourPeak = baseColourPeak * Math.Exp(-simpleRhythmPenalty / 14); + double rhythmPeak = rhythmPeaks[i] * 0.0379 * simpleColourPenalty; + double staminaPeak = staminaPeaks[i] * 0.0317; + double readingPeak = readingPeaks[i] * reading_skill_multiplier; double peak = norm(1.5, colourPeak, staminaPeak); - peak = norm(2, peak, rhythmPeak); + peak = norm(2, peak, rhythmPeak, readingPeak); // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). // These sections will not contribute to the difficulty. @@ -173,6 +236,34 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return difficulty; } + /// + /// Calculates the penalty for skills based on their relationship making up a pattern. + /// Penalising ratings where patterns have a major difference in value. + /// + private double patternRating(double rating, double threshold, double upperBound, double otherRating) + { + if (rating > threshold) + return 0; + + // To prevent against breaking values we define 0.01 as minimum difficulty. + rating = Math.Max(0.01, rating); + otherRating = Math.Max(0.01, otherRating); + + // Penalise based on logarithmic difference from the skill-based threshold, scaled by the influence of the other rating + return Math.Log(threshold / rating) * Math.Min(upperBound, Math.Log(Math.Max(1, otherRating - upperBound)) + upperBound); + } + + /// + /// Applies a final re-scaling of the star rating. + /// + /// The raw star rating value before re-scaling. + private double rescale(double sr) + { + if (sr < 0) return sr; + + return 10.43 * Math.Log(sr / 8 + 1); + } + /// /// Returns the p-norm of an n-dimensional vector. /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index c672b7a1d9..0d65e0c921 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -86,9 +86,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (score.Mods.Any(m => m is ModHidden)) difficultyValue *= 1.025; - if (score.Mods.Any(m => m is ModHardRock)) - difficultyValue *= 1.10; - if (score.Mods.Any(m => m is ModFlashlight)) difficultyValue *= Math.Max(1, 1.050 - Math.Min(attributes.MonoStaminaFactor / 50, 1) * lengthBonus); @@ -97,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps. double accScalingExponent = 2 + attributes.MonoStaminaFactor; - double accScalingShift = 300 - 100 * attributes.MonoStaminaFactor; + double accScalingShift = 400 - 100 * attributes.MonoStaminaFactor; return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); } @@ -159,7 +156,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double n = totalHits; // Proportion of greats + goods hit. - double p = totalSuccessfulHits / n; + double p = Math.Max(0, totalSuccessfulHits - 0.1 * h100) / n; // We can be 99% confident that p is at least this value. double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);