Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 130 additions & 16 deletions osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Mods;

namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
{
Expand All @@ -25,32 +28,143 @@

/// <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.
/// The bonus is determined based on the EffectiveBPM, object density and the effects of mods.
/// </summary>
/// <param name="noteObject">The hit object to evaluate.</param>
/// <param name="mods">The mods which were applied to the beatmap.</param>
/// <returns>The reading difficulty value for the given hit object.</returns>
public static double EvaluateDifficultyOf(TaikoDifficultyHitObject noteObject)
public static double EvaluateDifficultyOf(TaikoDifficultyHitObject noteObject, Mod[] mods, bool isHidden)

Check failure on line 36 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Test (Linux, ubuntu-latest, SingleThread)

Parameter 'isHidden' has no matching param tag in the XML comment for 'ReadingEvaluator.EvaluateDifficultyOf(TaikoDifficultyHitObject, Mod[], bool)' (but other parameters do)

Check failure on line 36 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Test (Linux, ubuntu-latest, SingleThread)

Parameter 'isHidden' has no matching param tag in the XML comment for 'ReadingEvaluator.EvaluateDifficultyOf(TaikoDifficultyHitObject, Mod[], bool)' (but other parameters do)

Check failure on line 36 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Test (Linux, ubuntu-latest, MultiThreaded)

Parameter 'isHidden' has no matching param tag in the XML comment for 'ReadingEvaluator.EvaluateDifficultyOf(TaikoDifficultyHitObject, Mod[], bool)' (but other parameters do)

Check failure on line 36 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Test (Linux, ubuntu-latest, MultiThreaded)

Parameter 'isHidden' has no matching param tag in the XML comment for 'ReadingEvaluator.EvaluateDifficultyOf(TaikoDifficultyHitObject, Mod[], bool)' (but other parameters do)

Check failure on line 36 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Parameter 'isHidden' has no matching param tag in the XML comment for 'ReadingEvaluator.EvaluateDifficultyOf(TaikoDifficultyHitObject, Mod[], bool)' (but other parameters do)

Check failure on line 36 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Parameter 'isHidden' has no matching param tag in the XML comment for 'ReadingEvaluator.EvaluateDifficultyOf(TaikoDifficultyHitObject, Mod[], bool)' (but other parameters do)

Check failure on line 36 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Test (Windows, windows-latest, SingleThread)

Parameter 'isHidden' has no matching param tag in the XML comment for 'ReadingEvaluator.EvaluateDifficultyOf(TaikoDifficultyHitObject, Mod[], bool)' (but other parameters do)

Check failure on line 36 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Test (Windows, windows-latest, SingleThread)

Parameter 'isHidden' has no matching param tag in the XML comment for 'ReadingEvaluator.EvaluateDifficultyOf(TaikoDifficultyHitObject, Mod[], bool)' (but other parameters do)

Check failure on line 36 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Test (Windows, windows-latest, MultiThreaded)

Parameter 'isHidden' has no matching param tag in the XML comment for 'ReadingEvaluator.EvaluateDifficultyOf(TaikoDifficultyHitObject, Mod[], bool)' (but other parameters do)

Check failure on line 36 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Test (Windows, windows-latest, MultiThreaded)

Parameter 'isHidden' has no matching param tag in the XML comment for 'ReadingEvaluator.EvaluateDifficultyOf(TaikoDifficultyHitObject, Mod[], bool)' (but other parameters do)

Check warning on line 36 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Build only (iOS)

Parameter 'isHidden' has no matching param tag in the XML comment for 'ReadingEvaluator.EvaluateDifficultyOf(TaikoDifficultyHitObject, Mod[], bool)' (but other parameters do)

Check warning on line 36 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Build only (iOS)

Parameter 'isHidden' has no matching param tag in the XML comment for 'ReadingEvaluator.EvaluateDifficultyOf(TaikoDifficultyHitObject, Mod[], bool)' (but other parameters do)

Check warning on line 36 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

Parameter 'isHidden' has no matching param tag in the XML comment for 'ReadingEvaluator.EvaluateDifficultyOf(TaikoDifficultyHitObject, Mod[], bool)' (but other parameters do)

Check warning on line 36 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

Parameter 'isHidden' has no matching param tag in the XML comment for 'ReadingEvaluator.EvaluateDifficultyOf(TaikoDifficultyHitObject, Mod[], bool)' (but other parameters do)
{
var highVelocity = new VelocityRange(480, 640);
var midVelocity = new VelocityRange(360, 480);
bool isFlashlight = mods.Any(m => m is TaikoModFlashlight);

// Apply a cap to prevent outlier values on maps that exceed the editor's parameters.
double effectiveBPM = Math.Max(1.0, noteObject.EffectiveBPM);
// With HDFL, all note objects are invisible and give the maximum reading difficulty
if (isHidden && isFlashlight)
return 1.0;

double midVelocityDifficulty = 0.5 * DifficultyCalculationUtils.Logistic(effectiveBPM, midVelocity.Center, 1.0 / (midVelocity.Range / 10));
double velocityDifficulty = calculateVelocityDifficulty(noteObject, mods, isHidden);
double densityDifficulty = calculateDensityDifficulty(noteObject);

// Expected DeltaTime is the DeltaTime this note would need to be spaced equally to a base slider velocity 1/4 note.
double expectedDeltaTime = 21000.0 / effectiveBPM;
double objectDensity = expectedDeltaTime / Math.Max(1.0, noteObject.DeltaTime);
double difficulty = Math.Max(velocityDifficulty, densityDifficulty);

// With hidden, all notes award a base difficulty
if (isHidden)
difficulty = 0.25 + 0.75 * difficulty;

return difficulty;
}

private static double calculateVelocityDifficulty(TaikoDifficultyHitObject noteObject, Mod[] mods, bool isHidden)
{
double highVelocityDifficulty = 0.0;
double timeInvisibleDifficulty = 0.0;

// To allow high velocity sections at lower actual BPM to award similar difficulty to high BPM sections with more frequent objects,
// a bonus is applied to the high velocity range at lower object density
double densityBonus = calculateHighVelocityDensityBonus(noteObject);

var highVelocity = new VelocityRange(
420 - 140 * densityBonus,
1000 - 320 * densityBonus
);

highVelocityDifficulty = DifficultyCalculationUtils.Logistic(
noteObject.EffectiveBPM * calculateHighVelocityModMultiplier(mods, isHidden),
highVelocity.Center,
10.0 / highVelocity.Range
);

// With hidden, notes that stay invisible for longer before being hit are harder to read
if (isHidden) {

Check failure on line 77 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 77 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)
var lowVelocity = new VelocityRange(280, 125);

timeInvisibleDifficulty = DifficultyCalculationUtils.Logistic(
noteObject.EffectiveBPM * calculateTimeInvisibleModMultiplier(mods),
lowVelocity.Center,
10.0 / lowVelocity.Range
);
}

return Math.Max(highVelocityDifficulty, timeInvisibleDifficulty);
}

