Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic tl-tapping consideration for stamina #20558

Merged
merged 19 commits into from
Nov 7, 2022
Merged
Show file tree
Hide file tree
Changes from 17 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
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ public class TaikoDifficultyCalculatorTest : DifficultyCalculatorTest
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";

[TestCase(3.1098944660126882d, 200, "diffcalc-test")]
[TestCase(3.1098944660126882d, 200, "diffcalc-test-strong")]
[TestCase(3.0920212594351191d, 200, "diffcalc-test")]
[TestCase(3.0920212594351191d, 200, "diffcalc-test-strong")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);

[TestCase(4.0974106752474251d, 200, "diffcalc-test")]
[TestCase(4.0974106752474251d, 200, "diffcalc-test-strong")]
[TestCase(4.0789820318081444d, 200, "diffcalc-test")]
[TestCase(4.0789820318081444d, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ public static double EvaluateDifficultyOf(DifficultyHitObject hitObject)
TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour;
double difficulty = 0.0d;

if (colour.MonoStreak != null) // Difficulty for MonoStreak
if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak
difficulty += EvaluateDifficultyOf(colour.MonoStreak);
if (colour.AlternatingMonoPattern != null) // Difficulty for AlternatingMonoPattern
if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern
difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern);
if (colour.RepeatingHitPattern != null) // Difficulty for RepeatingHitPattern
if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern);

return difficulty;
Expand Down
42 changes: 32 additions & 10 deletions osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,43 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
public class StaminaEvaluator
{
/// <summary>
/// Applies a speed bonus dependent on the time since the last hit performed using this key.
/// Applies a speed bonus dependent on the time since the last hit performed using this finger.
/// </summary>
/// <param name="interval">The interval between the current and previous note hit using the same key.</param>
/// <param name="interval">The interval between the current and previous note hit using the same finger.</param>
private static double speedBonus(double interval)
{
// Cap to 600bpm 1/4, 25ms note interval, 50ms key interval
// Interval will be capped at a very small value to avoid infinite/negative speed bonuses.
// TODO - This is a temporary measure as we need to implement methods of detecting playstyle-abuse of SpeedBonus.
interval = Math.Max(interval, 50);
// Interval is capped at a very small value to prevent infinite values.
interval = Math.Max(interval, 1);

return 30 / interval;
}

/// <summary>
/// Determines the number of fingers available to hit the current <see cref="TaikoDifficultyHitObject"/>.
/// Any mono notes that is more than 300ms apart from a colour change will be considered to have more than 2
/// fingers available, since players can hit the same key with multiple fingers.
/// </summary>
private static int availableFingersFor(TaikoDifficultyHitObject hitObject)
{
DifficultyHitObject? previousColourChange = hitObject.Colour.PreviousColourChange;
DifficultyHitObject? nextColourChange = hitObject.Colour.NextColourChange;

if (previousColourChange != null && hitObject.StartTime - previousColourChange.StartTime < 300)
{
return 2;
}

if (nextColourChange != null && nextColourChange.StartTime - hitObject.StartTime < 300)
{
return 2;
}

return 4;
}
Comment on lines +30 to +46
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is it 2 or 4 fingers, instead of 1 or 2 fingers?

Copy link
Member

Choose a reason for hiding this comment

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

mechanically, normal taiko players have 2 fingers available to hit each colour, with the TL playstyle, they use four and above fingers solely to hit the single colour.

2 fingers refers to the normal count, while 4 refers to the new count if the threshold is reached.


/// <summary>
/// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the
/// maximum possible interval between two hits using the same key, by alternating 2 keys for each colour.
/// maximum possible interval between two hits using the same key, by alternating available fingers for each colour.
/// </summary>
public static double EvaluateDifficultyOf(DifficultyHitObject current)
{
Expand All @@ -35,13 +56,14 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current)
return 0.0;
}

