1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-05 09:42:54 +08:00

new rhythm, introduce rhythm per-object difficulty

This commit is contained in:
Jay Lawton 2024-12-01 15:34:15 +10:00
parent 408980af96
commit e00a776133

View File

@ -0,0 +1,145 @@
// 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>
/// Gives a bonus for target ratio using a bell-shaped function.
/// </summary>
private static double bellCurve(double ratio, double targetRatio, double width, double multiplier)
{
return multiplier * Math.Exp(Math.E * -(Math.Pow(ratio - targetRatio, 2) / Math.Pow(width, 2)));
}
/// <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 += bellCurve(ratio, 1, 0.5, 1);
// Penalise ratios that are VERY near 1
difficulty -= bellCurve(ratio, 1, 0.3, 1);
return difficulty / Math.Sqrt(8);
}
/// <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;
}
}
}