From 68027fcc2c46dacfbfda5a64eb5745d40eae6c81 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Jun 2020 16:30:26 +0900 Subject: [PATCH] Update with latest changes --- .../Preprocessing/StaminaCheeseDetector.cs | 37 +++-- .../Preprocessing/TaikoDifficultyHitObject.cs | 28 ++-- .../TaikoDifficultyHitObjectRhythm.cs | 100 +----------- .../Difficulty/Skills/Colour.cs | 153 +++++++++--------- .../Difficulty/Skills/Rhythm.cs | 115 +++++++++++++ .../Difficulty/Skills/SpeedInvariantRhythm.cs | 135 ---------------- .../Difficulty/Skills/Stamina.cs | 79 ++++----- .../Difficulty/TaikoDifficultyCalculator.cs | 71 ++++---- .../Difficulty/TaikoPerformanceCalculator.cs | 4 - 9 files changed, 295 insertions(+), 427 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index 4f645d7e51..b52dad5198 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -14,25 +14,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public void FindCheese(List difficultyHitObjects) { - this.hitObjects = difficultyHitObjects; + hitObjects = difficultyHitObjects; findRolls(3); findRolls(4); - findTLTap(0, true); - findTLTap(1, true); - findTLTap(0, false); - findTLTap(1, false); + findTlTap(0, true); + findTlTap(1, true); + findTlTap(0, false); + findTlTap(1, false); } private void findRolls(int patternLength) { List history = new List(); - int repititionStart = 0; + int repetitionStart = 0; for (int i = 0; i < hitObjects.Count; i++) { history.Add(hitObjects[i]); if (history.Count < 2 * patternLength) continue; + if (history.Count > 2 * patternLength) history.RemoveAt(0); bool isRepeat = true; @@ -47,43 +48,41 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (!isRepeat) { - repititionStart = i - 2 * patternLength; + repetitionStart = i - 2 * patternLength; } - int repeatedLength = i - repititionStart; + int repeatedLength = i - repetitionStart; if (repeatedLength >= roll_min_repetitions) { - // Console.WriteLine("Found Roll Cheese.\tStart: " + repititionStart + "\tEnd: " + i); - for (int j = repititionStart; j < i; j++) + for (int j = repetitionStart; j < i; j++) { - (hitObjects[i]).StaminaCheese = true; + hitObjects[i].StaminaCheese = true; } } } } - private void findTLTap(int parity, bool kat) + private void findTlTap(int parity, bool kat) { - int tl_length = -2; + int tlLength = -2; for (int i = parity; i < hitObjects.Count; i += 2) { if (kat == hitObjects[i].IsKat) { - tl_length += 2; + tlLength += 2; } else { - tl_length = -2; + tlLength = -2; } - if (tl_length >= tl_min_repetitions) + if (tlLength >= tl_min_repetitions) { - // Console.WriteLine("Found TL Cheese.\tStart: " + (i - tl_length) + "\tEnd: " + i); - for (int j = i - tl_length; j < i; j++) + for (int j = i - tlLength; j < i; j++) { - (hitObjects[i]).StaminaCheese = true; + hitObjects[i].StaminaCheese = true; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 75b1b3e268..cd45db2119 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -1,6 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; @@ -9,38 +12,31 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { public class TaikoDifficultyHitObject : DifficultyHitObject { - public readonly bool HasTypeChange; - public readonly bool HasTimingChange; public readonly TaikoDifficultyHitObjectRhythm Rhythm; public readonly bool IsKat; public bool StaminaCheese = false; - public readonly int RhythmID; - public readonly double NoteLength; - public readonly int n; - private int counter = 0; + public readonly int N; - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, TaikoDifficultyHitObjectRhythm rhythm) + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int n, IEnumerable commonRhythms) : base(hitObject, lastObject, clockRate) { - var lastHit = lastObject as Hit; var currentHit = hitObject as Hit; NoteLength = DeltaTime; double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; - Rhythm = rhythm.GetClosest(NoteLength / prevLength); - RhythmID = Rhythm.ID; - HasTypeChange = lastHit?.Type != currentHit?.Type; - IsKat = lastHit?.Type == HitType.Rim; - HasTimingChange = !rhythm.IsRepeat(RhythmID); + Rhythm = getClosestRhythm(NoteLength / prevLength, commonRhythms); + IsKat = currentHit?.Type == HitType.Rim; - n = counter; - counter++; + N = n; } - public const int CONST_RHYTHM_ID = 0; + private TaikoDifficultyHitObjectRhythm getClosestRhythm(double ratio, IEnumerable commonRhythms) + { + return commonRhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs index 8a6f0e5bfe..0ad885d9bd 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs @@ -1,107 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; - namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { public class TaikoDifficultyHitObjectRhythm { - private readonly TaikoDifficultyHitObjectRhythm[] commonRhythms; - private readonly TaikoDifficultyHitObjectRhythm constRhythm; - private int constRhythmID; - - public int ID = 0; public readonly double Difficulty; - private readonly double ratio; + public readonly double Ratio; + public readonly bool IsRepeat; - public bool IsRepeat() + public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty, bool isRepeat) { - return ID == constRhythmID; - } - - public bool IsRepeat(int id) - { - return id == constRhythmID; - } - - public bool IsSpeedup() - { - return ratio < 1.0; - } - - public bool IsLargeSpeedup() - { - return ratio < 0.49; - } - - public TaikoDifficultyHitObjectRhythm() - { - /* - - ALCHYRS CODE - - If (change < 0.48) Then 'sometimes gaps are slightly different due to position rounding - Return 0.65 'This number increases value of anything that more than doubles speed. Affects doubles. - ElseIf (change < 0.52) Then - Return 0.5 'speed doubling - this one affects pretty much every map other than stream maps - ElseIf change <= 0.9 Then - Return 1.0 'This number increases value of 1/4 -> 1/6 and other weird rhythms. - ElseIf change < 0.95 Then - Return 0.25 '.9 - ElseIf change > 1.95 Then - Return 0.3 'half speed or more - this affects pretty much every map - ElseIf change > 1.15 Then - Return 0.425 'in between - this affects (mostly) 1/6 -> 1/4 - ElseIf change > 1.05 Then - Return 0.15 '.9, small speed changes - - */ - - commonRhythms = new[] - { - new TaikoDifficultyHitObjectRhythm(1, 1, 0.1), - new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), - new TaikoDifficultyHitObjectRhythm(1, 2, 0.5), - new TaikoDifficultyHitObjectRhythm(3, 1, 0.3), - new TaikoDifficultyHitObjectRhythm(1, 3, 0.35), - new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), - new TaikoDifficultyHitObjectRhythm(2, 3, 0.4), - new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), - new TaikoDifficultyHitObjectRhythm(4, 5, 0.7) - }; - - for (int i = 0; i < commonRhythms.Length; i++) - { - commonRhythms[i].ID = i; - } - - constRhythmID = 0; - constRhythm = commonRhythms[constRhythmID]; - } - - private TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty) - { - this.ratio = ((double)numerator) / ((double)denominator); - this.Difficulty = difficulty; - } - - // Code is inefficient - we are searching exhaustively through the sorted list commonRhythms - public TaikoDifficultyHitObjectRhythm GetClosest(double ratio) - { - TaikoDifficultyHitObjectRhythm closestRhythm = commonRhythms[0]; - double closestDistance = Double.MaxValue; - - foreach (TaikoDifficultyHitObjectRhythm r in commonRhythms) - { - if (Math.Abs(r.ratio - ratio) < closestDistance) - { - closestRhythm = r; - closestDistance = Math.Abs(r.ratio - ratio); - } - } - - return closestRhythm; + Ratio = numerator / (double)denominator; + Difficulty = difficulty; + IsRepeat = isRepeat; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index bd94c8aa65..7c1623c54e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -16,106 +15,100 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; - private bool prevIsKat = false; + private NoteColour prevNoteColour = NoteColour.None; private int currentMonoLength = 1; - private List monoHistory = new List(); - private readonly int mono_history_max_length = 5; - private int monoHistoryLength = 0; + private readonly List monoHistory = new List(); + private const int mono_history_max_length = 5; private double sameParityPenalty() { return 0.0; } - private double repititionPenalty(int notesSince) + private double repetitionPenalty(int notesSince) { - double d = notesSince; - return Math.Atan(d / 30) / (Math.PI / 2); + double n = notesSince; + return Math.Min(1.0, 0.032 * n); } - private double patternLengthPenalty(int patternLength) + private double repetitionPenalties() { - double shortPatternPenalty = Math.Min(0.25 * patternLength, 1.0); - double longPatternPenalty = Math.Max(Math.Min(2.5 - 0.15 * patternLength, 1.0), 0.0); - return Math.Min(shortPatternPenalty, longPatternPenalty); + double penalty = 1.0; + + monoHistory.Add(currentMonoLength); + + if (monoHistory.Count > mono_history_max_length) + monoHistory.RemoveAt(0); + + for (int l = 2; l <= mono_history_max_length / 2; l++) + { + for (int start = monoHistory.Count - l - 1; start >= 0; start--) + { + bool samePattern = true; + + for (int i = 0; i < l; i++) + { + if (monoHistory[start + i] != monoHistory[monoHistory.Count - l + i]) + { + samePattern = false; + } + } + + if (samePattern) // Repetition found! + { + int notesSince = 0; + for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i]; + penalty *= repetitionPenalty(notesSince); + break; + } + } + } + + return penalty; } protected override double StrainValueOf(DifficultyHitObject current) { - double objectDifficulty = 0.0; - - if (current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000) + if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000)) { - - TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject)current; - - if (currentHO.IsKat == prevIsKat) - { - currentMonoLength += 1; - } - - else - { - - objectDifficulty = 1.0; - - if (monoHistoryLength > 0 && (monoHistory[monoHistoryLength - 1] + currentMonoLength) % 2 == 0) - { - objectDifficulty *= sameParityPenalty(); - } - - monoHistory.Add(currentMonoLength); - monoHistoryLength += 1; - - if (monoHistoryLength > mono_history_max_length) - { - monoHistory.RemoveAt(0); - monoHistoryLength -= 1; - } - - for (int l = 2; l <= mono_history_max_length / 2; l++) - { - for (int start = monoHistoryLength - l - 1; start >= 0; start--) - { - bool samePattern = true; - - for (int i = 0; i < l; i++) - { - if (monoHistory[start + i] != monoHistory[monoHistoryLength - l + i]) - { - samePattern = false; - } - } - - if (samePattern) // Repitition found! - { - int notesSince = 0; - for (int i = start; i < monoHistoryLength; i++) notesSince += monoHistory[i]; - objectDifficulty *= repititionPenalty(notesSince); - break; - } - } - } - - currentMonoLength = 1; - prevIsKat = currentHO.IsKat; - - } - + prevNoteColour = NoteColour.None; + return 0.0; } - /* - string path = @"out.txt"; - using (StreamWriter sw = File.AppendText(path)) - { - if (((TaikoDifficultyHitObject)current).IsKat) sw.WriteLine("k " + Math.Min(1.25, returnVal) * returnMultiplier); - else sw.WriteLine("d " + Math.Min(1.25, returnVal) * returnMultiplier); - } - */ + TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current; - return objectDifficulty; + double objectStrain = 0.0; + + NoteColour noteColour = hitObject.IsKat ? NoteColour.Ka : NoteColour.Don; + + if (noteColour == NoteColour.Don && prevNoteColour == NoteColour.Ka || + noteColour == NoteColour.Ka && prevNoteColour == NoteColour.Don) + { + objectStrain = 1.0; + + if (monoHistory.Count < 2) + objectStrain = 0.0; + else if ((monoHistory[^1] + currentMonoLength) % 2 == 0) + objectStrain *= sameParityPenalty(); + + objectStrain *= repetitionPenalties(); + currentMonoLength = 1; + } + else + { + currentMonoLength += 1; + } + + prevNoteColour = noteColour; + return objectStrain; } + private enum NoteColour + { + Don, + Ka, + None + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs new file mode 100644 index 0000000000..c3e6ee4d12 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -0,0 +1,115 @@ +// Copyright (c) ppy Pty Ltd . 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.Skills; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Skills +{ + public class Rhythm : Skill + { + protected override double SkillMultiplier => 10; + protected override double StrainDecayBase => 0; + private const double strain_decay = 0.96; + private double currentStrain; + + private readonly List rhythmHistory = new List(); + private const int rhythm_history_max_length = 8; + + private int notesSinceRhythmChange; + + private double repetitionPenalty(int notesSince) + { + return Math.Min(1.0, 0.032 * notesSince); + } + + // Finds repetitions and applies penalties + private double repetitionPenalties(TaikoDifficultyHitObject hitobject) + { + double penalty = 1; + + rhythmHistory.Add(hitobject); + + if (rhythmHistory.Count > rhythm_history_max_length) + rhythmHistory.RemoveAt(0); + + for (int l = 2; l <= rhythm_history_max_length / 2; l++) + { + for (int start = rhythmHistory.Count - l - 1; start >= 0; start--) + { + bool samePattern = true; + + for (int i = 0; i < l; i++) + { + if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - l + i].Rhythm) + { + samePattern = false; + } + } + + if (samePattern) // Repetition found! + { + int notesSince = hitobject.N - rhythmHistory[start].N; + penalty *= repetitionPenalty(notesSince); + break; + } + } + } + + return penalty; + } + + private double patternLengthPenalty(int patternLength) + { + double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); + double longPatternPenalty = Math.Max(Math.Min(2.5 - 0.15 * patternLength, 1.0), 0.0); + return Math.Min(shortPatternPenalty, longPatternPenalty); + } + + // Penalty for notes so slow that alternating is not necessary. + private double speedPenalty(double noteLengthMs) + { + if (noteLengthMs < 80) return 1; + if (noteLengthMs < 210) return Math.Max(0, 1.4 - 0.005 * noteLengthMs); + + currentStrain = 0.0; + notesSinceRhythmChange = 0; + return 0.0; + } + + protected override double StrainValueOf(DifficultyHitObject current) + { + if (!(current.BaseObject is Hit)) + { + currentStrain = 0.0; + notesSinceRhythmChange = 0; + return 0.0; + } + + currentStrain *= strain_decay; + + TaikoDifficultyHitObject hitobject = (TaikoDifficultyHitObject)current; + notesSinceRhythmChange += 1; + + if (hitobject.Rhythm.IsRepeat) + { + return 0.0; + } + + double objectStrain = hitobject.Rhythm.Difficulty; + + objectStrain *= repetitionPenalties(hitobject); + objectStrain *= patternLengthPenalty(notesSinceRhythmChange); + objectStrain *= speedPenalty(hitobject.NoteLength); + + notesSinceRhythmChange = 0; + + currentStrain += objectStrain; + return currentStrain; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs deleted file mode 100644 index dd90463113..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) ppy Pty Ltd . 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.Skills; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Skills -{ - public class Rhythm : Skill - { - protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0; - private const double strain_decay = 0.96; - private double currentStrain; - - private readonly List ratioObjectHistory = new List(); - private int ratioHistoryLength; - private const int ratio_history_max_length = 8; - - private int rhythmLength; - - // Penalty for repeated sequences of rhythm changes - private double repititionPenalty(double timeSinceRepititionMS) - { - double t = Math.Atan(timeSinceRepititionMS / 3000) / (Math.PI / 2); - return t; - } - - private double repititionPenalty(int notesSince) - { - double t = notesSince * 150; - t = Math.Atan(t / 3000) / (Math.PI / 2); - return t; - } - - // Penalty for short patterns - // Must be low to buff maps like wizodmiot - // Must not be too low for maps like inverse world - private double patternLengthPenalty(int patternLength) - { - double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); - double longPatternPenalty = Math.Max(Math.Min(2.5 - 0.15 * patternLength, 1.0), 0.0); - return Math.Min(shortPatternPenalty, longPatternPenalty); - } - - // Penalty for notes so slow that alting is not necessary. - private double speedPenalty(double noteLengthMS) - { - if (noteLengthMS < 80) return 1; - // return Math.Max(0, 1.4 - 0.005 * noteLengthMS); - if (noteLengthMS < 210) return Math.Max(0, 1.4 - 0.005 * noteLengthMS); - if (noteLengthMS < 210) return 0.6; - - currentStrain = 0.0; - return 0.0; - } - - // Penalty for the first rhythm change in a pattern - private const double first_burst_penalty = 0.1; - private bool prevIsSpeedup = true; - - protected override double StrainValueOf(DifficultyHitObject dho) - { - currentStrain *= strain_decay; - - TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject)dho; - rhythmLength += 1; - - if (!currentHO.HasTimingChange) - { - return 0.0; - } - - double objectDifficulty = currentHO.Rhythm.Difficulty; - - // find repeated ratios - - ratioObjectHistory.Add(currentHO); - ratioHistoryLength += 1; - - if (ratioHistoryLength > ratio_history_max_length) - { - ratioObjectHistory.RemoveAt(0); - ratioHistoryLength -= 1; - } - - for (int l = 2; l <= ratio_history_max_length / 2; l++) - { - for (int start = ratioHistoryLength - l - 1; start >= 0; start--) - { - bool samePattern = true; - - for (int i = 0; i < l; i++) - { - if (ratioObjectHistory[start + i].RhythmID != ratioObjectHistory[ratioHistoryLength - l + i].RhythmID) - { - samePattern = false; - } - } - - if (samePattern) // Repitition found! - { - int notesSince = currentHO.n - ratioObjectHistory[start].n; - objectDifficulty *= repititionPenalty(notesSince); - break; - } - } - } - - if (currentHO.Rhythm.IsSpeedup()) - { - objectDifficulty *= 1; - if (currentHO.Rhythm.IsLargeSpeedup()) objectDifficulty *= 1; - if (prevIsSpeedup) objectDifficulty *= 1; - - prevIsSpeedup = true; - } - else - { - prevIsSpeedup = false; - } - - objectDifficulty *= patternLengthPenalty(rhythmLength); - objectDifficulty *= speedPenalty(currentHO.NoteLength); - - rhythmLength = 0; - - currentStrain += objectDifficulty; - return currentStrain; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 1ecca886df..29c1c3c322 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -2,91 +2,78 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class Stamina : Skill { - private int hand; - private int noteNumber = 0; + private readonly int hand; protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0.4; - // i only add strain every second note so its kind of like using 0.16 - private readonly int maxHistoryLength = 2; - private List noteDurationHistory = new List(); - - private List lastHitObjects = new List(); + private const int max_history_length = 2; + private readonly List notePairDurationHistory = new List(); private double offhandObjectDuration = double.MaxValue; // Penalty for tl tap or roll - private double cheesePenalty(double last2NoteDuration) + private double cheesePenalty(double notePairDuration) { - if (last2NoteDuration > 125) return 1; - if (last2NoteDuration < 100) return 0.6; + if (notePairDuration > 125) return 1; + if (notePairDuration < 100) return 0.6; - return 0.6 + (last2NoteDuration - 100) * 0.016; + return 0.6 + (notePairDuration - 100) * 0.016; } - private double speedBonus(double last2NoteDuration) + private double speedBonus(double notePairDuration) { - // note that we are only looking at every 2nd note, so a 300bpm stream has a note duration of 100ms. - if (last2NoteDuration >= 200) return 0; - double bonus = 200 - last2NoteDuration; + if (notePairDuration >= 200) return 0; + + double bonus = 200 - notePairDuration; bonus *= bonus; return bonus / 100000; } protected override double StrainValueOf(DifficultyHitObject current) { - noteNumber += 1; - - TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject)current; - - if (noteNumber % 2 == hand) + if (!(current.BaseObject is Hit)) { - lastHitObjects.Add(currentHO); - noteDurationHistory.Add(currentHO.NoteLength + offhandObjectDuration); + return 0.0; + } - if (noteNumber == 1) + TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current; + + if (hitObject.N % 2 == hand) + { + double objectStrain = 1; + + if (hitObject.N == 1) return 1; - if (noteDurationHistory.Count > maxHistoryLength) - noteDurationHistory.RemoveAt(0); + notePairDurationHistory.Add(hitObject.NoteLength + offhandObjectDuration); - double shortestRecentNote = min(noteDurationHistory); - double bonus = 0; - bonus += speedBonus(shortestRecentNote); + if (notePairDurationHistory.Count > max_history_length) + notePairDurationHistory.RemoveAt(0); - double objectStaminaStrain = 1 + bonus; - if (currentHO.StaminaCheese) objectStaminaStrain *= cheesePenalty(currentHO.NoteLength + offhandObjectDuration); + double shortestRecentNote = notePairDurationHistory.Min(); + objectStrain += speedBonus(shortestRecentNote); - return objectStaminaStrain; + if (hitObject.StaminaCheese) + objectStrain *= cheesePenalty(hitObject.NoteLength + offhandObjectDuration); + + return objectStrain; } - offhandObjectDuration = currentHO.NoteLength; + offhandObjectDuration = hitObject.NoteLength; return 0; } - private static double min(List l) - { - double minimum = double.MaxValue; - - foreach (double d in l) - { - if (d < minimum) - minimum = d; - } - - return minimum; - } - public Stamina(bool rightHand) { hand = 0; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index dc2b68e0ca..789fd7c63b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -20,9 +20,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyCalculator : DifficultyCalculator { - private const double rhythmSkillMultiplier = 0.15; - private const double colourSkillMultiplier = 0.01; - private const double staminaSkillMultiplier = 0.02; + private const double rhythm_skill_multiplier = 0.014; + private const double colour_skill_multiplier = 0.01; + private const double stamina_skill_multiplier = 0.02; + + private readonly TaikoDifficultyHitObjectRhythm[] commonRhythms = + { + new TaikoDifficultyHitObjectRhythm(1, 1, 0.0, true), + new TaikoDifficultyHitObjectRhythm(2, 1, 0.3, false), + new TaikoDifficultyHitObjectRhythm(1, 2, 0.5, false), + new TaikoDifficultyHitObjectRhythm(3, 1, 0.3, false), + new TaikoDifficultyHitObjectRhythm(1, 3, 0.35, false), + new TaikoDifficultyHitObjectRhythm(3, 2, 0.6, false), + new TaikoDifficultyHitObjectRhythm(2, 3, 0.4, false), + new TaikoDifficultyHitObjectRhythm(5, 4, 0.5, false), + new TaikoDifficultyHitObjectRhythm(4, 5, 0.7, false) + }; public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) @@ -32,6 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty) { if (colorDifficulty <= 0) return 0.79 - 0.25; + return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2; } @@ -46,25 +60,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double rescale(double sr) { - if (sr <= 1) return sr; - sr -= 1; - sr = 1.6 * Math.Pow(sr, 0.7); - sr += 1; - return sr; + if (sr < 0) return sr; + + return 10.43 * Math.Log(sr / 8 + 1); } - private double combinedDifficulty(double staminaPenalty, Skill colour, Skill rhythm, Skill stamina1, Skill stamina2) + private double locallyCombinedDifficulty(double staminaPenalty, Skill colour, Skill rhythm, Skill stamina1, Skill stamina2) { - double difficulty = 0; double weight = 1; List peaks = new List(); for (int i = 0; i < colour.StrainPeaks.Count; i++) { - double colourPeak = colour.StrainPeaks[i] * colourSkillMultiplier; - double rhythmPeak = rhythm.StrainPeaks[i] * rhythmSkillMultiplier; - double staminaPeak = (stamina1.StrainPeaks[i] + stamina2.StrainPeaks[i]) * staminaSkillMultiplier * staminaPenalty; + double colourPeak = colour.StrainPeaks[i] * colour_skill_multiplier; + double rhythmPeak = rhythm.StrainPeaks[i] * rhythm_skill_multiplier; + double staminaPeak = (stamina1.StrainPeaks[i] + stamina2.StrainPeaks[i]) * stamina_skill_multiplier * staminaPenalty; peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); } @@ -82,21 +93,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (beatmap.HitObjects.Count == 0) return new TaikoDifficultyAttributes { Mods = mods, Skills = skills }; - double staminaRating = (skills[2].DifficultyValue() + skills[3].DifficultyValue()) * staminaSkillMultiplier; - double colourRating = skills[0].DifficultyValue() * colourSkillMultiplier; - double rhythmRating = skills[1].DifficultyValue() * rhythmSkillMultiplier; + double colourRating = skills[0].DifficultyValue() * colour_skill_multiplier; + double rhythmRating = skills[1].DifficultyValue() * rhythm_skill_multiplier; + double staminaRating = (skills[2].DifficultyValue() + skills[3].DifficultyValue()) * stamina_skill_multiplier; double staminaPenalty = simpleColourPenalty(staminaRating, colourRating); staminaRating *= staminaPenalty; - double combinedRating = combinedDifficulty(staminaPenalty, skills[0], skills[1], skills[2], skills[3]); - - // Console.WriteLine("colour\t" + colourRating); - // Console.WriteLine("rhythm\t" + rhythmRating); - // Console.WriteLine("stamina\t" + staminaRating); + double combinedRating = locallyCombinedDifficulty(staminaPenalty, skills[0], skills[1], skills[2], skills[3]); double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating); - // Console.WriteLine("combinedRating\t" + combinedRating); - // Console.WriteLine("separatedRating\t" + separatedRating); double starRating = 1.4 * separatedRating + 0.5 * combinedRating; starRating = rescale(starRating); @@ -111,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty RhythmStrain = rhythmRating, ColourStrain = colourRating, // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future - GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate, + GreatHitWindow = (int)hitWindows.WindowFor(HitResult.Great) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), Skills = skills }; @@ -120,18 +125,23 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { List taikoDifficultyHitObjects = new List(); - var rhythm = new TaikoDifficultyHitObjectRhythm(); for (int i = 2; i < beatmap.HitObjects.Count; i++) { // Check for negative durations if (beatmap.HitObjects[i].StartTime > beatmap.HitObjects[i - 1].StartTime && beatmap.HitObjects[i - 1].StartTime > beatmap.HitObjects[i - 2].StartTime) - taikoDifficultyHitObjects.Add(new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, rhythm)); + { + taikoDifficultyHitObjects.Add( + new TaikoDifficultyHitObject( + beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, i, commonRhythms + ) + ); + } } new StaminaCheeseDetector().FindCheese(taikoDifficultyHitObjects); - for (int i = 0; i < taikoDifficultyHitObjects.Count; i++) - yield return taikoDifficultyHitObjects[i]; + foreach (var hitobject in taikoDifficultyHitObjects) + yield return hitobject; } protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] @@ -149,10 +159,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty new TaikoModEasy(), new TaikoModHardRock(), }; - - /* - protected override DifficultyAttributes VirtualCalculate(IBeatmap beatmap, Mod[] mods, double clockRate) - => taikoCalculate(beatmap, mods, clockRate); - */ } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 9585a6a369..e6dd9f5084 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -78,10 +78,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available strainValue *= Math.Pow(0.985, countMiss); - // Combo scaling - if (Attributes.MaxCombo > 0) - strainValue *= Math.Min(Math.Pow(Score.MaxCombo, 0.5) / Math.Pow(Attributes.MaxCombo, 0.5), 1.0); - if (mods.Any(m => m is ModHidden)) strainValue *= 1.025;