1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-06 21:02:59 +08:00

Merge branch 'pp-dev' into match-my-freak-sliders

This commit is contained in:
danielthirtle 2025-01-09 03:17:51 +13:00
commit 67d4826f26
16 changed files with 541 additions and 219 deletions

View File

@ -2,9 +2,13 @@
// 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.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
@ -24,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
/// <item><description>and how easily they can be cheesed.</description></item>
/// </list>
/// </summary>
public static double EvaluateDifficultyOf(DifficultyHitObject current)
public static double EvaluateDifficultyOf(DifficultyHitObject current, IReadOnlyList<Mod> mods)
{
if (current.BaseObject is Spinner)
return 0;
@ -56,6 +60,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier;
if (mods.OfType<OsuModAutopilot>().Any())
distanceBonus = 0;
// Base difficulty with all bonuses
double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime;

View File

@ -69,6 +69,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedRating = 0.0;
flashlightRating *= 0.7;
}
else if (mods.Any(h => h is OsuModAutopilot))
{
speedRating *= 0.5;
aimRating = 0.0;
flashlightRating *= 0.4;
}
double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);

View File

@ -136,6 +136,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
if (score.Mods.Any(h => h is OsuModAutopilot))
return 0.0;
double aimDifficulty = attributes.AimDifficulty;
if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0)
@ -218,6 +221,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (attributes.ApproachRate > 10.33)
approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
if (score.Mods.Any(h => h is OsuModAutopilot))
approachRateFactor = 0.0;
speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
if (score.Mods.Any(m => m is OsuModBlinds))

View File

@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
protected override double StrainValueAt(DifficultyHitObject current)
{
currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime);
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier;
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, Mods) * skillMultiplier;
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);

View File

@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
[TestCase(3.0920212594351191d, 200, "diffcalc-test")]
[TestCase(3.0920212594351191d, 200, "diffcalc-test-strong")]
[TestCase(3.0950934814938953d, 200, "diffcalc-test")]
[TestCase(3.0950934814938953d, 200, "diffcalc-test-strong")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(4.0789820318081444d, 200, "diffcalc-test")]
[TestCase(4.0789820318081444d, 200, "diffcalc-test-strong")]
[TestCase(4.0839365008715403d, 200, "diffcalc-test")]
[TestCase(4.0839365008715403d, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());

View File

@ -0,0 +1,149 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
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
{
/// <summary>
/// Multiplier for a given denominator term.
/// </summary>
private static double termPenalty(double ratio, int denominator, double power, double multiplier)
{
return -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power);
}
/// <summary>
/// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses.
/// </summary>
private static double ratioDifficulty(double ratio, int terms = 8)
{
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 += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.7);
// Penalize ratios that are VERY near 1
difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5);
return difficulty / Math.Sqrt(8);
}
/// <summary>
/// Determines if the changes in hit object intervals is consistent based on a given threshold.
/// </summary>
private static double repeatedIntervalPenalty(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow, double threshold = 0.1)
{
double longIntervalPenalty = sameInterval(sameRhythmHitObjects, 3);
double shortIntervalPenalty = sameRhythmHitObjects.Children.Count < 6
? sameInterval(sameRhythmHitObjects, 4)
: 1.0; // Returns a non-penalty if there are 6 or more notes within an interval.
// Scale penalties dynamically based on hit object duration relative to hitWindow.
double penaltyScaling = Math.Max(1 - sameRhythmHitObjects.Duration / (hitWindow * 2), 0.5);
return Math.Min(longIntervalPenalty, shortIntervalPenalty) * penaltyScaling;
double sameInterval(SameRhythmHitObjects startObject, int intervalCount)
{
List<double?> intervals = new List<double?>();
var currentObject = startObject;
for (int i = 0; i < intervalCount && currentObject != null; i++)
{
intervals.Add(currentObject.HitObjectInterval);
currentObject = currentObject.Previous;
}
intervals.RemoveAll(interval => interval == null);
if (intervals.Count < intervalCount)
return 1.0; // No penalty if there aren't enough valid intervals.
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, apply a penalty.
return 0.3;
}
}
return 1.0; // No penalty if all intervals are different.
}
}
private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow)
{
double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio);
double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval;
// If a previous interval exists and there are multiple hit objects in the sequence:
if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1)
{
double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count;
double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious;
if (durationDifference > 0)
{
intervalDifficulty *= DifficultyCalculationUtils.Logistic(
durationDifference / hitWindow,
midpointOffset: 0.7,
multiplier: 1.5,
maxValue: 1);
}
}
// Apply consistency penalty.
intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow);
// Penalise patterns that can be hit within a single hit window.
intervalDifficulty *= DifficultyCalculationUtils.Logistic(
sameRhythmHitObjects.Duration / hitWindow,
midpointOffset: 0.6,
multiplier: 1,
maxValue: 1);
return Math.Pow(intervalDifficulty, 0.75);
}
private static double evaluateDifficultyOf(SamePatterns samePatterns)
{
return ratioDifficulty(samePatterns.IntervalRatio);
}
/// <summary>
/// Evaluate the difficulty of a hitobject considering its interval change.
/// </summary>
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow)
{
TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm;
double difficulty = 0.0d;
if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects
difficulty += evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow);
if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns
difficulty += 0.5 * evaluateDifficultyOf(rhythm.SamePatterns);
return difficulty;
}
}
}