// Find the previous hit object hit by the current key, which is two notes of the same colour prior.
// Find the previous hit object hit by the current finger, which is n notes prior, n being the number of
// available fingers.
TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current;
TaikoDifficultyHitObject? keyPrevious = taikoCurrent.PreviousMono(1);
TaikoDifficultyHitObject? keyPrevious = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1);

if (keyPrevious == null)
{
// There is no previous hit object hit by the current key
// There is no previous hit object hit by the current finger
return 0.0;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ public class MonoStreak
/// </summary>
public TaikoDifficultyHitObject FirstHitObject => HitObjects[0];

/// <summary>
/// The last <see cref="TaikoDifficultyHitObject"/> in this <see cref="MonoStreak"/>.
/// </summary>
public TaikoDifficultyHitObject LastHitObject => HitObjects[^1];

/// <summary>
/// The hit type of all objects encoded within this <see cref="MonoStreak"/>
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class RepeatingHitPatterns
public readonly List<AlternatingMonoPattern> AlternatingMonoPatterns = new List<AlternatingMonoPattern>();

/// <summary>
/// The parent <see cref="TaikoDifficultyHitObject"/> in this <see cref="RepeatingHitPatterns"/>
/// The first <see cref="TaikoDifficultyHitObject"/> in this <see cref="RepeatingHitPatterns"/>
/// </summary>
public TaikoDifficultyHitObject FirstHitObject => AlternatingMonoPatterns[0].FirstHitObject;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,15 @@ public static class TaikoColourDifficultyPreprocessor
{
/// <summary>
/// Processes and encodes a list of <see cref="TaikoDifficultyHitObject"/>s into a list of <see cref="TaikoDifficultyHitObjectColour"/>s,
/// assigning the appropriate <see cref="TaikoDifficultyHitObjectColour"/>s to each <see cref="TaikoDifficultyHitObject"/>,
/// and pre-evaluating colour difficulty of each <see cref="TaikoDifficultyHitObject"/>.
/// assigning the appropriate <see cref="TaikoDifficultyHitObjectColour"/>s to each <see cref="TaikoDifficultyHitObject"/>.
/// </summary>
public static void ProcessAndAssign(List<DifficultyHitObject> hitObjects)
{
List<RepeatingHitPatterns> hitPatterns = encode(hitObjects);

// Assign indexing and encoding data to all relevant objects. Only the first note of each encoding type is
// assigned with the relevant encodings.
// Assign indexing and encoding data to all relevant objects.
foreach (var repeatingHitPattern in hitPatterns)
{
repeatingHitPattern.FirstHitObject.Colour.RepeatingHitPattern = repeatingHitPattern;

// The outermost loop is kept a ForEach loop since it doesn't need index information, and we want to
// keep i and j for AlternatingMonoPattern's and MonoStreak's index respectively, to keep it in line with
// documentation.
Expand All @@ -36,14 +32,19 @@ public static void ProcessAndAssign(List<DifficultyHitObject> hitObjects)
AlternatingMonoPattern monoPattern = repeatingHitPattern.AlternatingMonoPatterns[i];
monoPattern.Parent = repeatingHitPattern;
monoPattern.Index = i;
monoPattern.FirstHitObject.Colour.AlternatingMonoPattern = monoPattern;

for (int j = 0; j < monoPattern.MonoStreaks.Count; ++j)
{
MonoStreak monoStreak = monoPattern.MonoStreaks[j];
monoStreak.Parent = monoPattern;
monoStreak.Index = j;
monoStreak.FirstHitObject.Colour.MonoStreak = monoStreak;

foreach (var hitObject in monoStreak.HitObjects)
{
hitObject.Colour.RepeatingHitPattern = repeatingHitPattern;
hitObject.Colour.AlternatingMonoPattern = monoPattern;
hitObject.Colour.MonoStreak = monoStreak;
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,28 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
public class TaikoDifficultyHitObjectColour
{
/// <summary>
/// The <see cref="MonoStreak"/> that encodes this note, only present if this is the first note within a <see cref="MonoStreak"/>
/// The <see cref="MonoStreak"/> that encodes this note.
/// </summary>
public MonoStreak? MonoStreak;

/// <summary>
/// The <see cref="AlternatingMonoPattern"/> that encodes this note, only present if this is the first note within a <see cref="AlternatingMonoPattern"/>
/// The <see cref="AlternatingMonoPattern"/> that encodes this note.
/// </summary>
public AlternatingMonoPattern? AlternatingMonoPattern;

/// <summary>
/// The <see cref="RepeatingHitPattern"/> that encodes this note, only present if this is the first note within a <see cref="RepeatingHitPattern"/>
/// The <see cref="RepeatingHitPattern"/> that encodes this note.
/// </summary>
public RepeatingHitPatterns? RepeatingHitPattern;

/// <summary>
/// The closest past <see cref="TaikoDifficultyHitObject"/> that's not the same colour.
/// </summary>
public TaikoDifficultyHitObject? PreviousColourChange => MonoStreak?.FirstHitObject.PreviousNote(0);

/// <summary>
/// The closest future <see cref="TaikoDifficultyHitObject"/> that's not the same colour.
/// </summary>
public TaikoDifficultyHitObject? NextColourChange => MonoStreak?.LastHitObject.NextNote(0);
}
}
3 changes: 0 additions & 3 deletions osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
/// <summary>
/// Calculates the stamina coefficient of taiko difficulty.
/// </summary>
/// <remarks>
/// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit).
/// </remarks>
public class Stamina : StrainDecaySkill
{
protected override double SkillMultiplier => 1.1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,6 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
double combinedRating = combined.DifficultyValue() * difficulty_multiplier;
double starRating = rescale(combinedRating * 1.4);

// TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system.
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;
}

HitWindows hitWindows = new TaikoHitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);

Expand Down
19 changes: 11 additions & 8 deletions osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo s
if (totalSuccessfulHits > 0)
effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss;

// TODO: The detection of rulesets is temporary until the leftover old skills have been reworked.
bool rulesetTaiko = score.BeatmapInfo.Ruleset.OnlineID == 1;

double multiplier = 1.13;

if (score.Mods.Any(m => m is ModHidden))
Expand All @@ -51,8 +54,8 @@ protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo s
if (score.Mods.Any(m => m is ModEasy))
multiplier *= 0.975;

double difficultyValue = computeDifficultyValue(score, taikoAttributes);
double accuracyValue = computeAccuracyValue(score, taikoAttributes);
double difficultyValue = computeDifficultyValue(score, taikoAttributes, rulesetTaiko);
double accuracyValue = computeAccuracyValue(score, taikoAttributes, rulesetTaiko);
double totalValue =
Math.Pow(
Math.Pow(difficultyValue, 1.1) +
Expand All @@ -68,7 +71,7 @@ protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo s
};
}

private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool rulesetTaiko)
{
double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0, 2.25) / 1150.0;

Expand All @@ -80,7 +83,7 @@ private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes
if (score.Mods.Any(m => m is ModEasy))
difficultyValue *= 0.985;

if (score.Mods.Any(m => m is ModHidden))
if (score.Mods.Any(m => m is ModHidden) && rulesetTaiko)
difficultyValue *= 1.025;

if (score.Mods.Any(m => m is ModHardRock))
Expand All @@ -92,7 +95,7 @@ private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes
return difficultyValue * Math.Pow(accuracy, 2.0);
}

private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool rulesetTaiko)
{
if (attributes.GreatHitWindow <= 0)
return 0;
Expand All @@ -102,9 +105,9 @@ private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes a
double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
accuracyValue *= lengthBonus;

// Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>) && score.Mods.Any(m => m is ModHidden))
accuracyValue *= Math.Max(1.050, 1.075 * lengthBonus);
// Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values.
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>) && score.Mods.Any(m => m is ModHidden) && rulesetTaiko)
accuracyValue *= Math.Max(1.0, 1.1 * lengthBonus);

return accuracyValue;
}
Expand Down