@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.
Files changed (3) hide show
  1. package/dist/index.js +928 -632
  2. package/package.json +3 -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 instanceof osuBase.PlayableBeatmap
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.createPlayableBeatmapWithDifficultyAdjustmentMods(beatmap, mods);
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
- get doubletapness() {
316
- const next = this.next(0);
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, next.deltaTime);
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 CircleSize among beatmaps.
347
- let scalingFactor = DifficultyHitObject.normalizedRadius / this.object.radius;
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 *= 1 - prevObject.doubletapness * 0.75;
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.doubletapness;
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.doubletapness
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 = this.basePerformanceValue(Math.pow(attributes.aimDifficulty, 0.8));
1933
- const tapPerformanceValue = this.basePerformanceValue(attributes.tapDifficulty);
1934
- const flashlightPerformanceValue = Math.pow(attributes.flashlightDifficulty, 1.6) * 25;
1935
- const readingPerformanceValue = Math.pow(Math.pow(attributes.readingDifficulty, 2) * 25, 0.8);
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 amount of misses that are filtered out from sliderbreaks.
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.sliderTicksMissed = 0;
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
- let combo = (_b = options === null || options === void 0 ? void 0 : options.combo) !== null && _b !== void 0 ? _b : maxCombo - miss;
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.sliderEndsDropped = options.sliderEndsDropped;
2295
- this.sliderTicksMissed = options.sliderTicksMissed;
2302
+ this._sliderEndsDropped = options.sliderEndsDropped;
2303
+ this._sliderTicksMissed = options.sliderTicksMissed;
2296
2304
  }