View File

@ -0,0 +1,55 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
{
/// <summary>
/// Represents <see cref="SameRhythmHitObjects"/> grouped by their <see cref="SameRhythmHitObjects.StartTime"/>'s interval.
/// </summary>
public class SamePatterns : SameRhythm<SameRhythmHitObjects>
{
public SamePatterns? Previous { get; private set; }
/// <summary>
/// The <see cref="SameRhythmHitObjects.Interval"/> between children <see cref="SameRhythmHitObjects"/> within this group.
/// If there is only one child, this will have the value of the first child's <see cref="SameRhythmHitObjects.Interval"/>.
/// </summary>
public double ChildrenInterval => Children.Count > 1 ? Children[1].Interval : Children[0].Interval;
/// <summary>
/// The ratio of <see cref="ChildrenInterval"/> between this and the previous <see cref="SamePatterns"/>. In the
/// case where there is no previous <see cref="SamePatterns"/>, this will have a value of 1.
/// </summary>
public double IntervalRatio => ChildrenInterval / Previous?.ChildrenInterval ?? 1.0d;
public TaikoDifficultyHitObject FirstHitObject => Children[0].FirstHitObject;
public IEnumerable<TaikoDifficultyHitObject> AllHitObjects => Children.SelectMany(child => child.Children);
private SamePatterns(SamePatterns? previous, List<SameRhythmHitObjects> 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<SameRhythmHitObjects> data)
{
List<SamePatterns> samePatterns = new List<SamePatterns>();
// 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));
}
}
}
}

View File

@ -0,0 +1,73 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
{
/// <summary>
/// A base class for grouping <see cref="IHasInterval"/>s by their interval. In edges where an interval change
/// occurs, the <see cref="IHasInterval"/> is added to the group with the smaller interval.
/// </summary>
public abstract class SameRhythm<ChildType>
where ChildType : IHasInterval
{
public IReadOnlyList<ChildType> Children { get; private set; }
/// <summary>
/// Determines if the intervals between two child objects are within a specified margin of error,
/// indicating that the intervals are effectively "flat" or consistent.
/// </summary>
private bool isFlat(ChildType current, ChildType previous, double marginOfError)
{
return Math.Abs(current.Interval - previous.Interval) <= marginOfError;
}
/// <summary>
/// Create a new <see cref="SameRhythm{ChildType}"/> from a list of <see cref="IHasInterval"/>s, and add
/// them to the <see cref="Children"/> list until the end of the group.
/// </summary>
/// <param name="data">The list of <see cref="IHasInterval"/>s.</param>
/// <param name="i">
/// Index in <paramref name="data"/> to start adding children. This will be modified and should be passed into
/// the next <see cref="SameRhythm{ChildType}"/>'s constructor.
/// </param>
/// <param name="marginOfError">
/// The margin of error for the interval, within of which no interval change is considered to have occured.
/// </param>
protected SameRhythm(List<ChildType> data, ref int i, double marginOfError)
{
List<ChildType> children = new List<ChildType>();
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++;
}
}
}
}

View File

