Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
151 changes: 135 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,148 @@

/// <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>
/// <param name="isClassic">Whether the classic mod was 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 isClassic)
{
var highVelocity = new VelocityRange(480, 640);
var midVelocity = new VelocityRange(360, 480);
bool isHidden = mods.Any(m => m is TaikoModHidden);
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, isClassic);
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 isClassic)
{
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, isClassic),
highVelocity.Center,
10.0 / highVelocity.Range
);

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

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

Check failure on line 81 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 81 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);

// 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);
timeInvisibleDifficulty = DifficultyCalculationUtils.Logistic(
noteObject.EffectiveBPM * calculateTimeInvisibleModMultiplier(mods, isClassic),
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 100 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 100 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);
}

double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty)
* DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10));
private static double calculateHighVelocityModMultiplier(Mod[] mods, bool isClassic)
{
bool isHidden = mods.Any(m => m is TaikoModHidden);
bool isFlashlight = mods.Any(m => m is TaikoModFlashlight);
bool isEasy = mods.Any(m => m is TaikoModEasy);

double multiplier = 1.0;

if (isHidden)
{

Check failure on line 120 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 120 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 the classic mod enabled, the playfield is limited from the expected 1560px wide (equivalent to 16:9) to only 1080px (4:3)
// Considerations for HDHRCL are currently out of scope
if (isClassic)
multiplier *= 1560.0 / 1080.0;

// Notes fading out after a short time with hidden means their velocity is essentially higher. With EZCL notes fade out after longer.
// Both of these values are arbitrary and based on feedback
if (isClassic && 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 isClassic)
{
bool isEasy = mods.Any(m => m is TaikoModEasy);

double multiplier = 1.0;

// With EZCL, notes fade out later and are invisible for less time. This is equivalent to their effective BPM being higher
if (isEasy && isClassic)
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;
}
}
}
9 changes: 7 additions & 2 deletions osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,14 @@

private double currentStrain;

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

Check failure on line 25 in osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Naming rule violation: These words must begin with upper case characters: isClassic (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1006)

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

protected override double StrainValueOf(DifficultyHitObject current)
Expand All @@ -40,7 +45,7 @@
currentStrain *= DifficultyCalculationUtils.Logistic(index, 4, -1 / 25.0, 0.5) + 0.5;

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

return currentStrain;
}
Expand Down
17 changes: 17 additions & 0 deletions osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
public class TaikoDifficultyAttributes : DifficultyAttributes
{
/// <summary>
/// The combined star rating of all skills.
/// </summary>
[JsonProperty("star_rating_classic", Order = -2)]
public double StarRatingClassic { get; set; }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can't exist. You will have to wait for the realtime diffcalc for different classic/non-classic SR. For now assume everything is calculated with CL

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would this deploy be a good time to get realtime merged?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not up to us and as far as I know not coming in the roadmap any time soon. Please just take any changes that rely on realtime's existence out and we'll revisit when we can

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunate, i'll see what i can do


/// <summary>
/// The difficulty corresponding to the mechanical skills in osu!taiko.
/// This includes colour and stamina combined.
Expand All @@ -27,6 +33,11 @@ public class TaikoDifficultyAttributes : DifficultyAttributes
/// </summary>
public double ReadingDifficulty { get; set; }

/// <summary>
/// The difficulty corresponding to the reading skill with the classic mod enabled.
/// </summary>
public double ReadingDifficultyClassic { get; set; }

/// <summary>
/// The difficulty corresponding to the colour skill.
/// </summary>
Expand All @@ -49,6 +60,12 @@ public class TaikoDifficultyAttributes : DifficultyAttributes
[JsonProperty("consistency_factor")]
public double ConsistencyFactor { get; set; }

/// <summary>
/// The factor corresponding to the consistency of a map with the classic mod enabled.
/// </summary>
[JsonProperty("consistency_factor_classic")]
public double ConsistencyFactorClassic { get; set; }

public double StaminaTopStrains { get; set; }

public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
Expand Down
27 changes: 23 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 @@
{
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 @@
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 @@
new TaikoModHalfTime(),
new TaikoModEasy(),
new TaikoModHardRock(),
new TaikoModHidden(),
};

protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods)
Expand Down Expand Up @@ -101,7 +104,8 @@
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.isClassic);
var readingClassic = skills.OfType<Reading>().Single(s => s.isClassic);
var colour = skills.OfType<Colour>().Single();
var stamina = skills.OfType<Stamina>().Single(s => !s.SingleColourStamina);
var singleColourStamina = skills.OfType<Stamina>().Single(s => s.SingleColourStamina);
Expand All @@ -110,6 +114,7 @@

double rhythmSkill = rhythm.DifficultyValue() * rhythm_skill_multiplier;
double readingSkill = reading.DifficultyValue() * reading_skill_multiplier;
double readingClassicSkill = readingClassic.DifficultyValue() * reading_skill_multiplier;
double colourSkill = colour.DifficultyValue() * colour_skill_multiplier;
double staminaSkill = staminaDifficultyValue * stamina_skill_multiplier;
double monoStaminaSkill = singleColourStamina.DifficultyValue() * stamina_skill_multiplier;
Expand All @@ -125,27 +130,35 @@
double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, out double consistencyFactor);
double starRating = rescale(combinedRating * 1.4);

// Repeat difficulty calculation for if the classic mod is enabled.
double combinedRatingClassic = combinedDifficultyValue(rhythm, readingClassic, colour, stamina, out double consistencyFactorClassic);
double starRatingClassic = rescale(combinedRatingClassic * 1.4);

// Calculate proportional contribution of each skill to the combinedRating.
double skillRating = starRating / (rhythmSkill + readingSkill + colourSkill + staminaSkill);

double rhythmDifficulty = rhythmSkill * skillRating;
double readingDifficulty = readingSkill * skillRating;
double readingClassicDifficulty = readingClassicSkill * skillRating;
double colourDifficulty = colourSkill * skillRating;
double staminaDifficulty = staminaSkill * skillRating;
double mechanicalDifficulty = colourDifficulty + staminaDifficulty; // Mechanical difficulty is the sum of colour and stamina difficulties.

TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes
{
StarRating = starRating,
StarRatingClassic = starRatingClassic,
Mods = mods,
MechanicalDifficulty = mechanicalDifficulty,
RhythmDifficulty = rhythmDifficulty,
ReadingDifficulty = readingDifficulty,
ReadingDifficultyClassic = readingClassicDifficulty,
ColourDifficulty = colourDifficulty,
StaminaDifficulty = staminaDifficulty,
MonoStaminaFactor = monoStaminaFactor,
StaminaTopStrains = staminaDifficultStrains,
ConsistencyFactor = consistencyFactor,
ConsistencyFactorClassic = consistencyFactorClassic,
MaxCombo = beatmap.GetMaxCombo(),
};

Expand All @@ -161,6 +174,12 @@
/// </remarks>
private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, out double consistencyFactor)
{
double readingDifficultyValue = reading.DifficultyValue();
double readingDifficultStrains = reading.CountTopWeightedStrains(readingDifficultyValue);

Check failure on line 179 in osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.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 179 in osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)
// Apply a penalty to small amounts of reading that can be memorised.
readingLengthPenalty = DifficultyCalculationUtils.ReverseLerp(readingDifficultStrains, 0, 150);

List<double> peaks = combinePeaks(
rhythm.GetCurrentStrainPeaks().ToList(),
reading.GetCurrentStrainPeaks().ToList(),
Expand Down Expand Up @@ -216,7 +235,7 @@
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