2297
2305
  else {
2298
2306
  this._usingClassicSliderAccuracy = true;
2299
- this.sliderEndsDropped = 0;
2300
- this.sliderTicksMissed = 0;
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.sliderEndsDropped - this.sliderTicksMissed);
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
- * Applies a tap penalty value to this calculator.
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
- applyAimSliderCheesePenalty(value) {
2507
- if (value < 0) {
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
- * Applies a flashlight slider cheese penalty value to this calculator.
2522
- *
2523
- * The flashlight and total performance value will be recalculated afterwards.
2524
- *
2525
- * @param value The slider cheese penalty value. Must be between 0 and 1.
2526
- */
2527
- applyFlashlightSliderCheesePenalty(value) {
2528
- if (value < 0) {
2529
- throw new RangeError("New flashlight slider cheese penalty must be greater than or equal to zero.");
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
- if (value > 1) {
2532
- throw new RangeError("New flashlight slider cheese penalty must be less than or equal to one.");
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 (value === this._flashlightSliderCheesePenalty) {
2535
- return;
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
- calculateTotalValue() {
2551
- return (Math.pow(Math.pow(this.aim, 1.1) +
2552
- Math.pow(this.tap, 1.1) +
2553
- Math.pow(this.accuracy, 1.1) +
2554
- Math.pow(this.flashlight, 1.1) +
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 = this.baseValue(Math.pow(this.difficultyAttributes.aimDifficulty, 0.8));
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
- // Scale the aim value with slider factor to nerf very likely dropped sliderends.
2574
- aimValue *= this.sliderNerfFactor;
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 = this.baseValue(this.difficultyAttributes.tapDifficulty);
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.effectiveMissCount - 1));
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.effectiveMissCount === 0) {
2629
+ if (this._effectiveMissCount === 0) {
2696
2630
  return 1;
2697
2631
  }
2698
- const missProportion = (this.totalHits - this.effectiveMissCount) / (this.totalHits + 1);
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 = this.baseValue(this.difficultyAttributes.tapDifficulty);
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
- " acc, " +
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
- wideAngleBonus = this.calculateWideAngleBonus(current.angle);
3018
- acuteAngleBonus = this.calculateAcuteAngleBonus(current.angle);
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 = Math.pow(Math.sin(((Math.PI / 2) * Math.abs(prevVelocity - currentVelocity)) /
3058
- Math.max(prevVelocity, currentVelocity)), 2);
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(125 / Math.min(current.strainTime, last.strainTime), Math.abs(prevVelocity - currentVelocity));
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
- // Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
3072
- strain += Math.max(acuteAngleBonus * this.acuteAngleMultiplier, wideAngleBonus * this.wideAngleMultiplier +
3073
- velocityChangeBonus * this.velocityChangeMultiplier);
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.6;
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 = 25.6;
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 Flashlight skill.
3186
+ * An evaluator for calculating osu!standard Rhythm skill.
3209
3187
  */
3210
- class OsuFlashlightEvaluator {
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
- const currentDelta = currentObject.strainTime;
3366
- const prevDelta = prevObject.strainTime;
3367
- const lastDelta = lastObject.strainTime;
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 deltaDifferenceRatio = Math.min(prevDelta, currentDelta) /
3371
- Math.max(prevDelta, currentDelta);
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, Math.pow(Math.sin(Math.PI / deltaDifferenceRatio), 2));
3237
+ Math.min(0.5, osuBase.MathUtils.smoothstepBellCurve(deltaDifferenceFraction));
3375
3238
  // Reduce ratio bonus if delta difference is too big
3376
- const fraction = Math.max(prevDelta / currentDelta, currentDelta / prevDelta);
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 * fractionMultiplier;
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 *= 1 - prevObject.doubletapness * 0.75;
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 = 0.95;
3471
- OsuRhythmEvaluator.rhythmRatioMultiplier = 12;
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.doubletapness;
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 = 125;
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.9;
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.46;
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
- * A difficulty calculator for osu!standard gamemode.
3586
- */
3587
- class OsuDifficultyCalculator extends DifficultyCalculator {
3588
- constructor() {
3589
- super();
3590
- this.difficultyMultiplier = 0.0675;
3591
- this.difficultyAdjustmentMods.push(osuBase.ModTouchDevice);
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
- createDifficultyAttributes(beatmap, skills) {
3599
- const attributes = new OsuDifficultyAttributes();
3600
- attributes.mods = beatmap.mods;
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
- else if (attributes.mods.has(osuBase.ModAutopilot)) {
3615
- attributes.aimDifficulty = 0;
3616
- attributes.speedDifficulty *= 0.5;
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
- const aimPerformanceValue = this.basePerformanceValue(attributes.aimDifficulty);
3620
- const speedPerformanceValue = this.basePerformanceValue(attributes.speedDifficulty);
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
- else {
3635
- attributes.starRating = 0;
3482
+ if (this.mods.has(osuBase.ModMagnetised)) {
3483
+ const magnetisedStrength = this.mods.get(osuBase.ModMagnetised).attractionStrength.value;
3484
+ aimRating *= 1 - magnetisedStrength;
3636
3485
  }
3637
- const preempt = osuBase.BeatmapDifficulty.difficultyRange(beatmap.difficulty.ar, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin) / attributes.clockRate;
3638
- attributes.approachRate = osuBase.BeatmapDifficulty.inverseDifficultyRange(preempt, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin);
3639
- const { greatWindow } = new osuBase.OsuHitWindow(beatmap.difficulty.od);
3640
- attributes.overallDifficulty = osuBase.OsuHitWindow.greatWindowToOD(greatWindow / attributes.clockRate);
3641
- return attributes;
3642
- }
3643
- createPlayableBeatmap(beatmap, mods) {
3644
- return beatmap.createOsuPlayableBeatmap(mods);
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
- return difficultyObjects;
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 (!mods.has(osuBase.ModRelax)) {
3665
- skills.push(new OsuSpeed(mods));
3499
+ if (this.mods.has(osuBase.ModRelax)) {
3500
+ approachRateFactor = 0;
3666
3501
  }
3667
- if (mods.has(osuBase.ModFlashlight)) {
3668
- skills.push(new OsuFlashlight(mods));
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
- return skills;
3671
- }
3672
- createStrainPeakSkills(beatmap) {
3673
- const { mods } = beatmap;
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
- populateAimAttributes(attributes, skills) {
3682
- const aim = skills.find((s) => s instanceof OsuAim && s.withSliders);
3683
- const aimNoSlider = skills.find((s) => s instanceof OsuAim && !s.withSliders);
3684
- if (!aim || !aimNoSlider) {
3685
- return;
3513
+ computeSpeedRating(speedDifficultyValue) {
3514
+ if (this.mods.has(osuBase.ModRelax)) {
3515
+ return 0;
3686
3516
  }
3687
- attributes.aimDifficulty = this.calculateRating(aim);
3688
- attributes.aimDifficultSliderCount = aim.countDifficultSliders();
3689
- attributes.aimDifficultStrainCount = aim.countTopWeightedStrains();
3690
- if (attributes.aimDifficulty > 0) {
3691
- attributes.sliderFactor =
3692
- this.calculateRating(aimNoSlider) / attributes.aimDifficulty;
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
- else {
3695
- attributes.sliderFactor = 1;
3536
+ if (this.mods.has(osuBase.ModAutopilot)) {
3537
+ approachRateFactor = 0;
3696
3538
  }
3697
- }
3698
- populateSpeedAttributes(attributes, skills) {
3699
- const speed = skills.find((s) => s instanceof OsuSpeed);
3700
- if (!speed) {
3701
- return;
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
- attributes.speedDifficulty = this.calculateRating(speed);
3704
- attributes.speedNoteCount = speed.relevantNoteCount();
3705
- attributes.speedDifficultStrainCount = speed.countTopWeightedStrains();
3545
+ ratingMultiplier *=
3546
+ 0.95 + Math.pow(Math.max(0, this.overallDifficulty), 2) / 750;
3547
+ return speedRating * Math.cbrt(ratingMultiplier);
3706
3548
  }
3707
- populateFlashlightAttributes(attributes, skills) {
3708
- const flashlight = skills.find((s) => s instanceof OsuFlashlight);
3709
- if (!flashlight) {
3710
- return;
3549
+ computeFlashlightRating(flashlightDifficultyValue) {
3550
+ if (!this.mods.has(osuBase.ModFlashlight)) {
3551
+ return 0;
3711
3552
  }
3712
- attributes.flashlightDifficulty = this.calculateRating(flashlight);
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.finalMultiplier = 1.15;
3739
- this.mode = osuBase.Modes.osu;
3740
- this.comboPenalty = 1;
3663
+ this._effectiveMissCount = 0;
3741
3664
  this.speedDeviation = 0;
3742
3665
  }
3743
- calculateValues() {
3744
- this.speedDeviation = this.calculateSpeedDeviation();
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
- calculateTotalValue() {
3751
- return (Math.pow(Math.pow(this.aim, 1.1) +
3752
- Math.pow(this.speed, 1.1) +
3753
- Math.pow(this.accuracy, 1.1) +
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 aimValue = this.baseValue(this.difficultyAttributes.aimDifficulty);
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
- // Penalize misses by assessing # of misses relative to the total # of objects.
3780
- // Default a 3% reduction for any # of misses.
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
- 0.97 *
3783
- Math.pow(1 -
3784
- Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
3785
- }
3786
- aimValue *= this.calculateStrainBasedMissPenalty(this.difficultyAttributes.aimDifficultStrainCount);
3787
- const calculatedAR = this.difficultyAttributes.approachRate;
3788
- if (!this.mods.has(osuBase.ModRelax)) {
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
- // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
3801
- if (this.mods.has(osuBase.ModHidden) || this.mods.has(osuBase.ModTraceable)) {
3802
- aimValue *= 1 + 0.04 * (12 - calculatedAR);
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 = this.baseValue(this.difficultyAttributes.speedDifficulty);
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
- speedValue *= this.calculateStrainBasedMissPenalty(this.difficultyAttributes.speedDifficultStrainCount);
3829
- // AR scaling
3830
- const calculatedAR = this.difficultyAttributes.approachRate;
3831
- if (calculatedAR > 10.33 && !this.mods.has(osuBase.ModAutopilot)) {
3832
- // Buff for longer maps with high AR.
3833
- speedValue *= 1 + 0.3 * (calculatedAR - 10.33) * lengthBonus;
3834
- }
3835
- if (this.mods.has(osuBase.ModHidden) || this.mods.has(osuBase.ModTraceable)) {
3836
- speedValue *= 1 + 0.04 * (12 - calculatedAR);
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
- if (this.mods.has(osuBase.ModHidden) || this.mods.has(osuBase.ModTraceable)) {
3886
- accuracyValue *= 1.08;
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.comboPenalty;
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, relevantCountMiss);
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, relevantCountMiss) {
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(this.difficultyAttributes.overallDifficulty)
3967
- .greatWindow * this.difficultyAttributes.clockRate));
3968
- const { greatWindow, okWindow, mehWindow } = hitWindow;
3969
- // The probability that a player hits a circle is unknown, but we can estimate it to be
3970
- // the number of greats on circles divided by the number of circles, and then add one
3971
- // to the number of circles as a bias correction.
3972
- const n = Math.max(1, objectCount - relevantCountMiss - relevantCountMeh);
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
- // Proportion of greats hit on circles, ignoring misses and 50s.
3976
- const p = relevantCountGreat / n;
3977
- // We can be 99% confident that p is at least this value.
3978
- const pLowerBound = (n * p + (z * z) / 2) / (n + z * z) -
3979
- (z / (n + z * z)) * Math.sqrt(n * p * (1 - p) + (z * z) / 4);
3980
- // Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed.
3981
- // Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than:
3982
- let deviation = greatWindow / (Math.SQRT2 * osuBase.ErrorFunction.erfInv(pLowerBound));
3983
- const randomValue = (Math.sqrt(2 / Math.PI) *
3984
- okWindow *
3985
- Math.pow(Math.exp(-0.5 * (okWindow / deviation)), 2)) /
3986
- (deviation *
3987
- osuBase.ErrorFunction.erf(okWindow / (Math.SQRT2 * deviation)));
3988
- deviation *= Math.sqrt(1 - randomValue);
3989
- // Value deviation approach as greatCount approaches 0
3990
- const limitValue = okWindow / Math.sqrt(3);
3991
- // If precision is not enough to compute true deviation - use limit value
3992
- if (pLowerBound == 0.0 || randomValue >= 1 || deviation > limitValue) {
3993
- deviation = limitValue;
3994
- }
3995
- // Then compute the variance for mehs.
3996
- const mehVariance = (Math.pow(mehWindow, 2) +
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
- Math.pow(okWindow, 2)) /
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 = this.baseValue(this.difficultyAttributes.speedDifficulty);
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
- " acc, " +
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;