private static double calculateHighVelocityDensityBonus(TaikoDifficultyHitObject noteObject)
{
double density = calculateObjectDensity(noteObject);

// Single note gaps in otherwise dense sections would overly award the bonus for low density
// As a result, the higher density out of both the current and previous note is used
var prevNoteObject = (TaikoDifficultyHitObject) noteObject.Previous(0);

Check failure on line 96 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 96 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

if (prevNoteObject != null)
{
double prevDensity = calculateObjectDensity(prevNoteObject);
return DifficultyCalculationUtils.Smoothstep(Math.Max(density, prevDensity), 0.9, 0.35);
}

return DifficultyCalculationUtils.Smoothstep(density, 0.9, 0.35);
}

private static double calculateHighVelocityModMultiplier(Mod[] mods, bool isHidden)
{
bool isFlashlight = mods.Any(m => m is TaikoModFlashlight);
bool isEasy = mods.Any(m => m is TaikoModEasy);

// High density is penalised at high velocity as it is generally considered easier to read. See https://www.desmos.com/calculator/u63f3ntdsi
double densityPenalty = DifficultyCalculationUtils.Logistic(objectDensity, 0.925, 15);
double multiplier = 1.0;

if (isHidden)
{

Check failure on line 115 in osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)
// With hidden enabled, the playfield is limited from the expected 1560px wide (equivalent to 16:9) to only 1080px (4:3)
// This is not the case with the classic mod enabled, but due to current limitations this is penalised in performance calculation instead
// Considerations for HDHRCL are currently out of scope
multiplier *= 1560.0 / 1080.0;

// Notes fading out after a short time with hidden means their velocity is essentially higher. With easy enabled, notes fade out after longer.
// Both of these values are arbitrary and based on feedback
if (isEasy)
multiplier *= 1.1;
else
multiplier *= 1.2;
}

