@rian8337/osu-difficulty-calculator 4.0.0-beta.83 → 4.0.0-beta.85

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