mirror of
https://github.com/ppy/osu.git
synced 2024-12-05 03:13:22 +08:00
Merge 544ba25743
into f09d8f097a
This commit is contained in:
commit
fbc3000765
@ -0,0 +1,45 @@
|
||||
// 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 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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="noteObject">The hit object to evaluate.</param>
|
||||
/// <returns>The reading difficulty value for the given hit object.</returns>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the object density based on the DeltaTime, EffectiveBPM, and CurrentSliderVelocity.
|
||||
/// </summary>
|
||||
/// <param name="noteObject">The current noteObject to evaluate.</param>
|
||||
/// <returns>The calculated object density.</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
137
osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
Normal file
137
osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
Normal file
@ -0,0 +1,137 @@
|
||||
// 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)
|
||||
{
|
||||
// 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 += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5);
|
||||
|
||||
// Penalize ratios that are VERY near 1
|
||||
difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.3);
|
||||
|
||||
return difficulty / Math.Sqrt(8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the pattern of hit object intervals is consistent based on a given threshold.
|
||||
/// </summary>
|
||||
private static bool isConsistentPattern(EvenHitObjects evenHitObjects, double threshold = 0.1)
|
||||
{
|
||||
// Collect the last 4 intervals (current and the last 3 previous).
|
||||
List<double?> intervals = new List<double?>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
// 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;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data
|
||||
{
|
||||
|
@ -0,0 +1,50 @@
|
||||
// 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.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading
|
||||
{
|
||||
public class EffectiveBPMPreprocessor
|
||||
{
|
||||
private readonly IList<TaikoDifficultyHitObject> noteObjects;
|
||||
private readonly double globalSliderVelocity;
|
||||
|
||||
public EffectiveBPMPreprocessor(IBeatmap beatmap, List<TaikoDifficultyHitObject> noteObjects)
|
||||
{
|
||||
this.noteObjects = noteObjects;
|
||||
globalSliderVelocity = beatmap.Difficulty.SliderMultiplier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates and sets the effective BPM and slider velocity for each note object, considering clock rate and scroll speed.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the slider velocity based on control point info and clock rate.
|
||||
/// </summary>
|
||||
private double calculateSliderVelocity(ControlPointInfo controlPointInfo, double startTime, double clockRate)
|
||||
{
|
||||
var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime);
|
||||
return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 EvenHitObjects : EvenRhythm<TaikoDifficultyHitObject>, IHasInterval
|
||||
{
|
||||
public TaikoDifficultyHitObject FirstHitObject => Children[0];
|
||||
|
||||
public EvenHitObjects? 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="EvenHitObjects"/>. This is only defined if there is
|
||||
/// more than two hit objects in this <see cref="EvenHitObjects"/>.
|
||||
/// </summary>
|
||||
public double? HitObjectInterval;
|
||||
|
||||
/// <summary>
|
||||
/// The ratio of <see cref="HitObjectInterval"/> between this and the previous <see cref="EvenHitObjects"/>. 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="EvenHitObjects"/>.
|
||||
/// </summary>
|
||||
public double Interval { get; private set; } = double.PositiveInfinity;
|
||||
|
||||
public EvenHitObjects(EvenHitObjects? previous, List<TaikoDifficultyHitObject> 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<EvenHitObjects> GroupHitObjects(List<TaikoDifficultyHitObject> data)
|
||||
{
|
||||
List<EvenHitObjects> flatPatterns = new List<EvenHitObjects>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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="EvenHitObjects"/> grouped by their <see cref="EvenHitObjects.StartTime"/>'s interval.
|
||||
/// </summary>
|
||||
public class EvenPatterns : EvenRhythm<EvenHitObjects>
|
||||
{
|
||||
public EvenPatterns? Previous { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="EvenHitObjects.Interval"/> between children <see cref="EvenHitObjects"/> within this group.
|
||||
/// If there is only one child, this will have the value of the first child's <see cref="EvenHitObjects.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="EvenPatterns"/>. In the
|
||||
/// case where there is no previous <see cref="EvenPatterns"/>, 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 EvenPatterns(EvenPatterns? previous, List<EvenHitObjects> 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<EvenHitObjects> data)
|
||||
{
|
||||
List<EvenPatterns> evenPatterns = new List<EvenPatterns>();
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 EvenRhythm<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="EvenRhythm{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="EvenRhythm{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 EvenRhythm(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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -1,24 +1,82 @@
|
||||
// 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 group of hit objects with consistent rhythm that this object belongs to.
|
||||
/// </summary>
|
||||
public EvenHitObjects? EvenHitObjects;
|
||||
|
||||
/// <summary>
|
||||
/// The larger pattern of rhythm groups that this object is part of.
|
||||
/// </summary>
|
||||
public EvenPatterns? EvenPatterns;
|
||||
|
||||
/// <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>
|
||||
/// The difficulty multiplier associated with this rhythm change.
|
||||
/// </summary>
|
||||
public readonly double Difficulty;
|
||||
|
||||
/// <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.
|
||||
/// A <see cref="Ratio"/> above 1 indicates a slow-down; a <see cref="Ratio"/> below 1 indicates a speed-up.
|
||||
/// List of most common rhythm changes in taiko maps. This is used as a display value.
|
||||
/// </summary>
|
||||
public readonly double Ratio;
|
||||
/// <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),
|
||||
new TaikoDifficultyHitObjectRhythm(2, 3, 0.4),
|
||||
new TaikoDifficultyHitObjectRhythm(5, 4, 0.5),
|
||||
new TaikoDifficultyHitObjectRhythm(4, 5, 0.7)
|
||||
};
|
||||
|
||||
/// <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;
|
||||
Difficulty = 0.0;
|
||||
return;
|
||||
}
|
||||
|
||||
TaikoDifficultyHitObjectRhythm closestRhythm = getClosestRhythm(current.DeltaTime, previous.DeltaTime);
|
||||
Ratio = closestRhythm.Ratio;
|
||||
Difficulty = closestRhythm.Difficulty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an object representing a rhythm change.
|
||||
@ -26,10 +84,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
|
||||
/// <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, double difficulty)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,27 +1,36 @@
|
||||
// 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;
|
||||
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
|
||||
{
|
||||
/// <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.
|
||||
/// </summary>
|
||||
private readonly IReadOnlyList<TaikoDifficultyHitObject>? monoDifficultyHitObjects;
|
||||
|
||||
/// <summary>
|
||||
/// The adjusted BPM of this hit object, based on its slider velocity and scroll speed.
|
||||
/// </summary>
|
||||
public double EffectiveBPM;
|
||||
|
||||
/// <summary>
|
||||
/// The current slider velocity of this hit object.
|
||||
/// </summary>
|
||||
public double CurrentSliderVelocity;
|
||||
|
||||
/// <summary>
|
||||
/// The index of this <see cref="TaikoDifficultyHitObject"/> in <see cref="monoDifficultyHitObjects"/>.
|
||||
/// </summary>
|
||||
@ -42,12 +51,19 @@ 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.
|
||||
/// </summary>
|
||||
public readonly TaikoDifficultyHitObjectColour Colour;
|
||||
|
||||
public double Interval => DeltaTime;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new difficulty hit object.
|
||||
/// </summary>
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/// <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));
|
||||
|
49
osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs
Normal file
49
osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs
Normal file
@ -0,0 +1,49 @@
|
||||
// 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 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates the reading coefficient of taiko difficulty.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,10 @@
|
||||
// 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.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
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <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.
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
[JsonProperty("rhythm_difficulty")]
|
||||
public double RhythmDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the reading skill.
|
||||
/// </summary>
|
||||
[JsonProperty("reading_difficulty")]
|
||||
public double ReadingDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines the object density of a beatmap, based on DeltaTime.
|
||||
/// </summary>
|
||||
[JsonProperty("object_density")]
|
||||
public double ObjectDensity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the colour skill.
|
||||
/// </summary>
|
||||
@ -35,10 +47,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
public double ColourDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the hardest parts of the map.
|
||||
/// The penalty factor corresponding to patterns based on their correlation between skills.
|
||||
/// </summary>
|
||||
[JsonProperty("peak_difficulty")]
|
||||
public double PeakDifficulty { get; set; }
|
||||
[JsonProperty("simple_pattern_factor")]
|
||||
public double SimplePattern { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines the amount of difficult strain peaks present within each skill.
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
|
@ -8,10 +8,13 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
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.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 +24,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 +46,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 +69,67 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||
{
|
||||
List<DifficultyHitObject> difficultyHitObjects = new List<DifficultyHitObject>();
|
||||
List<TaikoDifficultyHitObject> centreObjects = new List<TaikoDifficultyHitObject>();
|
||||
List<TaikoDifficultyHitObject> rimObjects = new List<TaikoDifficultyHitObject>();
|
||||
List<TaikoDifficultyHitObject> noteObjects = new List<TaikoDifficultyHitObject>();
|
||||
var hitWindows = new HitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
var difficultyHitObjects = new List<DifficultyHitObject>();
|
||||
var centreObjects = new List<TaikoDifficultyHitObject>();
|
||||
var rimObjects = new List<TaikoDifficultyHitObject>();
|
||||
var noteObjects = new List<TaikoDifficultyHitObject>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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,24 +137,31 @@ 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;
|
||||
}
|
||||
|
||||
@ -109,9 +174,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 +191,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
return attributes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a final re-scaling of the star rating.
|
||||
/// </summary>
|
||||
/// <param name="sr">The raw star rating value before re-scaling.</param>
|
||||
private double rescale(double sr)
|
||||
{
|
||||
if (sr < 0) return sr;
|
||||
|
||||
return 10.43 * Math.Log(sr / 8 + 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map.
|
||||
/// </summary>
|
||||
@ -138,22 +198,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).
|
||||
/// </remarks>
|
||||
private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina)
|
||||
private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina)
|
||||
{
|
||||
List<double> peaks = new List<double>();
|
||||
|
||||
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);
|
||||
double peak = DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak);
|
||||
peak = DifficultyCalculationUtils.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.
|
||||
@ -174,10 +238,31 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector.
|
||||
/// Calculates the penalty for skills based on their relationship making up a pattern.
|
||||
/// Penalising ratings where patterns have a major difference in value.
|
||||
/// </summary>
|
||||
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param>
|
||||
/// <param name="values">The coefficients of the vector.</param>
|
||||
private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a final re-scaling of the star rating.
|
||||
/// </summary>
|
||||
/// <param name="sr">The raw star rating value before re-scaling.</param>
|
||||
private double rescale(double sr)
|
||||
{
|
||||
if (sr < 0) return sr;
|
||||
|
||||
return 10.43 * Math.Log(sr / 8 + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<TaikoHitObject>))
|
||||
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.0005 * countOk) / 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);
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace osu.Game.Rulesets.Difficulty.Utils
|
||||
{
|
||||
@ -46,5 +47,23 @@ namespace osu.Game.Rulesets.Difficulty.Utils
|
||||
/// <param name="exponent">Exponent</param>
|
||||
/// <returns>The output of logistic function</returns>
|
||||
public static double Logistic(double exponent, double maxValue = 1) => maxValue / (1 + Math.Exp(exponent));
|
||||
|
||||
/// <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>
|
||||
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector (https://en.wikipedia.org/wiki/Norm_(mathematics))
|
||||
/// </summary>
|
||||
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param>
|
||||
/// <param name="values">The coefficients of the vector.</param>
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user