diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs new file mode 100644 index 0000000000..ffdf4cb82a --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -0,0 +1,95 @@ +// 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; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing +{ + public class StaminaCheeseDetector + { + + private const int roll_min_repetitions = 12; + private const int tl_min_repetitions = 16; + + private List hitObjects; + + public void FindCheese(List difficultyHitObjects) + { + this.hitObjects = difficultyHitObjects; + findRolls(3); + findRolls(4); + findTLTap(0, true); + findTLTap(1, true); + findTLTap(0, false); + findTLTap(1, false); + } + + private void findRolls(int patternLength) + { + List history = new List(); + + int repititionStart = 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; + for (int j = 0; j < patternLength; j++) + { + if (history[j].IsKat != history[j + patternLength].IsKat) + { + isRepeat = false; + } + } + + if (!isRepeat) + { + repititionStart = i - 2 * patternLength; + } + + int repeatedLength = i - repititionStart; + + if (repeatedLength >= roll_min_repetitions) + { + // Console.WriteLine("Found Roll Cheese.\tStart: " + repititionStart + "\tEnd: " + i); + for (int j = repititionStart; j < i; j++) + { + (hitObjects[i]).StaminaCheese = true; + } + } + + } + } + + private void findTLTap(int parity, bool kat) + { + int tl_length = -2; + for (int i = parity; i < hitObjects.Count; i += 2) + { + if (kat == hitObjects[i].IsKat) + { + tl_length += 2; + } + else + { + tl_length = -2; + } + + if (tl_length >= tl_min_repetitions) + { + // Console.WriteLine("Found TL Cheese.\tStart: " + (i - tl_length) + "\tEnd: " + i); + for (int j = i - tl_length; j < i; j++) + { + (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 6807142327..abad494e62 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -1,6 +1,7 @@ // 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 osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; @@ -10,11 +11,36 @@ 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 TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate) + public bool StaminaCheese = false; + + public readonly int RhythmID; + + public readonly double NoteLength; + + public readonly int n; + private int counter = 0; + + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate) : base(hitObject, lastObject, clockRate) { - HasTypeChange = (lastObject as Hit)?.Type != (hitObject as Hit)?.Type; + NoteLength = DeltaTime; + double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; + Rhythm = TaikoDifficultyHitObjectRhythm.GetClosest(NoteLength / prevLength); + RhythmID = Rhythm.ID; + HasTypeChange = lastObject is RimHit != hitObject is RimHit; + IsKat = lastObject is RimHit; + HasTimingChange = !TaikoDifficultyHitObjectRhythm.IsRepeat(RhythmID); + + n = counter; + counter++; } + + public const int CONST_RHYTHM_ID = 0; + + } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs new file mode 100644 index 0000000000..74b3d285aa --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs @@ -0,0 +1,124 @@ +// 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 static TaikoDifficultyHitObjectRhythm[] commonRhythms; + private static TaikoDifficultyHitObjectRhythm constRhythm; + private static int constRhythmID; + + public int ID = 0; + public readonly double Difficulty; + private readonly double ratio; + + private static void initialiseCommonRhythms() + { + + /* + + 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 TaikoDifficultyHitObjectRhythm[] + { + 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]; + + } + + public bool IsRepeat() + { + return ID == constRhythmID; + } + + public static bool IsRepeat(int id) + { + return id == constRhythmID; + } + + public bool IsSpeedup() + { + return ratio < 1.0; + } + + public bool IsLargeSpeedup() + { + return ratio < 0.49; + } + + private TaikoDifficultyHitObjectRhythm(double ratio, double difficulty) + { + this.ratio = ratio; + this.Difficulty = difficulty; + } + + 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 static TaikoDifficultyHitObjectRhythm GetClosest(double ratio) + { + if (commonRhythms == null) + { + initialiseCommonRhythms(); + } + + 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; + + } + + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs new file mode 100644 index 0000000000..6ed826f345 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -0,0 +1,144 @@ +// 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 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 Colour : Skill + { + protected override double SkillMultiplier => 1; + protected override double StrainDecayBase => 0.3; + + private ColourSwitch lastColourSwitch = ColourSwitch.None; + private int sameColourCount = 1; + + private int[] previousDonLengths = {0, 0}, previousKatLengths = {0, 0}; + private int sameTypeCount = 1; + // TODO: make this smarter (dont initialise with "Don") + private bool previousIsKat = false; + + protected override double StrainValueOf(DifficultyHitObject current) + { + return StrainValueOfNew(current); + } + + protected double StrainValueOfNew(DifficultyHitObject current) + { + + double returnVal = 0.0; + double returnMultiplier = 1.0; + + if (previousIsKat != ((TaikoDifficultyHitObject) current).IsKat) + { + returnVal = 1.5 - (1.75 / (sameTypeCount + 0.65)); + + if (previousIsKat) + { + if (sameTypeCount % 2 == previousDonLengths[0] % 2) + { + returnMultiplier *= 0.8; + } + + if (previousKatLengths[0] == sameTypeCount) + { + returnMultiplier *= 0.525; + } + + if (previousKatLengths[1] == sameTypeCount) + { + returnMultiplier *= 0.75; + } + + previousKatLengths[1] = previousKatLengths[0]; + previousKatLengths[0] = sameTypeCount; + } + else + { + if (sameTypeCount % 2 == previousKatLengths[0] % 2) + { + returnMultiplier *= 0.8; + } + + if (previousDonLengths[0] == sameTypeCount) + { + returnMultiplier *= 0.525; + } + + if (previousDonLengths[1] == sameTypeCount) + { + returnMultiplier *= 0.75; + } + + previousDonLengths[1] = previousDonLengths[0]; + previousDonLengths[0] = sameTypeCount; + } + + + sameTypeCount = 1; + previousIsKat = ((TaikoDifficultyHitObject) current).IsKat; + + } + + else + { + sameTypeCount += 1; + } + + return Math.Min(1.25, returnVal) * returnMultiplier; + } + + protected double StrainValueOfOld(DifficultyHitObject current) + { + + double addition = 0; + + // We get an extra addition if we are not a slider or spinner + if (current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000) + { + if (hasColourChange(current)) + addition = 0.75; + } + else + { + lastColourSwitch = ColourSwitch.None; + sameColourCount = 1; + } + + return addition; + } + + + private bool hasColourChange(DifficultyHitObject current) + { + var taikoCurrent = (TaikoDifficultyHitObject) current; + + if (!taikoCurrent.HasTypeChange) + { + sameColourCount++; + return false; + } + + var oldColourSwitch = lastColourSwitch; + var newColourSwitch = sameColourCount % 2 == 0 ? ColourSwitch.Even : ColourSwitch.Odd; + + lastColourSwitch = newColourSwitch; + sameColourCount = 1; + + // We only want a bonus if the parity of the color switch changes + return oldColourSwitch != ColourSwitch.None && oldColourSwitch != newColourSwitch; + } + + private enum ColourSwitch + { + None, + Even, + Odd + } + + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs new file mode 100644 index 0000000000..b48cfc675f --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs @@ -0,0 +1,133 @@ +// 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 = 0.0; + + private readonly List ratioObjectHistory = new List(); + private int ratioHistoryLength = 0; + private const int ratio_history_max_length = 8; + + private int rhythmLength = 0; + + // 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; + if (noteLengthMS < 160) return Math.Max(0, 1.4 - 0.005 * noteLengthMS); + if (noteLengthMS < 300) return 0.6; + 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 new file mode 100644 index 0000000000..349f4c29fa --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -0,0 +1,103 @@ +// 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.Linq; +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Objects; +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; + + 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 double offhandObjectDuration = double.MaxValue; + + // Penalty for tl tap or roll + private double cheesePenalty(double last2NoteDuration) + { + if (last2NoteDuration > 125) return 1; + if (last2NoteDuration < 100) return 0.6; + + return 0.6 + (last2NoteDuration - 100) * 0.016; + } + + private double speedBonus(double last2NoteDuration) + { + // 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; + bonus *= bonus; + return bonus / 100000; + } + + protected override double StrainValueOf(DifficultyHitObject current) + { + noteNumber += 1; + + TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject) current; + + if (noteNumber % 2 == hand) + { + lastHitObjects.Add(currentHO); + noteDurationHistory.Add(currentHO.NoteLength + offhandObjectDuration); + + if (noteNumber == 1) + return 1; + + if (noteDurationHistory.Count > maxHistoryLength) + noteDurationHistory.RemoveAt(0); + + double shortestRecentNote = min(noteDurationHistory); + double bonus = 0; + bonus += speedBonus(shortestRecentNote); + + double objectStaminaStrain = 1 + bonus; + if (currentHO.StaminaCheese) objectStaminaStrain *= cheesePenalty(currentHO.NoteLength + offhandObjectDuration); + + return objectStaminaStrain; + } + + offhandObjectDuration = currentHO.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; + if (rightHand) + { + hand = 1; + } + } + + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs deleted file mode 100644 index c6fe273b50..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs +++ /dev/null @@ -1,95 +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 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 Strain : Skill - { - private const double rhythm_change_base_threshold = 0.2; - private const double rhythm_change_base = 2.0; - - protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0.3; - - private ColourSwitch lastColourSwitch = ColourSwitch.None; - - private int sameColourCount = 1; - - protected override double StrainValueOf(DifficultyHitObject current) - { - double addition = 1; - - // We get an extra addition if we are not a slider or spinner - if (current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000) - { - if (hasColourChange(current)) - addition += 0.75; - - if (hasRhythmChange(current)) - addition += 1; - } - else - { - lastColourSwitch = ColourSwitch.None; - sameColourCount = 1; - } - - double additionFactor = 1; - - // Scale the addition factor linearly from 0.4 to 1 for DeltaTime from 0 to 50 - if (current.DeltaTime < 50) - additionFactor = 0.4 + 0.6 * current.DeltaTime / 50; - - return additionFactor * addition; - } - - private bool hasRhythmChange(DifficultyHitObject current) - { - // We don't want a division by zero if some random mapper decides to put two HitObjects at the same time. - if (current.DeltaTime == 0 || Previous.Count == 0 || Previous[0].DeltaTime == 0) - return false; - - double timeElapsedRatio = Math.Max(Previous[0].DeltaTime / current.DeltaTime, current.DeltaTime / Previous[0].DeltaTime); - - if (timeElapsedRatio >= 8) - return false; - - double difference = Math.Log(timeElapsedRatio, rhythm_change_base) % 1.0; - - return difference > rhythm_change_base_threshold && difference < 1 - rhythm_change_base_threshold; - } - - private bool hasColourChange(DifficultyHitObject current) - { - var taikoCurrent = (TaikoDifficultyHitObject)current; - - if (!taikoCurrent.HasTypeChange) - { - sameColourCount++; - return false; - } - - var oldColourSwitch = lastColourSwitch; - var newColourSwitch = sameColourCount % 2 == 0 ? ColourSwitch.Even : ColourSwitch.Odd; - - lastColourSwitch = newColourSwitch; - sameColourCount = 1; - - // We only want a bonus if the parity of the color switch changes - return oldColourSwitch != ColourSwitch.None && oldColourSwitch != newColourSwitch; - } - - private enum ColourSwitch - { - None, - Even, - Odd - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 32d49ea39c..68da0f0e02 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -1,6 +1,7 @@ // 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.Beatmaps; @@ -19,39 +20,121 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyCalculator : DifficultyCalculator { - private const double star_scaling_factor = 0.04125; + + private const double rhythmSkillMultiplier = 0.15; + private const double colourSkillMultiplier = 0.01; + private const double staminaSkillMultiplier = 0.02; public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) { } + private double readingPenalty(double staminaDifficulty) + { + return Math.Max(0, 1 - staminaDifficulty / 14); + // return 1; + } + + private double norm(double p, double v1, double v2, double v3) + { + return Math.Pow( + Math.Pow(v1, p) + + Math.Pow(v2, p) + + Math.Pow(v3, p) + , 1 / p); + } + + private double rescale(double sr) + { + if (sr <= 1) return sr; + sr -= 1; + sr = 1.5 * Math.Pow(sr, 0.76); + sr += 1; + return sr; + } + + private double combinedDifficulty(Skill colour, Skill rhythm, Skill stamina1, Skill stamina2) + { + + double staminaRating = (stamina1.DifficultyValue() + stamina2.DifficultyValue()) * staminaSkillMultiplier; + double readingPenalty = this.readingPenalty(staminaRating); + + + 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 * readingPenalty; + double rhythmPeak = rhythm.StrainPeaks[i] * rhythmSkillMultiplier; + double staminaPeak = (stamina1.StrainPeaks[i] + stamina2.StrainPeaks[i]) * staminaSkillMultiplier; + peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); + } + foreach (double strain in peaks.OrderByDescending(d => d)) + { + difficulty += strain * weight; + weight *= 0.9; + } + + return difficulty; + } + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { if (beatmap.HitObjects.Count == 0) return new TaikoDifficultyAttributes { Mods = mods, Skills = skills }; + double staminaRating = (skills[2].DifficultyValue() + skills[3].DifficultyValue()) * staminaSkillMultiplier; + double readingPenalty = this.readingPenalty(staminaRating); + + double colourRating = skills[0].DifficultyValue() * colourSkillMultiplier * readingPenalty; + double rhythmRating = skills[1].DifficultyValue() * rhythmSkillMultiplier; + double combinedRating = combinedDifficulty(skills[0], skills[1], skills[2], skills[3]); + + // Console.WriteLine("colour\t" + colourRating); + // Console.WriteLine("rhythm\t" + rhythmRating); + // Console.WriteLine("stamina\t" + staminaRating); + 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); + HitWindows hitWindows = new TaikoHitWindows(); hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); return new TaikoDifficultyAttributes { - StarRating = skills.Single().DifficultyValue() * star_scaling_factor, + StarRating = starRating, Mods = mods, // 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, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), Skills = skills }; + } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - for (int i = 1; i < beatmap.HitObjects.Count; i++) - yield return new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate); + List taikoDifficultyHitObjects = new List(); + for (int i = 2; i < beatmap.HitObjects.Count; i++) + { + taikoDifficultyHitObjects.Add(new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate)); + } + new StaminaCheeseDetector().FindCheese(taikoDifficultyHitObjects); + for (int i = 0; i < taikoDifficultyHitObjects.Count; i++) + yield return taikoDifficultyHitObjects[i]; } - protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] { new Strain() }; + protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] + { + new Colour(), + new Rhythm(), + new Stamina(true), + new Stamina(false), + }; protected override Mod[] DifficultyAdjustmentMods => new Mod[] { @@ -60,5 +143,11 @@ 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 3a0fb64622..70249db0f6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -31,10 +31,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public override double Calculate(Dictionary categoryDifficulty = null) { mods = Score.Mods; - countGreat = Score.Statistics[HitResult.Great]; - countGood = Score.Statistics[HitResult.Good]; - countMeh = Score.Statistics[HitResult.Meh]; - countMiss = Score.Statistics[HitResult.Miss]; + countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]); + countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]); + countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]); + countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]); // Don't count scores made with supposedly unranked mods if (mods.Any(m => !m.Ranked)) @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double strainValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0; // Longer maps are worth more - double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); + double lengthBonus = 1 + 0.1f * Math.Min(1.0, totalHits / 1500.0); strainValue *= lengthBonus; // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available