@ -0,0 +1,94 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Rulesets.Difficulty.Preprocessing;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
{
/// <summary>
/// Represents a group of <see cref="TaikoDifficultyHitObject"/>s with no rhythm variation.
/// </summary>
public class SameRhythmHitObjects : SameRhythm<TaikoDifficultyHitObject>, IHasInterval
{
public TaikoDifficultyHitObject FirstHitObject => Children[0];
public SameRhythmHitObjects? Previous;
/// <summary>
/// <see cref="DifficultyHitObject.StartTime"/> of the first hit object.
/// </summary>
public double StartTime => Children[0].StartTime;
/// <summary>
/// The interval between the first and final hit object within this group.
/// </summary>
public double Duration => Children[^1].StartTime - Children[0].StartTime;
/// <summary>
/// The interval in ms of each hit object in this <see cref="SameRhythmHitObjects"/>. This is only defined if there is
/// more than two hit objects in this <see cref="SameRhythmHitObjects"/>.
/// </summary>
public double? HitObjectInterval;
/// <summary>
/// The ratio of <see cref="HitObjectInterval"/> between this and the previous <see cref="SameRhythmHitObjects"/>. In the
/// case where one or both of the <see cref="HitObjectInterval"/> is undefined, this will have a value of 1.
/// </summary>
public double HitObjectIntervalRatio = 1;
/// <summary>
/// The interval between the <see cref="StartTime"/> of this and the previous <see cref="SameRhythmHitObjects"/>.
/// </summary>
public double Interval { get; private set; } = double.PositiveInfinity;
public SameRhythmHitObjects(SameRhythmHitObjects? previous, List<TaikoDifficultyHitObject> 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<SameRhythmHitObjects> GroupHitObjects(List<TaikoDifficultyHitObject> data)
{
List<SameRhythmHitObjects> flatPatterns = new List<SameRhythmHitObjects>();
// 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;
}
}
}

View File

@ -0,0 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
{
/// <summary>
/// The interface for hitobjects that provide an interval value.
/// </summary>
public interface IHasInterval
{
double Interval { get; }
}
}

View File

@ -1,35 +1,98 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
{
/// <summary>
/// Represents a rhythm change in a taiko map.
/// Stores rhythm data for a <see cref="TaikoDifficultyHitObject"/>.
/// </summary>
public class TaikoDifficultyHitObjectRhythm
{
/// <summary>
/// The difficulty multiplier associated with this rhythm change.
/// The group of hit objects with consistent rhythm that this object belongs to.
/// </summary>
public readonly double Difficulty;
public SameRhythmHitObjects? SameRhythmHitObjects;
/// <summary>
/// The ratio of current <see cref="osu.Game.Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/>
/// to previous <see cref="osu.Game.Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/> for the rhythm change.
/// The larger pattern of rhythm groups that this object is part of.
/// </summary>
public SamePatterns? SamePatterns;
/// <summary>
/// The ratio of current <see cref="Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/>
/// to previous <see cref="Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/> for the rhythm change.
/// A <see cref="Ratio"/> above 1 indicates a slow-down; a <see cref="Ratio"/> below 1 indicates a speed-up.
/// </summary>
public readonly double Ratio;
/// <summary>
/// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object.
/// </summary>
/// <remarks>
/// The general guidelines for the values are:
/// <list type="bullet">
/// <item>rhythm changes with ratio closer to 1 (that are <i>not</i> 1) are harder to play,</item>
/// <item>speeding up is <i>generally</i> harder than slowing down (with exceptions of rhythm changes requiring a hand switch).</item>
/// </list>
/// </remarks>
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)
};
/// <summary>
/// Initialises a new instance of <see cref="TaikoDifficultyHitObjectRhythm"/>s,
/// calculating the closest rhythm change and its associated difficulty for the current hit object.
/// </summary>
/// <param name="current">The current <see cref="TaikoDifficultyHitObject"/> being processed.</param>
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;
}
/// <summary>
/// Creates an object representing a rhythm change.
/// </summary>
/// <param name="numerator">The numerator for <see cref="Ratio"/>.</param>
/// <param name="denominator">The denominator for <see cref="Ratio"/></param>
/// <param name="difficulty">The difficulty multiplier associated with this rhythm change.</param>
public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty)
private TaikoDifficultyHitObjectRhythm(int numerator, int denominator)
{
Ratio = numerator / (double)denominator;
Difficulty = difficulty;
}
/// <summary>
/// Determines the closest rhythm change from <see cref="common_rhythms"/> that matches the timing ratio
/// between the current and previous intervals.
/// </summary>
/// <param name="currentDeltaTime">The time difference between the current hit object and the previous one.</param>
/// <param name="previousDeltaTime">The time difference between the previous hit object and the one before it.</param>
/// <returns>The closest matching rhythm from <see cref="common_rhythms"/>.</returns>
private TaikoDifficultyHitObjectRhythm getClosestRhythm(double currentDeltaTime, double previousDeltaTime)
{
double ratio = currentDeltaTime / previousDeltaTime;
return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First();
}
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
@ -15,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
/// <summary>
/// Represents a single hit object in taiko difficulty calculation.
/// </summary>
public class TaikoDifficultyHitObject : DifficultyHitObject
public class TaikoDifficultyHitObject : DifficultyHitObject, IHasInterval
{
/// <summary>
/// The list of all <see cref="TaikoDifficultyHitObject"/> of the same colour as this <see cref="TaikoDifficultyHitObject"/> in the beatmap.
@ -42,6 +41,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
/// </summary>
public readonly TaikoDifficultyHitObjectRhythm Rhythm;
/// <summary>
/// The interval between this hit object and the surrounding hit objects in its rhythm group.
/// </summary>
public double? HitObjectInterval { get; set; }
/// <summary>
/// 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.
@ -58,6 +62,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
/// </summary>
public double CurrentSliderVelocity;
public double Interval => DeltaTime;
/// <summary>
/// Creates a new difficulty hit object.
/// </summary>
@ -81,7 +87,9 @@ 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);
// Create a Rhythm object, its properties are filled in by TaikoDifficultyHitObjectRhythm
Rhythm = new TaikoDifficultyHitObjectRhythm(this);
switch ((hitObject as Hit)?.Type)
{
@ -105,43 +113,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
}
}
/// <summary>
/// List of most common rhythm changes in taiko maps.
/// </summary>
/// <remarks>
/// The general guidelines for the values are:
/// <list type="bullet">
/// <item>rhythm changes with ratio closer to 1 (that are <i>not</i> 1) are harder to play,</item>
/// <item>speeding up is <i>generally</i> harder than slowing down (with exceptions of rhythm changes requiring a hand switch).</item>
/// </list>
/// </remarks>
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)
};
/// <summary>
/// Returns the closest rhythm change from <see cref="common_rhythms"/> required to hit this object.
/// </summary>
/// <param name="lastObject">The gameplay <see cref="HitObject"/> preceding this one.</param>
/// <param name="lastLastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="lastObject"/>.</param>
/// <param name="clockRate">The rate of the gameplay clock.</param>
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));