// With flashlight, the visible playfield is limited from the expected 1560px wide to around 468px
// Considerations for combo and smaller flashlights are currently out of scope
if (isFlashlight)
multiplier *= 1560.0 / 468.0;

return multiplier;
}

private static double calculateTimeInvisibleModMultiplier(Mod[] mods)
{
bool isEasy = mods.Any(m => m is TaikoModEasy);

double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty)
* DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10));
double multiplier = 1.0;

// With easy enabled, notes fade out later and are invisible for less time. This is equivalent to their effective BPM being higher
if (isEasy)
multiplier *= 1.35;

return multiplier;
}

private static double calculateDensityDifficulty(TaikoDifficultyHitObject noteObject)
{
// Notes at very high density are harder to read
return Math.Pow(
DifficultyCalculationUtils.Logistic(calculateObjectDensity(noteObject), 3.5, 1.5),
3.0
);
}

private static double calculateObjectDensity(TaikoDifficultyHitObject noteObject)
{
if (noteObject.EffectiveBPM == 0 || noteObject.DeltaTime == 0)
return 1.0;

// Expected DeltaTime is the DeltaTime this note would need to be spaced equally to a base slider velocity 1/4 note.
double expectedDeltaTime = 21000.0 / noteObject.EffectiveBPM;

return midVelocityDifficulty + highVelocityDifficulty;
return expectedDeltaTime / noteObject.DeltaTime;
}
}
}
29 changes: 26 additions & 3 deletions osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// 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.Linq;
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.Evaluators;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Objects;

namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
Expand All @@ -21,9 +23,14 @@ public class Reading : StrainDecaySkill

private double currentStrain;

public Reading(Mod[] mods)
private Mod[] mods;
public readonly bool HiddenDifficultyOnly;

public Reading(Mod[] mods, bool HiddenDifficultyOnly)
: base(mods)
{
this.mods = mods;
this.HiddenDifficultyOnly = HiddenDifficultyOnly;
}

protected override double StrainValueOf(DifficultyHitObject current)
Expand All @@ -34,13 +41,29 @@ protected override double StrainValueOf(DifficultyHitObject current)
return 0.0;
}

bool isHidden = mods.Any(m => m is TaikoModHidden);

var taikoObject = (TaikoDifficultyHitObject)current;
int index = taikoObject.ColourData.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0;

currentStrain *= DifficultyCalculationUtils.Logistic(index, 4, -1 / 25.0, 0.5) + 0.5;

currentStrain *= StrainDecayBase;
currentStrain += ReadingEvaluator.EvaluateDifficultyOf(taikoObject) * SkillMultiplier;

double difficulty = ReadingEvaluator.EvaluateDifficultyOf(taikoObject, mods, isHidden);

if (HiddenDifficultyOnly)
{
double hiddenDifficulty = 0.0;

if (isHidden)
hiddenDifficulty = difficulty - ReadingEvaluator.EvaluateDifficultyOf(taikoObject, mods, false);

currentStrain += hiddenDifficulty * SkillMultiplier;
}
else
{
currentStrain += difficulty * SkillMultiplier;
}

return currentStrain;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,15 @@ public class TaikoDifficultyAttributes : DifficultyAttributes
/// <summary>
/// The difficulty corresponding to the reading skill.
/// </summary>
[JsonProperty("reading_difficulty")]
public double ReadingDifficulty { get; set; }

/// <summary>
/// Contribution to reading difficulty from the hidden mod.
/// </summary>
[JsonProperty("hidden_reading_difficulty")]
public double HiddenReadingDifficulty { get; set; }

/// <summary>
/// The difficulty corresponding to the colour skill.
/// </summary>
Expand Down
20 changes: 16 additions & 4 deletions osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ public class TaikoDifficultyCalculator : DifficultyCalculator
{
private const double difficulty_multiplier = 0.084375;
private const double rhythm_skill_multiplier = 0.750 * difficulty_multiplier;
private const double reading_skill_multiplier = 0.100 * difficulty_multiplier;
private const double reading_skill_multiplier = 0.200 * difficulty_multiplier;
private const double colour_skill_multiplier = 0.375 * difficulty_multiplier;
private const double stamina_skill_multiplier = 0.445 * difficulty_multiplier;

private double strainLengthBonus;
private double patternMultiplier;
private double readingLengthPenalty;

private bool isRelax;
private bool isConvert;
Expand All @@ -48,7 +49,8 @@ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
return new Skill[]
{
new Rhythm(mods),
new Reading(mods),
new Reading(mods, false),
new Reading(mods, true),
new Colour(mods),
new Stamina(mods, false, isConvert),
new Stamina(mods, true, isConvert)
Expand All @@ -61,6 +63,7 @@ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
new TaikoModHalfTime(),
new TaikoModEasy(),
new TaikoModHardRock(),
new TaikoModHidden(),
};

protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods)
Expand Down Expand Up @@ -101,27 +104,34 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
return new TaikoDifficultyAttributes { Mods = mods };

