@rian8337/osu-difficulty-calculator 4.0.0-beta.82 → 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 +928 -632
- package/package.json +3 -3
- package/typings/index.d.ts +104 -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);
|
|
@@ -326,7 +307,6 @@ class DifficultyHitObject {
|
|
|
326
307
|
}
|
|
327
308
|
setDistances(clockRate) {
|
|
328
309
|
if (this.object instanceof osuBase.Slider) {
|
|
329
|
-
this.calculateSliderCursorPosition();
|
|
330
310
|
this.travelDistance = this.lazyTravelDistance;
|
|
331
311
|
// Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
|
332
312
|
if (this.mode === osuBase.Modes.droid) {
|
|
@@ -343,12 +323,8 @@ class DifficultyHitObject {
|
|
|
343
323
|
this.lastObject instanceof osuBase.Spinner) {
|
|
344
324
|
return;
|
|
345
325
|
}
|
|
346
|
-
// We will scale distances by this factor, so we can assume a uniform
|
|
347
|
-
|
|
348
|
-
// High circle size (small CS) bonus
|
|
349
|
-
if (this.mode === osuBase.Modes.osu && this.object.radius < 30) {
|
|
350
|
-
scalingFactor *= 1 + Math.min(30 - this.object.radius, 5) / 50;
|
|
351
|
-
}
|
|
326
|
+
// We will scale distances by this factor, so we can assume a uniform circle size among beatmaps.
|
|
327
|
+
const scalingFactor = DifficultyHitObject.normalizedRadius / this.object.radius;
|
|
352
328
|
const lastCursorPosition = this.lastDifficultyObject !== null
|
|
353
329
|
? this.getEndCursorPosition(this.lastDifficultyObject)
|
|
354
330
|
: this.lastObject.stackedPosition;
|
|
@@ -860,6 +836,15 @@ class StrainSkill extends Skill {
|
|
|
860
836
|
get objectStrains() {
|
|
861
837
|
return this._objectStrains;
|
|
862
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
|
+
}
|
|
863
848
|
process(current) {
|
|
864
849
|
// The first object doesn't generate a strain, so we begin with an incremented section end
|
|
865
850
|
if (current.index === 0) {
|
|
@@ -983,6 +968,9 @@ class DroidAim extends DroidSkill {
|
|
|
983
968
|
this.maxSliderStrain = 0;
|
|
984
969
|
this.withSliders = withSliders;
|
|
985
970
|
}
|
|
971
|
+
static difficultyToPerformance(difficulty) {
|
|
972
|
+
return super.difficultyToPerformance(Math.pow(difficulty, 0.8));
|
|
973
|
+
}
|
|
986
974
|
/**
|
|
987
975
|
* Obtains the amount of sliders that are considered difficult in terms of relative strain.
|
|
988
976
|
*/
|
|
@@ -1181,6 +1169,9 @@ class DroidFlashlight extends DroidSkill {
|
|
|
1181
1169
|
this.currentFlashlightStrain = 0;
|
|
1182
1170
|
this.withSliders = withSliders;
|
|
1183
1171
|
}
|
|
1172
|
+
static difficultyToPerformance(difficulty) {
|
|
1173
|
+
return Math.pow(difficulty, 1.6) * 25;
|
|
1174
|
+
}
|
|
1184
1175
|
strainValueAt(current) {
|
|
1185
1176
|
this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
|
|
1186
1177
|
this.currentFlashlightStrain +=
|
|
@@ -1445,6 +1436,15 @@ class DroidReading extends Skill {
|
|
|
1445
1436
|
this.difficulty = 0;
|
|
1446
1437
|
this.noteWeightSum = 0;
|
|
1447
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
|
+
}
|
|
1448
1448
|
process(current) {
|
|
1449
1449
|
this.currentNoteDifficulty *= this.strainDecay(current.deltaTime);
|
|
1450
1450
|
this.currentNoteDifficulty +=
|
|
@@ -1585,10 +1585,10 @@ class DroidRhythmEvaluator {
|
|
|
1585
1585
|
this.historyTimeMax) {
|
|
1586
1586
|
++rhythmStart;
|
|
1587
1587
|
}
|
|
1588
|
+
let prevObject = validPrevious[rhythmStart];
|
|
1589
|
+
let lastObject = validPrevious[rhythmStart + 1];
|
|
1588
1590
|
for (let i = rhythmStart; i > 0; --i) {
|
|
1589
1591
|
const currentObject = validPrevious[i - 1];
|
|
1590
|
-
const prevObject = validPrevious[i];
|
|
1591
|
-
const lastObject = validPrevious[i + 1];
|
|
1592
1592
|
// Scale note 0 to 1 from history to now.
|
|
1593
1593
|
const timeDecay = (this.historyTimeMax -
|
|
1594
1594
|
(current.startTime - currentObject.startTime)) /
|
|
@@ -1667,7 +1667,9 @@ class DroidRhythmEvaluator {
|
|
|
1667
1667
|
islandCounts.set(island, 1);
|
|
1668
1668
|
}
|
|
1669
1669
|
// Scale down the difficulty if the object is doubletappable.
|
|
1670
|
-
effectiveRatio *=
|
|
1670
|
+
effectiveRatio *=
|
|
1671
|
+
1 -
|
|
1672
|
+
prevObject.getDoubletapness(prevObject.next(0)) * 0.75;
|
|
1671
1673
|
rhythmComplexitySum +=
|
|
1672
1674
|
Math.sqrt(effectiveRatio * startRatio) *
|
|
1673
1675
|
currentHistoricalDecay;
|
|
@@ -1698,6 +1700,8 @@ class DroidRhythmEvaluator {
|
|
|
1698
1700
|
startRatio = effectiveRatio;
|
|
1699
1701
|
island = new Island(currentDelta, deltaDifferenceEpsilon);
|
|
1700
1702
|
}
|
|
1703
|
+
lastObject = prevObject;
|
|
1704
|
+
prevObject = currentObject;
|
|
1701
1705
|
}
|
|
1702
1706
|
return (Math.sqrt(4 + rhythmComplexitySum * this.rhythmOverallMultiplier) /
|
|
1703
1707
|
2);
|
|
@@ -1724,7 +1728,7 @@ class DroidRhythm extends DroidSkill {
|
|
|
1724
1728
|
}
|
|
1725
1729
|
strainValueAt(current) {
|
|
1726
1730
|
const rhythmMultiplier = DroidRhythmEvaluator.evaluateDifficultyOf(current, this.useSliderAccuracy);
|
|
1727
|
-
const doubletapness = 1 - current.
|
|
1731
|
+
const doubletapness = 1 - current.getDoubletapness(current.next(0));
|
|
1728
1732
|
this.currentRhythmStrain *= this.strainDecay(current.deltaTime);
|
|
1729
1733
|
this.currentRhythmStrain += (rhythmMultiplier - 1) * doubletapness;
|
|
1730
1734
|
this.currentRhythmMultiplier = rhythmMultiplier * doubletapness;
|
|
@@ -1767,7 +1771,7 @@ class DroidTapEvaluator {
|
|
|
1767
1771
|
return 0;
|
|
1768
1772
|
}
|
|
1769
1773
|
const doubletapness = considerCheesability
|
|
1770
|
-
? 1 - current.
|
|
1774
|
+
? 1 - current.getDoubletapness(current.next(0))
|
|
1771
1775
|
: 1;
|
|
1772
1776
|
const strainTime = strainTimeCap !== undefined
|
|
1773
1777
|
? // We cap the strain time to 50 here as the chance of vibro is higher in any BPM higher than 300.
|
|
@@ -1929,10 +1933,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1929
1933
|
this.populateRhythmAttributes(attributes, skills);
|
|
1930
1934
|
this.populateFlashlightAttributes(attributes, skills);
|
|
1931
1935
|
this.populateReadingAttributes(attributes, skills);
|
|
1932
|
-
const aimPerformanceValue =
|
|
1933
|
-
const tapPerformanceValue =
|
|
1934
|
-
const flashlightPerformanceValue =
|
|
1935
|
-
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);
|
|
1936
1940
|
const basePerformanceValue = Math.pow(Math.pow(aimPerformanceValue, 1.1) +
|
|
1937
1941
|
Math.pow(tapPerformanceValue, 1.1) +
|
|
1938
1942
|
Math.pow(flashlightPerformanceValue, 1.1) +
|
|
@@ -2162,6 +2166,15 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
2162
2166
|
Math.pow(Math.max(0, attributes.overallDifficulty), 2.2) / 800;
|
|
2163
2167
|
attributes.readingDifficulty *= Math.sqrt(ratingMultiplier);
|
|
2164
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
|
+
}
|
|
2165
2178
|
}
|
|
2166
2179
|
/**
|
|
2167
2180
|
* The strain threshold to start detecting for possible three-fingered section.
|
|
@@ -2174,6 +2187,20 @@ DroidDifficultyCalculator.threeFingerStrainThreshold = 175;
|
|
|
2174
2187
|
* The base class of performance calculators.
|
|
2175
2188
|
*/
|
|
2176
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
|
+
}
|
|
2177
2204
|
/**
|
|
2178
2205
|
* Whether this score uses classic slider accuracy.
|
|
2179
2206
|
*/
|
|
@@ -2193,24 +2220,12 @@ class PerformanceCalculator {
|
|
|
2193
2220
|
*/
|
|
2194
2221
|
this.computedAccuracy = new osuBase.Accuracy({});
|
|
2195
2222
|
/**
|
|
2196
|
-
* The
|
|
2197
|
-
*/
|
|
2198
|
-
this.effectiveMissCount = 0;
|
|
2199
|
-
/**
|
|
2200
|
-
* The amount of slider ends dropped in the score.
|
|
2201
|
-
*/
|
|
2202
|
-
this.sliderEndsDropped = 0;
|
|
2203
|
-
/**
|
|
2204
|
-
* The amount of slider ticks missed in the score.
|
|
2205
|
-
*
|
|
2206
|
-
* This is used to calculate the slider accuracy.
|
|
2223
|
+
* The calculated maximum combo.
|
|
2207
2224
|
*/
|
|
2208
|
-
this.
|
|
2225
|
+
this.combo = 0;
|
|
2226
|
+
this._sliderEndsDropped = 0;
|
|
2227
|
+
this._sliderTicksMissed = 0;
|
|
2209
2228
|
this._usingClassicSliderAccuracy = false;
|
|
2210
|
-
/**
|
|
2211
|
-
* Nerf factor used for nerfing beatmaps with very likely dropped sliderends.
|
|
2212
|
-
*/
|
|
2213
|
-
this.sliderNerfFactor = 1;
|
|
2214
2229
|
this.difficultyAttributes = difficultyAttributes;
|
|
2215
2230
|
this.mods = this.isCacheableAttribute(difficultyAttributes)
|
|
2216
2231
|
? osuBase.ModUtil.deserializeMods(difficultyAttributes.mods)
|
|
@@ -2225,7 +2240,6 @@ class PerformanceCalculator {
|
|
|
2225
2240
|
calculate(options) {
|
|
2226
2241
|
this.handleOptions(options);
|
|
2227
2242
|
this.calculateValues();
|
|
2228
|
-
this.total = this.calculateTotalValue();
|
|
2229
2243
|
return this;
|
|
2230
2244
|
}
|
|
2231
2245
|
/**
|
|
@@ -2252,12 +2266,6 @@ class PerformanceCalculator {
|
|
|
2252
2266
|
this.computedAccuracy.n50 +
|
|
2253
2267
|
this.computedAccuracy.nmiss);
|
|
2254
2268
|
}
|
|
2255
|
-
/**
|
|
2256
|
-
* Calculates the base performance value of a star rating.
|
|
2257
|
-
*/
|
|
2258
|
-
baseValue(stars) {
|
|
2259
|
-
return Math.pow(5 * Math.max(1, stars / 0.0675) - 4, 3) / 100000;
|
|
2260
|
-
}
|
|
2261
2269
|
/**
|
|
2262
2270
|
* Processes given options for usage in performance calculation.
|
|
2263
2271
|
*
|
|
@@ -2287,115 +2295,20 @@ class PerformanceCalculator {
|
|
|
2287
2295
|
}
|
|
2288
2296
|
const maxCombo = this.difficultyAttributes.maxCombo;
|
|
2289
2297
|
const miss = this.computedAccuracy.nmiss;
|
|
2290
|
-
|
|
2298
|
+
this.combo = (_b = options === null || options === void 0 ? void 0 : options.combo) !== null && _b !== void 0 ? _b : maxCombo - miss;
|
|
2291
2299
|
if ((options === null || options === void 0 ? void 0 : options.sliderEndsDropped) !== undefined &&
|
|
2292
2300
|
(options === null || options === void 0 ? void 0 : options.sliderTicksMissed) !== undefined) {
|
|
2293
2301
|
this._usingClassicSliderAccuracy = false;
|
|
2294
|
-
this.
|
|
2295
|
-
this.
|
|
2302
|
+
this._sliderEndsDropped = options.sliderEndsDropped;
|
|
2303
|
+
this._sliderTicksMissed = options.sliderTicksMissed;
|
|
2296
2304
|
}
|
|
2297
2305
|
else {
|
|
2298
2306
|
this._usingClassicSliderAccuracy = true;
|
|
2299
|
-
this.
|
|
2300
|
-
this.
|
|
2307
|
+
this._sliderEndsDropped = 0;
|
|
2308
|
+
this._sliderTicksMissed = 0;
|
|
2301
2309
|
}
|
|
2302
2310
|
// Ensure that combo is within possible bounds.
|
|
2303
|
-
combo = osuBase.MathUtils.clamp(combo, 0, maxCombo - miss - this.
|
|
2304
|
-
this.effectiveMissCount = this.calculateEffectiveMissCount(combo, maxCombo);
|
|
2305
|
-
if (this.mods.has(osuBase.ModNoFail)) {
|
|
2306
|
-
this.finalMultiplier *= Math.max(0.9, 1 - 0.02 * this.effectiveMissCount);
|
|
2307
|
-
}
|
|
2308
|
-
if (this.mods.has(osuBase.ModSpunOut)) {
|
|
2309
|
-
this.finalMultiplier *=
|
|
2310
|
-
1 -
|
|
2311
|
-
Math.pow(this.difficultyAttributes.spinnerCount / this.totalHits, 0.85);
|
|
2312
|
-
}
|
|
2313
|
-
if (this.mods.has(osuBase.ModRelax)) {
|
|
2314
|
-
// Graph: https://www.desmos.com/calculator/bc9eybdthb
|
|
2315
|
-
// We use OD13.3 as maximum since it's the value at which great hit window becomes 0.
|
|
2316
|
-
const n100Multiplier = Math.max(0, this.difficultyAttributes.overallDifficulty > 0
|
|
2317
|
-
? 1 -
|
|
2318
|
-
Math.pow(this.difficultyAttributes.overallDifficulty /
|
|
2319
|
-
13.33, 1.8)
|
|
2320
|
-
: 1);
|
|
2321
|
-
const n50Multiplier = Math.max(0, this.difficultyAttributes.overallDifficulty > 0
|
|
2322
|
-
? 1 -
|
|
2323
|
-
Math.pow(this.difficultyAttributes.overallDifficulty /
|
|
2324
|
-
13.33, 5)
|
|
2325
|
-
: 1);
|
|
2326
|
-
// As we're adding 100s and 50s to an approximated number of combo breaks, the result can be higher
|
|
2327
|
-
// than total hits in specific scenarios (which breaks some calculations), so we need to clamp it.
|
|
2328
|
-
this.effectiveMissCount = Math.min(this.effectiveMissCount +
|
|
2329
|
-
this.computedAccuracy.n100 * n100Multiplier +
|
|
2330
|
-
this.computedAccuracy.n50 * n50Multiplier, this.totalHits);
|
|
2331
|
-
}
|
|
2332
|
-
const { aimDifficultSliderCount, sliderFactor } = this.difficultyAttributes;
|
|
2333
|
-
if (aimDifficultSliderCount > 0) {
|
|
2334
|
-
let estimateImproperlyFollowedDifficultSliders;
|
|
2335
|
-
if (this.usingClassicSliderAccuracy) {
|
|
2336
|
-
// When the score is considered classic (regardless if it was made on old client or not),
|
|
2337
|
-
// we consider all missing combo to be dropped difficult sliders.
|
|
2338
|
-
estimateImproperlyFollowedDifficultSliders = osuBase.MathUtils.clamp(Math.min(this.totalImperfectHits, maxCombo - combo), 0, aimDifficultSliderCount);
|
|
2339
|
-
}
|
|
2340
|
-
else {
|
|
2341
|
-
// We add tick misses here since they too mean that the player didn't follow the slider
|
|
2342
|
-
// properly. However aren't adding misses here because missing slider heads has a harsh
|
|
2343
|
-
// penalty by itself and doesn't mean that the rest of the slider wasn't followed properly.
|
|
2344
|
-
estimateImproperlyFollowedDifficultSliders = osuBase.MathUtils.clamp(this.sliderEndsDropped + this.sliderTicksMissed, 0, aimDifficultSliderCount);
|
|
2345
|
-
}
|
|
2346
|
-
this.sliderNerfFactor =
|
|
2347
|
-
(1 - sliderFactor) *
|
|
2348
|
-
Math.pow(1 -
|
|
2349
|
-
estimateImproperlyFollowedDifficultSliders /
|
|
2350
|
-
aimDifficultSliderCount, 3) +
|
|
2351
|
-
sliderFactor;
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
/**
|
|
2355
|
-
* Calculates a strain-based miss penalty.
|
|
2356
|
-
*
|
|
2357
|
-
* Strain-based miss penalty assumes that a player will miss on the hardest parts of a map,
|
|
2358
|
-
* so we use the amount of relatively difficult sections to adjust miss penalty
|
|
2359
|
-
* to make it more punishing on maps with lower amount of hard sections.
|
|
2360
|
-
*/
|
|
2361
|
-
calculateStrainBasedMissPenalty(difficultStrainCount) {
|
|
2362
|
-
if (this.effectiveMissCount === 0) {
|
|
2363
|
-
return 1;
|
|
2364
|
-
}
|
|
2365
|
-
return (0.96 /
|
|
2366
|
-
(this.effectiveMissCount /
|
|
2367
|
-
(4 * Math.pow(Math.log(difficultStrainCount), 0.94)) +
|
|
2368
|
-
1));
|
|
2369
|
-
}
|
|
2370
|
-
/**
|
|
2371
|
-
* Calculates the amount of misses + sliderbreaks from combo.
|
|
2372
|
-
*/
|
|
2373
|
-
calculateEffectiveMissCount(combo, maxCombo) {
|
|
2374
|
-
let missCount = this.computedAccuracy.nmiss;
|
|
2375
|
-
const { sliderCount } = this.difficultyAttributes;
|
|
2376
|
-
if (sliderCount > 0) {
|
|
2377
|
-
if (this.usingClassicSliderAccuracy || this.mode === osuBase.Modes.droid) {
|
|
2378
|
-
// Consider that full combo is maximum combo minus dropped slider tails since
|
|
2379
|
-
// they don't contribute to combo but also don't break it.
|
|
2380
|
-
// In classic scores, we can't know the amount of dropped sliders so we estimate
|
|
2381
|
-
// to 10% of all sliders in the beatmap.
|
|
2382
|
-
const fullComboThreshold = maxCombo - 0.1 * sliderCount;
|
|
2383
|
-
if (combo < fullComboThreshold) {
|
|
2384
|
-
missCount = fullComboThreshold / Math.max(1, combo);
|
|
2385
|
-
}
|
|
2386
|
-
// In classic scores, there can't be more misses than a sum of all non-perfect judgements.
|
|
2387
|
-
missCount = Math.min(missCount, this.totalImperfectHits);
|
|
2388
|
-
}
|
|
2389
|
-
else {
|
|
2390
|
-
const fullComboThreshold = maxCombo - this.sliderEndsDropped;
|
|
2391
|
-
if (combo < fullComboThreshold) {
|
|
2392
|
-
missCount = fullComboThreshold / Math.max(1, combo);
|
|
2393
|
-
}
|
|
2394
|
-
// Combine regular misses with tick misses, since tick misses break combo as well.
|
|
2395
|
-
missCount = Math.min(missCount, this.sliderTicksMissed + this.computedAccuracy.nmiss);
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
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);
|
|
2399
2312
|
}
|
|
2400
2313
|
/**
|
|
2401
2314
|
* Determines whether an attribute is a cacheable attribute.
|
|
@@ -2434,11 +2347,10 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2434
2347
|
* The reading performance value.
|
|
2435
2348
|
*/
|
|
2436
2349
|
this.reading = 0;
|
|
2437
|
-
this.finalMultiplier = 1.24;
|
|
2438
|
-
this.mode = osuBase.Modes.droid;
|
|
2439
2350
|
this._aimSliderCheesePenalty = 1;
|
|
2440
2351
|
this._flashlightSliderCheesePenalty = 1;
|
|
2441
2352
|
this._tapPenalty = 1;
|
|
2353
|
+
this._effectiveMissCount = 0;
|
|
2442
2354
|
this._deviation = 0;
|
|
2443
2355
|
this._tapDeviation = 0;
|
|
2444
2356
|
}
|
|
@@ -2479,66 +2391,54 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2479
2391
|
return this._flashlightSliderCheesePenalty;
|
|
2480
2392
|
}
|
|
2481
2393
|
/**
|
|
2482
|
-
*
|
|
2483
|
-
*
|
|
2484
|
-
* The tap and total performance value will be recalculated afterwards.
|
|
2485
|
-
*
|
|
2486
|
-
* @param value The tap penalty value. Must be greater than or equal to 1.
|
|
2487
|
-
*/
|
|
2488
|
-
applyTapPenalty(value) {
|
|
2489
|
-
if (value < 1) {
|
|
2490
|
-
throw new RangeError("New tap penalty must be greater than or equal to one.");
|
|
2491
|
-
}
|
|
2492
|
-
if (value === this._tapPenalty) {
|
|
2493
|
-
return;
|
|
2494
|
-
}
|
|
2495
|
-
this._tapPenalty = value;
|
|
2496
|
-
this.tap = this.calculateTapValue();
|
|
2497
|
-
this.total = this.calculateTotalValue();
|
|
2498
|
-
}
|
|
2499
|
-
/**
|
|
2500
|
-
* Applies an aim slider cheese penalty value to this calculator.
|
|
2501
|
-
*
|
|
2502
|
-
* The aim and total performance value will be recalculated afterwards.
|
|
2503
|
-
*
|
|
2504
|
-
* @param value The slider cheese penalty value. Must be between than 0 and 1.
|
|
2394
|
+
* The amount of misses, including slider breaks.
|
|
2505
2395
|
*/
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
throw new RangeError("New aim slider cheese penalty must be greater than or equal to zero.");
|
|
2509
|
-
}
|
|
2510
|
-
if (value > 1) {
|
|
2511
|
-
throw new RangeError("New aim slider cheese penalty must be less than or equal to one.");
|
|
2512
|
-
}
|
|
2513
|
-
if (value === this._aimSliderCheesePenalty) {
|
|
2514
|
-
return;
|
|
2515
|
-
}
|
|
2516
|
-
this._aimSliderCheesePenalty = value;
|
|
2517
|
-
this.aim = this.calculateAimValue();
|
|
2518
|
-
this.total = this.calculateTotalValue();
|
|
2396
|
+
get effectiveMissCount() {
|
|
2397
|
+
return this._effectiveMissCount;
|
|
2519
2398
|
}
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
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
|
+
}
|
|
2530
2424
|
}
|
|
2531
|
-
|
|
2532
|
-
|
|
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);
|
|
2533
2429
|
}
|
|
2534
|
-
if (
|
|
2535
|
-
|
|
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);
|
|
2536
2441
|
}
|
|
2537
|
-
this._flashlightSliderCheesePenalty = value;
|
|
2538
|
-
this.flashlight = this.calculateFlashlightValue();
|
|
2539
|
-
this.total = this.calculateTotalValue();
|
|
2540
|
-
}
|
|
2541
|
-
calculateValues() {
|
|
2542
2442
|
this._deviation = this.calculateDeviation();
|
|
2543
2443
|
this._tapDeviation = this.calculateTapDeviation();
|
|
2544
2444
|
this.aim = this.calculateAimValue();
|
|
@@ -2546,13 +2446,12 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2546
2446
|
this.accuracy = this.calculateAccuracyValue();
|
|
2547
2447
|
this.flashlight = this.calculateFlashlightValue();
|
|
2548
2448
|
this.reading = this.calculateReadingValue();
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
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;
|
|
2556
2455
|
}
|
|
2557
2456
|
handleOptions(options) {
|
|
2558
2457
|
var _a, _b, _c;
|
|
@@ -2566,12 +2465,31 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2566
2465
|
* Calculates the aim performance value of the beatmap.
|
|
2567
2466
|
*/
|
|
2568
2467
|
calculateAimValue() {
|
|
2569
|
-
let aimValue =
|
|
2468
|
+
let aimValue = DroidAim.difficultyToPerformance(this.difficultyAttributes.aimDifficulty);
|
|
2570
2469
|
aimValue *= Math.min(this.calculateStrainBasedMissPenalty(this.difficultyAttributes.aimDifficultStrainCount), this.proportionalMissPenalty);
|
|
2571
2470
|
// Scale the aim value with estimated full combo deviation.
|
|
2572
2471
|
aimValue *= this.calculateDeviationBasedLengthScaling();
|
|
2573
|
-
|
|
2574
|
-
|
|
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
|
+
}
|
|
2575
2493
|
// Scale the aim value with slider cheese penalty.
|
|
2576
2494
|
aimValue *= this._aimSliderCheesePenalty;
|
|
2577
2495
|
// Scale the aim value with deviation.
|
|
@@ -2586,7 +2504,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2586
2504
|
* Calculates the tap performance value of the beatmap.
|
|
2587
2505
|
*/
|
|
2588
2506
|
calculateTapValue() {
|
|
2589
|
-
let tapValue =
|
|
2507
|
+
let tapValue = DroidTap.difficultyToPerformance(this.difficultyAttributes.tapDifficulty);
|
|
2590
2508
|
tapValue *= this.calculateStrainBasedMissPenalty(this.difficultyAttributes.tapDifficultStrainCount);
|
|
2591
2509
|
// Scale the tap value with estimated full combo deviation.
|
|
2592
2510
|
// Consider notes that are difficult to tap with respect to other notes, but
|
|
@@ -2644,7 +2562,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2644
2562
|
(1 +
|
|
2645
2563
|
Math.exp(-(this.difficultyAttributes.rhythmDifficulty - 1) / 2));
|
|
2646
2564
|
// Penalize accuracy pp after the first miss.
|
|
2647
|
-
accuracyValue *= Math.pow(0.97, Math.max(0, this.
|
|
2565
|
+
accuracyValue *= Math.pow(0.97, Math.max(0, this._effectiveMissCount - 1));
|
|
2648
2566
|
if (this.mods.has(osuBase.ModFlashlight)) {
|
|
2649
2567
|
accuracyValue *= 1.02;
|
|
2650
2568
|
}
|
|
@@ -2688,14 +2606,30 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2688
2606
|
readingValue *= 0.98 + Math.pow(5, 2) / 2500;
|
|
2689
2607
|
return readingValue;
|
|
2690
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
|
+
}
|
|
2691
2625
|
/**
|
|
2692
2626
|
* The object-based proportional miss penalty.
|
|
2693
2627
|
*/
|
|
2694
2628
|
get proportionalMissPenalty() {
|
|
2695
|
-
if (this.
|
|
2629
|
+
if (this._effectiveMissCount === 0) {
|
|
2696
2630
|
return 1;
|
|
2697
2631
|
}
|
|
2698
|
-
const missProportion = (this.totalHits - this.
|
|
2632
|
+
const missProportion = (this.totalHits - this._effectiveMissCount) / (this.totalHits + 1);
|
|
2699
2633
|
const noMissProportion = this.totalHits / (this.totalHits + 1);
|
|
2700
2634
|
return (
|
|
2701
2635
|
// Aim deviation-based scale.
|
|
@@ -2892,7 +2826,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2892
2826
|
if (this.tapDeviation == Number.POSITIVE_INFINITY) {
|
|
2893
2827
|
return 0;
|
|
2894
2828
|
}
|
|
2895
|
-
const tapValue =
|
|
2829
|
+
const tapValue = DroidTap.difficultyToPerformance(this.difficultyAttributes.tapDifficulty);
|
|
2896
2830
|
// Decide a point where the PP value achieved compared to the tap deviation is assumed to be tapped
|
|
2897
2831
|
// improperly. Any PP above this point is considered "excess" tap difficulty. This is used to cause
|
|
2898
2832
|
// PP above the cutoff to scale logarithmically towards the original tap value thus nerfing the value.
|
|
@@ -2925,13 +2859,14 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2925
2859
|
this.tap.toFixed(2) +
|
|
2926
2860
|
" tap, " +
|
|
2927
2861
|
this.accuracy.toFixed(2) +
|
|
2928
|
-
"
|
|
2862
|
+
" accuracy, " +
|
|
2929
2863
|
this.flashlight.toFixed(2) +
|
|
2930
2864
|
" flashlight, " +
|
|
2931
2865
|
this.reading.toFixed(2) +
|
|
2932
2866
|
" reading)");
|
|
2933
2867
|
}
|
|
2934
2868
|
}
|
|
2869
|
+
DroidPerformanceCalculator.finalMultiplier = 1.24;
|
|
2935
2870
|
|
|
2936
2871
|
/**
|
|
2937
2872
|
* Represents an osu!standard hit object with difficulty calculation values.
|
|
@@ -2977,6 +2912,7 @@ class OsuAimEvaluator {
|
|
|
2977
2912
|
return 0;
|
|
2978
2913
|
}
|
|
2979
2914
|
const lastLast = current.previous(1);
|
|
2915
|
+
const last2 = current.previous(2);
|
|
2980
2916
|
const radius = OsuDifficultyHitObject.normalizedRadius;
|
|
2981
2917
|
const diameter = OsuDifficultyHitObject.normalizedDiameter;
|
|
2982
2918
|
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
|
|
@@ -3004,36 +2940,37 @@ class OsuAimEvaluator {
|
|
|
3004
2940
|
let wiggleBonus = 0;
|
|
3005
2941
|
// Start strain with regular velocity.
|
|
3006
2942
|
let strain = currentVelocity;
|
|
3007
|
-
if (
|
|
3008
|
-
// If rhythms are the same.
|
|
3009
|
-
Math.max(current.strainTime, last.strainTime) <
|
|
3010
|
-
1.25 * Math.min(current.strainTime, last.strainTime) &&
|
|
3011
|
-
current.angle !== null &&
|
|
3012
|
-
last.angle !== null) {
|
|
2943
|
+
if (current.angle !== null && last.angle !== null) {
|
|
3013
2944
|
const currentAngle = current.angle;
|
|
3014
2945
|
const lastAngle = last.angle;
|
|
3015
2946
|
// Rewarding angles, take the smaller velocity as base.
|
|
3016
2947
|
const angleBonus = Math.min(currentVelocity, prevVelocity);
|
|
3017
|
-
|
|
3018
|
-
|
|
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);
|
|
3019
2966
|
// Penalize angle repetition.
|
|
3020
2967
|
wideAngleBonus *=
|
|
3021
2968
|
1 -
|
|
3022
2969
|
Math.min(wideAngleBonus, Math.pow(this.calculateWideAngleBonus(lastAngle), 3));
|
|
3023
|
-
acuteAngleBonus *=
|
|
3024
|
-
0.08 +
|
|
3025
|
-
0.92 *
|
|
3026
|
-
(1 -
|
|
3027
|
-
Math.min(acuteAngleBonus, Math.pow(this.calculateAcuteAngleBonus(lastAngle), 3)));
|
|
3028
2970
|
// Apply full wide angle bonus for distance more than one diameter
|
|
3029
2971
|
wideAngleBonus *=
|
|
3030
2972
|
angleBonus *
|
|
3031
2973
|
osuBase.MathUtils.smootherstep(current.lazyJumpDistance, 0, diameter);
|
|
3032
|
-
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
|
3033
|
-
acuteAngleBonus *=
|
|
3034
|
-
angleBonus *
|
|
3035
|
-
osuBase.MathUtils.smootherstep(osuBase.MathUtils.millisecondsToBPM(current.strainTime, 2), 300, 400) *
|
|
3036
|
-
osuBase.MathUtils.smootherstep(current.lazyJumpDistance, diameter, diameter * 2);
|
|
3037
2974
|
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
|
3038
2975
|
// https://www.desmos.com/calculator/dp0v0nvowc
|
|
3039
2976
|
wiggleBonus =
|
|
@@ -3044,6 +2981,15 @@ class OsuAimEvaluator {
|
|
|
3044
2981
|
osuBase.MathUtils.smootherstep(last.lazyJumpDistance, radius, diameter) *
|
|
3045
2982
|
Math.pow(osuBase.MathUtils.reverseLerp(last.lazyJumpDistance, diameter * 3, diameter), 1.8) *
|
|
3046
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
|
+
}
|
|
3047
2993
|
}
|
|
3048
2994
|
if (Math.max(prevVelocity, currentVelocity)) {
|
|
3049
2995
|
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
|
|
@@ -3054,10 +3000,11 @@ class OsuAimEvaluator {
|
|
|
3054
3000
|
(current.lazyJumpDistance + last.travelDistance) /
|
|
3055
3001
|
current.strainTime;
|
|
3056
3002
|
// Scale with ratio of difference compared to half the max distance.
|
|
3057
|
-
const distanceRatio =
|
|
3058
|
-
Math.max(prevVelocity, currentVelocity)
|
|
3003
|
+
const distanceRatio = osuBase.MathUtils.smoothstep(Math.abs(prevVelocity - currentVelocity) /
|
|
3004
|
+
Math.max(prevVelocity, currentVelocity), 0, 1);
|
|
3059
3005
|
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
|
3060
|
-
const overlapVelocityBuff = Math.min(
|
|
3006
|
+
const overlapVelocityBuff = Math.min((diameter * 1.25) /
|
|
3007
|
+
Math.min(current.strainTime, last.strainTime), Math.abs(prevVelocity - currentVelocity));
|
|
3061
3008
|
velocityChangeBonus = overlapVelocityBuff * distanceRatio;
|
|
3062
3009
|
// Penalize for rhythm changes.
|
|
3063
3010
|
velocityChangeBonus *= Math.pow(Math.min(current.strainTime, last.strainTime) /
|
|
@@ -3068,9 +3015,11 @@ class OsuAimEvaluator {
|
|
|
3068
3015
|
sliderBonus = last.travelDistance / last.travelTime;
|
|
3069
3016
|
}
|
|
3070
3017
|
strain += wiggleBonus * this.wiggleMultiplier;
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
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;
|
|
3074
3023
|
// Add in additional slider velocity bonus.
|
|
3075
3024
|
if (withSliders) {
|
|
3076
3025
|
strain += sliderBonus * this.sliderMultiplier;
|
|
@@ -3085,7 +3034,7 @@ class OsuAimEvaluator {
|
|
|
3085
3034
|
}
|
|
3086
3035
|
}
|
|
3087
3036
|
OsuAimEvaluator.wideAngleMultiplier = 1.5;
|
|
3088
|
-
OsuAimEvaluator.acuteAngleMultiplier = 2.
|
|
3037
|
+
OsuAimEvaluator.acuteAngleMultiplier = 2.55;
|
|
3089
3038
|
OsuAimEvaluator.sliderMultiplier = 1.35;
|
|
3090
3039
|
OsuAimEvaluator.velocityChangeMultiplier = 0.75;
|
|
3091
3040
|
OsuAimEvaluator.wiggleMultiplier = 1.02;
|
|
@@ -3121,6 +3070,21 @@ class OsuSkill extends StrainSkill {
|
|
|
3121
3070
|
}
|
|
3122
3071
|
}
|
|
3123
3072
|
|
|
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
|
+
|
|
3124
3088
|
/**
|
|
3125
3089
|
* Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
|
3126
3090
|
*/
|
|
@@ -3132,7 +3096,7 @@ class OsuAim extends OsuSkill {
|
|
|
3132
3096
|
this.reducedSectionBaseline = 0.75;
|
|
3133
3097
|
this.decayWeight = 0.9;
|
|
3134
3098
|
this.currentAimStrain = 0;
|
|
3135
|
-
this.skillMultiplier =
|
|
3099
|
+
this.skillMultiplier = 26;
|
|
3136
3100
|
this.sliderStrains = [];
|
|
3137
3101
|
this.withSliders = withSliders;
|
|
3138
3102
|
}
|
|
@@ -3150,6 +3114,12 @@ class OsuAim extends OsuSkill {
|
|
|
3150
3114
|
return this.sliderStrains.reduce((total, strain) => total +
|
|
3151
3115
|
1 / (1 + Math.exp(-((strain / maxSliderStrain) * 12 - 6))), 0);
|
|
3152
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
|
+
}
|
|
3153
3123
|
strainValueAt(current) {
|
|
3154
3124
|
this.currentAimStrain *= this.strainDecay(current.deltaTime);
|
|
3155
3125
|
this.currentAimStrain +=
|
|
@@ -3186,15 +3156,23 @@ class OsuDifficultyAttributes extends DifficultyAttributes {
|
|
|
3186
3156
|
constructor(cacheableAttributes) {
|
|
3187
3157
|
super(cacheableAttributes);
|
|
3188
3158
|
this.approachRate = 0;
|
|
3159
|
+
this.drainRate = 0;
|
|
3189
3160
|
this.speedDifficulty = 0;
|
|
3190
3161
|
this.speedDifficultStrainCount = 0;
|
|
3162
|
+
this.aimTopWeightedSliderFactor = 0;
|
|
3163
|
+
this.speedTopWeightedSliderFactor = 0;
|
|
3191
3164
|
if (!cacheableAttributes) {
|
|
3192
3165
|
return;
|
|
3193
3166
|
}
|
|
3194
3167
|
this.approachRate = cacheableAttributes.approachRate;
|
|
3168
|
+
this.drainRate = cacheableAttributes.drainRate;
|
|
3195
3169
|
this.speedDifficulty = cacheableAttributes.speedDifficulty;
|
|
3196
3170
|
this.speedDifficultStrainCount =
|
|
3197
3171
|
cacheableAttributes.speedDifficultStrainCount;
|
|
3172
|
+
this.aimTopWeightedSliderFactor =
|
|
3173
|
+
cacheableAttributes.aimTopWeightedSliderFactor;
|
|
3174
|
+
this.speedTopWeightedSliderFactor =
|
|
3175
|
+
cacheableAttributes.speedTopWeightedSliderFactor;
|
|
3198
3176
|
}
|
|
3199
3177
|
toString() {
|
|
3200
3178
|
return (super.toString() +
|
|
@@ -3205,127 +3183,9 @@ class OsuDifficultyAttributes extends DifficultyAttributes {
|
|
|
3205
3183
|
}
|
|
3206
3184
|
|
|
3207
3185
|
/**
|
|
3208
|
-
* An evaluator for calculating osu!standard
|
|
3186
|
+
* An evaluator for calculating osu!standard Rhythm skill.
|
|
3209
3187
|
*/
|
|
3210
|
-
class
|
|
3211
|
-
/**
|
|
3212
|
-
* Evaluates the difficulty of memorizing and hitting the current object, based on:
|
|
3213
|
-
*
|
|
3214
|
-
* - distance between a number of previous objects and the current object,
|
|
3215
|
-
* - the visual opacity of the current object,
|
|
3216
|
-
* - the angle made by the current object,
|
|
3217
|
-
* - length and speed of the current object (for sliders),
|
|
3218
|
-
* - and whether Hidden mod is enabled.
|
|
3219
|
-
*
|
|
3220
|
-
* @param current The current object.
|
|
3221
|
-
* @param mods The mods used.
|
|
3222
|
-
*/
|
|
3223
|
-
static evaluateDifficultyOf(current, mods) {
|
|
3224
|
-
if (current.object instanceof osuBase.Spinner) {
|
|
3225
|
-
return 0;
|
|
3226
|
-
}
|
|
3227
|
-
const scalingFactor = 52 / current.object.radius;
|
|
3228
|
-
let smallDistNerf = 1;
|
|
3229
|
-
let cumulativeStrainTime = 0;
|
|
3230
|
-
let result = 0;
|
|
3231
|
-
let last = current;
|
|
3232
|
-
let angleRepeatCount = 0;
|
|
3233
|
-
for (let i = 0; i < Math.min(current.index, 10); ++i) {
|
|
3234
|
-
const currentObject = current.previous(i);
|
|
3235
|
-
cumulativeStrainTime += last.strainTime;
|
|
3236
|
-
if (!(currentObject.object instanceof osuBase.Spinner)) {
|
|
3237
|
-
const jumpDistance = current.object.stackedPosition.subtract(currentObject.object.stackedEndPosition).length;
|
|
3238
|
-
// We want to nerf objects that can be easily seen within the Flashlight circle radius.
|
|
3239
|
-
if (i === 0) {
|
|
3240
|
-
smallDistNerf = Math.min(1, jumpDistance / 75);
|
|
3241
|
-
}
|
|
3242
|
-
// We also want to nerf stacks so that only the first object of the stack is accounted for.
|
|
3243
|
-
const stackNerf = Math.min(1, currentObject.lazyJumpDistance / scalingFactor / 25);
|
|
3244
|
-
// Bonus based on how visible the object is.
|
|
3245
|
-
const opacityBonus = 1 +
|
|
3246
|
-
this.maxOpacityBonus *
|
|
3247
|
-
(1 -
|
|
3248
|
-
current.opacityAt(currentObject.object.startTime, mods));
|
|
3249
|
-
result +=
|
|
3250
|
-
(stackNerf * opacityBonus * scalingFactor * jumpDistance) /
|
|
3251
|
-
cumulativeStrainTime;
|
|
3252
|
-
if (currentObject.angle !== null && current.angle !== null) {
|
|
3253
|
-
// Objects further back in time should count less for the nerf.
|
|
3254
|
-
if (Math.abs(currentObject.angle - current.angle) < 0.02) {
|
|
3255
|
-
angleRepeatCount += Math.max(0, 1 - 0.1 * i);
|
|
3256
|
-
}
|
|
3257
|
-
}
|
|
3258
|
-
}
|
|
3259
|
-
last = currentObject;
|
|
3260
|
-
}
|
|
3261
|
-
result = Math.pow(smallDistNerf * result, 2);
|
|
3262
|
-
// Additional bonus for Hidden due to there being no approach circles.
|
|
3263
|
-
if (mods.has(osuBase.ModHidden)) {
|
|
3264
|
-
result *= 1 + this.hiddenBonus;
|
|
3265
|
-
}
|
|
3266
|
-
// Nerf patterns with repeated angles.
|
|
3267
|
-
result *=
|
|
3268
|
-
this.minAngleMultiplier +
|
|
3269
|
-
(1 - this.minAngleMultiplier) / (angleRepeatCount + 1);
|
|
3270
|
-
let sliderBonus = 0;
|
|
3271
|
-
if (current.object instanceof osuBase.Slider) {
|
|
3272
|
-
// Invert the scaling factor to determine the true travel distance independent of circle size.
|
|
3273
|
-
const pixelTravelDistance = current.lazyTravelDistance / scalingFactor;
|
|
3274
|
-
// Reward sliders based on velocity.
|
|
3275
|
-
sliderBonus = Math.pow(Math.max(0, pixelTravelDistance / current.travelTime - this.minVelocity), 0.5);
|
|
3276
|
-
// Longer sliders require more memorization.
|
|
3277
|
-
sliderBonus *= pixelTravelDistance;
|
|
3278
|
-
// Nerf sliders with repeats, as less memorization is required.
|
|
3279
|
-
if (current.object.repeatCount > 0)
|
|
3280
|
-
sliderBonus /= current.object.repeatCount + 1;
|
|
3281
|
-
}
|
|
3282
|
-
result += sliderBonus * this.sliderMultiplier;
|
|
3283
|
-
return result;
|
|
3284
|
-
}
|
|
3285
|
-
}
|
|
3286
|
-
OsuFlashlightEvaluator.maxOpacityBonus = 0.4;
|
|
3287
|
-
OsuFlashlightEvaluator.hiddenBonus = 0.2;
|
|
3288
|
-
OsuFlashlightEvaluator.minVelocity = 0.5;
|
|
3289
|
-
OsuFlashlightEvaluator.sliderMultiplier = 1.3;
|
|
3290
|
-
OsuFlashlightEvaluator.minAngleMultiplier = 0.2;
|
|
3291
|
-
|
|
3292
|
-
/**
|
|
3293
|
-
* Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
|
|
3294
|
-
*/
|
|
3295
|
-
class OsuFlashlight extends OsuSkill {
|
|
3296
|
-
constructor() {
|
|
3297
|
-
super(...arguments);
|
|
3298
|
-
this.strainDecayBase = 0.15;
|
|
3299
|
-
this.reducedSectionCount = 0;
|
|
3300
|
-
this.reducedSectionBaseline = 1;
|
|
3301
|
-
this.decayWeight = 1;
|
|
3302
|
-
this.currentFlashlightStrain = 0;
|
|
3303
|
-
this.skillMultiplier = 0.05512;
|
|
3304
|
-
}
|
|
3305
|
-
difficultyValue() {
|
|
3306
|
-
return this.strainPeaks.reduce((a, b) => a + b, 0);
|
|
3307
|
-
}
|
|
3308
|
-
strainValueAt(current) {
|
|
3309
|
-
this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
|
|
3310
|
-
this.currentFlashlightStrain +=
|
|
3311
|
-
OsuFlashlightEvaluator.evaluateDifficultyOf(current, this.mods) *
|
|
3312
|
-
this.skillMultiplier;
|
|
3313
|
-
return this.currentFlashlightStrain;
|
|
3314
|
-
}
|
|
3315
|
-
calculateInitialStrain(time, current) {
|
|
3316
|
-
var _a, _b;
|
|
3317
|
-
return (this.currentFlashlightStrain *
|
|
3318
|
-
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
3319
|
-
}
|
|
3320
|
-
saveToHitObject(current) {
|
|
3321
|
-
current.flashlightStrain = this.currentFlashlightStrain;
|
|
3322
|
-
}
|
|
3323
|
-
}
|
|
3324
|
-
|
|
3325
|
-
/**
|
|
3326
|
-
* An evaluator for calculating osu!standard Rhythm skill.
|
|
3327
|
-
*/
|
|
3328
|
-
class OsuRhythmEvaluator {
|
|
3188
|
+
class OsuRhythmEvaluator {
|
|
3329
3189
|
/**
|
|
3330
3190
|
* Calculates a rhythm multiplier for the difficulty of the tap associated
|
|
3331
3191
|
* with historic data of the current object.
|
|
@@ -3351,10 +3211,10 @@ class OsuRhythmEvaluator {
|
|
|
3351
3211
|
this.historyTimeMax) {
|
|
3352
3212
|
++rhythmStart;
|
|
3353
3213
|
}
|
|
3214
|
+
let prevObject = current.previous(rhythmStart);
|
|
3215
|
+
let lastObject = current.previous(rhythmStart + 1);
|
|
3354
3216
|
for (let i = rhythmStart; i > 0; --i) {
|
|
3355
3217
|
const currentObject = current.previous(i - 1);
|
|
3356
|
-
const prevObject = current.previous(i);
|
|
3357
|
-
const lastObject = current.previous(i + 1);
|
|
3358
3218
|
// Scale note 0 to 1 from history to now.
|
|
3359
3219
|
const timeDecay = (this.historyTimeMax -
|
|
3360
3220
|
(current.startTime - currentObject.startTime)) /
|
|
@@ -3362,21 +3222,23 @@ class OsuRhythmEvaluator {
|
|
|
3362
3222
|
const noteDecay = (historicalNoteCount - i) / historicalNoteCount;
|
|
3363
3223
|
// Either we're limited by time or limited by object count.
|
|
3364
3224
|
const currentHistoricalDecay = Math.min(timeDecay, noteDecay);
|
|
3365
|
-
|
|
3366
|
-
const
|
|
3367
|
-
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);
|
|
3368
3229
|
// Calculate how much current delta difference deserves a rhythm bonus
|
|
3369
3230
|
// This function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e. 100 and 200)
|
|
3370
|
-
const
|
|
3371
|
-
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);
|
|
3372
3235
|
const currentRatio = 1 +
|
|
3373
3236
|
this.rhythmRatioMultiplier *
|
|
3374
|
-
Math.min(0.5,
|
|
3237
|
+
Math.min(0.5, osuBase.MathUtils.smoothstepBellCurve(deltaDifferenceFraction));
|
|
3375
3238
|
// Reduce ratio bonus if delta difference is too big
|
|
3376
|
-
const
|
|
3377
|
-
const fractionMultiplier = osuBase.MathUtils.clamp(2 - fraction / 8, 0, 1);
|
|
3239
|
+
const differenceMultiplier = osuBase.MathUtils.clamp(2 - deltaDifference / 8, 0, 1);
|
|
3378
3240
|
const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon);
|
|
3379
|
-
let effectiveRatio = windowPenalty * currentRatio *
|
|
3241
|
+
let effectiveRatio = windowPenalty * currentRatio * differenceMultiplier;
|
|
3380
3242
|
if (firstDeltaSwitch) {
|
|
3381
3243
|
if (Math.abs(prevDelta - currentDelta) < deltaDifferenceEpsilon) {
|
|
3382
3244
|
// Island is still progressing, count size.
|
|
@@ -3429,7 +3291,8 @@ class OsuRhythmEvaluator {
|
|
|
3429
3291
|
islandCounts.set(island, 1);
|
|
3430
3292
|
}
|
|
3431
3293
|
// Scale down the difficulty if the object is doubletappable.
|
|
3432
|
-
effectiveRatio *=
|
|
3294
|
+
effectiveRatio *=
|
|
3295
|
+
1 - prevObject.getDoubletapness(currentObject) * 0.75;
|
|
3433
3296
|
rhythmComplexitySum +=
|
|
3434
3297
|
Math.sqrt(effectiveRatio * startRatio) *
|
|
3435
3298
|
currentHistoricalDecay;
|
|
@@ -3460,15 +3323,18 @@ class OsuRhythmEvaluator {
|
|
|
3460
3323
|
startRatio = effectiveRatio;
|
|
3461
3324
|
island = new Island(currentDelta, deltaDifferenceEpsilon);
|
|
3462
3325
|
}
|
|
3326
|
+
lastObject = prevObject;
|
|
3327
|
+
prevObject = currentObject;
|
|
3463
3328
|
}
|
|
3464
|
-
return (Math.sqrt(4 + rhythmComplexitySum * this.rhythmOverallMultiplier)
|
|
3329
|
+
return ((Math.sqrt(4 + rhythmComplexitySum * this.rhythmOverallMultiplier) *
|
|
3330
|
+
(1 - current.getDoubletapness(current.next(0)))) /
|
|
3465
3331
|
2);
|
|
3466
3332
|
}
|
|
3467
3333
|
}
|
|
3468
3334
|
OsuRhythmEvaluator.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
|
|
3469
3335
|
OsuRhythmEvaluator.historyObjectsMax = 32;
|
|
3470
|
-
OsuRhythmEvaluator.rhythmOverallMultiplier =
|
|
3471
|
-
OsuRhythmEvaluator.rhythmRatioMultiplier =
|
|
3336
|
+
OsuRhythmEvaluator.rhythmOverallMultiplier = 1;
|
|
3337
|
+
OsuRhythmEvaluator.rhythmRatioMultiplier = 15;
|
|
3472
3338
|
|
|
3473
3339
|
/**
|
|
3474
3340
|
* An evaluator for calculating osu!standard speed skill.
|
|
@@ -3492,7 +3358,7 @@ class OsuSpeedEvaluator {
|
|
|
3492
3358
|
const prev = current.previous(0);
|
|
3493
3359
|
let strainTime = current.strainTime;
|
|
3494
3360
|
// Nerf doubletappable doubles.
|
|
3495
|
-
const doubletapness = 1 - current.
|
|
3361
|
+
const doubletapness = 1 - current.getDoubletapness(current.next(0));
|
|
3496
3362
|
// Cap deltatime to the OD 300 hitwindow.
|
|
3497
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.
|
|
3498
3364
|
strainTime /= osuBase.MathUtils.clamp(strainTime / current.fullGreatWindow / 0.93, 0.92, 1);
|
|
@@ -3509,6 +3375,8 @@ class OsuSpeedEvaluator {
|
|
|
3509
3375
|
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
|
|
3510
3376
|
let distanceBonus = Math.pow(distance / this.SINGLE_SPACING_THRESHOLD, 3.95) *
|
|
3511
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);
|
|
3512
3380
|
if (mods.has(osuBase.ModAutopilot)) {
|
|
3513
3381
|
distanceBonus = 0;
|
|
3514
3382
|
}
|
|
@@ -3523,10 +3391,10 @@ class OsuSpeedEvaluator {
|
|
|
3523
3391
|
*
|
|
3524
3392
|
* About 1.25 circles distance between hitobject centers.
|
|
3525
3393
|
*/
|
|
3526
|
-
OsuSpeedEvaluator.SINGLE_SPACING_THRESHOLD =
|
|
3394
|
+
OsuSpeedEvaluator.SINGLE_SPACING_THRESHOLD = OsuDifficultyHitObject.normalizedDiameter * 1.25;
|
|
3527
3395
|
// ~200 1/4 BPM streams
|
|
3528
3396
|
OsuSpeedEvaluator.minSpeedBonus = 75;
|
|
3529
|
-
OsuSpeedEvaluator.DISTANCE_MULTIPLIER = 0.
|
|
3397
|
+
OsuSpeedEvaluator.DISTANCE_MULTIPLIER = 0.8;
|
|
3530
3398
|
|
|
3531
3399
|
/**
|
|
3532
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.
|
|
@@ -3540,7 +3408,8 @@ class OsuSpeed extends OsuSkill {
|
|
|
3540
3408
|
this.decayWeight = 0.9;
|
|
3541
3409
|
this.currentSpeedStrain = 0;
|
|
3542
3410
|
this.currentRhythm = 0;
|
|
3543
|
-
this.skillMultiplier = 1.
|
|
3411
|
+
this.skillMultiplier = 1.47;
|
|
3412
|
+
this.sliderStrains = [];
|
|
3544
3413
|
this.maxStrain = 0;
|
|
3545
3414
|
}
|
|
3546
3415
|
/**
|
|
@@ -3552,6 +3421,12 @@ class OsuSpeed extends OsuSkill {
|
|
|
3552
3421
|
}
|
|
3553
3422
|
return this._objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / this.maxStrain) * 12 - 6))), 0);
|
|
3554
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
|
+
}
|
|
3555
3430
|
/**
|
|
3556
3431
|
* @param current The hitobject to calculate.
|
|
3557
3432
|
*/
|
|
@@ -3564,6 +3439,9 @@ class OsuSpeed extends OsuSkill {
|
|
|
3564
3439
|
const strain = this.currentSpeedStrain * this.currentRhythm;
|
|
3565
3440
|
this._objectStrains.push(strain);
|
|
3566
3441
|
this.maxStrain = Math.max(this.maxStrain, strain);
|
|
3442
|
+
if (current.object instanceof osuBase.Slider) {
|
|
3443
|
+
this.sliderStrains.push(strain);
|
|
3444
|
+
}
|
|
3567
3445
|
return strain;
|
|
3568
3446
|
}
|
|
3569
3447
|
calculateInitialStrain(time, current) {
|
|
@@ -3581,137 +3459,184 @@ class OsuSpeed extends OsuSkill {
|
|
|
3581
3459
|
}
|
|
3582
3460
|
}
|
|
3583
3461
|
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
this.
|
|
3591
|
-
this.
|
|
3592
|
-
}
|
|
3593
|
-
retainDifficultyAdjustmentMods(mods) {
|
|
3594
|
-
return mods.filter((mod) => mod.isApplicableToOsu() &&
|
|
3595
|
-
mod.isOsuRelevant &&
|
|
3596
|
-
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;
|
|
3597
3470
|
}
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
attributes.maxCombo = beatmap.maxCombo;
|
|
3602
|
-
attributes.clockRate = beatmap.speedMultiplier;
|
|
3603
|
-
attributes.hitCircleCount = beatmap.hitObjects.circles;
|
|
3604
|
-
attributes.sliderCount = beatmap.hitObjects.sliders;
|
|
3605
|
-
attributes.spinnerCount = beatmap.hitObjects.spinners;
|
|
3606
|
-
this.populateAimAttributes(attributes, skills);
|
|
3607
|
-
this.populateSpeedAttributes(attributes, skills);
|
|
3608
|
-
this.populateFlashlightAttributes(attributes, skills);
|
|
3609
|
-
if (attributes.mods.has(osuBase.ModRelax)) {
|
|
3610
|
-
attributes.aimDifficulty *= 0.9;
|
|
3611
|
-
attributes.speedDifficulty = 0;
|
|
3612
|
-
attributes.flashlightDifficulty *= 0.7;
|
|
3471
|
+
computeAimRating(aimDifficultyValue) {
|
|
3472
|
+
if (this.mods.has(osuBase.ModAutopilot)) {
|
|
3473
|
+
return 0;
|
|
3613
3474
|
}
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
attributes.flashlightDifficulty *= 0.4;
|
|
3475
|
+
let aimRating = OsuRatingCalculator.calculateDifficultyRating(aimDifficultyValue);
|
|
3476
|
+
if (this.mods.has(osuBase.ModTouchDevice)) {
|
|
3477
|
+
aimRating = Math.pow(aimRating, 0.8);
|
|
3618
3478
|
}
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
const flashlightPerformanceValue = Math.pow(attributes.flashlightDifficulty, 2) * 25;
|
|
3622
|
-
const basePerformanceValue = Math.pow(Math.pow(aimPerformanceValue, 1.1) +
|
|
3623
|
-
Math.pow(speedPerformanceValue, 1.1) +
|
|
3624
|
-
Math.pow(flashlightPerformanceValue, 1.1), 1 / 1.1);
|
|
3625
|
-
if (basePerformanceValue > 1e-5) {
|
|
3626
|
-
// Document for formula derivation:
|
|
3627
|
-
// https://docs.google.com/document/d/10DZGYYSsT_yjz2Mtp6yIJld0Rqx4E-vVHupCqiM4TNI/edit
|
|
3628
|
-
attributes.starRating =
|
|
3629
|
-
Math.cbrt(1.15) *
|
|
3630
|
-
0.027 *
|
|
3631
|
-
(Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformanceValue) +
|
|
3632
|
-
4);
|
|
3479
|
+
if (this.mods.has(osuBase.ModRelax)) {
|
|
3480
|
+
aimRating *= 0.9;
|
|
3633
3481
|
}
|
|
3634
|
-
|
|
3635
|
-
|
|
3482
|
+
if (this.mods.has(osuBase.ModMagnetised)) {
|
|
3483
|
+
const magnetisedStrength = this.mods.get(osuBase.ModMagnetised).attractionStrength.value;
|
|
3484
|
+
aimRating *= 1 - magnetisedStrength;
|
|
3636
3485
|
}
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
createDifficultyHitObjects(beatmap) {
|
|
3647
|
-
const clockRate = beatmap.speedMultiplier;
|
|
3648
|
-
const difficultyObjects = [];
|
|
3649
|
-
const { objects } = beatmap.hitObjects;
|
|
3650
|
-
for (let i = 1; i < objects.length; ++i) {
|
|
3651
|
-
const difficultyObject = new OsuDifficultyHitObject(objects[i], objects[i - 1], difficultyObjects, clockRate, i - 1);
|
|
3652
|
-
difficultyObject.computeProperties(clockRate);
|
|
3653
|
-
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);
|
|
3654
3495
|
}
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
createSkills(beatmap) {
|
|
3658
|
-
const { mods } = beatmap;
|
|
3659
|
-
const skills = [];
|
|
3660
|
-
if (!mods.has(osuBase.ModAutopilot)) {
|
|
3661
|
-
skills.push(new OsuAim(mods, true));
|
|
3662
|
-
skills.push(new OsuAim(mods, false));
|
|
3496
|
+
else if (this.approachRate < 8) {
|
|
3497
|
+
approachRateFactor = 0.05 * (8 - this.approachRate);
|
|
3663
3498
|
}
|
|
3664
|
-
if (
|
|
3665
|
-
|
|
3499
|
+
if (this.mods.has(osuBase.ModRelax)) {
|
|
3500
|
+
approachRateFactor = 0;
|
|
3666
3501
|
}
|
|
3667
|
-
|
|
3668
|
-
|
|
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);
|
|
3669
3507
|
}
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
return [
|
|
3675
|
-
new OsuAim(mods, true),
|
|
3676
|
-
new OsuAim(mods, false),
|
|
3677
|
-
new OsuSpeed(mods),
|
|
3678
|
-
new OsuFlashlight(mods),
|
|
3679
|
-
];
|
|
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);
|
|
3680
3512
|
}
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
if (!aim || !aimNoSlider) {
|
|
3685
|
-
return;
|
|
3513
|
+
computeSpeedRating(speedDifficultyValue) {
|
|
3514
|
+
if (this.mods.has(osuBase.ModRelax)) {
|
|
3515
|
+
return 0;
|
|
3686
3516
|
}
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
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);
|
|
3693
3535
|
}
|
|
3694
|
-
|
|
3695
|
-
|
|
3536
|
+
if (this.mods.has(osuBase.ModAutopilot)) {
|
|
3537
|
+
approachRateFactor = 0;
|
|
3696
3538
|
}
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
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);
|
|
3702
3544
|
}
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3545
|
+
ratingMultiplier *=
|
|
3546
|
+
0.95 + Math.pow(Math.max(0, this.overallDifficulty), 2) / 750;
|
|
3547
|
+
return speedRating * Math.cbrt(ratingMultiplier);
|
|
3706
3548
|
}
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
return;
|
|
3549
|
+
computeFlashlightRating(flashlightDifficultyValue) {
|
|
3550
|
+
if (!this.mods.has(osuBase.ModFlashlight)) {
|
|
3551
|
+
return 0;
|
|
3711
3552
|
}
|
|
3712
|
-
|
|
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;
|
|
3713
3637
|
}
|
|
3714
3638
|
}
|
|
3639
|
+
OsuRatingCalculator.difficultyMultiplier = 0.0675;
|
|
3715
3640
|
|
|
3716
3641
|
/**
|
|
3717
3642
|
* A performance points calculator that calculates performance points for osu!standard gamemode.
|
|
@@ -3735,31 +3660,48 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3735
3660
|
* The flashlight performance value.
|
|
3736
3661
|
*/
|
|
3737
3662
|
this.flashlight = 0;
|
|
3738
|
-
this.
|
|
3739
|
-
this.mode = osuBase.Modes.osu;
|
|
3740
|
-
this.comboPenalty = 1;
|
|
3663
|
+
this._effectiveMissCount = 0;
|
|
3741
3664
|
this.speedDeviation = 0;
|
|
3742
3665
|
}
|
|
3743
|
-
|
|
3744
|
-
|
|
3666
|
+
/**
|
|
3667
|
+
* The amount of misses, including slider breaks.
|
|
3668
|
+
*/
|
|
3669
|
+
get effectiveMissCount() {
|
|
3670
|
+
return this._effectiveMissCount;
|
|
3671
|
+
}
|
|
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
|
+
}
|
|
3695
|
+
this.speedDeviation = this.calculateSpeedDeviation();
|
|
3745
3696
|
this.aim = this.calculateAimValue();
|
|
3746
3697
|
this.speed = this.calculateSpeedValue();
|
|
3747
3698
|
this.accuracy = this.calculateAccuracyValue();
|
|
3748
3699
|
this.flashlight = this.calculateFlashlightValue();
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
Math.pow(this.flashlight, 1.1), 1 / 1.1) * this.finalMultiplier);
|
|
3755
|
-
}
|
|
3756
|
-
handleOptions(options) {
|
|
3757
|
-
var _a;
|
|
3758
|
-
super.handleOptions(options);
|
|
3759
|
-
const maxCombo = this.difficultyAttributes.maxCombo;
|
|
3760
|
-
const miss = this.computedAccuracy.nmiss;
|
|
3761
|
-
const combo = (_a = options === null || options === void 0 ? void 0 : options.combo) !== null && _a !== void 0 ? _a : maxCombo - miss;
|
|
3762
|
-
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;
|
|
3763
3705
|
}
|
|
3764
3706
|
/**
|
|
3765
3707
|
* Calculates the aim performance value of the beatmap.
|
|
@@ -3768,7 +3710,29 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3768
3710
|
if (this.mods.has(osuBase.ModAutopilot)) {
|
|
3769
3711
|
return 0;
|
|
3770
3712
|
}
|
|
3771
|
-
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);
|
|
3772
3736
|
// Longer maps are worth more
|
|
3773
3737
|
let lengthBonus = 0.95 + 0.4 * Math.min(1, this.totalHits / 2000);
|
|
3774
3738
|
if (this.totalHits > 2000) {
|
|
@@ -3776,38 +3740,29 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3776
3740
|
}
|
|
3777
3741
|
aimValue *= lengthBonus;
|
|
3778
3742
|
if (this.effectiveMissCount > 0) {
|
|
3779
|
-
|
|
3780
|
-
|
|
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)) {
|
|
3781
3750
|
aimValue *=
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
// AR scaling
|
|
3790
|
-
let arFactor = 0;
|
|
3791
|
-
if (calculatedAR > 10.33) {
|
|
3792
|
-
arFactor += 0.3 * (calculatedAR - 10.33);
|
|
3793
|
-
}
|
|
3794
|
-
else if (calculatedAR < 8) {
|
|
3795
|
-
arFactor += 0.05 * (8 - calculatedAR);
|
|
3796
|
-
}
|
|
3797
|
-
// Buff for longer maps with high AR.
|
|
3798
|
-
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));
|
|
3799
3758
|
}
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3759
|
+
else if (this.mods.has(osuBase.ModTraceable)) {
|
|
3760
|
+
aimValue *=
|
|
3761
|
+
1 +
|
|
3762
|
+
OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.difficultyAttributes.approachRate, undefined, this.difficultyAttributes.sliderFactor);
|
|
3803
3763
|
}
|
|
3804
|
-
// Scale the aim value with slider factor to nerf very likely dropped sliderends.
|
|
3805
|
-
aimValue *= this.sliderNerfFactor;
|
|
3806
3764
|
// Scale the aim value with accuracy.
|
|
3807
3765
|
aimValue *= this.computedAccuracy.value();
|
|
3808
|
-
// It is also important to consider accuracy difficulty when doing that.
|
|
3809
|
-
const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
|
|
3810
|
-
aimValue *= 0.98 + odScaling;
|
|
3811
3766
|
return aimValue;
|
|
3812
3767
|
}
|
|
3813
3768
|
/**
|
|
@@ -3818,22 +3773,28 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3818
3773
|
this.speedDeviation === Number.POSITIVE_INFINITY) {
|
|
3819
3774
|
return 0;
|
|
3820
3775
|
}
|
|
3821
|
-
let speedValue =
|
|
3776
|
+
let speedValue = OsuSpeed.difficultyToPerformance(this.difficultyAttributes.speedDifficulty);
|
|
3822
3777
|
// Longer maps are worth more
|
|
3823
3778
|
let lengthBonus = 0.95 + 0.4 * Math.min(1, this.totalHits / 2000);
|
|
3824
3779
|
if (this.totalHits > 2000) {
|
|
3825
3780
|
lengthBonus += Math.log10(this.totalHits / 2000) * 0.5;
|
|
3826
3781
|
}
|
|
3827
3782
|
speedValue *= lengthBonus;
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
if (this.mods.has(osuBase.
|
|
3836
|
-
|
|
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);
|
|
3837
3798
|
}
|
|
3838
3799
|
// Calculate accuracy assuming the worst case scenario.
|
|
3839
3800
|
const countGreat = this.computedAccuracy.n300;
|
|
@@ -3851,13 +3812,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3851
3812
|
{ n300: 0, nobjects: 1 });
|
|
3852
3813
|
speedValue *= this.calculateSpeedHighDeviationNerf();
|
|
3853
3814
|
// Scale the speed value with accuracy and OD.
|
|
3854
|
-
speedValue *=
|
|
3855
|
-
(0.95 +
|
|
3856
|
-
Math.pow(Math.max(0, this.difficultyAttributes.overallDifficulty), 2) /
|
|
3857
|
-
750) *
|
|
3858
|
-
Math.pow((this.computedAccuracy.value() +
|
|
3859
|
-
relevantAccuracy.value(this.difficultyAttributes.speedNoteCount)) /
|
|
3860
|
-
2, (14.5 - this.difficultyAttributes.overallDifficulty) / 2);
|
|
3815
|
+
speedValue *= Math.pow((this.computedAccuracy.value() + relevantAccuracy.value()) / 2, (14.5 - this.difficultyAttributes.overallDifficulty) / 2);
|
|
3861
3816
|
return speedValue;
|
|
3862
3817
|
}
|
|
3863
3818
|
/**
|
|
@@ -3882,8 +3837,16 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3882
3837
|
2.83;
|
|
3883
3838
|
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
|
|
3884
3839
|
accuracyValue *= Math.min(1.15, Math.pow(ncircles / 1000, 0.3));
|
|
3885
|
-
|
|
3886
|
-
|
|
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);
|
|
3887
3850
|
}
|
|
3888
3851
|
if (this.mods.has(osuBase.ModFlashlight)) {
|
|
3889
3852
|
accuracyValue *= 1.02;
|
|
@@ -3899,7 +3862,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3899
3862
|
}
|
|
3900
3863
|
let flashlightValue = Math.pow(this.difficultyAttributes.flashlightDifficulty, 2) * 25;
|
|
3901
3864
|
// Combo scaling
|
|
3902
|
-
flashlightValue *= this.
|
|
3865
|
+
flashlightValue *= Math.min(Math.pow(this.combo / this.difficultyAttributes.maxCombo, 0.8), 1);
|
|
3903
3866
|
if (this.effectiveMissCount > 0) {
|
|
3904
3867
|
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
3905
3868
|
flashlightValue *=
|
|
@@ -3916,11 +3879,23 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3916
3879
|
: 0);
|
|
3917
3880
|
// Scale the flashlight value with accuracy slightly.
|
|
3918
3881
|
flashlightValue *= 0.5 + this.computedAccuracy.value() / 2;
|
|
3919
|
-
// It is also important to consider accuracy difficulty when doing that.
|
|
3920
|
-
const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
|
|
3921
|
-
flashlightValue *= 0.98 + odScaling;
|
|
3922
3882
|
return flashlightValue;
|
|
3923
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
|
+
}
|
|
3924
3899
|
/**
|
|
3925
3900
|
* Estimates a player's deviation on speed notes using {@link calculateDeviation}, assuming worst-case.
|
|
3926
3901
|
*
|
|
@@ -3941,7 +3916,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3941
3916
|
relevantCountMiss -
|
|
3942
3917
|
relevantCountMeh -
|
|
3943
3918
|
relevantCountOk);
|
|
3944
|
-
return this.calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh
|
|
3919
|
+
return this.calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh);
|
|
3945
3920
|
}
|
|
3946
3921
|
/**
|
|
3947
3922
|
* Estimates the player's tap deviation based on the OD, given number of greats, oks, mehs and misses,
|
|
@@ -3952,52 +3927,51 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3952
3927
|
*
|
|
3953
3928
|
* Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution.
|
|
3954
3929
|
*/
|
|
3955
|
-
calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh
|
|
3930
|
+
calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh) {
|
|
3956
3931
|
if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0) {
|
|
3957
3932
|
return Number.POSITIVE_INFINITY;
|
|
3958
3933
|
}
|
|
3959
|
-
const objectCount = relevantCountGreat +
|
|
3960
|
-
relevantCountOk +
|
|
3961
|
-
relevantCountMeh +
|
|
3962
|
-
relevantCountMiss;
|
|
3963
3934
|
// Obtain the great, ok, and meh windows.
|
|
3935
|
+
const { clockRate, overallDifficulty } = this.difficultyAttributes;
|
|
3964
3936
|
const hitWindow = new osuBase.OsuHitWindow(osuBase.OsuHitWindow.greatWindowToOD(
|
|
3965
3937
|
// Convert current OD to non clock rate-adjusted OD.
|
|
3966
|
-
new osuBase.OsuHitWindow(
|
|
3967
|
-
|
|
3968
|
-
const
|
|
3969
|
-
|
|
3970
|
-
//
|
|
3971
|
-
|
|
3972
|
-
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;
|
|
3973
3945
|
// 99% critical value for the normal distribution (one-tailed).
|
|
3974
3946
|
const z = 2.32634787404;
|
|
3975
|
-
//
|
|
3976
|
-
const
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
//
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
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 +
|
|
3997
3972
|
okWindow * mehWindow +
|
|
3998
|
-
|
|
3973
|
+
okWindow * okWindow) /
|
|
3999
3974
|
3;
|
|
4000
|
-
// Find the total deviation.
|
|
4001
3975
|
deviation = Math.sqrt(((relevantCountGreat + relevantCountOk) * Math.pow(deviation, 2) +
|
|
4002
3976
|
relevantCountMeh * mehVariance) /
|
|
4003
3977
|
(relevantCountGreat + relevantCountOk + relevantCountMeh));
|
|
@@ -4012,7 +3986,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4012
3986
|
if (this.speedDeviation == Number.POSITIVE_INFINITY) {
|
|
4013
3987
|
return 0;
|
|
4014
3988
|
}
|
|
4015
|
-
const speedValue =
|
|
3989
|
+
const speedValue = OsuSpeed.difficultyToPerformance(this.difficultyAttributes.speedDifficulty);
|
|
4016
3990
|
// Decide a point where the PP value achieved compared to the speed deviation is assumed to be tapped
|
|
4017
3991
|
// improperly. Any PP above this point is considered "excess" speed difficulty. This is used to cause
|
|
4018
3992
|
// PP above the cutoff to scale logarithmically towards the original speed value thus nerfing the value.
|
|
@@ -4028,6 +4002,64 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4028
4002
|
const t = 1 - osuBase.Interpolation.reverseLerp(this.speedDeviation, 22, 27);
|
|
4029
4003
|
return (osuBase.Interpolation.lerp(adjustedSpeedValue, speedValue, t) / speedValue);
|
|
4030
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
|
+
}
|
|
4031
4063
|
toString() {
|
|
4032
4064
|
return (this.total.toFixed(2) +
|
|
4033
4065
|
" pp (" +
|
|
@@ -4036,11 +4068,275 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4036
4068
|
this.speed.toFixed(2) +
|
|
4037
4069
|
" speed, " +
|
|
4038
4070
|
this.accuracy.toFixed(2) +
|
|
4039
|
-
"
|
|
4071
|
+
" accuracy, " +
|
|
4040
4072
|
this.flashlight.toFixed(2) +
|
|
4041
4073
|
" flashlight)");
|
|
4042
4074
|
}
|
|
4043
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
|
+
}
|
|
4044
4340
|
|
|
4045
4341
|
exports.DifficultyAttributes = DifficultyAttributes;
|
|
4046
4342
|
exports.DifficultyCalculator = DifficultyCalculator;
|