@rian8337/osu-difficulty-calculator 4.0.0-beta.83 → 4.0.0-beta.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +930 -638
- package/package.json +3 -3
- package/typings/index.d.ts +100 -100
package/dist/index.js
CHANGED
|
@@ -72,6 +72,7 @@ class DifficultyCalculator {
|
|
|
72
72
|
osuBase.ModEasy,
|
|
73
73
|
osuBase.ModHardRock,
|
|
74
74
|
osuBase.ModFlashlight,
|
|
75
|
+
osuBase.ModTraceable,
|
|
75
76
|
osuBase.ModHidden,
|
|
76
77
|
osuBase.ModRelax,
|
|
77
78
|
osuBase.ModAutopilot,
|
|
@@ -79,10 +80,15 @@ class DifficultyCalculator {
|
|
|
79
80
|
osuBase.ModRandom,
|
|
80
81
|
];
|
|
81
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Calculates the difficulty of a `Beatmap` with specific `Mod`s.
|
|
85
|
+
*
|
|
86
|
+
* @param beatmap The `Beatmap` whose difficulty is to be calculated.
|
|
87
|
+
* @param mods The `Mod`s to apply to the beatmap. Defaults to No Mod.
|
|
88
|
+
* @returns A `DifficultyAttributes` object describing the difficulty of the `Beatmap`.
|
|
89
|
+
*/
|
|
82
90
|
calculate(beatmap, mods) {
|
|
83
|
-
const playableBeatmap = beatmap
|
|
84
|
-
? beatmap
|
|
85
|
-
: this.createPlayableBeatmapWithDifficultyAdjustmentMods(beatmap, mods);
|
|
91
|
+
const playableBeatmap = this.createPlayableBeatmap(beatmap, mods);
|
|
86
92
|
const skills = this.createSkills(playableBeatmap);
|
|
87
93
|
const objects = this.createDifficultyHitObjects(playableBeatmap);
|
|
88
94
|
for (const object of objects) {
|
|
@@ -95,7 +101,7 @@ class DifficultyCalculator {
|
|
|
95
101
|
calculateStrainPeaks(beatmap, mods) {
|
|
96
102
|
const playableBeatmap = beatmap instanceof osuBase.PlayableBeatmap
|
|
97
103
|
? beatmap
|
|
98
|
-
: this.
|
|
104
|
+
: this.createPlayableBeatmap(beatmap, mods);
|
|
99
105
|
const skills = this.createStrainPeakSkills(playableBeatmap);
|
|
100
106
|
const objects = this.createDifficultyHitObjects(playableBeatmap);
|
|
101
107
|
for (const object of objects) {
|
|
@@ -110,30 +116,6 @@ class DifficultyCalculator {
|
|
|
110
116
|
flashlight: skills[3].strainPeaks,
|
|
111
117
|
};
|
|
112
118
|
}
|
|
113
|
-
/**
|
|
114
|
-
* Calculates the base rating of a `Skill`.
|
|
115
|
-
*
|
|
116
|
-
* @param skill The `Skill` to calculate the rating of.
|
|
117
|
-
* @returns The rating of the `Skill`.
|
|
118
|
-
*/
|
|
119
|
-
calculateRating(skill) {
|
|
120
|
-
return Math.sqrt(skill.difficultyValue()) * this.difficultyMultiplier;
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Calculates the base performance value of a difficulty rating.
|
|
124
|
-
*
|
|
125
|
-
* @param rating The difficulty rating.
|
|
126
|
-
*/
|
|
127
|
-
basePerformanceValue(rating) {
|
|
128
|
-
return Math.pow(5 * Math.max(1, rating / 0.0675) - 4, 3) / 100000;
|
|
129
|
-
}
|
|
130
|
-
createPlayableBeatmapWithDifficultyAdjustmentMods(beatmap, mods) {
|
|
131
|
-
const difficultyAdjustmentMods = this.retainDifficultyAdjustmentMods(mods ? Array.from(mods.values()) : []);
|
|
132
|
-
return this.createPlayableBeatmap(beatmap, new osuBase.ModMap(difficultyAdjustmentMods.map((m) => [
|
|
133
|
-
m.constructor,
|
|
134
|
-
m,
|
|
135
|
-
])));
|
|
136
|
-
}
|
|
137
119
|
}
|
|
138
120
|
|
|
139
121
|
/**
|
|
@@ -312,13 +294,12 @@ class DifficultyHitObject {
|
|
|
312
294
|
*
|
|
313
295
|
* A value closer to 1 indicates a higher possibility.
|
|
314
296
|
*/
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (!next) {
|
|
297
|
+
getDoubletapness(nextObj) {
|
|
298
|
+
if (!nextObj) {
|
|
318
299
|
return 0;
|
|
319
300
|
}
|
|
320
301
|
const currentDeltaTime = Math.max(1, this.deltaTime);
|
|
321
|
-
const nextDeltaTime = Math.max(1,
|
|
302
|
+
const nextDeltaTime = Math.max(1, nextObj.deltaTime);
|
|
322
303
|
const deltaDifference = Math.abs(nextDeltaTime - currentDeltaTime);
|
|
323
304
|
const speedRatio = currentDeltaTime / Math.max(currentDeltaTime, deltaDifference);
|
|
324
305
|
const windowRatio = Math.pow(Math.min(1, currentDeltaTime / this.fullGreatWindow), 2);
|
|
@@ -343,11 +324,7 @@ class DifficultyHitObject {
|
|
|
343
324
|
return;
|
|
344
325
|
}
|
|
345
326
|
// We will scale distances by this factor, so we can assume a uniform circle size among beatmaps.
|
|
346
|
-
|
|
347
|
-
// High circle size (small CS) bonus
|
|
348
|
-
if (this.mode === osuBase.Modes.osu && this.object.radius < 30) {
|
|
349
|
-
scalingFactor *= 1 + Math.min(30 - this.object.radius, 5) / 50;
|
|
350
|
-
}
|
|
327
|
+
const scalingFactor = DifficultyHitObject.normalizedRadius / this.object.radius;
|
|
351
328
|
const lastCursorPosition = this.lastDifficultyObject !== null
|
|
352
329
|
? this.getEndCursorPosition(this.lastDifficultyObject)
|
|
353
330
|
: this.lastObject.stackedPosition;
|
|
@@ -859,6 +836,15 @@ class StrainSkill extends Skill {
|
|
|
859
836
|
get objectStrains() {
|
|
860
837
|
return this._objectStrains;
|
|
861
838
|
}
|
|
839
|
+
/**
|
|
840
|
+
* Converts a difficulty value to a performance value.
|
|
841
|
+
*
|
|
842
|
+
* @param difficulty The difficulty value to convert.
|
|
843
|
+
* @returns The performance value.
|
|
844
|
+
*/
|
|
845
|
+
static difficultyToPerformance(difficulty) {
|
|
846
|
+
return Math.pow(5 * Math.max(1, difficulty / 0.0675) - 4, 3) / 100000;
|
|
847
|
+
}
|
|
862
848
|
process(current) {
|
|
863
849
|
// The first object doesn't generate a strain, so we begin with an incremented section end
|
|
864
850
|
if (current.index === 0) {
|
|
@@ -982,6 +968,9 @@ class DroidAim extends DroidSkill {
|
|
|
982
968
|
this.maxSliderStrain = 0;
|
|
983
969
|
this.withSliders = withSliders;
|
|
984
970
|
}
|
|
971
|
+
static difficultyToPerformance(difficulty) {
|
|
972
|
+
return super.difficultyToPerformance(Math.pow(difficulty, 0.8));
|
|
973
|
+
}
|
|
985
974
|
/**
|
|
986
975
|
* Obtains the amount of sliders that are considered difficult in terms of relative strain.
|
|
987
976
|
*/
|
|
@@ -1180,6 +1169,9 @@ class DroidFlashlight extends DroidSkill {
|
|
|
1180
1169
|
this.currentFlashlightStrain = 0;
|
|
1181
1170
|
this.withSliders = withSliders;
|
|
1182
1171
|
}
|
|
1172
|
+
static difficultyToPerformance(difficulty) {
|
|
1173
|
+
return Math.pow(difficulty, 1.6) * 25;
|
|
1174
|
+
}
|
|
1183
1175
|
strainValueAt(current) {
|
|
1184
1176
|
this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
|
|
1185
1177
|
this.currentFlashlightStrain +=
|
|
@@ -1444,6 +1436,15 @@ class DroidReading extends Skill {
|
|
|
1444
1436
|
this.difficulty = 0;
|
|
1445
1437
|
this.noteWeightSum = 0;
|
|
1446
1438
|
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Converts a difficulty value to a performance value.
|
|
1441
|
+
*
|
|
1442
|
+
* @param difficulty The difficulty value to convert.
|
|
1443
|
+
* @returns The performance value.
|
|
1444
|
+
*/
|
|
1445
|
+
static difficultyToPerformance(difficulty) {
|
|
1446
|
+
return Math.pow(Math.pow(difficulty, 2) * 25, 0.8);
|
|
1447
|
+
}
|
|
1447
1448
|
process(current) {
|
|
1448
1449
|
this.currentNoteDifficulty *= this.strainDecay(current.deltaTime);
|
|
1449
1450
|
this.currentNoteDifficulty +=
|
|
@@ -1584,10 +1585,10 @@ class DroidRhythmEvaluator {
|
|
|
1584
1585
|
this.historyTimeMax) {
|
|
1585
1586
|
++rhythmStart;
|
|
1586
1587
|
}
|
|
1588
|
+
let prevObject = validPrevious[rhythmStart];
|
|
1589
|
+
let lastObject = validPrevious[rhythmStart + 1];
|
|
1587
1590
|
for (let i = rhythmStart; i > 0; --i) {
|
|
1588
1591
|
const currentObject = validPrevious[i - 1];
|
|
1589
|
-
const prevObject = validPrevious[i];
|
|
1590
|
-
const lastObject = validPrevious[i + 1];
|
|
1591
1592
|
// Scale note 0 to 1 from history to now.
|
|
1592
1593
|
const timeDecay = (this.historyTimeMax -
|
|
1593
1594
|
(current.startTime - currentObject.startTime)) /
|
|
@@ -1666,7 +1667,9 @@ class DroidRhythmEvaluator {
|
|
|
1666
1667
|
islandCounts.set(island, 1);
|
|
1667
1668
|
}
|
|
1668
1669
|
// Scale down the difficulty if the object is doubletappable.
|
|
1669
|
-
effectiveRatio *=
|
|
1670
|
+
effectiveRatio *=
|
|
1671
|
+
1 -
|
|
1672
|
+
prevObject.getDoubletapness(prevObject.next(0)) * 0.75;
|
|
1670
1673
|
rhythmComplexitySum +=
|
|
1671
1674
|
Math.sqrt(effectiveRatio * startRatio) *
|
|
1672
1675
|
currentHistoricalDecay;
|
|
@@ -1697,6 +1700,8 @@ class DroidRhythmEvaluator {
|
|
|
1697
1700
|
startRatio = effectiveRatio;
|
|
1698
1701
|
island = new Island(currentDelta, deltaDifferenceEpsilon);
|
|
1699
1702
|
}
|
|
1703
|
+
lastObject = prevObject;
|
|
1704
|
+
prevObject = currentObject;
|
|
1700
1705
|
}
|
|
1701
1706
|
return (Math.sqrt(4 + rhythmComplexitySum * this.rhythmOverallMultiplier) /
|
|
1702
1707
|
2);
|
|
@@ -1723,7 +1728,7 @@ class DroidRhythm extends DroidSkill {
|
|
|
1723
1728
|
}
|
|
1724
1729
|
strainValueAt(current) {
|
|
1725
1730
|
const rhythmMultiplier = DroidRhythmEvaluator.evaluateDifficultyOf(current, this.useSliderAccuracy);
|
|
1726
|
-
const doubletapness = 1 - current.
|
|
1731
|
+
const doubletapness = 1 - current.getDoubletapness(current.next(0));
|
|
1727
1732
|
this.currentRhythmStrain *= this.strainDecay(current.deltaTime);
|
|
1728
1733
|
this.currentRhythmStrain += (rhythmMultiplier - 1) * doubletapness;
|
|
1729
1734
|
this.currentRhythmMultiplier = rhythmMultiplier * doubletapness;
|
|
@@ -1766,7 +1771,7 @@ class DroidTapEvaluator {
|
|
|
1766
1771
|
return 0;
|
|
1767
1772
|
}
|
|
1768
1773
|
const doubletapness = considerCheesability
|
|
1769
|
-
? 1 - current.
|
|
1774
|
+
? 1 - current.getDoubletapness(current.next(0))
|
|
1770
1775
|
: 1;
|
|
1771
1776
|
const strainTime = strainTimeCap !== undefined
|
|
1772
1777
|
? // We cap the strain time to 50 here as the chance of vibro is higher in any BPM higher than 300.
|
|
@@ -1928,10 +1933,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1928
1933
|
this.populateRhythmAttributes(attributes, skills);
|
|
1929
1934
|
this.populateFlashlightAttributes(attributes, skills);
|
|
1930
1935
|
this.populateReadingAttributes(attributes, skills);
|
|
1931
|
-
const aimPerformanceValue =
|
|
1932
|
-
const tapPerformanceValue =
|
|
1933
|
-
const flashlightPerformanceValue =
|
|
1934
|
-
const readingPerformanceValue =
|
|
1936
|
+
const aimPerformanceValue = DroidAim.difficultyToPerformance(attributes.aimDifficulty);
|
|
1937
|
+
const tapPerformanceValue = DroidTap.difficultyToPerformance(attributes.tapDifficulty);
|
|
1938
|
+
const flashlightPerformanceValue = DroidFlashlight.difficultyToPerformance(attributes.flashlightDifficulty);
|
|
1939
|
+
const readingPerformanceValue = DroidReading.difficultyToPerformance(attributes.readingDifficulty);
|
|
1935
1940
|
const basePerformanceValue = Math.pow(Math.pow(aimPerformanceValue, 1.1) +
|
|
1936
1941
|
Math.pow(tapPerformanceValue, 1.1) +
|
|
1937
1942
|
Math.pow(flashlightPerformanceValue, 1.1) +
|
|
@@ -2161,6 +2166,15 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
2161
2166
|
Math.pow(Math.max(0, attributes.overallDifficulty), 2.2) / 800;
|
|
2162
2167
|
attributes.readingDifficulty *= Math.sqrt(ratingMultiplier);
|
|
2163
2168
|
}
|
|
2169
|
+
/**
|
|
2170
|
+
* Calculates the base rating of a `Skill`.
|
|
2171
|
+
*
|
|
2172
|
+
* @param skill The `Skill` to calculate the rating of.
|
|
2173
|
+
* @returns The rating of the `Skill`.
|
|
2174
|
+
*/
|
|
2175
|
+
calculateRating(skill) {
|
|
2176
|
+
return Math.sqrt(skill.difficultyValue()) * this.difficultyMultiplier;
|
|
2177
|
+
}
|
|
2164
2178
|
}
|
|
2165
2179
|
/**
|
|
2166
2180
|
* The strain threshold to start detecting for possible three-fingered section.
|
|
@@ -2173,6 +2187,20 @@ DroidDifficultyCalculator.threeFingerStrainThreshold = 175;
|
|
|
2173
2187
|
* The base class of performance calculators.
|
|
2174
2188
|
*/
|
|
2175
2189
|
class PerformanceCalculator {
|
|
2190
|
+
/**
|
|
2191
|
+
* The amount of slider ends dropped in the score.
|
|
2192
|
+
*/
|
|
2193
|
+
get sliderEndsDropped() {
|
|
2194
|
+
return this._sliderEndsDropped;
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* The amount of slider ticks missed in the score.
|
|
2198
|
+
*
|
|
2199
|
+
* This is used to calculate the slider accuracy.
|
|
2200
|
+
*/
|
|
2201
|
+
get sliderTicksMissed() {
|
|
2202
|
+
return this._sliderTicksMissed;
|
|
2203
|
+
}
|
|
2176
2204
|
/**
|
|
2177
2205
|
* Whether this score uses classic slider accuracy.
|
|
2178
2206
|
*/
|
|
@@ -2195,25 +2223,9 @@ class PerformanceCalculator {
|
|
|
2195
2223
|
* The calculated maximum combo.
|
|
2196
2224
|
*/
|
|
2197
2225
|
this.combo = 0;
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
*/
|
|
2201
|
-
this.effectiveMissCount = 0;
|
|
2202
|
-
/**
|
|
2203
|
-
* The amount of slider ends dropped in the score.
|
|
2204
|
-
*/
|
|
2205
|
-
this.sliderEndsDropped = 0;
|
|
2206
|
-
/**
|
|
2207
|
-
* The amount of slider ticks missed in the score.
|
|
2208
|
-
*
|
|
2209
|
-
* This is used to calculate the slider accuracy.
|
|
2210
|
-
*/
|
|
2211
|
-
this.sliderTicksMissed = 0;
|
|
2226
|
+
this._sliderEndsDropped = 0;
|
|
2227
|
+
this._sliderTicksMissed = 0;
|
|
2212
2228
|
this._usingClassicSliderAccuracy = false;
|
|
2213
|
-
/**
|
|
2214
|
-
* Nerf factor used for nerfing beatmaps with very likely dropped sliderends.
|
|
2215
|
-
*/
|
|
2216
|
-
this.sliderNerfFactor = 1;
|
|
2217
2229
|
this.difficultyAttributes = difficultyAttributes;
|
|
2218
2230
|
this.mods = this.isCacheableAttribute(difficultyAttributes)
|
|
2219
2231
|
? osuBase.ModUtil.deserializeMods(difficultyAttributes.mods)
|
|
@@ -2228,7 +2240,6 @@ class PerformanceCalculator {
|
|
|
2228
2240
|
calculate(options) {
|
|
2229
2241
|
this.handleOptions(options);
|
|
2230
2242
|
this.calculateValues();
|
|
2231
|
-
this.total = this.calculateTotalValue();
|
|
2232
2243
|
return this;
|
|
2233
2244
|
}
|
|
2234
2245
|
/**
|
|
@@ -2255,12 +2266,6 @@ class PerformanceCalculator {
|
|
|
2255
2266
|
this.computedAccuracy.n50 +
|
|
2256
2267
|
this.computedAccuracy.nmiss);
|
|
2257
2268
|
}
|
|
2258
|
-
/**
|
|
2259
|
-
* Calculates the base performance value of a star rating.
|
|
2260
|
-
*/
|
|
2261
|
-
baseValue(stars) {
|
|
2262
|
-
return Math.pow(5 * Math.max(1, stars / 0.0675) - 4, 3) / 100000;
|
|
2263
|
-
}
|
|
2264
2269
|
/**
|
|
2265
2270
|
* Processes given options for usage in performance calculation.
|
|
2266
2271
|
*
|
|
@@ -2294,112 +2299,16 @@ class PerformanceCalculator {
|
|
|
2294
2299
|
if ((options === null || options === void 0 ? void 0 : options.sliderEndsDropped) !== undefined &&
|
|
2295
2300
|
(options === null || options === void 0 ? void 0 : options.sliderTicksMissed) !== undefined) {
|
|
2296
2301
|
this._usingClassicSliderAccuracy = false;
|
|
2297
|
-
this.
|
|
2298
|
-
this.
|
|
2302
|
+
this._sliderEndsDropped = options.sliderEndsDropped;
|
|
2303
|
+
this._sliderTicksMissed = options.sliderTicksMissed;
|
|
2299
2304
|
}
|
|
2300
2305
|
else {
|
|
2301
2306
|
this._usingClassicSliderAccuracy = true;
|
|
2302
|
-
this.
|
|
2303
|
-
this.
|
|
2307
|
+
this._sliderEndsDropped = 0;
|
|
2308
|
+
this._sliderTicksMissed = 0;
|
|
2304
2309
|
}
|
|
2305
2310
|
// Ensure that combo is within possible bounds.
|
|
2306
|
-
this.combo = osuBase.MathUtils.clamp(this.combo, 0, maxCombo - miss - this.
|
|
2307
|
-
this.effectiveMissCount = this.calculateEffectiveMissCount(maxCombo);
|
|
2308
|
-
if (this.mods.has(osuBase.ModNoFail)) {
|
|
2309
|
-
this.finalMultiplier *= Math.max(0.9, 1 - 0.02 * this.effectiveMissCount);
|
|
2310
|
-
}
|
|
2311
|
-
if (this.mods.has(osuBase.ModSpunOut)) {
|
|
2312
|
-
this.finalMultiplier *=
|
|
2313
|
-
1 -
|
|
2314
|
-
Math.pow(this.difficultyAttributes.spinnerCount / this.totalHits, 0.85);
|
|
2315
|
-
}
|
|
2316
|
-
if (this.mods.has(osuBase.ModRelax)) {
|
|
2317
|
-
const { overallDifficulty: od } = this.difficultyAttributes;
|
|
2318
|
-
let n100Multiplier;
|
|
2319
|
-
if (this.mode === osuBase.Modes.droid) {
|
|
2320
|
-
// Graph: https://www.desmos.com/calculator/vspzsop6td
|
|
2321
|
-
// We use OD13.3 as maximum since it's the value at which great hit window becomes 0.
|
|
2322
|
-
n100Multiplier =
|
|
2323
|
-
0.75 * Math.max(0, od > 0 ? 1 - od / 13.33 : 1);
|
|
2324
|
-
}
|
|
2325
|
-
else {
|
|
2326
|
-
// Graph: https://www.desmos.com/calculator/bc9eybdthb
|
|
2327
|
-
// We use OD13.3 as maximum since it's the value at which great hit window becomes 0.
|
|
2328
|
-
n100Multiplier = Math.max(0, od > 0 ? 1 - Math.pow(od / 13.33, 1.8) : 1);
|
|
2329
|
-
}
|
|
2330
|
-
const n50Multiplier = Math.max(0, od > 0 ? 1 - Math.pow(od / 13.33, 5) : 1);
|
|
2331
|
-
// As we're adding 100s and 50s to an approximated number of combo breaks, the result can be higher
|
|
2332
|
-
// than total hits in specific scenarios (which breaks some calculations), so we need to clamp it.
|
|
2333
|
-
this.effectiveMissCount = Math.min(this.effectiveMissCount +
|
|
2334
|
-
this.computedAccuracy.n100 * n100Multiplier +
|
|
2335
|
-
this.computedAccuracy.n50 * n50Multiplier, this.totalHits);
|
|
2336
|
-
}
|
|
2337
|
-
const { aimDifficultSliderCount, sliderFactor } = this.difficultyAttributes;
|
|
2338
|
-
if (aimDifficultSliderCount > 0) {
|
|
2339
|
-
let estimateImproperlyFollowedDifficultSliders;
|
|
2340
|
-
if (this.usingClassicSliderAccuracy) {
|
|
2341
|
-
// When the score is considered classic (regardless if it was made on old client or not),
|
|
2342
|
-
// we consider all missing combo to be dropped difficult sliders.
|
|
2343
|
-
estimateImproperlyFollowedDifficultSliders = osuBase.MathUtils.clamp(Math.min(this.totalImperfectHits, maxCombo - this.combo), 0, aimDifficultSliderCount);
|
|
2344
|
-
}
|
|
2345
|
-
else {
|
|
2346
|
-
// We add tick misses here since they too mean that the player didn't follow the slider
|
|
2347
|
-
// properly. However aren't adding misses here because missing slider heads has a harsh
|
|
2348
|
-
// penalty by itself and doesn't mean that the rest of the slider wasn't followed properly.
|
|
2349
|
-
estimateImproperlyFollowedDifficultSliders = osuBase.MathUtils.clamp(this.sliderEndsDropped + this.sliderTicksMissed, 0, aimDifficultSliderCount);
|
|
2350
|
-
}
|
|
2351
|
-
this.sliderNerfFactor =
|
|
2352
|
-
(1 - sliderFactor) *
|
|
2353
|
-
Math.pow(1 -
|
|
2354
|
-
estimateImproperlyFollowedDifficultSliders /
|
|
2355
|
-
aimDifficultSliderCount, 3) +
|
|
2356
|
-
sliderFactor;
|
|
2357
|
-
}
|
|
2358
|
-
}
|
|
2359
|
-
/**
|
|
2360
|
-
* Calculates a strain-based miss penalty.
|
|
2361
|
-
*
|
|
2362
|
-
* Strain-based miss penalty assumes that a player will miss on the hardest parts of a map,
|
|
2363
|
-
* so we use the amount of relatively difficult sections to adjust miss penalty
|
|
2364
|
-
* to make it more punishing on maps with lower amount of hard sections.
|
|
2365
|
-
*/
|
|
2366
|
-
calculateStrainBasedMissPenalty(difficultStrainCount) {
|
|
2367
|
-
if (this.effectiveMissCount === 0) {
|
|
2368
|
-
return 1;
|
|
2369
|
-
}
|
|
2370
|
-
return (0.96 /
|
|
2371
|
-
(this.effectiveMissCount /
|
|
2372
|
-
(4 * Math.pow(Math.log(difficultStrainCount), 0.94)) +
|
|
2373
|
-
1));
|
|
2374
|
-
}
|
|
2375
|
-
/**
|
|
2376
|
-
* Calculates the amount of misses + sliderbreaks from combo.
|
|
2377
|
-
*/
|
|
2378
|
-
calculateEffectiveMissCount(maxCombo) {
|
|
2379
|
-
let missCount = this.computedAccuracy.nmiss;
|
|
2380
|
-
if (this.difficultyAttributes.sliderCount > 0) {
|
|
2381
|
-
if (this.usingClassicSliderAccuracy) {
|
|
2382
|
-
// Consider that full combo is maximum combo minus dropped slider tails since
|
|
2383
|
-
// they don't contribute to combo but also don't break it.
|
|
2384
|
-
// In classic scores, we can't know the amount of dropped sliders so we estimate
|
|
2385
|
-
// to 10% of all sliders in the beatmap.
|
|
2386
|
-
const fullComboThreshold = maxCombo - 0.1 * this.difficultyAttributes.sliderCount;
|
|
2387
|
-
if (this.combo < fullComboThreshold) {
|
|
2388
|
-
missCount = fullComboThreshold / Math.max(1, this.combo);
|
|
2389
|
-
}
|
|
2390
|
-
// In classic scores, there can't be more misses than a sum of all non-perfect judgements.
|
|
2391
|
-
missCount = Math.min(missCount, this.totalImperfectHits);
|
|
2392
|
-
}
|
|
2393
|
-
else {
|
|
2394
|
-
const fullComboThreshold = maxCombo - this.sliderEndsDropped;
|
|
2395
|
-
if (this.combo < fullComboThreshold) {
|
|
2396
|
-
missCount = fullComboThreshold / Math.max(1, this.combo);
|
|
2397
|
-
}
|
|
2398
|
-
// Combine regular misses with tick misses, since tick misses break combo as well.
|
|
2399
|
-
missCount = Math.min(missCount, this.sliderTicksMissed + this.computedAccuracy.nmiss);
|
|
2400
|
-
}
|
|
2401
|
-
}
|
|
2402
|
-
return osuBase.MathUtils.clamp(missCount, this.computedAccuracy.nmiss, this.totalHits);
|
|
2311
|
+
this.combo = osuBase.MathUtils.clamp(this.combo, 0, maxCombo - miss - this._sliderEndsDropped - this._sliderTicksMissed);
|
|
2403
2312
|
}
|
|
2404
2313
|
/**
|
|
2405
2314
|
* Determines whether an attribute is a cacheable attribute.
|
|
@@ -2438,11 +2347,10 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2438
2347
|
* The reading performance value.
|
|
2439
2348
|
*/
|
|
2440
2349
|
this.reading = 0;
|
|
2441
|
-
this.finalMultiplier = 1.24;
|
|
2442
|
-
this.mode = osuBase.Modes.droid;
|
|
2443
2350
|
this._aimSliderCheesePenalty = 1;
|
|
2444
2351
|
this._flashlightSliderCheesePenalty = 1;
|
|
2445
2352
|
this._tapPenalty = 1;
|
|
2353
|
+
this._effectiveMissCount = 0;
|
|
2446
2354
|
this._deviation = 0;
|
|
2447
2355
|
this._tapDeviation = 0;
|
|
2448
2356
|
}
|
|
@@ -2483,66 +2391,54 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2483
2391
|
return this._flashlightSliderCheesePenalty;
|
|
2484
2392
|
}
|
|
2485
2393
|
/**
|
|
2486
|
-
*
|
|
2487
|
-
*
|
|
2488
|
-
* The tap and total performance value will be recalculated afterwards.
|
|
2489
|
-
*
|
|
2490
|
-
* @param value The tap penalty value. Must be greater than or equal to 1.
|
|
2491
|
-
*/
|
|
2492
|
-
applyTapPenalty(value) {
|
|
2493
|
-
if (value < 1) {
|
|
2494
|
-
throw new RangeError("New tap penalty must be greater than or equal to one.");
|
|
2495
|
-
}
|
|
2496
|
-
if (value === this._tapPenalty) {
|
|
2497
|
-
return;
|
|
2498
|
-
}
|
|
2499
|
-
this._tapPenalty = value;
|
|
2500
|
-
this.tap = this.calculateTapValue();
|
|
2501
|
-
this.total = this.calculateTotalValue();
|
|
2502
|
-
}
|
|
2503
|
-
/**
|
|
2504
|
-
* Applies an aim slider cheese penalty value to this calculator.
|
|
2505
|
-
*
|
|
2506
|
-
* The aim and total performance value will be recalculated afterwards.
|
|
2507
|
-
*
|
|
2508
|
-
* @param value The slider cheese penalty value. Must be between than 0 and 1.
|
|
2394
|
+
* The amount of misses, including slider breaks.
|
|
2509
2395
|
*/
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
throw new RangeError("New aim slider cheese penalty must be greater than or equal to zero.");
|
|
2513
|
-
}
|
|
2514
|
-
if (value > 1) {
|
|
2515
|
-
throw new RangeError("New aim slider cheese penalty must be less than or equal to one.");
|
|
2516
|
-
}
|
|
2517
|
-
if (value === this._aimSliderCheesePenalty) {
|
|
2518
|
-
return;
|
|
2519
|
-
}
|
|
2520
|
-
this._aimSliderCheesePenalty = value;
|
|
2521
|
-
this.aim = this.calculateAimValue();
|
|
2522
|
-
this.total = this.calculateTotalValue();
|
|
2396
|
+
get effectiveMissCount() {
|
|
2397
|
+
return this._effectiveMissCount;
|
|
2523
2398
|
}
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2399
|
+
calculateValues() {
|
|
2400
|
+
const { sliderCount, maxCombo } = this.difficultyAttributes;
|
|
2401
|
+
if (sliderCount > 0) {
|
|
2402
|
+
if (this.usingClassicSliderAccuracy) {
|
|
2403
|
+
// Consider that full combo is maximum combo minus dropped slider tails since
|
|
2404
|
+
// they don't contribute to combo but also don't break it.
|
|
2405
|
+
// In classic scores, we can't know the amount of dropped sliders so we estimate
|
|
2406
|
+
// to 10% of all sliders in the beatmap.
|
|
2407
|
+
const fullComboThreshold = maxCombo - 0.1 * sliderCount;
|
|
2408
|
+
if (this.combo < fullComboThreshold) {
|
|
2409
|
+
this._effectiveMissCount =
|
|
2410
|
+
fullComboThreshold / Math.max(1, this.combo);
|
|
2411
|
+
}
|
|
2412
|
+
// In classic scores, there can't be more misses than a sum of all non-perfect judgements.
|
|
2413
|
+
this._effectiveMissCount = Math.min(this._effectiveMissCount, this.totalImperfectHits);
|
|
2414
|
+
}
|
|
2415
|
+
else {
|
|
2416
|
+
const fullComboThreshold = maxCombo - this.sliderEndsDropped;
|
|
2417
|
+
if (this.combo < fullComboThreshold) {
|
|
2418
|
+
this._effectiveMissCount =
|
|
2419
|
+
fullComboThreshold / Math.max(1, this.combo);
|
|
2420
|
+
}
|
|
2421
|
+
// Combine regular misses with tick misses, since tick misses break combo as well.
|
|
2422
|
+
this._effectiveMissCount = Math.min(this._effectiveMissCount, this.sliderTicksMissed + this.computedAccuracy.nmiss);
|
|
2423
|
+
}
|
|
2534
2424
|
}
|
|
2535
|
-
|
|
2536
|
-
|
|
2425
|
+
this._effectiveMissCount = osuBase.MathUtils.clamp(this._effectiveMissCount, this.computedAccuracy.nmiss, this.totalHits);
|
|
2426
|
+
let { finalMultiplier } = DroidPerformanceCalculator;
|
|
2427
|
+
if (this.mods.has(osuBase.ModNoFail)) {
|
|
2428
|
+
finalMultiplier *= Math.max(0.9, 1 - 0.02 * this._effectiveMissCount);
|
|
2537
2429
|
}
|
|
2538
|
-
if (
|
|
2539
|
-
|
|
2430
|
+
if (this.mods.has(osuBase.ModRelax)) {
|
|
2431
|
+
const { overallDifficulty: od } = this.difficultyAttributes;
|
|
2432
|
+
// Graph: https://www.desmos.com/calculator/vspzsop6td
|
|
2433
|
+
// We use OD13.3 as maximum since it's the value at which great hit window becomes 0.
|
|
2434
|
+
const n100Multiplier = 0.75 * Math.max(0, od > 0 ? 1 - od / 13.33 : 1);
|
|
2435
|
+
const n50Multiplier = Math.max(0, od > 0 ? 1 - Math.pow(od / 13.33, 5) : 1);
|
|
2436
|
+
// As we're adding 100s and 50s to an approximated number of combo breaks, the result can be higher
|
|
2437
|
+
// than total hits in specific scenarios (which breaks some calculations), so we need to clamp it.
|
|
2438
|
+
this._effectiveMissCount = Math.min(this._effectiveMissCount +
|
|
2439
|
+
this.computedAccuracy.n100 * n100Multiplier +
|
|
2440
|
+
this.computedAccuracy.n50 * n50Multiplier, this.totalHits);
|
|
2540
2441
|
}
|
|
2541
|
-
this._flashlightSliderCheesePenalty = value;
|
|
2542
|
-
this.flashlight = this.calculateFlashlightValue();
|
|
2543
|
-
this.total = this.calculateTotalValue();
|
|
2544
|
-
}
|
|
2545
|
-
calculateValues() {
|
|
2546
2442
|
this._deviation = this.calculateDeviation();
|
|
2547
2443
|
this._tapDeviation = this.calculateTapDeviation();
|
|
2548
2444
|
this.aim = this.calculateAimValue();
|
|
@@ -2550,13 +2446,12 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2550
2446
|
this.accuracy = this.calculateAccuracyValue();
|
|
2551
2447
|
this.flashlight = this.calculateFlashlightValue();
|
|
2552
2448
|
this.reading = this.calculateReadingValue();
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
Math.pow(this.reading, 1.1), 1 / 1.1) * this.finalMultiplier);
|
|
2449
|
+
this.total =
|
|
2450
|
+
Math.pow(Math.pow(this.aim, 1.1) +
|
|
2451
|
+
Math.pow(this.tap, 1.1) +
|
|
2452
|
+
Math.pow(this.accuracy, 1.1) +
|
|
2453
|
+
Math.pow(this.flashlight, 1.1) +
|
|
2454
|
+
Math.pow(this.reading, 1.1), 1 / 1.1) * finalMultiplier;
|
|
2560
2455
|
}
|
|
2561
2456
|
handleOptions(options) {
|
|
2562
2457
|
var _a, _b, _c;
|
|
@@ -2570,12 +2465,31 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2570
2465
|
* Calculates the aim performance value of the beatmap.
|
|
2571
2466
|
*/
|
|
2572
2467
|
calculateAimValue() {
|
|
2573
|
-
let aimValue =
|
|
2468
|
+
let aimValue = DroidAim.difficultyToPerformance(this.difficultyAttributes.aimDifficulty);
|
|
2574
2469
|
aimValue *= Math.min(this.calculateStrainBasedMissPenalty(this.difficultyAttributes.aimDifficultStrainCount), this.proportionalMissPenalty);
|
|
2575
2470
|
// Scale the aim value with estimated full combo deviation.
|
|
2576
2471
|
aimValue *= this.calculateDeviationBasedLengthScaling();
|
|
2577
|
-
|
|
2578
|
-
|
|
2472
|
+
const { aimDifficultSliderCount, sliderFactor, maxCombo } = this.difficultyAttributes;
|
|
2473
|
+
if (aimDifficultSliderCount > 0) {
|
|
2474
|
+
let estimateImproperlyFollowedDifficultSliders;
|
|
2475
|
+
if (this.usingClassicSliderAccuracy) {
|
|
2476
|
+
// When the score is considered classic (regardless if it was made on old client or not),
|
|
2477
|
+
// we consider all missing combo to be dropped difficult sliders.
|
|
2478
|
+
estimateImproperlyFollowedDifficultSliders = osuBase.MathUtils.clamp(Math.min(this.totalImperfectHits, maxCombo - this.combo), 0, aimDifficultSliderCount);
|
|
2479
|
+
}
|
|
2480
|
+
else {
|
|
2481
|
+
// We add tick misses here since they too mean that the player didn't follow the slider
|
|
2482
|
+
// properly. However aren't adding misses here because missing slider heads has a harsh
|
|
2483
|
+
// penalty by itself and doesn't mean that the rest of the slider wasn't followed properly.
|
|
2484
|
+
estimateImproperlyFollowedDifficultSliders = osuBase.MathUtils.clamp(this.sliderEndsDropped + this.sliderTicksMissed, 0, aimDifficultSliderCount);
|
|
2485
|
+
}
|
|
2486
|
+
aimValue *=
|
|
2487
|
+
(1 - sliderFactor) *
|
|
2488
|
+
Math.pow(1 -
|
|
2489
|
+
estimateImproperlyFollowedDifficultSliders /
|
|
2490
|
+
aimDifficultSliderCount, 3) +
|
|
2491
|
+
sliderFactor;
|
|
2492
|
+
}
|
|
2579
2493
|
// Scale the aim value with slider cheese penalty.
|
|
2580
2494
|
aimValue *= this._aimSliderCheesePenalty;
|
|
2581
2495
|
// Scale the aim value with deviation.
|
|
@@ -2590,7 +2504,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2590
2504
|
* Calculates the tap performance value of the beatmap.
|
|
2591
2505
|
*/
|
|
2592
2506
|
calculateTapValue() {
|
|
2593
|
-
let tapValue =
|
|
2507
|
+
let tapValue = DroidTap.difficultyToPerformance(this.difficultyAttributes.tapDifficulty);
|
|
2594
2508
|
tapValue *= this.calculateStrainBasedMissPenalty(this.difficultyAttributes.tapDifficultStrainCount);
|
|
2595
2509
|
// Scale the tap value with estimated full combo deviation.
|
|
2596
2510
|
// Consider notes that are difficult to tap with respect to other notes, but
|
|
@@ -2648,7 +2562,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2648
2562
|
(1 +
|
|
2649
2563
|
Math.exp(-(this.difficultyAttributes.rhythmDifficulty - 1) / 2));
|
|
2650
2564
|
// Penalize accuracy pp after the first miss.
|
|
2651
|
-
accuracyValue *= Math.pow(0.97, Math.max(0, this.
|
|
2565
|
+
accuracyValue *= Math.pow(0.97, Math.max(0, this._effectiveMissCount - 1));
|
|
2652
2566
|
if (this.mods.has(osuBase.ModFlashlight)) {
|
|
2653
2567
|
accuracyValue *= 1.02;
|
|
2654
2568
|
}
|
|
@@ -2692,14 +2606,30 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2692
2606
|
readingValue *= 0.98 + Math.pow(5, 2) / 2500;
|
|
2693
2607
|
return readingValue;
|
|
2694
2608
|
}
|
|
2609
|
+
/**
|
|
2610
|
+
* Calculates a strain-based miss penalty.
|
|
2611
|
+
*
|
|
2612
|
+
* Strain-based miss penalty assumes that a player will miss on the hardest parts of a map,
|
|
2613
|
+
* so we use the amount of relatively difficult sections to adjust miss penalty
|
|
2614
|
+
* to make it more punishing on maps with lower amount of hard sections.
|
|
2615
|
+
*/
|
|
2616
|
+
calculateStrainBasedMissPenalty(difficultStrainCount) {
|
|
2617
|
+
if (this.effectiveMissCount === 0) {
|
|
2618
|
+
return 1;
|
|
2619
|
+
}
|
|
2620
|
+
return (0.96 /
|
|
2621
|
+
(this.effectiveMissCount /
|
|
2622
|
+
(4 * Math.pow(Math.log(difficultStrainCount), 0.94)) +
|
|
2623
|
+
1));
|
|
2624
|
+
}
|
|
2695
2625
|
/**
|
|
2696
2626
|
* The object-based proportional miss penalty.
|
|
2697
2627
|
*/
|
|
2698
2628
|
get proportionalMissPenalty() {
|
|
2699
|
-
if (this.
|
|
2629
|
+
if (this._effectiveMissCount === 0) {
|
|
2700
2630
|
return 1;
|
|
2701
2631
|
}
|
|
2702
|
-
const missProportion = (this.totalHits - this.
|
|
2632
|
+
const missProportion = (this.totalHits - this._effectiveMissCount) / (this.totalHits + 1);
|
|
2703
2633
|
const noMissProportion = this.totalHits / (this.totalHits + 1);
|
|
2704
2634
|
return (
|
|
2705
2635
|
// Aim deviation-based scale.
|
|
@@ -2896,7 +2826,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2896
2826
|
if (this.tapDeviation == Number.POSITIVE_INFINITY) {
|
|
2897
2827
|
return 0;
|
|
2898
2828
|
}
|
|
2899
|
-
const tapValue =
|
|
2829
|
+
const tapValue = DroidTap.difficultyToPerformance(this.difficultyAttributes.tapDifficulty);
|
|
2900
2830
|
// Decide a point where the PP value achieved compared to the tap deviation is assumed to be tapped
|
|
2901
2831
|
// improperly. Any PP above this point is considered "excess" tap difficulty. This is used to cause
|
|
2902
2832
|
// PP above the cutoff to scale logarithmically towards the original tap value thus nerfing the value.
|
|
@@ -2929,13 +2859,14 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2929
2859
|
this.tap.toFixed(2) +
|
|
2930
2860
|
" tap, " +
|
|
2931
2861
|
this.accuracy.toFixed(2) +
|
|
2932
|
-
"
|
|
2862
|
+
" accuracy, " +
|
|
2933
2863
|
this.flashlight.toFixed(2) +
|
|
2934
2864
|
" flashlight, " +
|
|
2935
2865
|
this.reading.toFixed(2) +
|
|
2936
2866
|
" reading)");
|
|
2937
2867
|
}
|
|
2938
2868
|
}
|
|
2869
|
+
DroidPerformanceCalculator.finalMultiplier = 1.24;
|
|
2939
2870
|
|
|
2940
2871
|
/**
|
|
2941
2872
|
* Represents an osu!standard hit object with difficulty calculation values.
|
|
@@ -2981,6 +2912,7 @@ class OsuAimEvaluator {
|
|
|
2981
2912
|
return 0;
|
|
2982
2913
|
}
|
|
2983
2914
|
const lastLast = current.previous(1);
|
|
2915
|
+
const last2 = current.previous(2);
|
|
2984
2916
|
const radius = OsuDifficultyHitObject.normalizedRadius;
|
|
2985
2917
|
const diameter = OsuDifficultyHitObject.normalizedDiameter;
|
|
2986
2918
|
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
|
|
@@ -3008,36 +2940,37 @@ class OsuAimEvaluator {
|
|
|
3008
2940
|
let wiggleBonus = 0;
|
|
3009
2941
|
// Start strain with regular velocity.
|
|
3010
2942
|
let strain = currentVelocity;
|
|
3011
|
-
if (
|
|
3012
|
-
// If rhythms are the same.
|
|
3013
|
-
Math.max(current.strainTime, last.strainTime) <
|
|
3014
|
-
1.25 * Math.min(current.strainTime, last.strainTime) &&
|
|
3015
|
-
current.angle !== null &&
|
|
3016
|
-
last.angle !== null) {
|
|
2943
|
+
if (current.angle !== null && last.angle !== null) {
|
|
3017
2944
|
const currentAngle = current.angle;
|
|
3018
2945
|
const lastAngle = last.angle;
|
|
3019
2946
|
// Rewarding angles, take the smaller velocity as base.
|
|
3020
2947
|
const angleBonus = Math.min(currentVelocity, prevVelocity);
|
|
3021
|
-
|
|
3022
|
-
|
|
2948
|
+
if (
|
|
2949
|
+
// If rhythms are the same.
|
|
2950
|
+
Math.max(current.strainTime, last.strainTime) <
|
|
2951
|
+
1.25 * Math.min(current.strainTime, last.strainTime)) {
|
|
2952
|
+
acuteAngleBonus = this.calculateAcuteAngleBonus(currentAngle);
|
|
2953
|
+
// Penalize angle repetition.
|
|
2954
|
+
acuteAngleBonus *=
|
|
2955
|
+
0.08 +
|
|
2956
|
+
0.92 *
|
|
2957
|
+
(1 -
|
|
2958
|
+
Math.min(acuteAngleBonus, Math.pow(this.calculateAcuteAngleBonus(lastAngle), 3)));
|
|
2959
|
+
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
|
2960
|
+
acuteAngleBonus *=
|
|
2961
|
+
angleBonus *
|
|
2962
|
+
osuBase.MathUtils.smootherstep(osuBase.MathUtils.millisecondsToBPM(current.strainTime, 2), 300, 400) *
|
|
2963
|
+
osuBase.MathUtils.smootherstep(current.lazyJumpDistance, diameter, diameter * 2);
|
|
2964
|
+
}
|
|
2965
|
+
wideAngleBonus = this.calculateWideAngleBonus(currentAngle);
|
|
3023
2966
|
// Penalize angle repetition.
|
|
3024
2967
|
wideAngleBonus *=
|
|
3025
2968
|
1 -
|
|
3026
2969
|
Math.min(wideAngleBonus, Math.pow(this.calculateWideAngleBonus(lastAngle), 3));
|
|
3027
|
-
acuteAngleBonus *=
|
|
3028
|
-
0.08 +
|
|
3029
|
-
0.92 *
|
|
3030
|
-
(1 -
|
|
3031
|
-
Math.min(acuteAngleBonus, Math.pow(this.calculateAcuteAngleBonus(lastAngle), 3)));
|
|
3032
2970
|
// Apply full wide angle bonus for distance more than one diameter
|
|
3033
2971
|
wideAngleBonus *=
|
|
3034
2972
|
angleBonus *
|
|
3035
2973
|
osuBase.MathUtils.smootherstep(current.lazyJumpDistance, 0, diameter);
|
|
3036
|
-
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
|
3037
|
-
acuteAngleBonus *=
|
|
3038
|
-
angleBonus *
|
|
3039
|
-
osuBase.MathUtils.smootherstep(osuBase.MathUtils.millisecondsToBPM(current.strainTime, 2), 300, 400) *
|
|
3040
|
-
osuBase.MathUtils.smootherstep(current.lazyJumpDistance, diameter, diameter * 2);
|
|
3041
2974
|
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
|
3042
2975
|
// https://www.desmos.com/calculator/dp0v0nvowc
|
|
3043
2976
|
wiggleBonus =
|
|
@@ -3048,6 +2981,15 @@ class OsuAimEvaluator {
|
|
|
3048
2981
|
osuBase.MathUtils.smootherstep(last.lazyJumpDistance, radius, diameter) *
|
|
3049
2982
|
Math.pow(osuBase.MathUtils.reverseLerp(last.lazyJumpDistance, diameter * 3, diameter), 1.8) *
|
|
3050
2983
|
osuBase.MathUtils.smootherstep(lastAngle, osuBase.MathUtils.degreesToRadians(110), osuBase.MathUtils.degreesToRadians(60));
|
|
2984
|
+
if (last2 !== null) {
|
|
2985
|
+
// If objects just go back and forth through a middle point - don't give as much wide bonus.
|
|
2986
|
+
// Use previous(2) and previous(0) because angles calculation is done prevprev-prev-curr, so any
|
|
2987
|
+
// object's angle's center point is always the previous object.
|
|
2988
|
+
const distance = last2.object.stackedPosition.getDistance(last.object.stackedPosition);
|
|
2989
|
+
if (distance < 1) {
|
|
2990
|
+
wideAngleBonus *= 1 - 0.35 * (1 - distance);
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
3051
2993
|
}
|
|
3052
2994
|
if (Math.max(prevVelocity, currentVelocity)) {
|
|
3053
2995
|
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
|
|
@@ -3058,10 +3000,11 @@ class OsuAimEvaluator {
|
|
|
3058
3000
|
(current.lazyJumpDistance + last.travelDistance) /
|
|
3059
3001
|
current.strainTime;
|
|
3060
3002
|
// Scale with ratio of difference compared to half the max distance.
|
|
3061
|
-
const distanceRatio =
|
|
3062
|
-
Math.max(prevVelocity, currentVelocity)
|
|
3003
|
+
const distanceRatio = osuBase.MathUtils.smoothstep(Math.abs(prevVelocity - currentVelocity) /
|
|
3004
|
+
Math.max(prevVelocity, currentVelocity), 0, 1);
|
|
3063
3005
|
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
|
3064
|
-
const overlapVelocityBuff = Math.min(
|
|
3006
|
+
const overlapVelocityBuff = Math.min((diameter * 1.25) /
|
|
3007
|
+
Math.min(current.strainTime, last.strainTime), Math.abs(prevVelocity - currentVelocity));
|
|
3065
3008
|
velocityChangeBonus = overlapVelocityBuff * distanceRatio;
|
|
3066
3009
|
// Penalize for rhythm changes.
|
|
3067
3010
|
velocityChangeBonus *= Math.pow(Math.min(current.strainTime, last.strainTime) /
|
|
@@ -3072,9 +3015,11 @@ class OsuAimEvaluator {
|
|
|
3072
3015
|
sliderBonus = last.travelDistance / last.travelTime;
|
|
3073
3016
|
}
|
|
3074
3017
|
strain += wiggleBonus * this.wiggleMultiplier;
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3018
|
+
strain += velocityChangeBonus * this.velocityChangeMultiplier;
|
|
3019
|
+
// Add in acute angle bonus or wide angle bonus, whichever is larger.
|
|
3020
|
+
strain += Math.max(acuteAngleBonus * this.acuteAngleMultiplier, wideAngleBonus * this.wideAngleMultiplier);
|
|
3021
|
+
// Apply high circle size bonus
|
|
3022
|
+
strain *= current.smallCircleBonus;
|
|
3078
3023
|
// Add in additional slider velocity bonus.
|
|
3079
3024
|
if (withSliders) {
|
|
3080
3025
|
strain += sliderBonus * this.sliderMultiplier;
|
|
@@ -3089,7 +3034,7 @@ class OsuAimEvaluator {
|
|
|
3089
3034
|
}
|
|
3090
3035
|
}
|
|
3091
3036
|
OsuAimEvaluator.wideAngleMultiplier = 1.5;
|
|
3092
|
-
OsuAimEvaluator.acuteAngleMultiplier = 2.
|
|
3037
|
+
OsuAimEvaluator.acuteAngleMultiplier = 2.55;
|
|
3093
3038
|
OsuAimEvaluator.sliderMultiplier = 1.35;
|
|
3094
3039
|
OsuAimEvaluator.velocityChangeMultiplier = 0.75;
|
|
3095
3040
|
OsuAimEvaluator.wiggleMultiplier = 1.02;
|
|
@@ -3125,9 +3070,24 @@ class OsuSkill extends StrainSkill {
|
|
|
3125
3070
|
}
|
|
3126
3071
|
}
|
|
3127
3072
|
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3073
|
+
class StrainUtils {
|
|
3074
|
+
static countTopWeightedSliders(sliderStrains, difficultyValue) {
|
|
3075
|
+
if (sliderStrains.length === 0) {
|
|
3076
|
+
return 0;
|
|
3077
|
+
}
|
|
3078
|
+
const consistentTopStrain = difficultyValue / 10;
|
|
3079
|
+
if (consistentTopStrain === 0) {
|
|
3080
|
+
return 0;
|
|
3081
|
+
}
|
|
3082
|
+
// Use a weighted sum of all strains. Constants are arbitrary and give nice values
|
|
3083
|
+
return sliderStrains.reduce((total, next) => total +
|
|
3084
|
+
osuBase.MathUtils.offsetLogistic(next / consistentTopStrain, 0.88, 10, 1.1));
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
/**
|
|
3089
|
+
* Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
|
3090
|
+
*/
|
|
3131
3091
|
class OsuAim extends OsuSkill {
|
|
3132
3092
|
constructor(mods, withSliders) {
|
|
3133
3093
|
super(mods);
|
|
@@ -3136,7 +3096,7 @@ class OsuAim extends OsuSkill {
|
|
|
3136
3096
|
this.reducedSectionBaseline = 0.75;
|
|
3137
3097
|
this.decayWeight = 0.9;
|
|
3138
3098
|
this.currentAimStrain = 0;
|
|
3139
|
-
this.skillMultiplier =
|
|
3099
|
+
this.skillMultiplier = 26;
|
|
3140
3100
|
this.sliderStrains = [];
|
|
3141
3101
|
this.withSliders = withSliders;
|
|
3142
3102
|
}
|
|
@@ -3154,6 +3114,12 @@ class OsuAim extends OsuSkill {
|
|
|
3154
3114
|
return this.sliderStrains.reduce((total, strain) => total +
|
|
3155
3115
|
1 / (1 + Math.exp(-((strain / maxSliderStrain) * 12 - 6))), 0);
|
|
3156
3116
|
}
|
|
3117
|
+
/**
|
|
3118
|
+
* Obtains the amount of sliders that are considered difficult in terms of relative strain, weighted by consistency.
|
|
3119
|
+
*/
|
|
3120
|
+
countTopWeightedSliders() {
|
|
3121
|
+
return StrainUtils.countTopWeightedSliders(this.sliderStrains, this.difficulty);
|
|
3122
|
+
}
|
|
3157
3123
|
strainValueAt(current) {
|
|
3158
3124
|
this.currentAimStrain *= this.strainDecay(current.deltaTime);
|
|
3159
3125
|
this.currentAimStrain +=
|
|
@@ -3190,15 +3156,23 @@ class OsuDifficultyAttributes extends DifficultyAttributes {
|
|
|
3190
3156
|
constructor(cacheableAttributes) {
|
|
3191
3157
|
super(cacheableAttributes);
|
|
3192
3158
|
this.approachRate = 0;
|
|
3159
|
+
this.drainRate = 0;
|
|
3193
3160
|
this.speedDifficulty = 0;
|
|
3194
3161
|
this.speedDifficultStrainCount = 0;
|
|
3162
|
+
this.aimTopWeightedSliderFactor = 0;
|
|
3163
|
+
this.speedTopWeightedSliderFactor = 0;
|
|
3195
3164
|
if (!cacheableAttributes) {
|
|
3196
3165
|
return;
|
|
3197
3166
|
}
|
|
3198
3167
|
this.approachRate = cacheableAttributes.approachRate;
|
|
3168
|
+
this.drainRate = cacheableAttributes.drainRate;
|
|
3199
3169
|
this.speedDifficulty = cacheableAttributes.speedDifficulty;
|
|
3200
3170
|
this.speedDifficultStrainCount =
|
|
3201
3171
|
cacheableAttributes.speedDifficultStrainCount;
|
|
3172
|
+
this.aimTopWeightedSliderFactor =
|
|
3173
|
+
cacheableAttributes.aimTopWeightedSliderFactor;
|
|
3174
|
+
this.speedTopWeightedSliderFactor =
|
|
3175
|
+
cacheableAttributes.speedTopWeightedSliderFactor;
|
|
3202
3176
|
}
|
|
3203
3177
|
toString() {
|
|
3204
3178
|
return (super.toString() +
|
|
@@ -3208,124 +3182,6 @@ class OsuDifficultyAttributes extends DifficultyAttributes {
|
|
|
3208
3182
|
}
|
|
3209
3183
|
}
|
|
3210
3184
|
|
|
3211
|
-
/**
|
|
3212
|
-
* An evaluator for calculating osu!standard Flashlight skill.
|
|
3213
|
-
*/
|
|
3214
|
-
class OsuFlashlightEvaluator {
|
|
3215
|
-
/**
|
|
3216
|
-
* Evaluates the difficulty of memorizing and hitting the current object, based on:
|
|
3217
|
-
*
|
|
3218
|
-
* - distance between a number of previous objects and the current object,
|
|
3219
|
-
* - the visual opacity of the current object,
|
|
3220
|
-
* - the angle made by the current object,
|
|
3221
|
-
* - length and speed of the current object (for sliders),
|
|
3222
|
-
* - and whether Hidden mod is enabled.
|
|
3223
|
-
*
|
|
3224
|
-
* @param current The current object.
|
|
3225
|
-
* @param mods The mods used.
|
|
3226
|
-
*/
|
|
3227
|
-
static evaluateDifficultyOf(current, mods) {
|
|
3228
|
-
if (current.object instanceof osuBase.Spinner) {
|
|
3229
|
-
return 0;
|
|
3230
|
-
}
|
|
3231
|
-
const scalingFactor = 52 / current.object.radius;
|
|
3232
|
-
let smallDistNerf = 1;
|
|
3233
|
-
let cumulativeStrainTime = 0;
|
|
3234
|
-
let result = 0;
|
|
3235
|
-
let last = current;
|
|
3236
|
-
let angleRepeatCount = 0;
|
|
3237
|
-
for (let i = 0; i < Math.min(current.index, 10); ++i) {
|
|
3238
|
-
const currentObject = current.previous(i);
|
|
3239
|
-
cumulativeStrainTime += last.strainTime;
|
|
3240
|
-
if (!(currentObject.object instanceof osuBase.Spinner)) {
|
|
3241
|
-
const jumpDistance = current.object.stackedPosition.subtract(currentObject.object.stackedEndPosition).length;
|
|
3242
|
-
// We want to nerf objects that can be easily seen within the Flashlight circle radius.
|
|
3243
|
-
if (i === 0) {
|
|
3244
|
-
smallDistNerf = Math.min(1, jumpDistance / 75);
|
|
3245
|
-
}
|
|
3246
|
-
// We also want to nerf stacks so that only the first object of the stack is accounted for.
|
|
3247
|
-
const stackNerf = Math.min(1, currentObject.lazyJumpDistance / scalingFactor / 25);
|
|
3248
|
-
// Bonus based on how visible the object is.
|
|
3249
|
-
const opacityBonus = 1 +
|
|
3250
|
-
this.maxOpacityBonus *
|
|
3251
|
-
(1 -
|
|
3252
|
-
current.opacityAt(currentObject.object.startTime, mods));
|
|
3253
|
-
result +=
|
|
3254
|
-
(stackNerf * opacityBonus * scalingFactor * jumpDistance) /
|
|
3255
|
-
cumulativeStrainTime;
|
|
3256
|
-
if (currentObject.angle !== null && current.angle !== null) {
|
|
3257
|
-
// Objects further back in time should count less for the nerf.
|
|
3258
|
-
if (Math.abs(currentObject.angle - current.angle) < 0.02) {
|
|
3259
|
-
angleRepeatCount += Math.max(0, 1 - 0.1 * i);
|
|
3260
|
-
}
|
|
3261
|
-
}
|
|
3262
|
-
}
|
|
3263
|
-
last = currentObject;
|
|
3264
|
-
}
|
|
3265
|
-
result = Math.pow(smallDistNerf * result, 2);
|
|
3266
|
-
// Additional bonus for Hidden due to there being no approach circles.
|
|
3267
|
-
if (mods.has(osuBase.ModHidden)) {
|
|
3268
|
-
result *= 1 + this.hiddenBonus;
|
|
3269
|
-
}
|
|
3270
|
-
// Nerf patterns with repeated angles.
|
|
3271
|
-
result *=
|
|
3272
|
-
this.minAngleMultiplier +
|
|
3273
|
-
(1 - this.minAngleMultiplier) / (angleRepeatCount + 1);
|
|
3274
|
-
let sliderBonus = 0;
|
|
3275
|
-
if (current.object instanceof osuBase.Slider) {
|
|
3276
|
-
// Invert the scaling factor to determine the true travel distance independent of circle size.
|
|
3277
|
-
const pixelTravelDistance = current.lazyTravelDistance / scalingFactor;
|
|
3278
|
-
// Reward sliders based on velocity.
|
|
3279
|
-
sliderBonus = Math.pow(Math.max(0, pixelTravelDistance / current.travelTime - this.minVelocity), 0.5);
|
|
3280
|
-
// Longer sliders require more memorization.
|
|
3281
|
-
sliderBonus *= pixelTravelDistance;
|
|
3282
|
-
// Nerf sliders with repeats, as less memorization is required.
|
|
3283
|
-
if (current.object.repeatCount > 0)
|
|
3284
|
-
sliderBonus /= current.object.repeatCount + 1;
|
|
3285
|
-
}
|
|
3286
|
-
result += sliderBonus * this.sliderMultiplier;
|
|
3287
|
-
return result;
|
|
3288
|
-
}
|
|
3289
|
-
}
|
|
3290
|
-
OsuFlashlightEvaluator.maxOpacityBonus = 0.4;
|
|
3291
|
-
OsuFlashlightEvaluator.hiddenBonus = 0.2;
|
|
3292
|
-
OsuFlashlightEvaluator.minVelocity = 0.5;
|
|
3293
|
-
OsuFlashlightEvaluator.sliderMultiplier = 1.3;
|
|
3294
|
-
OsuFlashlightEvaluator.minAngleMultiplier = 0.2;
|
|
3295
|
-
|
|
3296
|
-
/**
|
|
3297
|
-
* Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
|
|
3298
|
-
*/
|
|
3299
|
-
class OsuFlashlight extends OsuSkill {
|
|
3300
|
-
constructor() {
|
|
3301
|
-
super(...arguments);
|
|
3302
|
-
this.strainDecayBase = 0.15;
|
|
3303
|
-
this.reducedSectionCount = 0;
|
|
3304
|
-
this.reducedSectionBaseline = 1;
|
|
3305
|
-
this.decayWeight = 1;
|
|
3306
|
-
this.currentFlashlightStrain = 0;
|
|
3307
|
-
this.skillMultiplier = 0.05512;
|
|
3308
|
-
}
|
|
3309
|
-
difficultyValue() {
|
|
3310
|
-
return this.strainPeaks.reduce((a, b) => a + b, 0);
|
|
3311
|
-
}
|
|
3312
|
-
strainValueAt(current) {
|
|
3313
|
-
this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
|
|
3314
|
-
this.currentFlashlightStrain +=
|
|
3315
|
-
OsuFlashlightEvaluator.evaluateDifficultyOf(current, this.mods) *
|
|
3316
|
-
this.skillMultiplier;
|
|
3317
|
-
return this.currentFlashlightStrain;
|
|
3318
|
-
}
|
|
3319
|
-
calculateInitialStrain(time, current) {
|
|
3320
|
-
var _a, _b;
|
|
3321
|
-
return (this.currentFlashlightStrain *
|
|
3322
|
-
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
3323
|
-
}
|
|
3324
|
-
saveToHitObject(current) {
|
|
3325
|
-
current.flashlightStrain = this.currentFlashlightStrain;
|
|
3326
|
-
}
|
|
3327
|
-
}
|
|
3328
|
-
|
|
3329
3185
|
/**
|
|
3330
3186
|
* An evaluator for calculating osu!standard Rhythm skill.
|
|
3331
3187
|
*/
|
|
@@ -3355,10 +3211,10 @@ class OsuRhythmEvaluator {
|
|
|
3355
3211
|
this.historyTimeMax) {
|
|
3356
3212
|
++rhythmStart;
|
|
3357
3213
|
}
|
|
3214
|
+
let prevObject = current.previous(rhythmStart);
|
|
3215
|
+
let lastObject = current.previous(rhythmStart + 1);
|
|
3358
3216
|
for (let i = rhythmStart; i > 0; --i) {
|
|
3359
3217
|
const currentObject = current.previous(i - 1);
|
|
3360
|
-
const prevObject = current.previous(i);
|
|
3361
|
-
const lastObject = current.previous(i + 1);
|
|
3362
3218
|
// Scale note 0 to 1 from history to now.
|
|
3363
3219
|
const timeDecay = (this.historyTimeMax -
|
|
3364
3220
|
(current.startTime - currentObject.startTime)) /
|
|
@@ -3366,21 +3222,23 @@ class OsuRhythmEvaluator {
|
|
|
3366
3222
|
const noteDecay = (historicalNoteCount - i) / historicalNoteCount;
|
|
3367
3223
|
// Either we're limited by time or limited by object count.
|
|
3368
3224
|
const currentHistoricalDecay = Math.min(timeDecay, noteDecay);
|
|
3369
|
-
|
|
3370
|
-
const
|
|
3371
|
-
const
|
|
3225
|
+
// Use custom cap value to ensure that that at this point delta time is actually zero.
|
|
3226
|
+
const currentDelta = Math.max(currentObject.deltaTime, 1e-7);
|
|
3227
|
+
const prevDelta = Math.max(prevObject.deltaTime, 1e-7);
|
|
3228
|
+
const lastDelta = Math.max(lastObject.deltaTime, 1e-7);
|
|
3372
3229
|
// Calculate how much current delta difference deserves a rhythm bonus
|
|
3373
3230
|
// This function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e. 100 and 200)
|
|
3374
|
-
const
|
|
3375
|
-
Math.
|
|
3231
|
+
const deltaDifference = Math.max(prevDelta, currentDelta) /
|
|
3232
|
+
Math.min(prevDelta, currentDelta);
|
|
3233
|
+
// Take only the fractional part of the value since we are only interested in punishing multiples.
|
|
3234
|
+
const deltaDifferenceFraction = deltaDifference - Math.trunc(deltaDifference);
|
|
3376
3235
|
const currentRatio = 1 +
|
|
3377
3236
|
this.rhythmRatioMultiplier *
|
|
3378
|
-
Math.min(0.5,
|
|
3237
|
+
Math.min(0.5, osuBase.MathUtils.smoothstepBellCurve(deltaDifferenceFraction));
|
|
3379
3238
|
// Reduce ratio bonus if delta difference is too big
|
|
3380
|
-
const
|
|
3381
|
-
const fractionMultiplier = osuBase.MathUtils.clamp(2 - fraction / 8, 0, 1);
|
|
3239
|
+
const differenceMultiplier = osuBase.MathUtils.clamp(2 - deltaDifference / 8, 0, 1);
|
|
3382
3240
|
const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon);
|
|
3383
|
-
let effectiveRatio = windowPenalty * currentRatio *
|
|
3241
|
+
let effectiveRatio = windowPenalty * currentRatio * differenceMultiplier;
|
|
3384
3242
|
if (firstDeltaSwitch) {
|
|
3385
3243
|
if (Math.abs(prevDelta - currentDelta) < deltaDifferenceEpsilon) {
|
|
3386
3244
|
// Island is still progressing, count size.
|
|
@@ -3433,7 +3291,8 @@ class OsuRhythmEvaluator {
|
|
|
3433
3291
|
islandCounts.set(island, 1);
|
|
3434
3292
|
}
|
|
3435
3293
|
// Scale down the difficulty if the object is doubletappable.
|
|
3436
|
-
effectiveRatio *=
|
|
3294
|
+
effectiveRatio *=
|
|
3295
|
+
1 - prevObject.getDoubletapness(currentObject) * 0.75;
|
|
3437
3296
|
rhythmComplexitySum +=
|
|
3438
3297
|
Math.sqrt(effectiveRatio * startRatio) *
|
|
3439
3298
|
currentHistoricalDecay;
|
|
@@ -3464,15 +3323,18 @@ class OsuRhythmEvaluator {
|
|
|
3464
3323
|
startRatio = effectiveRatio;
|
|
3465
3324
|
island = new Island(currentDelta, deltaDifferenceEpsilon);
|
|
3466
3325
|
}
|
|
3326
|
+
lastObject = prevObject;
|
|
3327
|
+
prevObject = currentObject;
|
|
3467
3328
|
}
|
|
3468
|
-
return (Math.sqrt(4 + rhythmComplexitySum * this.rhythmOverallMultiplier)
|
|
3329
|
+
return ((Math.sqrt(4 + rhythmComplexitySum * this.rhythmOverallMultiplier) *
|
|
3330
|
+
(1 - current.getDoubletapness(current.next(0)))) /
|
|
3469
3331
|
2);
|
|
3470
3332
|
}
|
|
3471
3333
|
}
|
|
3472
3334
|
OsuRhythmEvaluator.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
|
|
3473
3335
|
OsuRhythmEvaluator.historyObjectsMax = 32;
|
|
3474
|
-
OsuRhythmEvaluator.rhythmOverallMultiplier =
|
|
3475
|
-
OsuRhythmEvaluator.rhythmRatioMultiplier =
|
|
3336
|
+
OsuRhythmEvaluator.rhythmOverallMultiplier = 1;
|
|
3337
|
+
OsuRhythmEvaluator.rhythmRatioMultiplier = 15;
|
|
3476
3338
|
|
|
3477
3339
|
/**
|
|
3478
3340
|
* An evaluator for calculating osu!standard speed skill.
|
|
@@ -3496,7 +3358,7 @@ class OsuSpeedEvaluator {
|
|
|
3496
3358
|
const prev = current.previous(0);
|
|
3497
3359
|
let strainTime = current.strainTime;
|
|
3498
3360
|
// Nerf doubletappable doubles.
|
|
3499
|
-
const doubletapness = 1 - current.
|
|
3361
|
+
const doubletapness = 1 - current.getDoubletapness(current.next(0));
|
|
3500
3362
|
// Cap deltatime to the OD 300 hitwindow.
|
|
3501
3363
|
// 0.93 is derived from making sure 260 BPM 1/4 OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
|
3502
3364
|
strainTime /= osuBase.MathUtils.clamp(strainTime / current.fullGreatWindow / 0.93, 0.92, 1);
|
|
@@ -3513,6 +3375,8 @@ class OsuSpeedEvaluator {
|
|
|
3513
3375
|
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
|
|
3514
3376
|
let distanceBonus = Math.pow(distance / this.SINGLE_SPACING_THRESHOLD, 3.95) *
|
|
3515
3377
|
this.DISTANCE_MULTIPLIER;
|
|
3378
|
+
// Apply reduced small circle bonus because flow aim difficulty on small circles does not scale as hard as jumps.
|
|
3379
|
+
distanceBonus *= Math.sqrt(current.smallCircleBonus);
|
|
3516
3380
|
if (mods.has(osuBase.ModAutopilot)) {
|
|
3517
3381
|
distanceBonus = 0;
|
|
3518
3382
|
}
|
|
@@ -3527,10 +3391,10 @@ class OsuSpeedEvaluator {
|
|
|
3527
3391
|
*
|
|
3528
3392
|
* About 1.25 circles distance between hitobject centers.
|
|
3529
3393
|
*/
|
|
3530
|
-
OsuSpeedEvaluator.SINGLE_SPACING_THRESHOLD =
|
|
3394
|
+
OsuSpeedEvaluator.SINGLE_SPACING_THRESHOLD = OsuDifficultyHitObject.normalizedDiameter * 1.25;
|
|
3531
3395
|
// ~200 1/4 BPM streams
|
|
3532
3396
|
OsuSpeedEvaluator.minSpeedBonus = 75;
|
|
3533
|
-
OsuSpeedEvaluator.DISTANCE_MULTIPLIER = 0.
|
|
3397
|
+
OsuSpeedEvaluator.DISTANCE_MULTIPLIER = 0.8;
|
|
3534
3398
|
|
|
3535
3399
|
/**
|
|
3536
3400
|
* Represents the skill required to press keys or tap with regards to keeping up with the speed at which objects need to be hit.
|
|
@@ -3544,7 +3408,8 @@ class OsuSpeed extends OsuSkill {
|
|
|
3544
3408
|
this.decayWeight = 0.9;
|
|
3545
3409
|
this.currentSpeedStrain = 0;
|
|
3546
3410
|
this.currentRhythm = 0;
|
|
3547
|
-
this.skillMultiplier = 1.
|
|
3411
|
+
this.skillMultiplier = 1.47;
|
|
3412
|
+
this.sliderStrains = [];
|
|
3548
3413
|
this.maxStrain = 0;
|
|
3549
3414
|
}
|
|
3550
3415
|
/**
|
|
@@ -3556,6 +3421,12 @@ class OsuSpeed extends OsuSkill {
|
|
|
3556
3421
|
}
|
|
3557
3422
|
return this._objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / this.maxStrain) * 12 - 6))), 0);
|
|
3558
3423
|
}
|
|
3424
|
+
/**
|
|
3425
|
+
* Obtains the amount of sliders that are considered difficult in terms of relative strain, weighted by consistency.
|
|
3426
|
+
*/
|
|
3427
|
+
countTopWeightedSliders() {
|
|
3428
|
+
return StrainUtils.countTopWeightedSliders(this.sliderStrains, this.difficulty);
|
|
3429
|
+
}
|
|
3559
3430
|
/**
|
|
3560
3431
|
* @param current The hitobject to calculate.
|
|
3561
3432
|
*/
|
|
@@ -3568,6 +3439,9 @@ class OsuSpeed extends OsuSkill {
|
|
|
3568
3439
|
const strain = this.currentSpeedStrain * this.currentRhythm;
|
|
3569
3440
|
this._objectStrains.push(strain);
|
|
3570
3441
|
this.maxStrain = Math.max(this.maxStrain, strain);
|
|
3442
|
+
if (current.object instanceof osuBase.Slider) {
|
|
3443
|
+
this.sliderStrains.push(strain);
|
|
3444
|
+
}
|
|
3571
3445
|
return strain;
|
|
3572
3446
|
}
|
|
3573
3447
|
calculateInitialStrain(time, current) {
|
|
@@ -3585,142 +3459,189 @@ class OsuSpeed extends OsuSkill {
|
|
|
3585
3459
|
}
|
|
3586
3460
|
}
|
|
3587
3461
|
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
this.
|
|
3595
|
-
this.
|
|
3596
|
-
}
|
|
3597
|
-
retainDifficultyAdjustmentMods(mods) {
|
|
3598
|
-
return mods.filter((mod) => mod.isApplicableToOsu() &&
|
|
3599
|
-
mod.isOsuRelevant &&
|
|
3600
|
-
this.difficultyAdjustmentMods.some((m) => mod instanceof m));
|
|
3462
|
+
class OsuRatingCalculator {
|
|
3463
|
+
constructor(mods, totalHits, approachRate, overallDifficulty, mechanicalDifficultyRating, sliderFactor) {
|
|
3464
|
+
this.mods = mods;
|
|
3465
|
+
this.totalHits = totalHits;
|
|
3466
|
+
this.approachRate = approachRate;
|
|
3467
|
+
this.overallDifficulty = overallDifficulty;
|
|
3468
|
+
this.mechanicalDifficultyRating = mechanicalDifficultyRating;
|
|
3469
|
+
this.sliderFactor = sliderFactor;
|
|
3601
3470
|
}
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
attributes.maxCombo = beatmap.maxCombo;
|
|
3606
|
-
attributes.clockRate = beatmap.speedMultiplier;
|
|
3607
|
-
attributes.hitCircleCount = beatmap.hitObjects.circles;
|
|
3608
|
-
attributes.sliderCount = beatmap.hitObjects.sliders;
|
|
3609
|
-
attributes.spinnerCount = beatmap.hitObjects.spinners;
|
|
3610
|
-
this.populateAimAttributes(attributes, skills);
|
|
3611
|
-
this.populateSpeedAttributes(attributes, skills);
|
|
3612
|
-
this.populateFlashlightAttributes(attributes, skills);
|
|
3613
|
-
if (attributes.mods.has(osuBase.ModRelax)) {
|
|
3614
|
-
attributes.aimDifficulty *= 0.9;
|
|
3615
|
-
attributes.speedDifficulty = 0;
|
|
3616
|
-
attributes.flashlightDifficulty *= 0.7;
|
|
3471
|
+
computeAimRating(aimDifficultyValue) {
|
|
3472
|
+
if (this.mods.has(osuBase.ModAutopilot)) {
|
|
3473
|
+
return 0;
|
|
3617
3474
|
}
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
attributes.flashlightDifficulty *= 0.4;
|
|
3475
|
+
let aimRating = OsuRatingCalculator.calculateDifficultyRating(aimDifficultyValue);
|
|
3476
|
+
if (this.mods.has(osuBase.ModTouchDevice)) {
|
|
3477
|
+
aimRating = Math.pow(aimRating, 0.8);
|
|
3622
3478
|
}
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
const flashlightPerformanceValue = Math.pow(attributes.flashlightDifficulty, 2) * 25;
|
|
3626
|
-
const basePerformanceValue = Math.pow(Math.pow(aimPerformanceValue, 1.1) +
|
|
3627
|
-
Math.pow(speedPerformanceValue, 1.1) +
|
|
3628
|
-
Math.pow(flashlightPerformanceValue, 1.1), 1 / 1.1);
|
|
3629
|
-
if (basePerformanceValue > 1e-5) {
|
|
3630
|
-
// Document for formula derivation:
|
|
3631
|
-
// https://docs.google.com/document/d/10DZGYYSsT_yjz2Mtp6yIJld0Rqx4E-vVHupCqiM4TNI/edit
|
|
3632
|
-
attributes.starRating =
|
|
3633
|
-
Math.cbrt(1.15) *
|
|
3634
|
-
0.027 *
|
|
3635
|
-
(Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformanceValue) +
|
|
3636
|
-
4);
|
|
3479
|
+
if (this.mods.has(osuBase.ModRelax)) {
|
|
3480
|
+
aimRating *= 0.9;
|
|
3637
3481
|
}
|
|
3638
|
-
|
|
3639
|
-
|
|
3482
|
+
if (this.mods.has(osuBase.ModMagnetised)) {
|
|
3483
|
+
const magnetisedStrength = this.mods.get(osuBase.ModMagnetised).attractionStrength.value;
|
|
3484
|
+
aimRating *= 1 - magnetisedStrength;
|
|
3640
3485
|
}
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
createDifficultyHitObjects(beatmap) {
|
|
3651
|
-
const clockRate = beatmap.speedMultiplier;
|
|
3652
|
-
const difficultyObjects = [];
|
|
3653
|
-
const { objects } = beatmap.hitObjects;
|
|
3654
|
-
for (let i = 1; i < objects.length; ++i) {
|
|
3655
|
-
const difficultyObject = new OsuDifficultyHitObject(objects[i], objects[i - 1], difficultyObjects, clockRate, i - 1);
|
|
3656
|
-
difficultyObject.computeProperties(clockRate);
|
|
3657
|
-
difficultyObjects.push(difficultyObject);
|
|
3486
|
+
let ratingMultiplier = 1;
|
|
3487
|
+
const approachRateLengthBonus = 0.95 +
|
|
3488
|
+
0.4 * Math.min(1, this.totalHits / 2000) +
|
|
3489
|
+
(this.totalHits > 2000
|
|
3490
|
+
? Math.log10(this.totalHits / 2000) * 0.5
|
|
3491
|
+
: 0);
|
|
3492
|
+
let approachRateFactor = 0;
|
|
3493
|
+
if (this.approachRate > 10.33) {
|
|
3494
|
+
approachRateFactor = 0.3 * (this.approachRate - 10.33);
|
|
3658
3495
|
}
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
createSkills(beatmap) {
|
|
3662
|
-
const { mods } = beatmap;
|
|
3663
|
-
const skills = [];
|
|
3664
|
-
if (!mods.has(osuBase.ModAutopilot)) {
|
|
3665
|
-
skills.push(new OsuAim(mods, true));
|
|
3666
|
-
skills.push(new OsuAim(mods, false));
|
|
3496
|
+
else if (this.approachRate < 8) {
|
|
3497
|
+
approachRateFactor = 0.05 * (8 - this.approachRate);
|
|
3667
3498
|
}
|
|
3668
|
-
if (
|
|
3669
|
-
|
|
3499
|
+
if (this.mods.has(osuBase.ModRelax)) {
|
|
3500
|
+
approachRateFactor = 0;
|
|
3670
3501
|
}
|
|
3671
|
-
|
|
3672
|
-
|
|
3502
|
+
// Buff for longer beatmaps with high AR.
|
|
3503
|
+
ratingMultiplier += approachRateFactor * approachRateLengthBonus;
|
|
3504
|
+
if (this.mods.has(osuBase.ModHidden)) {
|
|
3505
|
+
const visibilityFactor = this.calculateAimVisibilityFactor();
|
|
3506
|
+
ratingMultiplier += OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.approachRate, visibilityFactor, this.sliderFactor);
|
|
3673
3507
|
}
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
return [
|
|
3679
|
-
new OsuAim(mods, true),
|
|
3680
|
-
new OsuAim(mods, false),
|
|
3681
|
-
new OsuSpeed(mods),
|
|
3682
|
-
new OsuFlashlight(mods),
|
|
3683
|
-
];
|
|
3508
|
+
// It is important to consider accuracy difficulty when scaling with accuracy.
|
|
3509
|
+
ratingMultiplier *=
|
|
3510
|
+
0.98 + Math.pow(Math.max(0, this.overallDifficulty), 2) / 2500;
|
|
3511
|
+
return aimRating * Math.cbrt(ratingMultiplier);
|
|
3684
3512
|
}
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
if (!aim || !aimNoSlider) {
|
|
3689
|
-
return;
|
|
3513
|
+
computeSpeedRating(speedDifficultyValue) {
|
|
3514
|
+
if (this.mods.has(osuBase.ModRelax)) {
|
|
3515
|
+
return 0;
|
|
3690
3516
|
}
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3517
|
+
let speedRating = OsuRatingCalculator.calculateDifficultyRating(speedDifficultyValue);
|
|
3518
|
+
if (this.mods.has(osuBase.ModAutopilot)) {
|
|
3519
|
+
speedRating *= 0.5;
|
|
3520
|
+
}
|
|
3521
|
+
if (this.mods.has(osuBase.ModMagnetised)) {
|
|
3522
|
+
// Reduce speed rating because of the distance scaling, with maximum reduction being 0.7.
|
|
3523
|
+
const magnetisedStrength = this.mods.get(osuBase.ModMagnetised).attractionStrength.value;
|
|
3524
|
+
speedRating *= 1 - magnetisedStrength * 0.3;
|
|
3525
|
+
}
|
|
3526
|
+
let ratingMultiplier = 1;
|
|
3527
|
+
const approachRateLengthBonus = 0.95 +
|
|
3528
|
+
0.4 * Math.min(1, this.totalHits / 2000) +
|
|
3529
|
+
(this.totalHits > 2000
|
|
3530
|
+
? Math.log10(this.totalHits / 2000) * 0.5
|
|
3531
|
+
: 0);
|
|
3532
|
+
let approachRateFactor = 0;
|
|
3533
|
+
if (this.approachRate > 10.33) {
|
|
3534
|
+
approachRateFactor = 0.3 * (this.approachRate - 10.33);
|
|
3697
3535
|
}
|
|
3698
|
-
|
|
3699
|
-
|
|
3536
|
+
if (this.mods.has(osuBase.ModAutopilot)) {
|
|
3537
|
+
approachRateFactor = 0;
|
|
3700
3538
|
}
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3539
|
+
// Buff for longer beatmaps with high AR.
|
|
3540
|
+
ratingMultiplier += approachRateFactor * approachRateLengthBonus;
|
|
3541
|
+
if (this.mods.has(osuBase.ModHidden)) {
|
|
3542
|
+
const visibilityFactor = this.calculateSpeedVisibilityFactor();
|
|
3543
|
+
ratingMultiplier += OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.approachRate, visibilityFactor, this.sliderFactor);
|
|
3706
3544
|
}
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3545
|
+
ratingMultiplier *=
|
|
3546
|
+
0.95 + Math.pow(Math.max(0, this.overallDifficulty), 2) / 750;
|
|
3547
|
+
return speedRating * Math.cbrt(ratingMultiplier);
|
|
3710
3548
|
}
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
return;
|
|
3549
|
+
computeFlashlightRating(flashlightDifficultyValue) {
|
|
3550
|
+
if (!this.mods.has(osuBase.ModFlashlight)) {
|
|
3551
|
+
return 0;
|
|
3715
3552
|
}
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3553
|
+
let flashlightRating = OsuRatingCalculator.calculateDifficultyRating(flashlightDifficultyValue);
|
|
3554
|
+
if (this.mods.has(osuBase.ModTouchDevice)) {
|
|
3555
|
+
flashlightRating = Math.pow(flashlightRating, 0.8);
|
|
3556
|
+
}
|
|
3557
|
+
if (this.mods.has(osuBase.ModRelax)) {
|
|
3558
|
+
flashlightRating *= 0.7;
|
|
3559
|
+
}
|
|
3560
|
+
else if (this.mods.has(osuBase.ModAutopilot)) {
|
|
3561
|
+
flashlightRating *= 0.4;
|
|
3562
|
+
}
|
|
3563
|
+
if (this.mods.has(osuBase.ModMagnetised)) {
|
|
3564
|
+
const magnetisedStrength = this.mods.get(osuBase.ModMagnetised).attractionStrength.value;
|
|
3565
|
+
flashlightRating *= 1 - magnetisedStrength;
|
|
3566
|
+
}
|
|
3567
|
+
if (this.mods.has(osuBase.ModDeflate)) {
|
|
3568
|
+
const deflateInitialScale = this.mods.get(osuBase.ModDeflate).startScale.value;
|
|
3569
|
+
flashlightRating *= osuBase.MathUtils.clamp(osuBase.Interpolation.reverseLerp(deflateInitialScale, 11, 1), 0.1, 1);
|
|
3570
|
+
}
|
|
3571
|
+
let ratingMultiplier = 1;
|
|
3572
|
+
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
3573
|
+
ratingMultiplier *=
|
|
3574
|
+
0.7 +
|
|
3575
|
+
0.1 * Math.min(1, this.totalHits / 200) +
|
|
3576
|
+
(this.totalHits > 200
|
|
3577
|
+
? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
|
|
3578
|
+
: 0);
|
|
3579
|
+
// It is important to consider accuracy difficulty when scaling with accuracy.
|
|
3580
|
+
ratingMultiplier *=
|
|
3581
|
+
0.98 + Math.pow(Math.max(0, this.overallDifficulty), 2) / 2500;
|
|
3582
|
+
return flashlightRating * Math.sqrt(ratingMultiplier);
|
|
3583
|
+
}
|
|
3584
|
+
calculateAimVisibilityFactor() {
|
|
3585
|
+
const approachRateFactorEndpoint = 11.5;
|
|
3586
|
+
const mechanicalDifficultyFactor = osuBase.Interpolation.reverseLerp(this.mechanicalDifficultyRating, 5, 10);
|
|
3587
|
+
const approachRateFactorStartingPoint = osuBase.Interpolation.lerp(9, 10.33, mechanicalDifficultyFactor);
|
|
3588
|
+
return osuBase.Interpolation.reverseLerp(this.approachRate, approachRateFactorEndpoint, approachRateFactorStartingPoint);
|
|
3589
|
+
}
|
|
3590
|
+
calculateSpeedVisibilityFactor() {
|
|
3591
|
+
const approachRateFactorEndpoint = 11.5;
|
|
3592
|
+
const mechanicalDifficultyFactor = osuBase.Interpolation.reverseLerp(this.mechanicalDifficultyRating, 5, 10);
|
|
3593
|
+
const approachRateFactorStartingPoint = osuBase.Interpolation.lerp(10, 10.33, mechanicalDifficultyFactor);
|
|
3594
|
+
return osuBase.Interpolation.reverseLerp(this.approachRate, approachRateFactorEndpoint, approachRateFactorStartingPoint);
|
|
3595
|
+
}
|
|
3596
|
+
/**
|
|
3597
|
+
* Calculates a visibility bonus that is applicable to Hidden and Traceable.
|
|
3598
|
+
*
|
|
3599
|
+
* @param mods The mods applied to the calculation.
|
|
3600
|
+
* @param approachRate The approach rate of the beatmap.
|
|
3601
|
+
* @param visibilityFactor The visibility factor to apply.
|
|
3602
|
+
* @param sliderFactor The slider factor to apply.
|
|
3603
|
+
* @returns The visibility bonus multiplier.
|
|
3604
|
+
*/
|
|
3605
|
+
static calculateVisibilityBonus(mods, approachRate, visibilityFactor = 1, sliderFactor = 1) {
|
|
3606
|
+
var _a, _b;
|
|
3607
|
+
const isAlwaysPartiallyVisible = (_b = (_a = mods.get(osuBase.ModHidden)) === null || _a === void 0 ? void 0 : _a.onlyFadeApproachCircles.value) !== null && _b !== void 0 ? _b : mods.has(osuBase.ModTraceable);
|
|
3608
|
+
// Start from normal curve, rewarding lower AR up to AR 7.
|
|
3609
|
+
// Traceable forcefully requires a lower reading bonus for now as it is post-applied in pp, which make
|
|
3610
|
+
// it multiplicative with the regular AR bonuses.
|
|
3611
|
+
// This means it has an advantage over Hidden, so we decrease the multiplier to compensate.
|
|
3612
|
+
// This should be removed once we are able to apply Traceable bonuses in star rating (requires real-time
|
|
3613
|
+
// difficulty calculations being possible).
|
|
3614
|
+
let readingBonus = (isAlwaysPartiallyVisible ? 0.025 : 0.04) *
|
|
3615
|
+
(12 - Math.max(approachRate, 7));
|
|
3616
|
+
readingBonus *= visibilityFactor;
|
|
3617
|
+
// We want to reward slideraim on low AR less.
|
|
3618
|
+
const sliderVisibilityFactor = Math.pow(sliderFactor, 3);
|
|
3619
|
+
// For AR up to 0, reduce reward for very low ARs when object is visible.
|
|
3620
|
+
if (approachRate < 7) {
|
|
3621
|
+
readingBonus +=
|
|
3622
|
+
(isAlwaysPartiallyVisible ? 0.02 : 0.045) *
|
|
3623
|
+
(7 - Math.max(approachRate, 0)) *
|
|
3624
|
+
sliderVisibilityFactor;
|
|
3625
|
+
}
|
|
3626
|
+
// Starting from AR 0, cap values so they won't grow to infinity.
|
|
3627
|
+
if (approachRate < 0) {
|
|
3628
|
+
readingBonus +=
|
|
3629
|
+
(isAlwaysPartiallyVisible ? 0.01 : 0.1) *
|
|
3630
|
+
(1 - Math.pow(1.5, approachRate)) *
|
|
3631
|
+
sliderVisibilityFactor;
|
|
3632
|
+
}
|
|
3633
|
+
return readingBonus;
|
|
3634
|
+
}
|
|
3635
|
+
static calculateDifficultyRating(difficultyValue) {
|
|
3636
|
+
return Math.sqrt(difficultyValue) * this.difficultyMultiplier;
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
OsuRatingCalculator.difficultyMultiplier = 0.0675;
|
|
3640
|
+
|
|
3641
|
+
/**
|
|
3642
|
+
* A performance points calculator that calculates performance points for osu!standard gamemode.
|
|
3643
|
+
*/
|
|
3644
|
+
class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
3724
3645
|
constructor() {
|
|
3725
3646
|
super(...arguments);
|
|
3726
3647
|
/**
|
|
@@ -3739,31 +3660,48 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3739
3660
|
* The flashlight performance value.
|
|
3740
3661
|
*/
|
|
3741
3662
|
this.flashlight = 0;
|
|
3742
|
-
this.
|
|
3743
|
-
this.mode = osuBase.Modes.osu;
|
|
3744
|
-
this.comboPenalty = 1;
|
|
3663
|
+
this._effectiveMissCount = 0;
|
|
3745
3664
|
this.speedDeviation = 0;
|
|
3746
3665
|
}
|
|
3666
|
+
/**
|
|
3667
|
+
* The amount of misses, including slider breaks.
|
|
3668
|
+
*/
|
|
3669
|
+
get effectiveMissCount() {
|
|
3670
|
+
return this._effectiveMissCount;
|
|
3671
|
+
}
|
|
3747
3672
|
calculateValues() {
|
|
3673
|
+
this._effectiveMissCount = osuBase.MathUtils.clamp(this.calculateComboBasedEstimatedMissCount(), this.computedAccuracy.nmiss, this.totalHits);
|
|
3674
|
+
let { finalMultiplier } = OsuPerformanceCalculator;
|
|
3675
|
+
if (this.mods.has(osuBase.ModNoFail)) {
|
|
3676
|
+
finalMultiplier *= Math.max(0.9, 1 - 0.02 * this._effectiveMissCount);
|
|
3677
|
+
}
|
|
3678
|
+
if (this.mods.has(osuBase.ModSpunOut)) {
|
|
3679
|
+
finalMultiplier *=
|
|
3680
|
+
1 -
|
|
3681
|
+
Math.pow(this.difficultyAttributes.spinnerCount / this.totalHits, 0.85);
|
|
3682
|
+
}
|
|
3683
|
+
if (this.mods.has(osuBase.ModRelax)) {
|
|
3684
|
+
const { overallDifficulty: od } = this.difficultyAttributes;
|
|
3685
|
+
// Graph: https://www.desmos.com/calculator/vspzsop6td
|
|
3686
|
+
// We use OD13.3 as maximum since it's the value at which great hit window becomes 0.
|
|
3687
|
+
const n100Multiplier = 0.75 * Math.max(0, od > 0 ? 1 - od / 13.33 : 1);
|
|
3688
|
+
const n50Multiplier = Math.max(0, od > 0 ? 1 - Math.pow(od / 13.33, 5) : 1);
|
|
3689
|
+
// As we're adding 100s and 50s to an approximated number of combo breaks, the result can be higher
|
|
3690
|
+
// than total hits in specific scenarios (which breaks some calculations), so we need to clamp it.
|
|
3691
|
+
this._effectiveMissCount = Math.min(this._effectiveMissCount +
|
|
3692
|
+
this.computedAccuracy.n100 * n100Multiplier +
|
|
3693
|
+
this.computedAccuracy.n50 * n50Multiplier, this.totalHits);
|
|
3694
|
+
}
|
|
3748
3695
|
this.speedDeviation = this.calculateSpeedDeviation();
|
|
3749
3696
|
this.aim = this.calculateAimValue();
|
|
3750
3697
|
this.speed = this.calculateSpeedValue();
|
|
3751
3698
|
this.accuracy = this.calculateAccuracyValue();
|
|
3752
3699
|
this.flashlight = this.calculateFlashlightValue();
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
Math.pow(this.flashlight, 1.1), 1 / 1.1) * this.finalMultiplier);
|
|
3759
|
-
}
|
|
3760
|
-
handleOptions(options) {
|
|
3761
|
-
var _a;
|
|
3762
|
-
super.handleOptions(options);
|
|
3763
|
-
const maxCombo = this.difficultyAttributes.maxCombo;
|
|
3764
|
-
const miss = this.computedAccuracy.nmiss;
|
|
3765
|
-
const combo = (_a = options === null || options === void 0 ? void 0 : options.combo) !== null && _a !== void 0 ? _a : maxCombo - miss;
|
|
3766
|
-
this.comboPenalty = Math.min(Math.pow(combo / maxCombo, 0.8), 1);
|
|
3700
|
+
this.total =
|
|
3701
|
+
Math.pow(Math.pow(this.aim, 1.1) +
|
|
3702
|
+
Math.pow(this.speed, 1.1) +
|
|
3703
|
+
Math.pow(this.accuracy, 1.1) +
|
|
3704
|
+
Math.pow(this.flashlight, 1.1), 1 / 1.1) * finalMultiplier;
|
|
3767
3705
|
}
|
|
3768
3706
|
/**
|
|
3769
3707
|
* Calculates the aim performance value of the beatmap.
|
|
@@ -3772,7 +3710,29 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3772
3710
|
if (this.mods.has(osuBase.ModAutopilot)) {
|
|
3773
3711
|
return 0;
|
|
3774
3712
|
}
|
|
3775
|
-
let
|
|
3713
|
+
let { aimDifficulty } = this.difficultyAttributes;
|
|
3714
|
+
const { aimDifficultSliderCount, sliderFactor } = this.difficultyAttributes;
|
|
3715
|
+
if (aimDifficultSliderCount > 0) {
|
|
3716
|
+
let estimateImproperlyFollowedDifficultSliders;
|
|
3717
|
+
if (this.usingClassicSliderAccuracy) {
|
|
3718
|
+
// When the score is considered classic (regardless if it was made on old client or not),
|
|
3719
|
+
// we consider all missing combo to be dropped difficult sliders.
|
|
3720
|
+
estimateImproperlyFollowedDifficultSliders = osuBase.MathUtils.clamp(Math.min(this.totalImperfectHits, this.difficultyAttributes.maxCombo - this.combo), 0, aimDifficultSliderCount);
|
|
3721
|
+
}
|
|
3722
|
+
else {
|
|
3723
|
+
// We add tick misses here since they too mean that the player didn't follow the slider
|
|
3724
|
+
// properly. However aren't adding misses here because missing slider heads has a harsh
|
|
3725
|
+
// penalty by itself and doesn't mean that the rest of the slider wasn't followed properly.
|
|
3726
|
+
estimateImproperlyFollowedDifficultSliders = osuBase.MathUtils.clamp(this.sliderEndsDropped + this.sliderTicksMissed, 0, aimDifficultSliderCount);
|
|
3727
|
+
}
|
|
3728
|
+
aimDifficulty *=
|
|
3729
|
+
(1 - sliderFactor) *
|
|
3730
|
+
Math.pow(1 -
|
|
3731
|
+
estimateImproperlyFollowedDifficultSliders /
|
|
3732
|
+
aimDifficultSliderCount, 3) +
|
|
3733
|
+
sliderFactor;
|
|
3734
|
+
}
|
|
3735
|
+
let aimValue = OsuAim.difficultyToPerformance(aimDifficulty);
|
|
3776
3736
|
// Longer maps are worth more
|
|
3777
3737
|
let lengthBonus = 0.95 + 0.4 * Math.min(1, this.totalHits / 2000);
|
|
3778
3738
|
if (this.totalHits > 2000) {
|
|
@@ -3780,38 +3740,29 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3780
3740
|
}
|
|
3781
3741
|
aimValue *= lengthBonus;
|
|
3782
3742
|
if (this.effectiveMissCount > 0) {
|
|
3783
|
-
|
|
3784
|
-
|
|
3743
|
+
const aimEstimatedSliderBreaks = this.calculateEstimatedSliderBreaks(this.difficultyAttributes.aimTopWeightedSliderFactor);
|
|
3744
|
+
const relevantMissCount = Math.min(this._effectiveMissCount + aimEstimatedSliderBreaks, this.totalImperfectHits + this.sliderTicksMissed);
|
|
3745
|
+
aimValue *= this.calculateMissPenalty(relevantMissCount, this.difficultyAttributes.aimDifficultStrainCount);
|
|
3746
|
+
}
|
|
3747
|
+
// Traceable bonuses are excluded when Blinds is present, as the increased visual difficulty is
|
|
3748
|
+
// redundant when notes cannot be seen.
|
|
3749
|
+
if (this.mods.has(osuBase.ModBlinds)) {
|
|
3785
3750
|
aimValue *=
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
// AR scaling
|
|
3794
|
-
let arFactor = 0;
|
|
3795
|
-
if (calculatedAR > 10.33) {
|
|
3796
|
-
arFactor += 0.3 * (calculatedAR - 10.33);
|
|
3797
|
-
}
|
|
3798
|
-
else if (calculatedAR < 8) {
|
|
3799
|
-
arFactor += 0.05 * (8 - calculatedAR);
|
|
3800
|
-
}
|
|
3801
|
-
// Buff for longer maps with high AR.
|
|
3802
|
-
aimValue *= 1 + arFactor * lengthBonus;
|
|
3751
|
+
1.3 +
|
|
3752
|
+
this.totalHits *
|
|
3753
|
+
(0.0016 / (1 + 2 * this._effectiveMissCount)) *
|
|
3754
|
+
Math.pow(this.computedAccuracy.value(), 16) *
|
|
3755
|
+
(1 -
|
|
3756
|
+
0.003 *
|
|
3757
|
+
Math.pow(this.difficultyAttributes.drainRate, 2));
|
|
3803
3758
|
}
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3759
|
+
else if (this.mods.has(osuBase.ModTraceable)) {
|
|
3760
|
+
aimValue *=
|
|
3761
|
+
1 +
|
|
3762
|
+
OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.difficultyAttributes.approachRate, undefined, this.difficultyAttributes.sliderFactor);
|
|
3807
3763
|
}
|
|
3808
|
-
// Scale the aim value with slider factor to nerf very likely dropped sliderends.
|
|
3809
|
-
aimValue *= this.sliderNerfFactor;
|
|
3810
3764
|
// Scale the aim value with accuracy.
|
|
3811
3765
|
aimValue *= this.computedAccuracy.value();
|
|
3812
|
-
// It is also important to consider accuracy difficulty when doing that.
|
|
3813
|
-
const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
|
|
3814
|
-
aimValue *= 0.98 + odScaling;
|
|
3815
3766
|
return aimValue;
|
|
3816
3767
|
}
|
|
3817
3768
|
/**
|
|
@@ -3822,22 +3773,28 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3822
3773
|
this.speedDeviation === Number.POSITIVE_INFINITY) {
|
|
3823
3774
|
return 0;
|
|
3824
3775
|
}
|
|
3825
|
-
let speedValue =
|
|
3776
|
+
let speedValue = OsuSpeed.difficultyToPerformance(this.difficultyAttributes.speedDifficulty);
|
|
3826
3777
|
// Longer maps are worth more
|
|
3827
3778
|
let lengthBonus = 0.95 + 0.4 * Math.min(1, this.totalHits / 2000);
|
|
3828
3779
|
if (this.totalHits > 2000) {
|
|
3829
3780
|
lengthBonus += Math.log10(this.totalHits / 2000) * 0.5;
|
|
3830
3781
|
}
|
|
3831
3782
|
speedValue *= lengthBonus;
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
if (this.mods.has(osuBase.
|
|
3840
|
-
|
|
3783
|
+
if (this._effectiveMissCount > 0) {
|
|
3784
|
+
const speedEstimatedSliderBreaks = this.calculateEstimatedSliderBreaks(this.difficultyAttributes.speedTopWeightedSliderFactor);
|
|
3785
|
+
const relevantMissCount = Math.min(this._effectiveMissCount + speedEstimatedSliderBreaks, this.totalImperfectHits + this.sliderTicksMissed);
|
|
3786
|
+
speedValue *= this.calculateMissPenalty(relevantMissCount, this.difficultyAttributes.speedDifficultStrainCount);
|
|
3787
|
+
}
|
|
3788
|
+
// Traceable bonuses are excluded when Blinds is present, as the increased visual difficulty is
|
|
3789
|
+
// redundant when notes cannot be seen.
|
|
3790
|
+
if (this.mods.has(osuBase.ModBlinds)) {
|
|
3791
|
+
// Increasing the speed value by object count for Blinds is not ideal, so the minimum buff is given.
|
|
3792
|
+
speedValue *= 1.12;
|
|
3793
|
+
}
|
|
3794
|
+
else if (this.mods.has(osuBase.ModTraceable)) {
|
|
3795
|
+
speedValue *=
|
|
3796
|
+
1 +
|
|
3797
|
+
OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.difficultyAttributes.approachRate, undefined, this.difficultyAttributes.sliderFactor);
|
|
3841
3798
|
}
|
|
3842
3799
|
// Calculate accuracy assuming the worst case scenario.
|
|
3843
3800
|
const countGreat = this.computedAccuracy.n300;
|
|
@@ -3855,13 +3812,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3855
3812
|
{ n300: 0, nobjects: 1 });
|
|
3856
3813
|
speedValue *= this.calculateSpeedHighDeviationNerf();
|
|
3857
3814
|
// Scale the speed value with accuracy and OD.
|
|
3858
|
-
speedValue *=
|
|
3859
|
-
(0.95 +
|
|
3860
|
-
Math.pow(Math.max(0, this.difficultyAttributes.overallDifficulty), 2) /
|
|
3861
|
-
750) *
|
|
3862
|
-
Math.pow((this.computedAccuracy.value() +
|
|
3863
|
-
relevantAccuracy.value(this.difficultyAttributes.speedNoteCount)) /
|
|
3864
|
-
2, (14.5 - this.difficultyAttributes.overallDifficulty) / 2);
|
|
3815
|
+
speedValue *= Math.pow((this.computedAccuracy.value() + relevantAccuracy.value()) / 2, (14.5 - this.difficultyAttributes.overallDifficulty) / 2);
|
|
3865
3816
|
return speedValue;
|
|
3866
3817
|
}
|
|
3867
3818
|
/**
|
|
@@ -3886,8 +3837,16 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3886
3837
|
2.83;
|
|
3887
3838
|
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
|
|
3888
3839
|
accuracyValue *= Math.min(1.15, Math.pow(ncircles / 1000, 0.3));
|
|
3889
|
-
|
|
3890
|
-
|
|
3840
|
+
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
|
|
3841
|
+
if (this.mods.has(osuBase.ModBlinds)) {
|
|
3842
|
+
accuracyValue *= 1.14;
|
|
3843
|
+
}
|
|
3844
|
+
else if (this.mods.has(osuBase.ModHidden) || this.mods.has(osuBase.ModTraceable)) {
|
|
3845
|
+
// Decrease bonus for AR > 10.
|
|
3846
|
+
accuracyValue *=
|
|
3847
|
+
1 +
|
|
3848
|
+
0.08 *
|
|
3849
|
+
osuBase.Interpolation.reverseLerp(this.difficultyAttributes.approachRate, 11.5, 10);
|
|
3891
3850
|
}
|
|
3892
3851
|
if (this.mods.has(osuBase.ModFlashlight)) {
|
|
3893
3852
|
accuracyValue *= 1.02;
|
|
@@ -3903,7 +3862,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3903
3862
|
}
|
|
3904
3863
|
let flashlightValue = Math.pow(this.difficultyAttributes.flashlightDifficulty, 2) * 25;
|
|
3905
3864
|
// Combo scaling
|
|
3906
|
-
flashlightValue *= this.
|
|
3865
|
+
flashlightValue *= Math.min(Math.pow(this.combo / this.difficultyAttributes.maxCombo, 0.8), 1);
|
|
3907
3866
|
if (this.effectiveMissCount > 0) {
|
|
3908
3867
|
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
3909
3868
|
flashlightValue *=
|
|
@@ -3920,11 +3879,23 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3920
3879
|
: 0);
|
|
3921
3880
|
// Scale the flashlight value with accuracy slightly.
|
|
3922
3881
|
flashlightValue *= 0.5 + this.computedAccuracy.value() / 2;
|
|
3923
|
-
// It is also important to consider accuracy difficulty when doing that.
|
|
3924
|
-
const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
|
|
3925
|
-
flashlightValue *= 0.98 + odScaling;
|
|
3926
3882
|
return flashlightValue;
|
|
3927
3883
|
}
|
|
3884
|
+
/**
|
|
3885
|
+
* Calculates a strain-based miss penalty.
|
|
3886
|
+
*
|
|
3887
|
+
* Strain-based miss penalty assumes that a player will miss on the hardest parts of a map,
|
|
3888
|
+
* so we use the amount of relatively difficult sections to adjust miss penalty
|
|
3889
|
+
* to make it more punishing on maps with lower amount of hard sections.
|
|
3890
|
+
*/
|
|
3891
|
+
calculateMissPenalty(missCount, difficultStrainCount) {
|
|
3892
|
+
if (missCount === 0) {
|
|
3893
|
+
return 1;
|
|
3894
|
+
}
|
|
3895
|
+
return (0.96 /
|
|
3896
|
+
(missCount / (4 * Math.pow(Math.log(difficultStrainCount), 0.94)) +
|
|
3897
|
+
1));
|
|
3898
|
+
}
|
|
3928
3899
|
/**
|
|
3929
3900
|
* Estimates a player's deviation on speed notes using {@link calculateDeviation}, assuming worst-case.
|
|
3930
3901
|
*
|
|
@@ -3945,7 +3916,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3945
3916
|
relevantCountMiss -
|
|
3946
3917
|
relevantCountMeh -
|
|
3947
3918
|
relevantCountOk);
|
|
3948
|
-
return this.calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh
|
|
3919
|
+
return this.calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh);
|
|
3949
3920
|
}
|
|
3950
3921
|
/**
|
|
3951
3922
|
* Estimates the player's tap deviation based on the OD, given number of greats, oks, mehs and misses,
|
|
@@ -3956,52 +3927,51 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3956
3927
|
*
|
|
3957
3928
|
* Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution.
|
|
3958
3929
|
*/
|
|
3959
|
-
calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh
|
|
3930
|
+
calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh) {
|
|
3960
3931
|
if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0) {
|
|
3961
3932
|
return Number.POSITIVE_INFINITY;
|
|
3962
3933
|
}
|
|
3963
|
-
const objectCount = relevantCountGreat +
|
|
3964
|
-
relevantCountOk +
|
|
3965
|
-
relevantCountMeh +
|
|
3966
|
-
relevantCountMiss;
|
|
3967
3934
|
// Obtain the great, ok, and meh windows.
|
|
3935
|
+
const { clockRate, overallDifficulty } = this.difficultyAttributes;
|
|
3968
3936
|
const hitWindow = new osuBase.OsuHitWindow(osuBase.OsuHitWindow.greatWindowToOD(
|
|
3969
3937
|
// Convert current OD to non clock rate-adjusted OD.
|
|
3970
|
-
new osuBase.OsuHitWindow(
|
|
3971
|
-
|
|
3972
|
-
const
|
|
3973
|
-
|
|
3974
|
-
//
|
|
3975
|
-
|
|
3976
|
-
const
|
|
3938
|
+
new osuBase.OsuHitWindow(overallDifficulty).greatWindow * clockRate));
|
|
3939
|
+
const greatWindow = hitWindow.greatWindow / clockRate;
|
|
3940
|
+
const okWindow = hitWindow.okWindow / clockRate;
|
|
3941
|
+
const mehWindow = hitWindow.mehWindow / clockRate;
|
|
3942
|
+
// The sample proportion of successful hits.
|
|
3943
|
+
const n = Math.max(1, relevantCountGreat + relevantCountOk);
|
|
3944
|
+
const p = relevantCountGreat / n;
|
|
3977
3945
|
// 99% critical value for the normal distribution (one-tailed).
|
|
3978
3946
|
const z = 2.32634787404;
|
|
3979
|
-
//
|
|
3980
|
-
const
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
//
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
3947
|
+
// We can be 99% confident that the population proportion is at least this value.
|
|
3948
|
+
const pLowerBound = Math.min(p, (n * p + Math.pow(z, 2) / 2) / (n + Math.pow(z, 2)) -
|
|
3949
|
+
(z / (n + Math.pow(z, 2))) *
|
|
3950
|
+
Math.sqrt(n * p * (1 - p) + Math.pow(z, 2) / 4));
|
|
3951
|
+
let deviation;
|
|
3952
|
+
// Tested maximum precision for the deviation calculation.
|
|
3953
|
+
if (pLowerBound > 0.01) {
|
|
3954
|
+
// Compute deviation assuming 300s and 109s are normally distributed.
|
|
3955
|
+
deviation =
|
|
3956
|
+
greatWindow / (Math.SQRT2 * osuBase.ErrorFunction.erfInv(pLowerBound));
|
|
3957
|
+
// Subtract the deviation provided by tails that land outside the 100 hit window from the deviation computed above.
|
|
3958
|
+
// This is equivalent to calculating the deviation of a normal distribution truncated at +-okHitWindow.
|
|
3959
|
+
const okWindowTailAmount = (Math.sqrt(2 / Math.PI) *
|
|
3960
|
+
okWindow *
|
|
3961
|
+
Math.exp(-0.5 * Math.pow(okWindow / deviation, 2))) /
|
|
3962
|
+
(deviation *
|
|
3963
|
+
osuBase.ErrorFunction.erf(okWindow / (Math.SQRT2 * deviation)));
|
|
3964
|
+
deviation *= Math.sqrt(1 - okWindowTailAmount);
|
|
3965
|
+
}
|
|
3966
|
+
else {
|
|
3967
|
+
// A tested limit value for the case of a score only containing 100s.
|
|
3968
|
+
deviation = okWindow / Math.sqrt(3);
|
|
3969
|
+
}
|
|
3970
|
+
// Compute and add the variance for 50s, assuming that they are uniformly distriubted.
|
|
3971
|
+
const mehVariance = (mehWindow * mehWindow +
|
|
4001
3972
|
okWindow * mehWindow +
|
|
4002
|
-
|
|
3973
|
+
okWindow * okWindow) /
|
|
4003
3974
|
3;
|
|
4004
|
-
// Find the total deviation.
|
|
4005
3975
|
deviation = Math.sqrt(((relevantCountGreat + relevantCountOk) * Math.pow(deviation, 2) +
|
|
4006
3976
|
relevantCountMeh * mehVariance) /
|
|
4007
3977
|
(relevantCountGreat + relevantCountOk + relevantCountMeh));
|
|
@@ -4016,7 +3986,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4016
3986
|
if (this.speedDeviation == Number.POSITIVE_INFINITY) {
|
|
4017
3987
|
return 0;
|
|
4018
3988
|
}
|
|
4019
|
-
const speedValue =
|
|
3989
|
+
const speedValue = OsuSpeed.difficultyToPerformance(this.difficultyAttributes.speedDifficulty);
|
|
4020
3990
|
// Decide a point where the PP value achieved compared to the speed deviation is assumed to be tapped
|
|
4021
3991
|
// improperly. Any PP above this point is considered "excess" speed difficulty. This is used to cause
|
|
4022
3992
|
// PP above the cutoff to scale logarithmically towards the original speed value thus nerfing the value.
|
|
@@ -4032,6 +4002,64 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4032
4002
|
const t = 1 - osuBase.Interpolation.reverseLerp(this.speedDeviation, 22, 27);
|
|
4033
4003
|
return (osuBase.Interpolation.lerp(adjustedSpeedValue, speedValue, t) / speedValue);
|
|
4034
4004
|
}
|
|
4005
|
+
calculateEstimatedSliderBreaks(topWeightedSliderFactor) {
|
|
4006
|
+
const { n100 } = this.computedAccuracy;
|
|
4007
|
+
if (!this.usingClassicSliderAccuracy || n100 === 0) {
|
|
4008
|
+
return 0;
|
|
4009
|
+
}
|
|
4010
|
+
const missedComboPercent = 1 - this.combo / this.difficultyAttributes.maxCombo;
|
|
4011
|
+
let estimatedSliderBreaks = Math.min(n100, this._effectiveMissCount * topWeightedSliderFactor);
|
|
4012
|
+
// Scores with more Oks are more likely to have slider breaks.
|
|
4013
|
+
const okAdjustment = (n100 - estimatedSliderBreaks + 0.5) / n100;
|
|
4014
|
+
// There is a low probability of extra slider breaks on effective miss counts close to 1, as
|
|
4015
|
+
// score based calculations are good at indicating if only a single break occurred.
|
|
4016
|
+
estimatedSliderBreaks *= osuBase.MathUtils.smoothstep(this._effectiveMissCount, 1, 2);
|
|
4017
|
+
return (estimatedSliderBreaks *
|
|
4018
|
+
okAdjustment *
|
|
4019
|
+
osuBase.MathUtils.offsetLogistic(missedComboPercent, 0.33, 15));
|
|
4020
|
+
}
|
|
4021
|
+
/**
|
|
4022
|
+
* Calculates the amount of misses + sliderbreaks from combo.
|
|
4023
|
+
*/
|
|
4024
|
+
calculateComboBasedEstimatedMissCount() {
|
|
4025
|
+
let missCount = this.computedAccuracy.nmiss;
|
|
4026
|
+
const { combo } = this;
|
|
4027
|
+
const { sliderCount, maxCombo } = this.difficultyAttributes;
|
|
4028
|
+
if (sliderCount <= 0) {
|
|
4029
|
+
return missCount;
|
|
4030
|
+
}
|
|
4031
|
+
if (this.usingClassicSliderAccuracy) {
|
|
4032
|
+
// Consider that full combo is maximum combo minus dropped slider tails since
|
|
4033
|
+
// they don't contribute to combo but also don't break it.
|
|
4034
|
+
// In classic scores, we can't know the amount of dropped sliders so we estimate
|
|
4035
|
+
// to 10% of all sliders in the beatmap.
|
|
4036
|
+
const fullComboThreshold = maxCombo - 0.1 * sliderCount;
|
|
4037
|
+
if (combo < fullComboThreshold) {
|
|
4038
|
+
missCount = fullComboThreshold / Math.max(1, combo);
|
|
4039
|
+
}
|
|
4040
|
+
// In classic scores, there can't be more misses than a sum of all non-perfect judgements.
|
|
4041
|
+
missCount = Math.min(missCount, this.totalImperfectHits);
|
|
4042
|
+
// Every slider has *at least* 2 combo attributed in classic mechanics.
|
|
4043
|
+
// If they broke on a slider with a tick, then this still works since they would have lost at least 2 combo (the tick and the end).
|
|
4044
|
+
// Using this as a max means a score that loses 1 combo on a map can't possibly have been a slider break.
|
|
4045
|
+
// It must have been a slider end.
|
|
4046
|
+
const maxPossibleSliderBreaks = Math.min(sliderCount, (maxCombo - combo) / 2);
|
|
4047
|
+
const sliderBreaks = missCount - this.computedAccuracy.nmiss;
|
|
4048
|
+
if (sliderBreaks > maxPossibleSliderBreaks) {
|
|
4049
|
+
missCount =
|
|
4050
|
+
this.computedAccuracy.nmiss + maxPossibleSliderBreaks;
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
else {
|
|
4054
|
+
const fullComboThreshold = maxCombo - this.sliderEndsDropped;
|
|
4055
|
+
if (combo < fullComboThreshold) {
|
|
4056
|
+
missCount = fullComboThreshold / Math.max(1, combo);
|
|
4057
|
+
}
|
|
4058
|
+
// Combine regular misses with tick misses, since tick misses break combo as well.
|
|
4059
|
+
missCount = Math.min(missCount, this.sliderTicksMissed + this.computedAccuracy.nmiss);
|
|
4060
|
+
}
|
|
4061
|
+
return missCount;
|
|
4062
|
+
}
|
|
4035
4063
|
toString() {
|
|
4036
4064
|
return (this.total.toFixed(2) +
|
|
4037
4065
|
" pp (" +
|
|
@@ -4040,11 +4068,275 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4040
4068
|
this.speed.toFixed(2) +
|
|
4041
4069
|
" speed, " +
|
|
4042
4070
|
this.accuracy.toFixed(2) +
|
|
4043
|
-
"
|
|
4071
|
+
" accuracy, " +
|
|
4044
4072
|
this.flashlight.toFixed(2) +
|
|
4045
4073
|
" flashlight)");
|
|
4046
4074
|
}
|
|
4047
4075
|
}
|
|
4076
|
+
OsuPerformanceCalculator.finalMultiplier = 1.14;
|
|
4077
|
+
|
|
4078
|
+
/**
|
|
4079
|
+
* An evaluator for calculating osu!standard Flashlight skill.
|
|
4080
|
+
*/
|
|
4081
|
+
class OsuFlashlightEvaluator {
|
|
4082
|
+
/**
|
|
4083
|
+
* Evaluates the difficulty of memorizing and hitting the current object, based on:
|
|
4084
|
+
*
|
|
4085
|
+
* - distance between a number of previous objects and the current object,
|
|
4086
|
+
* - the visual opacity of the current object,
|
|
4087
|
+
* - the angle made by the current object,
|
|
4088
|
+
* - length and speed of the current object (for sliders),
|
|
4089
|
+
* - and whether Hidden mod is enabled.
|
|
4090
|
+
*
|
|
4091
|
+
* @param current The current object.
|
|
4092
|
+
* @param mods The mods used.
|
|
4093
|
+
*/
|
|
4094
|
+
static evaluateDifficultyOf(current, mods) {
|
|
4095
|
+
if (current.object instanceof osuBase.Spinner) {
|
|
4096
|
+
return 0;
|
|
4097
|
+
}
|
|
4098
|
+
const scalingFactor = 52 / current.object.radius;
|
|
4099
|
+
let smallDistNerf = 1;
|
|
4100
|
+
let cumulativeStrainTime = 0;
|
|
4101
|
+
let result = 0;
|
|
4102
|
+
let last = current;
|
|
4103
|
+
let angleRepeatCount = 0;
|
|
4104
|
+
for (let i = 0; i < Math.min(current.index, 10); ++i) {
|
|
4105
|
+
const currentObject = current.previous(i);
|
|
4106
|
+
cumulativeStrainTime += last.strainTime;
|
|
4107
|
+
if (!(currentObject.object instanceof osuBase.Spinner)) {
|
|
4108
|
+
const jumpDistance = current.object.stackedPosition.subtract(currentObject.object.stackedEndPosition).length;
|
|
4109
|
+
// We want to nerf objects that can be easily seen within the Flashlight circle radius.
|
|
4110
|
+
if (i === 0) {
|
|
4111
|
+
smallDistNerf = Math.min(1, jumpDistance / 75);
|
|
4112
|
+
}
|
|
4113
|
+
// We also want to nerf stacks so that only the first object of the stack is accounted for.
|
|
4114
|
+
const stackNerf = Math.min(1, currentObject.lazyJumpDistance / scalingFactor / 25);
|
|
4115
|
+
// Bonus based on how visible the object is.
|
|
4116
|
+
const opacityBonus = 1 +
|
|
4117
|
+
this.maxOpacityBonus *
|
|
4118
|
+
(1 -
|
|
4119
|
+
current.opacityAt(currentObject.object.startTime, mods));
|
|
4120
|
+
result +=
|
|
4121
|
+
(stackNerf * opacityBonus * scalingFactor * jumpDistance) /
|
|
4122
|
+
cumulativeStrainTime;
|
|
4123
|
+
if (currentObject.angle !== null && current.angle !== null) {
|
|
4124
|
+
// Objects further back in time should count less for the nerf.
|
|
4125
|
+
if (Math.abs(currentObject.angle - current.angle) < 0.02) {
|
|
4126
|
+
angleRepeatCount += Math.max(0, 1 - 0.1 * i);
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
last = currentObject;
|
|
4131
|
+
}
|
|
4132
|
+
result = Math.pow(smallDistNerf * result, 2);
|
|
4133
|
+
// Additional bonus for Hidden due to there being no approach circles.
|
|
4134
|
+
if (mods.has(osuBase.ModHidden)) {
|
|
4135
|
+
result *= 1 + this.hiddenBonus;
|
|
4136
|
+
}
|
|
4137
|
+
// Nerf patterns with repeated angles.
|
|
4138
|
+
result *=
|
|
4139
|
+
this.minAngleMultiplier +
|
|
4140
|
+
(1 - this.minAngleMultiplier) / (angleRepeatCount + 1);
|
|
4141
|
+
let sliderBonus = 0;
|
|
4142
|
+
if (current.object instanceof osuBase.Slider) {
|
|
4143
|
+
// Invert the scaling factor to determine the true travel distance independent of circle size.
|
|
4144
|
+
const pixelTravelDistance = current.lazyTravelDistance / scalingFactor;
|
|
4145
|
+
// Reward sliders based on velocity.
|
|
4146
|
+
sliderBonus = Math.pow(Math.max(0, pixelTravelDistance / current.travelTime - this.minVelocity), 0.5);
|
|
4147
|
+
// Longer sliders require more memorization.
|
|
4148
|
+
sliderBonus *= pixelTravelDistance;
|
|
4149
|
+
// Nerf sliders with repeats, as less memorization is required.
|
|
4150
|
+
if (current.object.repeatCount > 0)
|
|
4151
|
+
sliderBonus /= current.object.repeatCount + 1;
|
|
4152
|
+
}
|
|
4153
|
+
result += sliderBonus * this.sliderMultiplier;
|
|
4154
|
+
return result;
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
OsuFlashlightEvaluator.maxOpacityBonus = 0.4;
|
|
4158
|
+
OsuFlashlightEvaluator.hiddenBonus = 0.2;
|
|
4159
|
+
OsuFlashlightEvaluator.minVelocity = 0.5;
|
|
4160
|
+
OsuFlashlightEvaluator.sliderMultiplier = 1.3;
|
|
4161
|
+
OsuFlashlightEvaluator.minAngleMultiplier = 0.2;
|
|
4162
|
+
|
|
4163
|
+
/**
|
|
4164
|
+
* Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
|
|
4165
|
+
*/
|
|
4166
|
+
class OsuFlashlight extends OsuSkill {
|
|
4167
|
+
constructor() {
|
|
4168
|
+
super(...arguments);
|
|
4169
|
+
this.strainDecayBase = 0.15;
|
|
4170
|
+
this.reducedSectionCount = 0;
|
|
4171
|
+
this.reducedSectionBaseline = 1;
|
|
4172
|
+
this.decayWeight = 1;
|
|
4173
|
+
this.currentFlashlightStrain = 0;
|
|
4174
|
+
this.skillMultiplier = 0.05512;
|
|
4175
|
+
}
|
|
4176
|
+
static difficultyToPerformance(difficulty) {
|
|
4177
|
+
return Math.pow(difficulty, 2) * 25;
|
|
4178
|
+
}
|
|
4179
|
+
difficultyValue() {
|
|
4180
|
+
return this.strainPeaks.reduce((a, b) => a + b, 0);
|
|
4181
|
+
}
|
|
4182
|
+
strainValueAt(current) {
|
|
4183
|
+
this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
|
|
4184
|
+
this.currentFlashlightStrain +=
|
|
4185
|
+
OsuFlashlightEvaluator.evaluateDifficultyOf(current, this.mods) *
|
|
4186
|
+
this.skillMultiplier;
|
|
4187
|
+
return this.currentFlashlightStrain;
|
|
4188
|
+
}
|
|
4189
|
+
calculateInitialStrain(time, current) {
|
|
4190
|
+
var _a, _b;
|
|
4191
|
+
return (this.currentFlashlightStrain *
|
|
4192
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
4193
|
+
}
|
|
4194
|
+
saveToHitObject(current) {
|
|
4195
|
+
current.flashlightStrain = this.currentFlashlightStrain;
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
/**
|
|
4200
|
+
* A difficulty calculator for osu!standard gamemode.
|
|
4201
|
+
*/
|
|
4202
|
+
class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
4203
|
+
constructor() {
|
|
4204
|
+
super();
|
|
4205
|
+
this.starRatingMultiplier = 0.0265;
|
|
4206
|
+
this.difficultyAdjustmentMods.push(osuBase.ModTouchDevice, osuBase.ModBlinds);
|
|
4207
|
+
}
|
|
4208
|
+
retainDifficultyAdjustmentMods(mods) {
|
|
4209
|
+
return mods.filter((mod) => mod.isApplicableToOsu() &&
|
|
4210
|
+
mod.isOsuRelevant &&
|
|
4211
|
+
this.difficultyAdjustmentMods.some((m) => mod instanceof m));
|
|
4212
|
+
}
|
|
4213
|
+
createDifficultyAttributes(beatmap, skills) {
|
|
4214
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
4215
|
+
const attributes = new OsuDifficultyAttributes();
|
|
4216
|
+
if (beatmap.hitObjects.objects.length === 0) {
|
|
4217
|
+
return attributes;
|
|
4218
|
+
}
|
|
4219
|
+
attributes.mods = beatmap.mods;
|
|
4220
|
+
attributes.maxCombo = beatmap.maxCombo;
|
|
4221
|
+
attributes.clockRate = beatmap.speedMultiplier;
|
|
4222
|
+
attributes.hitCircleCount = beatmap.hitObjects.circles;
|
|
4223
|
+
attributes.sliderCount = beatmap.hitObjects.sliders;
|
|
4224
|
+
attributes.spinnerCount = beatmap.hitObjects.spinners;
|
|
4225
|
+
attributes.drainRate = beatmap.difficulty.hp;
|
|
4226
|
+
attributes.approachRate =
|
|
4227
|
+
OsuDifficultyCalculator.calculateRateAdjustedApproachRate(beatmap.difficulty.ar, attributes.clockRate);
|
|
4228
|
+
attributes.overallDifficulty =
|
|
4229
|
+
OsuDifficultyCalculator.calculateRateAdjustedOverallDifficulty(beatmap.difficulty.od, attributes.clockRate);
|
|
4230
|
+
const aim = skills.find((s) => s instanceof OsuAim && s.withSliders);
|
|
4231
|
+
const aimNoSlider = skills.find((s) => s instanceof OsuAim && !s.withSliders);
|
|
4232
|
+
const speed = skills.find((s) => s instanceof OsuSpeed);
|
|
4233
|
+
const flashlight = skills.find((s) => s instanceof OsuFlashlight);
|
|
4234
|
+
// Aim attributes
|
|
4235
|
+
const aimDifficultyValue = (_a = aim === null || aim === void 0 ? void 0 : aim.difficultyValue()) !== null && _a !== void 0 ? _a : 0;
|
|
4236
|
+
attributes.aimDifficultSliderCount = (_b = aim === null || aim === void 0 ? void 0 : aim.countDifficultSliders()) !== null && _b !== void 0 ? _b : 0;
|
|
4237
|
+
attributes.aimDifficultStrainCount =
|
|
4238
|
+
(_c = aim === null || aim === void 0 ? void 0 : aim.countTopWeightedStrains()) !== null && _c !== void 0 ? _c : 0;
|
|
4239
|
+
attributes.sliderFactor =
|
|
4240
|
+
aimDifficultyValue > 0
|
|
4241
|
+
? OsuRatingCalculator.calculateDifficultyRating((_d = aimNoSlider === null || aimNoSlider === void 0 ? void 0 : aimNoSlider.difficultyValue()) !== null && _d !== void 0 ? _d : 0) /
|
|
4242
|
+
OsuRatingCalculator.calculateDifficultyRating(aimDifficultyValue)
|
|
4243
|
+
: 1;
|
|
4244
|
+
const aimNoSliderTopWeightedSliderCount = (_e = aimNoSlider === null || aimNoSlider === void 0 ? void 0 : aimNoSlider.countTopWeightedSliders()) !== null && _e !== void 0 ? _e : 0;
|
|
4245
|
+
const aimNoSliderDifficultStrainCount = (_f = aimNoSlider === null || aimNoSlider === void 0 ? void 0 : aimNoSlider.countTopWeightedStrains()) !== null && _f !== void 0 ? _f : 0;
|
|
4246
|
+
attributes.aimTopWeightedSliderFactor =
|
|
4247
|
+
aimNoSliderTopWeightedSliderCount /
|
|
4248
|
+
Math.max(1, aimNoSliderDifficultStrainCount -
|
|
4249
|
+
aimNoSliderTopWeightedSliderCount);
|
|
4250
|
+
// Speed attributes
|
|
4251
|
+
const speedDifficultyValue = (_g = speed === null || speed === void 0 ? void 0 : speed.difficultyValue()) !== null && _g !== void 0 ? _g : 0;
|
|
4252
|
+
attributes.speedNoteCount = (_h = speed === null || speed === void 0 ? void 0 : speed.relevantNoteCount()) !== null && _h !== void 0 ? _h : 0;
|
|
4253
|
+
attributes.speedDifficultStrainCount =
|
|
4254
|
+
(_j = speed === null || speed === void 0 ? void 0 : speed.countTopWeightedStrains()) !== null && _j !== void 0 ? _j : 0;
|
|
4255
|
+
const speedTopWeightedSliderCount = (_k = speed === null || speed === void 0 ? void 0 : speed.countTopWeightedSliders()) !== null && _k !== void 0 ? _k : 0;
|
|
4256
|
+
attributes.speedTopWeightedSliderFactor =
|
|
4257
|
+
speedTopWeightedSliderCount /
|
|
4258
|
+
Math.max(1, attributes.speedDifficultStrainCount -
|
|
4259
|
+
speedTopWeightedSliderCount);
|
|
4260
|
+
// Final rating
|
|
4261
|
+
const mechanicalDifficultyRating = this.calculateMechanicalDifficultyRating(aimDifficultyValue, speedDifficultyValue);
|
|
4262
|
+
const ratingCalculator = new OsuRatingCalculator(attributes.mods, beatmap.hitObjects.objects.length, attributes.approachRate, attributes.overallDifficulty, mechanicalDifficultyRating, attributes.sliderFactor);
|
|
4263
|
+
attributes.aimDifficulty =
|
|
4264
|
+
ratingCalculator.computeAimRating(aimDifficultyValue);
|
|
4265
|
+
attributes.speedDifficulty =
|
|
4266
|
+
ratingCalculator.computeSpeedRating(speedDifficultyValue);
|
|
4267
|
+
attributes.flashlightDifficulty =
|
|
4268
|
+
ratingCalculator.computeFlashlightRating((_l = flashlight === null || flashlight === void 0 ? void 0 : flashlight.difficultyValue()) !== null && _l !== void 0 ? _l : 0);
|
|
4269
|
+
const baseAimPerformance = OsuAim.difficultyToPerformance(attributes.aimDifficulty);
|
|
4270
|
+
const baseSpeedPerformance = OsuSpeed.difficultyToPerformance(attributes.speedDifficulty);
|
|
4271
|
+
const baseFlashlightPerformance = OsuFlashlight.difficultyToPerformance(attributes.flashlightDifficulty);
|
|
4272
|
+
const basePerformance = Math.pow(Math.pow(baseAimPerformance, 1.1) +
|
|
4273
|
+
Math.pow(baseSpeedPerformance, 1.1) +
|
|
4274
|
+
Math.pow(baseFlashlightPerformance, 1.1), 1 / 1.1);
|
|
4275
|
+
attributes.starRating = this.calculateStarRating(basePerformance);
|
|
4276
|
+
return attributes;
|
|
4277
|
+
}
|
|
4278
|
+
createPlayableBeatmap(beatmap, mods) {
|
|
4279
|
+
return beatmap.createOsuPlayableBeatmap(mods);
|
|
4280
|
+
}
|
|
4281
|
+
createDifficultyHitObjects(beatmap) {
|
|
4282
|
+
var _a;
|
|
4283
|
+
const clockRate = beatmap.speedMultiplier;
|
|
4284
|
+
const difficultyObjects = [];
|
|
4285
|
+
const { objects } = beatmap.hitObjects;
|
|
4286
|
+
for (let i = 1; i < objects.length; ++i) {
|
|
4287
|
+
const difficultyObject = new OsuDifficultyHitObject(objects[i], (_a = objects[i - 1]) !== null && _a !== void 0 ? _a : null, difficultyObjects, clockRate, i - 1);
|
|
4288
|
+
difficultyObject.computeProperties(clockRate);
|
|
4289
|
+
difficultyObjects.push(difficultyObject);
|
|
4290
|
+
}
|
|
4291
|
+
return difficultyObjects;
|
|
4292
|
+
}
|
|
4293
|
+
createSkills(beatmap) {
|
|
4294
|
+
const { mods } = beatmap;
|
|
4295
|
+
const skills = [];
|
|
4296
|
+
if (!mods.has(osuBase.ModAutopilot)) {
|
|
4297
|
+
skills.push(new OsuAim(mods, true));
|
|
4298
|
+
skills.push(new OsuAim(mods, false));
|
|
4299
|
+
}
|
|
4300
|
+
if (!mods.has(osuBase.ModRelax)) {
|
|
4301
|
+
skills.push(new OsuSpeed(mods));
|
|
4302
|
+
}
|
|
4303
|
+
if (mods.has(osuBase.ModFlashlight)) {
|
|
4304
|
+
skills.push(new OsuFlashlight(mods));
|
|
4305
|
+
}
|
|
4306
|
+
return skills;
|
|
4307
|
+
}
|
|
4308
|
+
createStrainPeakSkills(beatmap) {
|
|
4309
|
+
const { mods } = beatmap;
|
|
4310
|
+
return [
|
|
4311
|
+
new OsuAim(mods, true),
|
|
4312
|
+
new OsuAim(mods, false),
|
|
4313
|
+
new OsuSpeed(mods),
|
|
4314
|
+
new OsuFlashlight(mods),
|
|
4315
|
+
];
|
|
4316
|
+
}
|
|
4317
|
+
calculateMechanicalDifficultyRating(aimDifficultyValue, speedDifficultyValue) {
|
|
4318
|
+
const aimValue = OsuSkill.difficultyToPerformance(OsuRatingCalculator.calculateDifficultyRating(aimDifficultyValue));
|
|
4319
|
+
const speedValue = OsuSkill.difficultyToPerformance(OsuRatingCalculator.calculateDifficultyRating(speedDifficultyValue));
|
|
4320
|
+
const totalValue = Math.pow(Math.pow(aimValue, 1.1) + Math.pow(speedValue, 1.1), 1 / 1.1);
|
|
4321
|
+
return this.calculateStarRating(totalValue);
|
|
4322
|
+
}
|
|
4323
|
+
calculateStarRating(basePerformance) {
|
|
4324
|
+
if (basePerformance <= 1e-5) {
|
|
4325
|
+
return 0;
|
|
4326
|
+
}
|
|
4327
|
+
return (Math.cbrt(OsuPerformanceCalculator.finalMultiplier) *
|
|
4328
|
+
this.starRatingMultiplier *
|
|
4329
|
+
(Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformance) + 4));
|
|
4330
|
+
}
|
|
4331
|
+
static calculateRateAdjustedApproachRate(approachRate, clockRate) {
|
|
4332
|
+
const preempt = osuBase.BeatmapDifficulty.difficultyRange(approachRate, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin) / clockRate;
|
|
4333
|
+
return osuBase.BeatmapDifficulty.inverseDifficultyRange(preempt, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin);
|
|
4334
|
+
}
|
|
4335
|
+
static calculateRateAdjustedOverallDifficulty(overallDifficulty, clockRate) {
|
|
4336
|
+
const greatWindow = new osuBase.OsuHitWindow(overallDifficulty).greatWindow / clockRate;
|
|
4337
|
+
return osuBase.OsuHitWindow.greatWindowToOD(greatWindow);
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4048
4340
|
|
|
4049
4341
|
exports.DifficultyAttributes = DifficultyAttributes;
|
|
4050
4342
|
exports.DifficultyCalculator = DifficultyCalculator;
|