var rhythm = skills.OfType<Rhythm>().Single();
var reading = skills.OfType<Reading>().Single();
var reading = skills.OfType<Reading>().Single(s => !s.HiddenDifficultyOnly);
var hiddenReading = skills.OfType<Reading>().Single(s => s.HiddenDifficultyOnly);
var colour = skills.OfType<Colour>().Single();
var stamina = skills.OfType<Stamina>().Single(s => !s.SingleColourStamina);
var singleColourStamina = skills.OfType<Stamina>().Single(s => s.SingleColourStamina);

double staminaDifficultyValue = stamina.DifficultyValue();
double readingDifficultyValue = reading.DifficultyValue();

double rhythmSkill = rhythm.DifficultyValue() * rhythm_skill_multiplier;
double readingSkill = reading.DifficultyValue() * reading_skill_multiplier;
double hiddenReadingSkill = hiddenReading.DifficultyValue() * reading_skill_multiplier;
double colourSkill = colour.DifficultyValue() * colour_skill_multiplier;
double staminaSkill = staminaDifficultyValue * stamina_skill_multiplier;
double monoStaminaSkill = singleColourStamina.DifficultyValue() * stamina_skill_multiplier;
double monoStaminaFactor = staminaSkill == 0 ? 1 : Math.Pow(monoStaminaSkill / staminaSkill, 5);

double staminaDifficultStrains = stamina.CountTopWeightedStrains(staminaDifficultyValue);
double readingDifficultStrains = reading.CountTopWeightedStrains(readingDifficultyValue);

// As we don't have pattern integration in osu!taiko, we apply the other two skills relative to rhythm.
patternMultiplier = Math.Pow(staminaSkill * colourSkill, 0.10);

strainLengthBonus = 1 + 0.15 * DifficultyCalculationUtils.ReverseLerp(staminaDifficultStrains, 1000, 1555);

// Apply a penalty to small amounts of reading that can be memorised.
readingLengthPenalty = DifficultyCalculationUtils.ReverseLerp(readingDifficultStrains, 0, 150);

double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, out double consistencyFactor);
double starRating = rescale(combinedRating * 1.4);

Expand All @@ -130,6 +140,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat

double rhythmDifficulty = rhythmSkill * skillRating;
double readingDifficulty = readingSkill * skillRating;
double hiddenReadingDifficulty = hiddenReadingSkill * skillRating;
double colourDifficulty = colourSkill * skillRating;
double staminaDifficulty = staminaSkill * skillRating;
double mechanicalDifficulty = colourDifficulty + staminaDifficulty; // Mechanical difficulty is the sum of colour and stamina difficulties.
Expand All @@ -141,6 +152,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
MechanicalDifficulty = mechanicalDifficulty,
RhythmDifficulty = rhythmDifficulty,
ReadingDifficulty = readingDifficulty,
HiddenReadingDifficulty = hiddenReadingDifficulty,
ColourDifficulty = colourDifficulty,
StaminaDifficulty = staminaDifficulty,
MonoStaminaFactor = monoStaminaFactor,
Expand Down Expand Up @@ -216,7 +228,7 @@ private List<double> combinePeaks(IReadOnlyList<double> rhythmPeaks, IReadOnlyLi
for (int i = 0; i < colourPeaks.Count; i++)
{
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier * patternMultiplier;
double readingPeak = readingPeaks[i] * reading_skill_multiplier;
double readingPeak = readingPeaks[i] * reading_skill_multiplier * readingLengthPenalty;
double colourPeak = isRelax ? 0 : colourPeaks[i] * colour_skill_multiplier; // There is no colour difficulty in relax.
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier * strainLengthBonus;
staminaPeak /= isConvert || isRelax ? 1.5 : 1.0; // Available finger count is increased by 150%, thus we adjust accordingly.
Expand Down
Loading
Loading