mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 09:27:29 +08:00
Merge pull request #20558 from vunyunt/stamina-available-fingers
Basic tl-tapping consideration for stamina
This commit is contained in:
commit
a2b4e5b658
@ -16,13 +16,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
||||||
|
|
||||||
[TestCase(3.1098944660126882d, 200, "diffcalc-test")]
|
[TestCase(3.0920212594351191d, 200, "diffcalc-test")]
|
||||||
[TestCase(3.1098944660126882d, 200, "diffcalc-test-strong")]
|
[TestCase(3.0920212594351191d, 200, "diffcalc-test-strong")]
|
||||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||||
|
|
||||||
[TestCase(4.0974106752474251d, 200, "diffcalc-test")]
|
[TestCase(4.0789820318081444d, 200, "diffcalc-test")]
|
||||||
[TestCase(4.0974106752474251d, 200, "diffcalc-test-strong")]
|
[TestCase(4.0789820318081444d, 200, "diffcalc-test-strong")]
|
||||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
|
||||||
|
|
||||||
|
@ -54,11 +54,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
|||||||
TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour;
|
TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour;
|
||||||
double difficulty = 0.0d;
|
double difficulty = 0.0d;
|
||||||
|
|
||||||
if (colour.MonoStreak != null) // Difficulty for MonoStreak
|
if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak
|
||||||
difficulty += EvaluateDifficultyOf(colour.MonoStreak);
|
difficulty += EvaluateDifficultyOf(colour.MonoStreak);
|
||||||
if (colour.AlternatingMonoPattern != null) // Difficulty for AlternatingMonoPattern
|
if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern
|
||||||
difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern);
|
difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern);
|
||||||
if (colour.RepeatingHitPattern != null) // Difficulty for RepeatingHitPattern
|
if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
|
||||||
difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern);
|
difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern);
|
||||||
|
|
||||||
return difficulty;
|
return difficulty;
|
||||||
|
@ -11,22 +11,43 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
|||||||
public class StaminaEvaluator
|
public class StaminaEvaluator
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <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>
|
/// </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)
|
private static double speedBonus(double interval)
|
||||||
{
|
{
|
||||||
// Cap to 600bpm 1/4, 25ms note interval, 50ms key interval
|
// Interval is capped at a very small value to prevent infinite values.
|
||||||
// Interval will be capped at a very small value to avoid infinite/negative speed bonuses.
|
interval = Math.Max(interval, 1);
|
||||||
// TODO - This is a temporary measure as we need to implement methods of detecting playstyle-abuse of SpeedBonus.
|
|
||||||
interval = Math.Max(interval, 50);
|
|
||||||
|
|
||||||
return 30 / interval;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the
|
/// 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>
|
/// </summary>
|
||||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
@ -35,13 +56,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
|||||||
return 0.0;
|
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 taikoCurrent = (TaikoDifficultyHitObject)current;
|
||||||
TaikoDifficultyHitObject? keyPrevious = taikoCurrent.PreviousMono(1);
|
TaikoDifficultyHitObject? keyPrevious = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1);
|
||||||
|
|
||||||
if (keyPrevious == null)
|
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;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +33,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TaikoDifficultyHitObject FirstHitObject => HitObjects[0];
|
public TaikoDifficultyHitObject FirstHitObject => HitObjects[0];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The last <see cref="TaikoDifficultyHitObject"/> in this <see cref="MonoStreak"/>.
|
||||||
|
/// </summary>
|
||||||
|
public TaikoDifficultyHitObject LastHitObject => HitObjects[^1];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The hit type of all objects encoded within this <see cref="MonoStreak"/>
|
/// The hit type of all objects encoded within this <see cref="MonoStreak"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data
|
|||||||
public readonly List<AlternatingMonoPattern> AlternatingMonoPatterns = new List<AlternatingMonoPattern>();
|
public readonly List<AlternatingMonoPattern> AlternatingMonoPatterns = new List<AlternatingMonoPattern>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The parent <see cref="TaikoDifficultyHitObject"/> in this <see cref="RepeatingHitPatterns"/>
|
/// The first <see cref="TaikoDifficultyHitObject"/> in this <see cref="RepeatingHitPatterns"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TaikoDifficultyHitObject FirstHitObject => AlternatingMonoPatterns[0].FirstHitObject;
|
public TaikoDifficultyHitObject FirstHitObject => AlternatingMonoPatterns[0].FirstHitObject;
|
||||||
|
|
||||||
|
@ -15,19 +15,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Processes and encodes a list of <see cref="TaikoDifficultyHitObject"/>s into a list of <see cref="TaikoDifficultyHitObjectColour"/>s,
|
/// 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"/>,
|
/// assigning the appropriate <see cref="TaikoDifficultyHitObjectColour"/>s to each <see cref="TaikoDifficultyHitObject"/>.
|
||||||
/// and pre-evaluating colour difficulty of each <see cref="TaikoDifficultyHitObject"/>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void ProcessAndAssign(List<DifficultyHitObject> hitObjects)
|
public static void ProcessAndAssign(List<DifficultyHitObject> hitObjects)
|
||||||
{
|
{
|
||||||
List<RepeatingHitPatterns> hitPatterns = encode(hitObjects);
|
List<RepeatingHitPatterns> hitPatterns = encode(hitObjects);
|
||||||
|
|
||||||
// Assign indexing and encoding data to all relevant objects. Only the first note of each encoding type is
|
// Assign indexing and encoding data to all relevant objects.
|
||||||
// assigned with the relevant encodings.
|
|
||||||
foreach (var repeatingHitPattern in hitPatterns)
|
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
|
// 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
|
// keep i and j for AlternatingMonoPattern's and MonoStreak's index respectively, to keep it in line with
|
||||||
// documentation.
|
// documentation.
|
||||||
@ -36,14 +32,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
|
|||||||
AlternatingMonoPattern monoPattern = repeatingHitPattern.AlternatingMonoPatterns[i];
|
AlternatingMonoPattern monoPattern = repeatingHitPattern.AlternatingMonoPatterns[i];
|
||||||
monoPattern.Parent = repeatingHitPattern;
|
monoPattern.Parent = repeatingHitPattern;
|
||||||
monoPattern.Index = i;
|
monoPattern.Index = i;
|
||||||
monoPattern.FirstHitObject.Colour.AlternatingMonoPattern = monoPattern;
|
|
||||||
|
|
||||||
for (int j = 0; j < monoPattern.MonoStreaks.Count; ++j)
|
for (int j = 0; j < monoPattern.MonoStreaks.Count; ++j)
|
||||||
{
|
{
|
||||||
MonoStreak monoStreak = monoPattern.MonoStreaks[j];
|
MonoStreak monoStreak = monoPattern.MonoStreaks[j];
|
||||||
monoStreak.Parent = monoPattern;
|
monoStreak.Parent = monoPattern;
|
||||||
monoStreak.Index = j;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,18 +11,28 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
|
|||||||
public class TaikoDifficultyHitObjectColour
|
public class TaikoDifficultyHitObjectColour
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public MonoStreak? MonoStreak;
|
public MonoStreak? MonoStreak;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public AlternatingMonoPattern? AlternatingMonoPattern;
|
public AlternatingMonoPattern? AlternatingMonoPattern;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public RepeatingHitPatterns? RepeatingHitPattern;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates the stamina coefficient of taiko difficulty.
|
/// Calculates the stamina coefficient of taiko difficulty.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit).
|
|
||||||
/// </remarks>
|
|
||||||
public class Stamina : StrainDecaySkill
|
public class Stamina : StrainDecaySkill
|
||||||
{
|
{
|
||||||
protected override double SkillMultiplier => 1.1;
|
protected override double SkillMultiplier => 1.1;
|
||||||
|
@ -83,15 +83,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
double combinedRating = combined.DifficultyValue() * difficulty_multiplier;
|
double combinedRating = combined.DifficultyValue() * difficulty_multiplier;
|
||||||
double starRating = rescale(combinedRating * 1.4);
|
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 hitWindows = new TaikoHitWindows();
|
||||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||||
|
|
||||||
|
@ -43,6 +43,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
if (totalSuccessfulHits > 0)
|
if (totalSuccessfulHits > 0)
|
||||||
effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss;
|
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 isConvert = score.BeatmapInfo.Ruleset.OnlineID != 1;
|
||||||
|
|
||||||
double multiplier = 1.13;
|
double multiplier = 1.13;
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is ModHidden))
|
if (score.Mods.Any(m => m is ModHidden))
|
||||||
@ -51,8 +54,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
if (score.Mods.Any(m => m is ModEasy))
|
if (score.Mods.Any(m => m is ModEasy))
|
||||||
multiplier *= 0.975;
|
multiplier *= 0.975;
|
||||||
|
|
||||||
double difficultyValue = computeDifficultyValue(score, taikoAttributes);
|
double difficultyValue = computeDifficultyValue(score, taikoAttributes, isConvert);
|
||||||
double accuracyValue = computeAccuracyValue(score, taikoAttributes);
|
double accuracyValue = computeAccuracyValue(score, taikoAttributes, isConvert);
|
||||||
double totalValue =
|
double totalValue =
|
||||||
Math.Pow(
|
Math.Pow(
|
||||||
Math.Pow(difficultyValue, 1.1) +
|
Math.Pow(difficultyValue, 1.1) +
|
||||||
@ -68,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
|
private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
|
||||||
{
|
{
|
||||||
double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0, 2.25) / 1150.0;
|
double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0, 2.25) / 1150.0;
|
||||||
|
|
||||||
@ -80,7 +83,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
if (score.Mods.Any(m => m is ModEasy))
|
if (score.Mods.Any(m => m is ModEasy))
|
||||||
difficultyValue *= 0.985;
|
difficultyValue *= 0.985;
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is ModHidden))
|
if (score.Mods.Any(m => m is ModHidden) && !isConvert)
|
||||||
difficultyValue *= 1.025;
|
difficultyValue *= 1.025;
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is ModHardRock))
|
if (score.Mods.Any(m => m is ModHardRock))
|
||||||
@ -92,7 +95,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
return difficultyValue * Math.Pow(accuracy, 2.0);
|
return difficultyValue * Math.Pow(accuracy, 2.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
|
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
|
||||||
{
|
{
|
||||||
if (attributes.GreatHitWindow <= 0)
|
if (attributes.GreatHitWindow <= 0)
|
||||||
return 0;
|
return 0;
|
||||||
@ -102,9 +105,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
|
double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
|
||||||
accuracyValue *= lengthBonus;
|
accuracyValue *= lengthBonus;
|
||||||
|
|
||||||
// Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values
|
// 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))
|
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>) && score.Mods.Any(m => m is ModHidden) && !isConvert)
|
||||||
accuracyValue *= Math.Max(1.050, 1.075 * lengthBonus);
|
accuracyValue *= Math.Max(1.0, 1.1 * lengthBonus);
|
||||||
|
|
||||||
return accuracyValue;
|
return accuracyValue;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user