@rian8337/osu-difficulty-calculator 4.0.0-beta.12 → 4.0.0-beta.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ class AimEvaluator {
29
29
  }
30
30
 
31
31
  /**
32
- * Represents an osu!standard hit object with difficulty calculation values.
32
+ * Represents a hit object with difficulty calculation values.
33
33
  */
34
34
  class DifficultyHitObject {
35
35
  /**
@@ -62,41 +62,10 @@ class DifficultyHitObject {
62
62
  * The aim strain generated by the hitobject if sliders are not considered.
63
63
  */
64
64
  aimStrainWithoutSliders = 0;
65
- /**
66
- * The tap strain generated by the hitobject.
67
- *
68
- * This is also used for osu!standard as opposed to "speed strain".
69
- */
70
- tapStrain = 0;
71
- /**
72
- * The tap strain generated by the hitobject if `strainTime` isn't modified by
73
- * OD. This is used in three-finger detection.
74
- */
75
- originalTapStrain = 0;
76
65
  /**
77
66
  * The rhythm multiplier generated by the hitobject. This is used to alter tap strain.
78
67
  */
79
68
  rhythmMultiplier = 0;
80
- /**
81
- * The rhythm strain generated by the hitobject.
82
- */
83
- rhythmStrain = 0;
84
- /**
85
- * The flashlight strain generated by the hitobject if sliders are considered.
86
- */
87
- flashlightStrainWithSliders = 0;
88
- /**
89
- * The flashlight strain generated by the hitobject if sliders are not considered.
90
- */
91
- flashlightStrainWithoutSliders = 0;
92
- /**
93
- * The visual strain generated by the hitobject if sliders are considered.
94
- */
95
- visualStrainWithSliders = 0;
96
- /**
97
- * The visual strain generated by the hitobject if sliders are not considered.
98
- */
99
- visualStrainWithoutSliders = 0;
100
69
  /**
101
70
  * The normalized distance from the "lazy" end position of the previous hitobject to the start position of this hitobject.
102
71
  *
@@ -150,20 +119,6 @@ class DifficultyHitObject {
150
119
  * Adjusted end time of the hitobject, taking speed multiplier into account.
151
120
  */
152
121
  endTime = 0;
153
- /**
154
- * The note density of the hitobject.
155
- */
156
- noteDensity = 1;
157
- /**
158
- * The overlapping factor of the hitobject.
159
- *
160
- * This is used to scale visual skill.
161
- */
162
- overlappingFactor = 0;
163
- /**
164
- * Adjusted velocity of the hitobject, taking speed multiplier into account.
165
- */
166
- velocity = 0;
167
122
  /**
168
123
  * Other hitobjects in the beatmap, including this hitobject.
169
124
  */
@@ -200,7 +155,7 @@ class DifficultyHitObject {
200
155
  * difficulty hitobject's index, `null` if the index is out of range.
201
156
  */
202
157
  next(forwardsIndex) {
203
- return this.hitObjects[this.index + forwardsIndex + 2] ?? null;
158
+ return (this.hitObjects[this.index + forwardsIndex + 2] ?? null);
204
159
  }
205
160
  /**
206
161
  * Calculates the opacity of the hitobject at a given time.
@@ -261,6 +216,79 @@ class DifficultyHitObject {
261
216
  }
262
217
  }
263
218
 
219
+ /**
220
+ * Represents an osu!droid hit object with difficulty calculation values.
221
+ */
222
+ class DroidDifficultyHitObject extends DifficultyHitObject {
223
+ /**
224
+ * The tap strain generated by the hitobject.
225
+ */
226
+ tapStrain = 0;
227
+ /**
228
+ * The tap strain generated by the hitobject if `strainTime` isn't modified by
229
+ * OD. This is used in three-finger detection.
230
+ */
231
+ originalTapStrain = 0;
232
+ /**
233
+ * The rhythm strain generated by the hitobject.
234
+ */
235
+ rhythmStrain = 0;
236
+ /**
237
+ * The flashlight strain generated by the hitobject if sliders are considered.
238
+ */
239
+ flashlightStrainWithSliders = 0;
240
+ /**
241
+ * The flashlight strain generated by the hitobject if sliders are not considered.
242
+ */
243
+ flashlightStrainWithoutSliders = 0;
244
+ /**
245
+ * The visual strain generated by the hitobject if sliders are considered.
246
+ */
247
+ visualStrainWithSliders = 0;
248
+ /**
249
+ * The visual strain generated by the hitobject if sliders are not considered.
250
+ */
251
+ visualStrainWithoutSliders = 0;
252
+ /**
253
+ * The note density of the hitobject.
254
+ */
255
+ noteDensity = 1;
256
+ /**
257
+ * The overlapping factor of the hitobject.
258
+ *
259
+ * This is used to scale visual skill.
260
+ */
261
+ overlappingFactor = 0;
262
+ /**
263
+ * @param object The underlying hitobject.
264
+ * @param hitObjects All difficulty hitobjects in the processed beatmap.
265
+ */
266
+ constructor(object, hitObjects) {
267
+ super(object, hitObjects);
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Represents an osu!standard hit object with difficulty calculation values.
273
+ */
274
+ class OsuDifficultyHitObject extends DifficultyHitObject {
275
+ /**
276
+ * The speed strain generated by the hitobject.
277
+ */
278
+ speedStrain = 0;
279
+ /**
280
+ * The flashlight strain generated by this hitobject.
281
+ */
282
+ flashlightStrain = 0;
283
+ /**
284
+ * @param object The underlying hitobject.
285
+ * @param hitObjects All difficulty hitobjects in the processed beatmap.
286
+ */
287
+ constructor(object, hitObjects) {
288
+ super(object, hitObjects);
289
+ }
290
+ }
291
+
264
292
  /**
265
293
  * A converter used to convert normal hitobjects into difficulty hitobjects.
266
294
  */
@@ -284,9 +312,6 @@ class DifficultyHitObjectCreator {
284
312
  maximumSliderRadius = this.normalizedRadius * 2.4;
285
313
  assumedSliderRadius = this.normalizedRadius * 1.8;
286
314
  minDeltaTime = 25;
287
- /**
288
- * Generates difficulty hitobjects for difficulty calculation.
289
- */
290
315
  generateDifficultyObjects(params) {
291
316
  params.preempt ??= 600;
292
317
  this.mode = params.mode;
@@ -296,13 +321,13 @@ class DifficultyHitObjectCreator {
296
321
  const scalingFactor = this.getScalingFactor(params.objects[0].getRadius(this.mode));
297
322
  const difficultyObjects = [];
298
323
  for (let i = 0; i < params.objects.length; ++i) {
299
- const object = new DifficultyHitObject(params.objects[i], difficultyObjects);
324
+ const object = this.mode === osuBase.Modes.droid
325
+ ? new DroidDifficultyHitObject(params.objects[i], difficultyObjects)
326
+ : new OsuDifficultyHitObject(params.objects[i], difficultyObjects);
300
327
  object.index = difficultyObjects.length - 1;
301
328
  object.timePreempt = params.preempt;
302
329
  object.baseTimePreempt = params.preempt * params.speedMultiplier;
303
330
  if (object.object instanceof osuBase.Slider) {
304
- object.velocity =
305
- object.object.velocity * params.speedMultiplier;
306
331
  this.calculateSliderCursorPosition(object.object);
307
332
  object.travelDistance = object.object.lazyTravelDistance;
308
333
  // Bonus for repeat sliders until a better per nested object strain system can be achieved.
@@ -331,53 +356,57 @@ class DifficultyHitObjectCreator {
331
356
  difficultyObjects.push(object);
332
357
  continue;
333
358
  }
334
- // We'll have two visible object arrays. The first array contains objects before the current object starts in a reversed order,
335
- // while the second array contains objects after the current object ends.
336
- // For overlapping factor, we also need to consider previous visible objects.
337
- const prevVisibleObjects = [];
338
- const nextVisibleObjects = [];
339
- for (let j = i + 1; j < params.objects.length; ++j) {
340
- const o = params.objects[j];
341
- if (o instanceof osuBase.Spinner) {
342
- continue;
343
- }
344
- if (o.startTime / params.speedMultiplier >
345
- object.endTime + object.timePreempt) {
346
- break;
347
- }
348
- nextVisibleObjects.push(o);
349
- }
350
- for (let j = 0; j < object.index; ++j) {
351
- const prev = object.previous(j);
352
- if (prev.object instanceof osuBase.Spinner) {
353
- continue;
359
+ if (object instanceof DroidDifficultyHitObject) {
360
+ // We'll have two visible object arrays. The first array contains objects before the current object starts in a reversed order,
361
+ // while the second array contains objects after the current object ends.
362
+ // For overlapping factor, we also need to consider previous visible objects.
363
+ const prevVisibleObjects = [];
364
+ const nextVisibleObjects = [];
365
+ for (let j = i + 1; j < params.objects.length; ++j) {
366
+ const o = params.objects[j];
367
+ if (o instanceof osuBase.Spinner) {
368
+ continue;
369
+ }
370
+ if (o.startTime / params.speedMultiplier >
371
+ object.endTime + object.timePreempt) {
372
+ break;
373
+ }
374
+ nextVisibleObjects.push(o);
354
375
  }
355
- if (prev.startTime >= object.startTime) {
356
- continue;
376
+ for (let j = 0; j < object.index; ++j) {
377
+ const prev = object.previous(j);
378
+ if (prev.object instanceof osuBase.Spinner) {
379
+ continue;
380
+ }
381
+ if (prev.startTime >= object.startTime) {
382
+ continue;
383
+ }
384
+ if (prev.startTime <
385
+ object.startTime - object.timePreempt) {
386
+ break;
387
+ }
388
+ prevVisibleObjects.push(prev.object);
357
389
  }
358
- if (prev.startTime < object.startTime - object.timePreempt) {
359
- break;
390
+ for (const hitObject of prevVisibleObjects) {
391
+ const distance = object.object
392
+ .getStackedPosition(this.mode)
393
+ .getDistance(hitObject.getStackedEndPosition(this.mode));
394
+ const deltaTime = object.startTime -
395
+ hitObject.endTime / params.speedMultiplier;
396
+ this.applyToOverlappingFactor(object, distance, deltaTime);
360
397
  }
361
- prevVisibleObjects.push(prev.object);
362
- }
363
- for (const hitObject of prevVisibleObjects) {
364
- const distance = object.object
365
- .getStackedPosition(this.mode)
366
- .getDistance(hitObject.getStackedEndPosition(this.mode));
367
- const deltaTime = object.startTime -
368
- hitObject.endTime / params.speedMultiplier;
369
- this.applyToOverlappingFactor(object, distance, deltaTime);
370
- }
371
- for (const hitObject of nextVisibleObjects) {
372
- const distance = hitObject
373
- .getStackedPosition(this.mode)
374
- .getDistance(object.object.getStackedEndPosition(this.mode));
375
- const deltaTime = hitObject.startTime / params.speedMultiplier -
376
- object.endTime;
377
- if (deltaTime >= 0) {
378
- object.noteDensity += 1 - deltaTime / object.timePreempt;
398
+ for (const hitObject of nextVisibleObjects) {
399
+ const distance = hitObject
400
+ .getStackedPosition(this.mode)
401
+ .getDistance(object.object.getStackedEndPosition(this.mode));
402
+ const deltaTime = hitObject.startTime / params.speedMultiplier -
403
+ object.endTime;
404
+ if (deltaTime >= 0) {
405
+ object.noteDensity +=
406
+ 1 - deltaTime / object.timePreempt;
407
+ }
408
+ this.applyToOverlappingFactor(object, distance, deltaTime);
379
409
  }
380
- this.applyToOverlappingFactor(object, distance, deltaTime);
381
410
  }
382
411
  if (lastObject.object instanceof osuBase.Spinner) {
383
412
  difficultyObjects.push(object);
@@ -1606,6 +1635,31 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1606
1635
  difficultSliders: [],
1607
1636
  averageSpeedDeltaTime: 0,
1608
1637
  };
1638
+ get cacheableAttributes() {
1639
+ return {
1640
+ tapDifficulty: this.tap,
1641
+ rhythmDifficulty: this.rhythm,
1642
+ visualDifficulty: this.visual,
1643
+ mods: osuBase.ModUtil.modsToOsuString(this.attributes.mods),
1644
+ starRating: this.total,
1645
+ maxCombo: this.attributes.maxCombo,
1646
+ aimDifficulty: this.aim,
1647
+ flashlightDifficulty: this.flashlight,
1648
+ speedNoteCount: this.attributes.speedNoteCount,
1649
+ sliderFactor: this.attributes.sliderFactor,
1650
+ clockRate: this.attributes.clockRate,
1651
+ approachRate: this.attributes.approachRate,
1652
+ overallDifficulty: this.attributes.overallDifficulty,
1653
+ hitCircleCount: this.attributes.hitCircleCount,
1654
+ sliderCount: this.attributes.sliderCount,
1655
+ spinnerCount: this.attributes.spinnerCount,
1656
+ aimDifficultStrainCount: this.attributes.aimDifficultStrainCount,
1657
+ tapDifficultStrainCount: this.attributes.tapDifficultStrainCount,
1658
+ flashlightDifficultStrainCount: this.attributes.flashlightDifficultStrainCount,
1659
+ visualDifficultStrainCount: this.attributes.visualDifficultStrainCount,
1660
+ averageSpeedDeltaTime: this.attributes.averageSpeedDeltaTime,
1661
+ };
1662
+ }
1609
1663
  difficultyMultiplier = 0.18;
1610
1664
  mode = osuBase.Modes.droid;
1611
1665
  /**
@@ -1693,7 +1747,6 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1693
1747
  const visualSkillWithoutSliders = skills[8];
1694
1748
  this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
1695
1749
  this.postCalculateTap(tapSkillCheese);
1696
- this.calculateTapAttributes();
1697
1750
  this.postCalculateRhythm(rhythmSkill);
1698
1751
  this.postCalculateFlashlight(flashlightSkill, flashlightSkillWithoutSliders);
1699
1752
  this.postCalculateVisual(visualSkill, visualSkillWithoutSliders);
@@ -1992,6 +2045,10 @@ class PerformanceCalculator {
1992
2045
  * The calculated accuracy.
1993
2046
  */
1994
2047
  computedAccuracy = new osuBase.Accuracy({});
2048
+ /**
2049
+ * The difficulty attributes that is being calculated.
2050
+ */
2051
+ difficultyAttributes;
1995
2052
  /**
1996
2053
  * Penalty for combo breaks.
1997
2054
  */
@@ -2004,6 +2061,20 @@ class PerformanceCalculator {
2004
2061
  * Nerf factor used for nerfing beatmaps with very likely dropped sliderends.
2005
2062
  */
2006
2063
  sliderNerfFactor = 1;
2064
+ /**
2065
+ * @param difficultyAttributes The difficulty attributes to calculate.
2066
+ */
2067
+ constructor(difficultyAttributes) {
2068
+ if (this.isCacheableAttribute(difficultyAttributes)) {
2069
+ this.difficultyAttributes = {
2070
+ ...difficultyAttributes,
2071
+ mods: osuBase.ModUtil.pcStringToMods(difficultyAttributes.mods),
2072
+ };
2073
+ }
2074
+ else {
2075
+ this.difficultyAttributes = osuBase.Utils.deepCopy(difficultyAttributes);
2076
+ }
2077
+ }
2007
2078
  /**
2008
2079
  * Calculates the performance points of the beatmap.
2009
2080
  *
@@ -2127,6 +2198,15 @@ class PerformanceCalculator {
2127
2198
  }
2128
2199
  return Math.max(this.computedAccuracy.nmiss, comboBasedMissCount);
2129
2200
  }
2201
+ /**
2202
+ * Determines whether an attribute is a cacheable attribute.
2203
+ *
2204
+ * @param attributes The attributes to check.
2205
+ * @returns Whether the attributes are cacheable.
2206
+ */
2207
+ isCacheableAttribute(attributes) {
2208
+ return typeof attributes.mods === "string";
2209
+ }
2130
2210
  }
2131
2211
 
2132
2212
  /**
@@ -2197,7 +2277,6 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2197
2277
  get visualSliderCheesePenalty() {
2198
2278
  return this._visualSliderCheesePenalty;
2199
2279
  }
2200
- difficultyAttributes;
2201
2280
  finalMultiplier = 1.24;
2202
2281
  mode = osuBase.Modes.droid;
2203
2282
  _aimSliderCheesePenalty = 1;
@@ -2206,13 +2285,6 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2206
2285
  _tapPenalty = 1;
2207
2286
  _deviation = 0;
2208
2287
  _tapDeviation = 0;
2209
- /**
2210
- * @param difficultyAttributes The difficulty attributes to calculate.
2211
- */
2212
- constructor(difficultyAttributes) {
2213
- super();
2214
- this.difficultyAttributes = osuBase.Utils.deepCopy(difficultyAttributes);
2215
- }
2216
2288
  /**
2217
2289
  * Applies a tap penalty value to this calculator.
2218
2290
  *
@@ -3021,7 +3093,7 @@ class OsuSpeed extends OsuSkill {
3021
3093
  * @param current The hitobject to save to.
3022
3094
  */
3023
3095
  saveToHitObject(current) {
3024
- current.tapStrain = this.currentSpeedStrain * this.currentRhythm;
3096
+ current.speedStrain = this.currentSpeedStrain * this.currentRhythm;
3025
3097
  current.rhythmMultiplier = this.currentRhythm;
3026
3098
  }
3027
3099
  }
@@ -3134,7 +3206,7 @@ class OsuFlashlight extends OsuSkill {
3134
3206
  this.strainDecay(time - (current.previous(0)?.startTime ?? 0)));
3135
3207
  }
3136
3208
  saveToHitObject(current) {
3137
- current.flashlightStrainWithSliders = this.currentFlashlightStrain;
3209
+ current.flashlightStrain = this.currentFlashlightStrain;
3138
3210
  }
3139
3211
  }
3140
3212
 
@@ -3170,6 +3242,12 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3170
3242
  sliderCount: 0,
3171
3243
  spinnerCount: 0,
3172
3244
  };
3245
+ get cacheableAttributes() {
3246
+ return {
3247
+ ...this.attributes,
3248
+ mods: osuBase.ModUtil.modsToOsuString(this.attributes.mods),
3249
+ };
3250
+ }
3173
3251
  difficultyMultiplier = 0.0675;
3174
3252
  mode = osuBase.Modes.osu;
3175
3253
  /**
@@ -3309,7 +3387,7 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3309
3387
  * Calculates speed-related attributes.
3310
3388
  */
3311
3389
  calculateSpeedAttributes() {
3312
- const objectStrains = this.objects.map((v) => v.tapStrain);
3390
+ const objectStrains = this.objects.map((v) => v.speedStrain);
3313
3391
  const maxStrain = Math.max(...objectStrains);
3314
3392
  if (maxStrain) {
3315
3393
  this.attributes.speedNoteCount = objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
@@ -3394,7 +3472,6 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
3394
3472
  * The flashlight performance value.
3395
3473
  */
3396
3474
  flashlight = 0;
3397
- difficultyAttributes;
3398
3475
  finalMultiplier = 1.14;
3399
3476
  mode = osuBase.Modes.osu;
3400
3477
  calculateValues() {
@@ -3409,13 +3486,6 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
3409
3486
  Math.pow(this.accuracy, 1.1) +
3410
3487
  Math.pow(this.flashlight, 1.1), 1 / 1.1) * this.finalMultiplier);
3411
3488
  }
3412
- /**
3413
- * @param difficultyAttributes The difficulty attributes to calculate.
3414
- */
3415
- constructor(difficultyAttributes) {
3416
- super();
3417
- this.difficultyAttributes = osuBase.Utils.deepCopy(difficultyAttributes);
3418
- }
3419
3489
  /**
3420
3490
  * Calculates the aim performance value of the beatmap.
3421
3491
  */
@@ -3609,6 +3679,7 @@ exports.DifficultyHitObjectCreator = DifficultyHitObjectCreator;
3609
3679
  exports.DroidAim = DroidAim;
3610
3680
  exports.DroidAimEvaluator = DroidAimEvaluator;
3611
3681
  exports.DroidDifficultyCalculator = DroidDifficultyCalculator;
3682
+ exports.DroidDifficultyHitObject = DroidDifficultyHitObject;
3612
3683
  exports.DroidFlashlight = DroidFlashlight;
3613
3684
  exports.DroidFlashlightEvaluator = DroidFlashlightEvaluator;
3614
3685
  exports.DroidPerformanceCalculator = DroidPerformanceCalculator;
@@ -3623,6 +3694,7 @@ exports.MapStars = MapStars;
3623
3694
  exports.OsuAim = OsuAim;
3624
3695
  exports.OsuAimEvaluator = OsuAimEvaluator;
3625
3696
  exports.OsuDifficultyCalculator = OsuDifficultyCalculator;
3697
+ exports.OsuDifficultyHitObject = OsuDifficultyHitObject;
3626
3698
  exports.OsuFlashlight = OsuFlashlight;
3627
3699
  exports.OsuFlashlightEvaluator = OsuFlashlightEvaluator;
3628
3700
  exports.OsuPerformanceCalculator = OsuPerformanceCalculator;