mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 17:43:05 +08:00
Add initial changes
This commit is contained in:
parent
adf9a90fc4
commit
d613888803
@ -0,0 +1,95 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<TaikoDifficultyHitObject> hitObjects;
|
||||
|
||||
public void FindCheese(List<TaikoDifficultyHitObject> 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<TaikoDifficultyHitObject> history = new List<TaikoDifficultyHitObject>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,124 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
144
osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
Normal file
144
osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
Normal file
@ -0,0 +1,144 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<TaikoDifficultyHitObject> ratioObjectHistory = new List<TaikoDifficultyHitObject>();
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
103
osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
Normal file
103
osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
Normal file
@ -0,0 +1,103 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<double> noteDurationHistory = new List<double>();
|
||||
|
||||
private List<TaikoDifficultyHitObject> lastHitObjects = new List<TaikoDifficultyHitObject>();
|
||||
|
||||
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<double> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<double> peaks = new List<double>();
|
||||
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<DifficultyHitObject> 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<TaikoDifficultyHitObject> taikoDifficultyHitObjects = new List<TaikoDifficultyHitObject>();
|
||||
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);
|
||||
*/
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -31,10 +31,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
public override double Calculate(Dictionary<string, double> 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
|
||||
|
Loading…
Reference in New Issue
Block a user