View File

@ -1,13 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
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 +14,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
/// </summary>
public class Rhythm : StrainDecaySkill
{
protected override double SkillMultiplier => 10;
protected override double StrainDecayBase => 0;
protected override double SkillMultiplier => 1.0;
protected override double StrainDecayBase => 0.4;
/// <summary>
/// The note-based decay for rhythm strain.
/// </summary>
/// <remarks>
/// <see cref="StrainDecayBase"/> is not used here, as it's time- and not note-based.
/// </remarks>
private const double strain_decay = 0.96;
private readonly double greatHitWindow;
/// <summary>
/// Maximum number of entries in <see cref="rhythmHistory"/>.
/// </summary>
private const int rhythm_history_max_length = 8;
/// <summary>
/// Contains the last <see cref="rhythm_history_max_length"/> changes in note sequence rhythms.
/// </summary>
private readonly LimitedCapacityQueue<TaikoDifficultyHitObject> rhythmHistory = new LimitedCapacityQueue<TaikoDifficultyHitObject>(rhythm_history_max_length);
/// <summary>
/// Contains the rolling rhythm strain.
/// Used to apply per-note decay.
/// </summary>
private double currentStrain;
/// <summary>
/// Number of notes since the last rhythm change has taken place.
/// </summary>
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.
difficulty *= DifficultyCalculationUtils.Logistic(current.DeltaTime, 350, -1 / 25.0, 0.5) + 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;
}
/// <summary>
/// Returns a penalty to apply to the current hit object caused by repeating rhythm changes.
/// </summary>
/// <remarks>
/// Repetitions of more recent patterns are associated with a higher penalty.
/// </remarks>
/// <param name="hitObject">The current hit object being considered.</param>
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;
}
/// <summary>
/// Determines whether the rhythm change pattern starting at <paramref name="start"/> is a repeat of any of the
/// <paramref name="mostRecentPatternsToCompare"/>.
/// </summary>
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;
}
/// <summary>
/// Calculates a single rhythm repetition penalty.
/// </summary>
/// <param name="notesSince">Number of notes since the last repetition of a rhythm change.</param>
private static double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince);
/// <summary>
/// Calculates a penalty based on the number of notes since the last rhythm change.
/// Both rare and frequent rhythm changes are penalised.
/// </summary>
/// <param name="patternLength">Number of notes since the last rhythm change.</param>
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);
}
/// <summary>
/// Calculates a penalty for objects that do not require alternating hands.
/// </summary>
/// <param name="deltaTime">Time (in milliseconds) since the last hit object.</param>
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;
}
/// <summary>
/// Resets the rolling strain value and <see cref="notesSinceRhythmChange"/> counter.
/// </summary>
private void resetRhythmAndStrain()
{
currentStrain = 0.0;
notesSinceRhythmChange = 0;
return difficulty;
}
}
}

View File

@ -10,18 +10,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
public class TaikoDifficultyAttributes : DifficultyAttributes
{
/// <summary>
/// The difficulty corresponding to the stamina skill.
/// </summary>
[JsonProperty("stamina_difficulty")]
public double StaminaDifficulty { get; set; }
/// <summary>
/// The ratio of stamina difficulty from mono-color (single colour) streams to total stamina difficulty.
/// </summary>
[JsonProperty("mono_stamina_factor")]
public double MonoStaminaFactor { get; set; }
/// <summary>
/// The difficulty corresponding to the rhythm skill.
/// </summary>
@ -40,8 +28,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
[JsonProperty("colour_difficulty")]
public double ColourDifficulty { get; set; }
[JsonProperty("rhythm_difficult_strains")]
public double RhythmTopStrains { get; set; }
/// <summary>
/// The difficulty corresponding to the stamina skill.
/// </summary>
[JsonProperty("stamina_difficulty")]
public double StaminaDifficulty { get; set; }
/// <summary>
/// The ratio of stamina difficulty from mono-color (single colour) streams to total stamina difficulty.
/// </summary>
[JsonProperty("mono_stamina_factor")]
public double MonoStaminaFactor { get; set; }
[JsonProperty("reading_difficult_strains")]
public double ReadingTopStrains { get; set; }
[JsonProperty("colour_difficult_strains")]
public double ColourTopStrains { get; set; }

View File

@ -14,6 +14,7 @@ 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;
@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
public class TaikoDifficultyCalculator : DifficultyCalculator
{
private const double difficulty_multiplier = 0.084375;
private const double rhythm_skill_multiplier = 0.200 * difficulty_multiplier;
private const double rhythm_skill_multiplier = 1.24 * difficulty_multiplier;
private const double reading_skill_multiplier = 0.100 * difficulty_multiplier;
private const double colour_skill_multiplier = 0.375 * difficulty_multiplier;
private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier;
@ -37,9 +38,12 @@ 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),
@ -57,6 +61,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
var hitWindows = new HitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
var difficultyHitObjects = new List<DifficultyHitObject>();
var centreObjects = new List<TaikoDifficultyHitObject>();
var rimObjects = new List<TaikoDifficultyHitObject>();
@ -79,7 +86,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
));
}
var groupedHitObjects = SameRhythmHitObjects.GroupHitObjects(noteObjects);
TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects);
SamePatterns.GroupPatterns(groupedHitObjects);
bpmLoader.ProcessEffectiveBPM(beatmap.ControlPointInfo, clockRate);
return difficultyHitObjects;
@ -105,8 +115,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier;
double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5);
double rhythmDifficultStrains = rhythm.CountTopWeightedStrains();
double colourDifficultStrains = colour.CountTopWeightedStrains();
double readingDifficultStrains = reading.CountTopWeightedStrains();
double staminaDifficultStrains = stamina.CountTopWeightedStrains();
double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax);
@ -134,9 +144,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
ColourDifficulty = colourRating,
StaminaDifficulty = staminaRating,
MonoStaminaFactor = monoStaminaFactor,
StaminaTopStrains = staminaDifficultStrains,
RhythmTopStrains = rhythmDifficultStrains,
ReadingTopStrains = readingDifficultStrains,
ColourTopStrains = colourDifficultStrains,
StaminaTopStrains = staminaDifficultStrains,
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate,
MaxCombo = beatmap.GetMaxCombo(),

View File

@ -56,6 +56,16 @@ namespace osu.Game.Rulesets.Difficulty.Utils
/// <returns>The <i>p</i>-norm of the vector.</returns>
public static double Norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
/// <summary>
/// Calculates a Gaussian-based bell curve function (https://en.wikipedia.org/wiki/Gaussian_function)
/// </summary>
/// <param name="x">Value to calculate the function for</param>
/// <param name="mean">The mean (center) of the bell curve</param>
/// <param name="width">The width (spread) of the curve</param>
/// <param name="multiplier">Multiplier to adjust the curve's height</param>
/// <returns>The output of the bell curve function of <paramref name="x"/></returns>
public static double BellCurve(double x, double mean, double width, double multiplier = 1.0) => multiplier * Math.Exp(Math.E * -(Math.Pow(x - mean, 2) / Math.Pow(width, 2)));
/// <summary>
/// Smootherstep function (https://en.wikipedia.org/wiki/Smoothstep#Variations)
/// </summary>