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

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,31 +29,143 @@ class AimEvaluator {
29
29
  }
30
30
 
31
31
  /**
32
- * Represents a hit object with difficulty calculation values.
32
+ * The base of a difficulty calculator.
33
33
  */
34
- class DifficultyHitObject {
34
+ class DifficultyCalculator {
35
35
  /**
36
- * The underlying hitobject.
36
+ * The calculated beatmap.
37
37
  */
38
- object;
38
+ beatmap;
39
39
  /**
40
- * The index of this hitobject in the list of all hitobjects.
40
+ * The difficulty objects of the beatmap.
41
+ */
42
+ objects = [];
43
+ /**
44
+ * The modifications applied.
45
+ */
46
+ mods = [];
47
+ /**
48
+ * The total star rating of the beatmap.
49
+ */
50
+ get total() {
51
+ return this.attributes.starRating;
52
+ }
53
+ /**
54
+ * The difficulty statistics of the beatmap after modifications are applied.
55
+ */
56
+ difficultyStatistics;
57
+ /**
58
+ * The strain peaks of various calculated difficulties.
59
+ */
60
+ strainPeaks = {
61
+ aimWithSliders: [],
62
+ aimWithoutSliders: [],
63
+ speed: [],
64
+ flashlight: [],
65
+ };
66
+ /**
67
+ * Constructs a new instance of the calculator.
41
68
  *
42
- * This is one less than the actual index of the hitobject in the beatmap.
69
+ * @param beatmap The beatmap to calculate. This beatmap will be deep-cloned to prevent reference changes.
43
70
  */
44
- index = 0;
71
+ constructor(beatmap) {
72
+ this.beatmap = beatmap;
73
+ this.difficultyStatistics = {
74
+ circleSize: beatmap.difficulty.cs,
75
+ approachRate: beatmap.difficulty.ar ?? beatmap.difficulty.od,
76
+ overallDifficulty: beatmap.difficulty.od,
77
+ healthDrain: beatmap.difficulty.hp,
78
+ overallSpeedMultiplier: 1,
79
+ };
80
+ }
45
81
  /**
46
- * The preempt time of the hitobject.
82
+ * Calculates the star rating of the specified beatmap.
83
+ *
84
+ * The beatmap is analyzed in chunks of `sectionLength` duration.
85
+ * For each chunk the highest hitobject strains are added to
86
+ * a list which is then collapsed into a weighted sum, much
87
+ * like scores are weighted on a user's profile.
88
+ *
89
+ * For subsequent chunks, the initial max strain is calculated
90
+ * by decaying the previous hitobject's strain until the
91
+ * beginning of the new chunk.
92
+ *
93
+ * @param options Options for the difficulty calculation.
94
+ * @returns The current instance.
47
95
  */
48
- baseTimePreempt = 600;
96
+ calculate(options) {
97
+ this.mods = options?.mods ?? [];
98
+ const converted = new osuBase.BeatmapConverter(this.beatmap).convert({
99
+ mode: this.mode,
100
+ mods: this.mods,
101
+ customSpeedMultiplier: options?.customSpeedMultiplier,
102
+ });
103
+ this.difficultyStatistics = Object.seal(this.computeDifficultyStatistics(options));
104
+ this.populateDifficultyAttributes();
105
+ this.objects.push(...this.generateDifficultyHitObjects(converted));
106
+ this.calculateAll();
107
+ return this;
108
+ }
49
109
  /**
50
- * Adjusted preempt time of the hitobject, taking speed multiplier into account.
110
+ * Calculates the skills provided.
111
+ *
112
+ * @param skills The skills to calculate.
51
113
  */
52
- timePreempt = 600;
114
+ calculateSkills(...skills) {
115
+ // The first object doesn't generate a strain, so we begin calculating from the second object.
116
+ for (const object of this.objects.slice(1)) {
117
+ for (const skill of skills) {
118
+ skill.process(object);
119
+ }
120
+ }
121
+ }
53
122
  /**
54
- * The fade in time of the hitobject.
123
+ * Populates the stored difficulty attributes with necessary data.
55
124
  */
56
- timeFadeIn = 400;
125
+ populateDifficultyAttributes() {
126
+ this.attributes.approachRate = this.difficultyStatistics.approachRate;
127
+ this.attributes.hitCircleCount = this.beatmap.hitObjects.circles;
128
+ this.attributes.maxCombo = this.beatmap.maxCombo;
129
+ this.attributes.mods = this.mods.slice();
130
+ this.attributes.overallDifficulty =
131
+ this.difficultyStatistics.overallDifficulty;
132
+ this.attributes.sliderCount = this.beatmap.hitObjects.sliders;
133
+ this.attributes.spinnerCount = this.beatmap.hitObjects.spinners;
134
+ this.attributes.clockRate =
135
+ this.difficultyStatistics.overallSpeedMultiplier;
136
+ }
137
+ /**
138
+ * Calculates the star rating value of a difficulty.
139
+ *
140
+ * @param difficulty The difficulty to calculate.
141
+ */
142
+ starValue(difficulty) {
143
+ return Math.sqrt(difficulty) * this.difficultyMultiplier;
144
+ }
145
+ /**
146
+ * Calculates the base performance value of a difficulty rating.
147
+ *
148
+ * @param rating The difficulty rating.
149
+ */
150
+ basePerformanceValue(rating) {
151
+ return Math.pow(5 * Math.max(1, rating / 0.0675) - 4, 3) / 100000;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Represents a hit object with difficulty calculation values.
157
+ */
158
+ class DifficultyHitObject {
159
+ /**
160
+ * The underlying hitobject.
161
+ */
162
+ object;
163
+ /**
164
+ * The index of this hitobject in the list of all hitobjects.
165
+ *
166
+ * This is one less than the actual index of the hitobject in the beatmap.
167
+ */
168
+ index;
57
169
  /**
58
170
  * The aim strain generated by the hitobject if sliders are considered.
59
171
  */
@@ -106,30 +218,71 @@ class DifficultyHitObject {
106
218
  /**
107
219
  * The amount of milliseconds elapsed between this hitobject and the last hitobject.
108
220
  */
109
- deltaTime = 0;
221
+ deltaTime;
110
222
  /**
111
223
  * The amount of milliseconds elapsed since the start time of the previous hitobject, with a minimum of 25ms.
112
224
  */
113
- strainTime = 0;
225
+ strainTime;
114
226
  /**
115
227
  * Adjusted start time of the hitobject, taking speed multiplier into account.
116
228
  */
117
- startTime = 0;
229
+ startTime;
118
230
  /**
119
231
  * Adjusted end time of the hitobject, taking speed multiplier into account.
120
232
  */
121
- endTime = 0;
233
+ endTime;
122
234
  /**
123
235
  * Other hitobjects in the beatmap, including this hitobject.
124
236
  */
125
237
  hitObjects;
238
+ normalizedRadius = 50;
239
+ maximumSliderRadius = this.normalizedRadius * 2.4;
240
+ assumedSliderRadius = this.normalizedRadius * 1.8;
241
+ minDeltaTime = 25;
242
+ lastObject;
243
+ lastLastObject;
126
244
  /**
245
+ * Note: You **must** call `computeProperties` at some point due to how TypeScript handles
246
+ * overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue.).
247
+ *
127
248
  * @param object The underlying hitobject.
128
- * @param hitObjects All difficulty hitobjects in the processed beatmap.
129
- */
130
- constructor(object, hitObjects) {
249
+ * @param lastObject The hitobject before this hitobject.
250
+ * @param lastLastObject The hitobject before the last hitobject.
251
+ * @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
252
+ * @param clockRate The clock rate of the beatmap.
253
+ * @param timePreempt The time preempt with clock rate.
254
+ * @param isForceAR Whether force AR is enabled.
255
+ * @param mode The gamemode to compute properties for.
256
+ */
257
+ constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate) {
131
258
  this.object = object;
132
- this.hitObjects = hitObjects;
259
+ this.lastObject = lastObject;
260
+ this.lastLastObject = lastLastObject;
261
+ this.hitObjects = difficultyHitObjects;
262
+ this.index = difficultyHitObjects.length - 1;
263
+ // Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
264
+ this.startTime = object.startTime / clockRate;
265
+ this.endTime = object.endTime / clockRate;
266
+ if (lastObject) {
267
+ this.deltaTime = this.startTime - lastObject.startTime / clockRate;
268
+ this.strainTime = Math.max(this.deltaTime, this.minDeltaTime);
269
+ }
270
+ else {
271
+ this.deltaTime = 0;
272
+ this.strainTime = 0;
273
+ }
274
+ }
275
+ /**
276
+ * Computes the properties of this hitobject.
277
+ *
278
+ * @param clockRate The clock rate of the beatmap.
279
+ * @param hitObjects The hitobjects in the beatmap.
280
+ */
281
+ computeProperties(clockRate,
282
+ // Required for `DroidDifficultyHitObject` override.
283
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
284
+ hitObjects) {
285
+ this.setDistances(clockRate);
133
286
  }
134
287
  /**
135
288
  * Gets the difficulty hitobject at a specific index with respect to the current
@@ -171,322 +324,88 @@ class DifficultyHitObject {
171
324
  // but this is an approximation and such a case is unlikely to be hit where this function is used.
172
325
  return 0;
173
326
  }
174
- const fadeInStartTime = this.object.startTime - this.baseTimePreempt;
175
- const fadeInDuration = this.timeFadeIn;
327
+ const fadeInStartTime = this.object.startTime - this.object.timePreempt;
328
+ const fadeInDuration = this.object.timeFadeIn;
176
329
  if (isHidden) {
177
330
  const fadeOutStartTime = fadeInStartTime + fadeInDuration;
178
- const fadeOutDuration = this.baseTimePreempt * osuBase.ModHidden.fadeOutDurationMultiplier;
331
+ const fadeOutDuration = this.object.timePreempt * osuBase.ModHidden.fadeOutDurationMultiplier;
179
332
  return Math.min(osuBase.MathUtils.clamp((time - fadeInStartTime) / fadeInDuration, 0, 1), 1 -
180
333
  osuBase.MathUtils.clamp((time - fadeOutStartTime) / fadeOutDuration, 0, 1));
181
334
  }
182
335
  return osuBase.MathUtils.clamp((time - fadeInStartTime) / fadeInDuration, 0, 1);
183
336
  }
184
- /**
185
- * Determines whether this hitobject is considered overlapping with the hitobject before it.
186
- *
187
- * Keep in mind that "overlapping" in this case is overlapping to the point where both hitobjects
188
- * can be hit with just a single tap in osu!droid.
189
- *
190
- * @param considerDistance Whether to consider the distance between both hitobjects.
191
- * @returns Whether the hitobject is considered overlapping.
192
- */
193
- isOverlapping(considerDistance) {
194
- if (this.object instanceof osuBase.Spinner) {
195
- return false;
196
- }
197
- const previous = this.previous(0);
198
- if (!previous || previous.object instanceof osuBase.Spinner) {
199
- return false;
200
- }
201
- if (this.deltaTime >= 5) {
202
- return false;
203
- }
204
- if (considerDistance) {
205
- const endPosition = this.object.getStackedPosition(osuBase.Modes.droid);
206
- let distance = previous.object
207
- .getStackedEndPosition(osuBase.Modes.droid)
208
- .getDistance(endPosition);
209
- if (previous.object instanceof osuBase.Slider &&
210
- previous.object.lazyEndPosition) {
211
- distance = Math.min(distance, previous.object.lazyEndPosition.getDistance(endPosition));
337
+ setDistances(clockRate) {
338
+ if (this.object instanceof osuBase.Slider) {
339
+ this.calculateSliderCursorPosition(this.object);
340
+ this.travelDistance = this.object.lazyTravelDistance;
341
+ // Bonus for repeat sliders until a better per nested object strain system can be achieved.
342
+ if (this.mode === osuBase.Modes.droid) {
343
+ this.travelDistance *= Math.pow(1 + this.object.repeatCount / 4, 1 / 4);
344
+ }
345
+ else {
346
+ this.travelDistance *= Math.pow(1 + this.object.repeatCount / 2.5, 1 / 2.5);
212
347
  }
213
- return distance <= 2 * this.object.getRadius(osuBase.Modes.droid);
348
+ this.travelTime = Math.max(this.object.lazyTravelTime / clockRate, this.minDeltaTime);
214
349
  }
215
- return true;
216
- }
217
- }
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
-
292
- /**
293
- * A converter used to convert normal hitobjects into difficulty hitobjects.
294
- */
295
- class DifficultyHitObjectCreator {
296
- /**
297
- * The threshold for small circle buff for osu!droid.
298
- */
299
- DROID_CIRCLESIZE_BUFF_THRESHOLD = 70;
300
- /**
301
- * The threshold for small circle buff for osu!standard.
302
- */
303
- PC_CIRCLESIZE_BUFF_THRESHOLD = 30;
304
- /**
305
- * The gamemode this creator is creating for.
306
- */
307
- mode = osuBase.Modes.osu;
308
- /**
309
- * The base normalized radius of hitobjects.
310
- */
311
- normalizedRadius = 50;
312
- maximumSliderRadius = this.normalizedRadius * 2.4;
313
- assumedSliderRadius = this.normalizedRadius * 1.8;
314
- minDeltaTime = 25;
315
- generateDifficultyObjects(params) {
316
- params.preempt ??= 600;
317
- this.mode = params.mode;
318
- if (this.mode === osuBase.Modes.droid) {
319
- this.maximumSliderRadius = this.normalizedRadius * 2;
350
+ // We don't need to calculate either angle or distance when one of the last->curr objects is a spinner.
351
+ if (!this.lastObject ||
352
+ this.object instanceof osuBase.Spinner ||
353
+ this.lastObject instanceof osuBase.Spinner) {
354
+ return;
320
355
  }
321
- const scalingFactor = this.getScalingFactor(params.objects[0].getRadius(this.mode));
322
- const difficultyObjects = [];
323
- for (let i = 0; i < params.objects.length; ++i) {
324
- const object = this.mode === osuBase.Modes.droid
325
- ? new DroidDifficultyHitObject(params.objects[i], difficultyObjects)
326
- : new OsuDifficultyHitObject(params.objects[i], difficultyObjects);
327
- object.index = difficultyObjects.length - 1;
328
- object.timePreempt = params.preempt;
329
- object.baseTimePreempt = params.preempt * params.speedMultiplier;
330
- if (object.object instanceof osuBase.Slider) {
331
- this.calculateSliderCursorPosition(object.object);
332
- object.travelDistance = object.object.lazyTravelDistance;
333
- // Bonus for repeat sliders until a better per nested object strain system can be achieved.
334
- if (this.mode === osuBase.Modes.droid) {
335
- object.travelDistance *= Math.pow(1 + object.object.repeats / 4, 1 / 4);
336
- }
337
- else {
338
- object.travelDistance *= Math.pow(1 + object.object.repeats / 2.5, 1 / 2.5);
339
- }
340
- object.travelTime = Math.max(object.object.lazyTravelTime / params.speedMultiplier, this.minDeltaTime);
341
- }
342
- const lastObject = difficultyObjects[i - 1];
343
- const lastLastObject = difficultyObjects[i - 2];
344
- object.startTime = object.object.startTime / params.speedMultiplier;
345
- object.endTime = object.object.endTime / params.speedMultiplier;
346
- if (!lastObject) {
347
- difficultyObjects.push(object);
348
- continue;
349
- }
350
- object.deltaTime =
351
- (object.object.startTime - lastObject.object.startTime) /
352
- params.speedMultiplier;
353
- // Cap to 25ms to prevent difficulty calculation breaking from simultaneous objects.
354
- object.strainTime = Math.max(this.minDeltaTime, object.deltaTime);
355
- if (object.object instanceof osuBase.Spinner) {
356
- difficultyObjects.push(object);
357
- continue;
358
- }
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);
375
- }
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);
389
- }
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);
397
- }
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);
409
- }
410
- }
411
- if (lastObject.object instanceof osuBase.Spinner) {
412
- difficultyObjects.push(object);
413
- continue;
414
- }
415
- const lastCursorPosition = this.getEndCursorPosition(lastObject.object);
416
- object.lazyJumpDistance = object.object
356
+ // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
357
+ const { scalingFactor } = this;
358
+ const lastCursorPosition = this.getEndCursorPosition(this.lastObject);
359
+ this.lazyJumpDistance = this.object
360
+ .getStackedPosition(this.mode)
361
+ .scale(scalingFactor)
362
+ .subtract(lastCursorPosition.scale(scalingFactor)).length;
363
+ this.minimumJumpTime = this.strainTime;
364
+ this.minimumJumpDistance = this.lazyJumpDistance;
365
+ if (this.lastObject instanceof osuBase.Slider) {
366
+ const lastTravelTime = Math.max(this.lastObject.lazyTravelTime / clockRate, this.minDeltaTime);
367
+ this.minimumJumpTime = Math.max(this.strainTime - lastTravelTime, this.minDeltaTime);
368
+ // There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects.
369
+ //
370
+ // 1. The anti-flow pattern, where players cut the slider short in order to move to the next hitobject.
371
+ //
372
+ // <======o==> ← slider
373
+ // | ← most natural jump path
374
+ // o ← a follow-up hitcircle
375
+ //
376
+ // In this case the most natural jump path is approximated by LazyJumpDistance.
377
+ //
378
+ // 2. The flow pattern, where players follow through the slider to its visual extent into the next hitobject.
379
+ //
380
+ // <======o==>---o
381
+ // ↑
382
+ // most natural jump path
383
+ //
384
+ // In this case the most natural jump path is better approximated by a new distance called "tailJumpDistance" - the distance between the slider's tail and the next hitobject.
385
+ //
386
+ // Thus, the player is assumed to jump the minimum of these two distances in all cases.
387
+ const tailJumpDistance = this.lastObject.tail
417
388
  .getStackedPosition(this.mode)
418
- .scale(scalingFactor)
419
- .subtract(lastCursorPosition.scale(scalingFactor)).length;
420
- object.minimumJumpTime = object.strainTime;
421
- object.minimumJumpDistance = object.lazyJumpDistance;
422
- if (lastObject.object instanceof osuBase.Slider) {
423
- object.minimumJumpTime = Math.max(object.strainTime - lastObject.travelTime, this.minDeltaTime);
424
- // There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects.
425
- //
426
- // 1. The anti-flow pattern, where players cut the slider short in order to move to the next hitobject.
427
- //
428
- // <======o==> ← slider
429
- // | ← most natural jump path
430
- // o ← a follow-up hitcircle
431
- //
432
- // In this case the most natural jump path is approximated by LazyJumpDistance.
433
- //
434
- // 2. The flow pattern, where players follow through the slider to its visual extent into the next hitobject.
435
- //
436
- // <======o==>---o
437
- // ↑
438
- // most natural jump path
439
- //
440
- // In this case the most natural jump path is better approximated by a new distance called "tailJumpDistance" - the distance between the slider's tail and the next hitobject.
441
- //
442
- // Thus, the player is assumed to jump the minimum of these two distances in all cases.
443
- const tailJumpDistance = lastObject.object.tail
444
- .getStackedPosition(this.mode)
445
- .subtract(object.object.getStackedPosition(this.mode))
446
- .length * scalingFactor;
447
- object.minimumJumpDistance = Math.max(0, Math.min(object.lazyJumpDistance -
448
- (this.maximumSliderRadius -
449
- this.assumedSliderRadius), tailJumpDistance - this.maximumSliderRadius));
450
- }
451
- if (lastLastObject && !(lastLastObject.object instanceof osuBase.Spinner)) {
452
- const lastLastCursorPosition = this.getEndCursorPosition(lastLastObject.object);
453
- const v1 = lastLastCursorPosition.subtract(lastObject.object.getStackedPosition(this.mode));
454
- const v2 = object.object
455
- .getStackedPosition(this.mode)
456
- .subtract(lastCursorPosition);
457
- const dot = v1.dot(v2);
458
- const det = v1.x * v2.y - v1.y * v2.x;
459
- object.angle = Math.abs(Math.atan2(det, dot));
460
- }
461
- difficultyObjects.push(object);
389
+ .subtract(this.object.getStackedPosition(this.mode))
390
+ .length * scalingFactor;
391
+ this.minimumJumpDistance = Math.max(0, Math.min(this.lazyJumpDistance -
392
+ (this.maximumSliderRadius - this.assumedSliderRadius), tailJumpDistance - this.maximumSliderRadius));
393
+ }
394
+ if (this.lastLastObject && !(this.lastLastObject instanceof osuBase.Spinner)) {
395
+ const lastLastCursorPosition = this.getEndCursorPosition(this.lastLastObject);
396
+ const v1 = lastLastCursorPosition.subtract(this.lastObject.getStackedPosition(this.mode));
397
+ const v2 = this.object
398
+ .getStackedPosition(this.mode)
399
+ .subtract(lastCursorPosition);
400
+ const dot = v1.dot(v2);
401
+ const det = v1.x * v2.y - v1.y * v2.x;
402
+ this.angle = Math.abs(Math.atan2(det, dot));
462
403
  }
463
- return difficultyObjects;
464
404
  }
465
- /**
466
- * Calculates a slider's cursor position.
467
- */
468
405
  calculateSliderCursorPosition(slider) {
469
406
  if (slider.lazyEndPosition) {
470
407
  return;
471
408
  }
472
- // Droid doesn't have a legacy slider tail. Since beatmap parser defaults slider tail
473
- // to legacy slider tail, it needs to be changed to real slider tail first.
474
- if (this.mode === osuBase.Modes.droid) {
475
- slider.tail.startTime += osuBase.Slider.legacyLastTickOffset;
476
- slider.tail.endTime += osuBase.Slider.legacyLastTickOffset;
477
- slider.nestedHitObjects.sort((a, b) => a.startTime - b.startTime);
478
- // Temporary lazy end position until a real result can be derived.
479
- slider.lazyEndPosition = slider.getStackedPosition(this.mode);
480
- // Stop here if the slider has too short duration due to float number limitation.
481
- // Incredibly close start and end time fluctuates travel distance and lazy
482
- // end position heavily, which we do not want to happen.
483
- //
484
- // In the real game, this shouldn't happen. Perhaps we need to reinvestigate this
485
- // in the future.
486
- if (osuBase.Precision.almostEqualsNumber(slider.startTime, slider.endTime)) {
487
- return;
488
- }
489
- }
490
409
  // Not using slider.endTime due to legacy last tick offset.
491
410
  slider.lazyTravelTime =
492
411
  slider.nestedHitObjects.at(-1).startTime - slider.startTime;
@@ -502,228 +421,52 @@ class DifficultyHitObjectCreator {
502
421
  .getStackedPosition(this.mode)
503
422
  .add(slider.path.positionAt(endTimeMin));
504
423
  let currentCursorPosition = slider.getStackedPosition(this.mode);
505
- const scalingFactor = this.normalizedRadius / slider.getRadius(this.mode);
424
+ const scalingFactor = this.normalizedRadius / slider.radius;
506
425
  for (let i = 1; i < slider.nestedHitObjects.length; ++i) {
507
426
  const currentMovementObject = slider.nestedHitObjects[i];
508
427
  let currentMovement = currentMovementObject
509
428
  .getStackedPosition(this.mode)
510
429
  .subtract(currentCursorPosition);
511
430
  let currentMovementLength = scalingFactor * currentMovement.length;
512
- // The amount of movement required so that the cursor position needs to be updated.
513
- let requiredMovement = this.assumedSliderRadius;
514
- if (i === slider.nestedHitObjects.length - 1) {
515
- // The end of a slider has special aim rules due to the relaxed time constraint on position.
516
- // There is both a lazy end position as well as the actual end slider position. We assume the player takes the simpler movement.
517
- // For sliders that are circular, the lazy end position may actually be farther away than the sliders' true end.
518
- // This code is designed to prevent buffing situations where lazy end is actually a less efficient movement.
519
- const lazyMovement = slider.lazyEndPosition.subtract(currentCursorPosition);
520
- if (lazyMovement.length < currentMovement.length) {
521
- currentMovement = lazyMovement;
522
- }
523
- currentMovementLength = scalingFactor * currentMovement.length;
524
- }
525
- else if (currentMovementObject instanceof osuBase.SliderRepeat) {
526
- // For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.
527
- requiredMovement = this.normalizedRadius;
528
- }
529
- if (currentMovementLength > requiredMovement) {
530
- // This finds the positional delta from the required radius and the current position,
531
- // and updates the currentCursorPosition accordingly, as well as rewarding distance.
532
- currentCursorPosition = currentCursorPosition.add(currentMovement.scale((currentMovementLength - requiredMovement) /
533
- currentMovementLength));
534
- currentMovementLength *=
535
- (currentMovementLength - requiredMovement) /
536
- currentMovementLength;
537
- slider.lazyTravelDistance += currentMovementLength;
538
- }
539
- if (i === slider.nestedHitObjects.length - 1) {
540
- slider.lazyEndPosition = currentCursorPosition;
541
- }
542
- }
543
- }
544
- /**
545
- * Gets the scaling factor of a radius.
546
- *
547
- * @param radius The radius to get the scaling factor from.
548
- */
549
- getScalingFactor(radius) {
550
- // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
551
- let scalingFactor = this.normalizedRadius / radius;
552
- // High circle size (small CS) bonus
553
- switch (this.mode) {
554
- case osuBase.Modes.droid:
555
- if (radius < this.DROID_CIRCLESIZE_BUFF_THRESHOLD) {
556
- scalingFactor *=
557
- 1 +
558
- Math.pow((this.DROID_CIRCLESIZE_BUFF_THRESHOLD - radius) /
559
- 50, 2);
560
- }
561
- break;
562
- case osuBase.Modes.osu:
563
- if (radius < this.PC_CIRCLESIZE_BUFF_THRESHOLD) {
564
- scalingFactor *=
565
- 1 +
566
- Math.min(this.PC_CIRCLESIZE_BUFF_THRESHOLD - radius, 5) /
567
- 50;
568
- }
569
- }
570
- return scalingFactor;
571
- }
572
- /**
573
- * Returns the end cursor position of a hitobject.
574
- */
575
- getEndCursorPosition(object) {
576
- let pos = object.getStackedPosition(this.mode);
577
- if (object instanceof osuBase.Slider) {
578
- this.calculateSliderCursorPosition(object);
579
- pos = object.lazyEndPosition ?? pos;
580
- }
581
- return pos;
582
- }
583
- applyToOverlappingFactor(object, distance, deltaTime) {
584
- // Penalize objects that are too close to the object in both distance
585
- // and delta time to prevent stream maps from being overweighted.
586
- object.overlappingFactor +=
587
- Math.max(0, 1 - distance / (3 * object.object.getRadius(this.mode))) *
588
- (7.5 /
589
- (1 +
590
- Math.exp(0.15 * (Math.max(deltaTime, this.minDeltaTime) - 75))));
591
- }
592
- }
593
-
594
- /**
595
- * The base of a difficulty calculator.
596
- */
597
- class DifficultyCalculator {
598
- /**
599
- * The calculated beatmap.
600
- */
601
- beatmap;
602
- /**
603
- * The difficulty objects of the beatmap.
604
- */
605
- objects = [];
606
- /**
607
- * The modifications applied.
608
- */
609
- mods = [];
610
- /**
611
- * The total star rating of the beatmap.
612
- */
613
- total = 0;
614
- /**
615
- * The map statistics of the beatmap after modifications are applied.
616
- */
617
- stats = new osuBase.MapStats();
618
- /**
619
- * The strain peaks of various calculated difficulties.
620
- */
621
- strainPeaks = {
622
- aimWithSliders: [],
623
- aimWithoutSliders: [],
624
- speed: [],
625
- flashlight: [],
626
- };
627
- /**
628
- * Constructs a new instance of the calculator.
629
- *
630
- * @param beatmap The beatmap to calculate. This beatmap will be deep-cloned to prevent reference changes.
631
- */
632
- constructor(beatmap) {
633
- this.beatmap = osuBase.Utils.deepCopy(beatmap);
634
- }
635
- /**
636
- * Calculates the star rating of the specified beatmap.
637
- *
638
- * The beatmap is analyzed in chunks of `sectionLength` duration.
639
- * For each chunk the highest hitobject strains are added to
640
- * a list which is then collapsed into a weighted sum, much
641
- * like scores are weighted on a user's profile.
642
- *
643
- * For subsequent chunks, the initial max strain is calculated
644
- * by decaying the previous hitobject's strain until the
645
- * beginning of the new chunk.
646
- *
647
- * @param options Options for the difficulty calculation.
648
- * @returns The current instance.
649
- */
650
- calculate(options) {
651
- this.mods = options?.mods ?? [];
652
- this.stats = new osuBase.MapStats({
653
- cs: this.beatmap.difficulty.cs,
654
- ar: this.beatmap.difficulty.ar,
655
- od: this.beatmap.difficulty.od,
656
- hp: this.beatmap.difficulty.hp,
657
- mods: options?.mods,
658
- speedMultiplier: options?.stats?.speedMultiplier ?? 1,
659
- oldStatistics: options?.stats?.oldStatistics ?? false,
660
- }).calculate({ mode: this.mode });
661
- this.preProcess();
662
- this.populateDifficultyAttributes();
663
- this.generateDifficultyHitObjects();
664
- this.calculateAll();
665
- return this;
666
- }
667
- /**
668
- * Generates difficulty hitobjects for this calculator.
669
- */
670
- generateDifficultyHitObjects() {
671
- this.objects.length = 0;
672
- this.objects.push(...new DifficultyHitObjectCreator().generateDifficultyObjects({
673
- objects: this.beatmap.hitObjects.objects,
674
- circleSize: this.beatmap.difficulty.cs,
675
- mods: this.mods,
676
- speedMultiplier: this.stats.speedMultiplier,
677
- mode: this.mode,
678
- preempt: osuBase.MapStats.arToMS(this.stats.ar),
679
- }));
680
- }
681
- /**
682
- * Performs some pre-processing before proceeding with difficulty calculation.
683
- */
684
- preProcess() {
685
- }
686
- /**
687
- * Calculates the skills provided.
688
- *
689
- * @param skills The skills to calculate.
690
- */
691
- calculateSkills(...skills) {
692
- // The first object doesn't generate a strain, so we begin calculating from the second object.
693
- for (const object of this.objects.slice(1)) {
694
- for (const skill of skills) {
695
- skill.process(object);
431
+ // The amount of movement required so that the cursor position needs to be updated.
432
+ let requiredMovement = this.assumedSliderRadius;
433
+ if (i === slider.nestedHitObjects.length - 1) {
434
+ // The end of a slider has special aim rules due to the relaxed time constraint on position.
435
+ // There is both a lazy end position as well as the actual end slider position. We assume the player takes the simpler movement.
436
+ // For sliders that are circular, the lazy end position may actually be farther away than the sliders' true end.
437
+ // This code is designed to prevent buffing situations where lazy end is actually a less efficient movement.
438
+ const lazyMovement = slider.lazyEndPosition.subtract(currentCursorPosition);
439
+ if (lazyMovement.length < currentMovement.length) {
440
+ currentMovement = lazyMovement;
441
+ }
442
+ currentMovementLength = scalingFactor * currentMovement.length;
443
+ }
444
+ else if (currentMovementObject instanceof osuBase.SliderRepeat) {
445
+ // For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.
446
+ requiredMovement = this.normalizedRadius;
447
+ }
448
+ if (currentMovementLength > requiredMovement) {
449
+ // This finds the positional delta from the required radius and the current position,
450
+ // and updates the currentCursorPosition accordingly, as well as rewarding distance.
451
+ currentCursorPosition = currentCursorPosition.add(currentMovement.scale((currentMovementLength - requiredMovement) /
452
+ currentMovementLength));
453
+ currentMovementLength *=
454
+ (currentMovementLength - requiredMovement) /
455
+ currentMovementLength;
456
+ slider.lazyTravelDistance += currentMovementLength;
457
+ }
458
+ if (i === slider.nestedHitObjects.length - 1) {
459
+ slider.lazyEndPosition = currentCursorPosition;
696
460
  }
697
461
  }
698
462
  }
699
- /**
700
- * Populates the stored difficulty attributes with necessary data.
701
- */
702
- populateDifficultyAttributes() {
703
- this.attributes.approachRate = this.stats.ar;
704
- this.attributes.hitCircleCount = this.beatmap.hitObjects.circles;
705
- this.attributes.maxCombo = this.beatmap.maxCombo;
706
- this.attributes.mods = this.mods.slice();
707
- this.attributes.overallDifficulty = this.stats.od;
708
- this.attributes.sliderCount = this.beatmap.hitObjects.sliders;
709
- this.attributes.spinnerCount = this.beatmap.hitObjects.spinners;
710
- this.attributes.clockRate = this.stats.speedMultiplier;
711
- }
712
- /**
713
- * Calculates the star rating value of a difficulty.
714
- *
715
- * @param difficulty The difficulty to calculate.
716
- */
717
- starValue(difficulty) {
718
- return Math.sqrt(difficulty) * this.difficultyMultiplier;
719
- }
720
- /**
721
- * Calculates the base performance value of a difficulty rating.
722
- *
723
- * @param rating The difficulty rating.
724
- */
725
- basePerformanceValue(rating) {
726
- return Math.pow(5 * Math.max(1, rating / 0.0675) - 4, 3) / 100000;
463
+ getEndCursorPosition(object) {
464
+ let pos = object.getStackedPosition(this.mode);
465
+ if (object instanceof osuBase.Slider) {
466
+ this.calculateSliderCursorPosition(object);
467
+ pos = object.lazyEndPosition ?? pos;
468
+ }
469
+ return pos;
727
470
  }
728
471
  }
729
472
 
@@ -1163,7 +906,7 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
1163
906
  current.isOverlapping(true)) {
1164
907
  return 0;
1165
908
  }
1166
- const scalingFactor = 52 / current.object.getRadius(osuBase.Modes.droid);
909
+ const scalingFactor = 52 / current.object.radius;
1167
910
  let smallDistNerf = 1;
1168
911
  let cumulativeStrainTime = 0;
1169
912
  let result = 0;
@@ -1219,8 +962,8 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
1219
962
  // Longer sliders require more memorization.
1220
963
  sliderBonus *= pixelTravelDistance;
1221
964
  // Nerf sliders with repeats, as less memorization is required.
1222
- if (current.object.repeats > 0)
1223
- sliderBonus /= current.object.repeats + 1;
965
+ if (current.object.repeatCount > 0)
966
+ sliderBonus /= current.object.repeatCount + 1;
1224
967
  }
1225
968
  result += sliderBonus * this.sliderMultiplier;
1226
969
  return result;
@@ -1485,7 +1228,7 @@ class DroidVisualEvaluator {
1485
1228
  }
1486
1229
  // Do not consider objects that don't fall under time preempt.
1487
1230
  if (current.object.startTime - previous.object.endTime >
1488
- current.baseTimePreempt) {
1231
+ current.object.timePreempt) {
1489
1232
  break;
1490
1233
  }
1491
1234
  strain +=
@@ -1500,7 +1243,7 @@ class DroidVisualEvaluator {
1500
1243
  strain += Math.pow(400 - current.timePreempt, 1.3) / 100;
1501
1244
  }
1502
1245
  if (current.object instanceof osuBase.Slider && withSliders) {
1503
- const scalingFactor = 50 / current.object.getRadius(osuBase.Modes.droid);
1246
+ const scalingFactor = 50 / current.object.radius;
1504
1247
  // Invert the scaling factor to determine the true travel distance independent of circle size.
1505
1248
  const pixelTravelDistance = current.object.lazyTravelDistance / scalingFactor;
1506
1249
  const currentVelocity = pixelTravelDistance / current.travelTime;
@@ -1577,6 +1320,180 @@ class DroidVisual extends DroidSkill {
1577
1320
  }
1578
1321
  }
1579
1322
 
1323
+ /**
1324
+ * Represents an osu!droid hit object with difficulty calculation values.
1325
+ */
1326
+ class DroidDifficultyHitObject extends DifficultyHitObject {
1327
+ /**
1328
+ * The tap strain generated by the hitobject.
1329
+ */
1330
+ tapStrain = 0;
1331
+ /**
1332
+ * The tap strain generated by the hitobject if `strainTime` isn't modified by
1333
+ * OD. This is used in three-finger detection.
1334
+ */
1335
+ originalTapStrain = 0;
1336
+ /**
1337
+ * The rhythm strain generated by the hitobject.
1338
+ */
1339
+ rhythmStrain = 0;
1340
+ /**
1341
+ * The flashlight strain generated by the hitobject if sliders are considered.
1342
+ */
1343
+ flashlightStrainWithSliders = 0;
1344
+ /**
1345
+ * The flashlight strain generated by the hitobject if sliders are not considered.
1346
+ */
1347
+ flashlightStrainWithoutSliders = 0;
1348
+ /**
1349
+ * The visual strain generated by the hitobject if sliders are considered.
1350
+ */
1351
+ visualStrainWithSliders = 0;
1352
+ /**
1353
+ * The visual strain generated by the hitobject if sliders are not considered.
1354
+ */
1355
+ visualStrainWithoutSliders = 0;
1356
+ /**
1357
+ * The note density of the hitobject.
1358
+ */
1359
+ noteDensity = 1;
1360
+ /**
1361
+ * The overlapping factor of the hitobject.
1362
+ *
1363
+ * This is used to scale visual skill.
1364
+ */
1365
+ overlappingFactor = 0;
1366
+ /**
1367
+ * Adjusted preempt time of the hitobject, taking speed multiplier into account.
1368
+ */
1369
+ timePreempt;
1370
+ radiusBuffThreshold = 70;
1371
+ mode = osuBase.Modes.droid;
1372
+ maximumSliderRadius = this.normalizedRadius * 2;
1373
+ get scalingFactor() {
1374
+ const radius = this.object.radius;
1375
+ // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
1376
+ let scalingFactor = this.normalizedRadius / radius;
1377
+ // High circle size (small CS) bonus
1378
+ if (radius < this.radiusBuffThreshold) {
1379
+ scalingFactor *=
1380
+ 1 + Math.pow((this.radiusBuffThreshold - radius) / 50, 2);
1381
+ }
1382
+ return scalingFactor;
1383
+ }
1384
+ /**
1385
+ * Note: You **must** call `computeProperties` at some point due to how TypeScript handles
1386
+ * overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue.).
1387
+ *
1388
+ * @param object The underlying hitobject.
1389
+ * @param lastObject The hitobject before this hitobject.
1390
+ * @param lastLastObject The hitobject before the last hitobject.
1391
+ * @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
1392
+ * @param clockRate The clock rate of the beatmap.
1393
+ * @param isForceAR Whether force AR is enabled.
1394
+ */
1395
+ constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, isForceAR) {
1396
+ super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate);
1397
+ this.timePreempt = object.timePreempt;
1398
+ if (!isForceAR) {
1399
+ this.timePreempt /= clockRate;
1400
+ }
1401
+ }
1402
+ computeProperties(clockRate, hitObjects) {
1403
+ super.computeProperties(clockRate, hitObjects);
1404
+ this.setVisuals(clockRate, hitObjects);
1405
+ }
1406
+ /**
1407
+ * Determines whether this hitobject is considered overlapping with the hitobject before it.
1408
+ *
1409
+ * Keep in mind that "overlapping" in this case is overlapping to the point where both hitobjects
1410
+ * can be hit with just a single tap in osu!droid.
1411
+ *
1412
+ * @param considerDistance Whether to consider the distance between both hitobjects.
1413
+ * @returns Whether the hitobject is considered overlapping.
1414
+ */
1415
+ isOverlapping(considerDistance) {
1416
+ if (this.object instanceof osuBase.Spinner) {
1417
+ return false;
1418
+ }
1419
+ const previous = this.previous(0);
1420
+ if (!previous || previous.object instanceof osuBase.Spinner) {
1421
+ return false;
1422
+ }
1423
+ if (this.deltaTime >= 5) {
1424
+ return false;
1425
+ }
1426
+ if (considerDistance) {
1427
+ const endPosition = this.object.getStackedPosition(osuBase.Modes.droid);
1428
+ let distance = previous.object
1429
+ .getStackedEndPosition(osuBase.Modes.droid)
1430
+ .getDistance(endPosition);
1431
+ if (previous.object instanceof osuBase.Slider &&
1432
+ previous.object.lazyEndPosition) {
1433
+ distance = Math.min(distance, previous.object.lazyEndPosition.getDistance(endPosition));
1434
+ }
1435
+ return distance <= 2 * this.object.radius;
1436
+ }
1437
+ return true;
1438
+ }
1439
+ setVisuals(clockRate, hitObjects) {
1440
+ // We'll have two visible object arrays. The first array contains objects before the current object starts in a reversed order,
1441
+ // while the second array contains objects after the current object ends.
1442
+ // For overlapping factor, we also need to consider previous visible objects.
1443
+ const prevVisibleObjects = [];
1444
+ const nextVisibleObjects = [];
1445
+ for (let j = this.index + 2; j < hitObjects.length; ++j) {
1446
+ const o = hitObjects[j];
1447
+ if (o instanceof osuBase.Spinner) {
1448
+ continue;
1449
+ }
1450
+ if (o.startTime / clockRate > this.endTime + this.timePreempt) {
1451
+ break;
1452
+ }
1453
+ nextVisibleObjects.push(o);
1454
+ }
1455
+ for (let j = 0; j < this.index; ++j) {
1456
+ const prev = this.previous(j);
1457
+ if (prev.object instanceof osuBase.Spinner) {
1458
+ continue;
1459
+ }
1460
+ if (prev.startTime >= this.startTime) {
1461
+ continue;
1462
+ }
1463
+ if (prev.startTime < this.startTime - this.timePreempt) {
1464
+ break;
1465
+ }
1466
+ prevVisibleObjects.push(prev.object);
1467
+ }
1468
+ for (const hitObject of prevVisibleObjects) {
1469
+ const distance = this.object
1470
+ .getStackedPosition(this.mode)
1471
+ .getDistance(hitObject.getStackedEndPosition(this.mode));
1472
+ const deltaTime = this.startTime - hitObject.endTime / clockRate;
1473
+ this.applyToOverlappingFactor(distance, deltaTime);
1474
+ }
1475
+ for (const hitObject of nextVisibleObjects) {
1476
+ const distance = hitObject
1477
+ .getStackedPosition(this.mode)
1478
+ .getDistance(this.object.getStackedEndPosition(this.mode));
1479
+ const deltaTime = hitObject.startTime / clockRate - this.endTime;
1480
+ if (deltaTime >= 0) {
1481
+ this.noteDensity += 1 - deltaTime / this.timePreempt;
1482
+ }
1483
+ this.applyToOverlappingFactor(distance, deltaTime);
1484
+ }
1485
+ }
1486
+ applyToOverlappingFactor(distance, deltaTime) {
1487
+ // Penalize objects that are too close to the object in both distance
1488
+ // and delta time to prevent stream maps from being overweighted.
1489
+ this.overlappingFactor +=
1490
+ Math.max(0, 1 - distance / (3 * this.object.radius)) *
1491
+ (7.5 /
1492
+ (1 +
1493
+ Math.exp(0.15 * (Math.max(deltaTime, this.minDeltaTime) - 75))));
1494
+ }
1495
+ }
1496
+
1580
1497
  /**
1581
1498
  * A difficulty calculator for osu!droid gamemode.
1582
1499
  */
@@ -1584,23 +1501,33 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1584
1501
  /**
1585
1502
  * The aim star rating of the beatmap.
1586
1503
  */
1587
- aim = 0;
1504
+ get aim() {
1505
+ return this.attributes.aimDifficulty;
1506
+ }
1588
1507
  /**
1589
1508
  * The tap star rating of the beatmap.
1590
1509
  */
1591
- tap = 0;
1510
+ get tap() {
1511
+ return this.attributes.tapDifficulty;
1512
+ }
1592
1513
  /**
1593
1514
  * The rhythm star rating of the beatmap.
1594
1515
  */
1595
- rhythm = 0;
1516
+ get rhythm() {
1517
+ return this.attributes.rhythmDifficulty;
1518
+ }
1596
1519
  /**
1597
1520
  * The flashlight star rating of the beatmap.
1598
1521
  */
1599
- flashlight = 0;
1522
+ get flashlight() {
1523
+ return this.attributes.flashlightDifficulty;
1524
+ }
1600
1525
  /**
1601
1526
  * The visual star rating of the beatmap.
1602
1527
  */
1603
- visual = 0;
1528
+ get visual() {
1529
+ return this.attributes.visualDifficulty;
1530
+ }
1604
1531
  /**
1605
1532
  * The strain threshold to start detecting for possible three-fingered section.
1606
1533
  *
@@ -1608,6 +1535,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1608
1535
  */
1609
1536
  static threeFingerStrainThreshold = 175;
1610
1537
  attributes = {
1538
+ mode: "live",
1611
1539
  tapDifficulty: 0,
1612
1540
  rhythmDifficulty: 0,
1613
1541
  visualDifficulty: 0,
@@ -1662,6 +1590,9 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1662
1590
  }
1663
1591
  difficultyMultiplier = 0.18;
1664
1592
  mode = osuBase.Modes.droid;
1593
+ calculate(options) {
1594
+ return super.calculate(options);
1595
+ }
1665
1596
  /**
1666
1597
  * Calculates the aim star rating of the beatmap and stores it in this instance.
1667
1598
  */
@@ -1675,7 +1606,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1675
1606
  * Calculates the tap star rating of the beatmap and stores it in this instance.
1676
1607
  */
1677
1608
  calculateTap() {
1678
- const od = this.stats.od;
1609
+ const od = this.difficultyStatistics.overallDifficulty;
1679
1610
  const tapSkillCheese = new DroidTap(this.mods, od, true);
1680
1611
  const tapSkillNoCheese = new DroidTap(this.mods, od, false);
1681
1612
  this.calculateSkills(tapSkillCheese, tapSkillNoCheese);
@@ -1685,7 +1616,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1685
1616
  * Calculates the rhythm star rating of the beatmap and stores it in this instance.
1686
1617
  */
1687
1618
  calculateRhythm() {
1688
- const rhythmSkill = new DroidRhythm(this.mods, this.stats.od);
1619
+ const rhythmSkill = new DroidRhythm(this.mods, this.difficultyStatistics.overallDifficulty);
1689
1620
  this.calculateSkills(rhythmSkill);
1690
1621
  this.postCalculateRhythm(rhythmSkill);
1691
1622
  }
@@ -1703,7 +1634,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1703
1634
  */
1704
1635
  calculateVisual() {
1705
1636
  if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
1706
- this.visual = this.attributes.visualDifficulty = 0;
1637
+ this.attributes.visualDifficulty = 0;
1707
1638
  return;
1708
1639
  }
1709
1640
  const visualSkill = new DroidVisual(this.mods, true);
@@ -1725,13 +1656,13 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1725
1656
  if (basePerformanceValue > 1e-5) {
1726
1657
  // Document for formula derivation:
1727
1658
  // https://docs.google.com/document/d/10DZGYYSsT_yjz2Mtp6yIJld0Rqx4E-vVHupCqiM4TNI/edit
1728
- this.total = this.attributes.starRating =
1659
+ this.attributes.starRating =
1729
1660
  0.027 *
1730
1661
  (Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformanceValue) +
1731
1662
  4);
1732
1663
  }
1733
1664
  else {
1734
- this.total = this.attributes.starRating = 0;
1665
+ this.attributes.starRating = 0;
1735
1666
  }
1736
1667
  }
1737
1668
  calculateAll() {
@@ -1766,15 +1697,31 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1766
1697
  this.visual.toFixed(2) +
1767
1698
  " visual)");
1768
1699
  }
1769
- preProcess() {
1770
- const scale = osuBase.CircleSizeCalculator.standardCSToStandardScale(this.stats.cs);
1771
- for (const object of this.beatmap.hitObjects.objects) {
1772
- object.droidScale = scale;
1700
+ generateDifficultyHitObjects(beatmap) {
1701
+ const difficultyObjects = [];
1702
+ const { objects } = beatmap.hitObjects;
1703
+ const difficultyAdjustMod = this.mods.find((m) => m instanceof osuBase.ModDifficultyAdjust);
1704
+ for (let i = 0; i < objects.length; ++i) {
1705
+ const difficultyObject = new DroidDifficultyHitObject(objects[i], objects[i - 1] ?? null, objects[i - 2] ?? null, difficultyObjects, this.difficultyStatistics.overallSpeedMultiplier, difficultyAdjustMod?.ar !== undefined);
1706
+ difficultyObject.computeProperties(this.difficultyStatistics.overallSpeedMultiplier, objects);
1707
+ difficultyObjects.push(difficultyObject);
1773
1708
  }
1774
- osuBase.HitObjectStackEvaluator.applyDroidStacking(this.beatmap.hitObjects.objects, this.beatmap.general.stackLeniency);
1709
+ return difficultyObjects;
1710
+ }
1711
+ computeDifficultyStatistics(options) {
1712
+ const { difficulty } = this.beatmap;
1713
+ return osuBase.calculateDroidDifficultyStatistics({
1714
+ circleSize: difficulty.cs,
1715
+ approachRate: difficulty.ar ?? difficulty.od,
1716
+ overallDifficulty: difficulty.od,
1717
+ healthDrain: difficulty.hp,
1718
+ mods: this.mods,
1719
+ customSpeedMultiplier: options?.customSpeedMultiplier,
1720
+ oldStatistics: options?.oldStatistics,
1721
+ });
1775
1722
  }
1776
1723
  createSkills() {
1777
- const od = this.stats.od;
1724
+ const od = this.difficultyStatistics.overallDifficulty;
1778
1725
  return [
1779
1726
  new DroidAim(this.mods, true),
1780
1727
  new DroidAim(this.mods, false),
@@ -1797,16 +1744,15 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1797
1744
  postCalculateAim(aimSkill, aimSkillWithoutSliders) {
1798
1745
  this.strainPeaks.aimWithSliders = aimSkill.strainPeaks;
1799
1746
  this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
1800
- this.aim = this.starValue(aimSkill.difficultyValue());
1747
+ this.attributes.aimDifficulty = this.starValue(aimSkill.difficultyValue());
1801
1748
  if (this.aim) {
1802
1749
  this.attributes.sliderFactor =
1803
1750
  this.starValue(aimSkillWithoutSliders.difficultyValue()) /
1804
1751
  this.aim;
1805
1752
  }
1806
1753
  if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
1807
- this.aim *= 0.9;
1754
+ this.attributes.aimDifficulty *= 0.9;
1808
1755
  }
1809
- this.attributes.aimDifficulty = this.aim;
1810
1756
  this.calculateAimAttributes();
1811
1757
  }
1812
1758
  /**
@@ -1858,11 +1804,11 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1858
1804
  postCalculateTap(tapSkillCheese) {
1859
1805
  this.strainPeaks.speed = tapSkillCheese.strainPeaks;
1860
1806
  if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
1861
- this.tap = this.attributes.tapDifficulty = 0;
1807
+ this.attributes.tapDifficulty = 0;
1862
1808
  this.attributes.possibleThreeFingeredSections = [];
1863
1809
  }
1864
1810
  else {
1865
- this.tap = this.attributes.tapDifficulty = this.starValue(tapSkillCheese.difficultyValue());
1811
+ this.attributes.tapDifficulty = this.starValue(tapSkillCheese.difficultyValue());
1866
1812
  }
1867
1813
  this.calculateTapAttributes();
1868
1814
  }
@@ -1982,7 +1928,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1982
1928
  * @param rhythmSkill The rhythm skill.
1983
1929
  */
1984
1930
  postCalculateRhythm(rhythmSkill) {
1985
- this.rhythm = this.attributes.rhythmDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
1931
+ this.attributes.rhythmDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
1986
1932
  ? 0
1987
1933
  : this.starValue(rhythmSkill.difficultyValue());
1988
1934
  }
@@ -1994,13 +1940,13 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1994
1940
  */
1995
1941
  postCalculateFlashlight(flashlightSkill, flashlightSkillWithoutSliders) {
1996
1942
  this.strainPeaks.flashlight = flashlightSkill.strainPeaks;
1997
- this.flashlight = this.starValue(flashlightSkill.difficultyValue());
1943
+ this.attributes.flashlightDifficulty = this.starValue(flashlightSkill.difficultyValue());
1998
1944
  if (this.flashlight) {
1999
1945
  this.attributes.flashlightSliderFactor =
2000
1946
  this.starValue(flashlightSkillWithoutSliders.difficultyValue()) / this.flashlight;
2001
1947
  }
2002
1948
  if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
2003
- this.flashlight *= 0.7;
1949
+ this.attributes.flashlightDifficulty *= 0.7;
2004
1950
  }
2005
1951
  const objectStrains = this.objects.map((v) => v.flashlightStrainWithSliders);
2006
1952
  const maxStrain = Math.max(...objectStrains);
@@ -2008,7 +1954,6 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
2008
1954
  this.attributes.flashlightDifficultStrainCount =
2009
1955
  objectStrains.reduce((total, next) => total + Math.pow(next / maxStrain, 4), 0);
2010
1956
  }
2011
- this.attributes.flashlightDifficulty = this.flashlight;
2012
1957
  }
2013
1958
  /**
2014
1959
  * Called after visual skill calculation.
@@ -2017,7 +1962,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
2017
1962
  * @param visualSkillWithoutSliders The visual skill that doesn't consider sliders.
2018
1963
  */
2019
1964
  postCalculateVisual(visualSkillWithSliders, visualSkillWithoutSliders) {
2020
- this.visual = this.attributes.visualDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
1965
+ this.attributes.visualDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
2021
1966
  ? 0
2022
1967
  : this.starValue(visualSkillWithSliders.difficultyValue());
2023
1968
  if (this.visual) {
@@ -3069,9 +3014,9 @@ class OsuSpeed extends OsuSkill {
3069
3014
  currentRhythm = 0;
3070
3015
  skillMultiplier = 1375;
3071
3016
  greatWindow;
3072
- constructor(mods, greatWindow) {
3017
+ constructor(mods, overallDifficulty) {
3073
3018
  super(mods);
3074
- this.greatWindow = greatWindow;
3019
+ this.greatWindow = new osuBase.OsuHitWindow(overallDifficulty).hitWindowFor300();
3075
3020
  }
3076
3021
  /**
3077
3022
  * @param current The hitobject to calculate.
@@ -3118,7 +3063,7 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
3118
3063
  if (current.object instanceof osuBase.Spinner) {
3119
3064
  return 0;
3120
3065
  }
3121
- const scalingFactor = 52 / current.object.getRadius(osuBase.Modes.osu);
3066
+ const scalingFactor = 52 / current.object.radius;
3122
3067
  let smallDistNerf = 1;
3123
3068
  let cumulativeStrainTime = 0;
3124
3069
  let result = 0;
@@ -3172,8 +3117,8 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
3172
3117
  // Longer sliders require more memorization.
3173
3118
  sliderBonus *= pixelTravelDistance;
3174
3119
  // Nerf sliders with repeats, as less memorization is required.
3175
- if (current.object.repeats > 0)
3176
- sliderBonus /= current.object.repeats + 1;
3120
+ if (current.object.repeatCount > 0)
3121
+ sliderBonus /= current.object.repeatCount + 1;
3177
3122
  }
3178
3123
  result += sliderBonus * this.sliderMultiplier;
3179
3124
  return result;
@@ -3210,6 +3155,46 @@ class OsuFlashlight extends OsuSkill {
3210
3155
  }
3211
3156
  }
3212
3157
 
3158
+ /**
3159
+ * Represents an osu!standard hit object with difficulty calculation values.
3160
+ */
3161
+ class OsuDifficultyHitObject extends DifficultyHitObject {
3162
+ /**
3163
+ * The speed strain generated by the hitobject.
3164
+ */
3165
+ speedStrain = 0;
3166
+ /**
3167
+ * The flashlight strain generated by this hitobject.
3168
+ */
3169
+ flashlightStrain = 0;
3170
+ radiusBuffThreshold = 30;
3171
+ mode = osuBase.Modes.osu;
3172
+ get scalingFactor() {
3173
+ const radius = this.object.radius;
3174
+ // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
3175
+ let scalingFactor = this.normalizedRadius / radius;
3176
+ // High circle size (small CS) bonus
3177
+ if (radius < this.radiusBuffThreshold) {
3178
+ scalingFactor *=
3179
+ 1 + Math.min(this.radiusBuffThreshold - radius, 5) / 50;
3180
+ }
3181
+ return scalingFactor;
3182
+ }
3183
+ /**
3184
+ * Note: You **must** call `computeProperties` at some point due to how TypeScript handles
3185
+ * overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue.).
3186
+ *
3187
+ * @param object The underlying hitobject.
3188
+ * @param lastObject The hitobject before this hitobject.
3189
+ * @param lastLastObject The hitobject before the last hitobject.
3190
+ * @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
3191
+ * @param clockRate The clock rate of the beatmap.
3192
+ */
3193
+ constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate) {
3194
+ super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate);
3195
+ }
3196
+ }
3197
+
3213
3198
  /**
3214
3199
  * A difficulty calculator for osu!standard gamemode.
3215
3200
  */
@@ -3217,15 +3202,21 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3217
3202
  /**
3218
3203
  * The aim star rating of the beatmap.
3219
3204
  */
3220
- aim = 0;
3205
+ get aim() {
3206
+ return this.attributes.aimDifficulty;
3207
+ }
3221
3208
  /**
3222
3209
  * The speed star rating of the beatmap.
3223
3210
  */
3224
- speed = 0;
3211
+ get speed() {
3212
+ return this.attributes.speedDifficulty;
3213
+ }
3225
3214
  /**
3226
3215
  * The flashlight star rating of the beatmap.
3227
3216
  */
3228
- flashlight = 0;
3217
+ get flashlight() {
3218
+ return this.attributes.flashlightDifficulty;
3219
+ }
3229
3220
  attributes = {
3230
3221
  speedDifficulty: 0,
3231
3222
  mods: [],
@@ -3264,10 +3255,10 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3264
3255
  */
3265
3256
  calculateSpeed() {
3266
3257
  if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
3267
- this.speed = this.attributes.speedDifficulty = 0;
3258
+ this.attributes.speedDifficulty = 0;
3268
3259
  return;
3269
3260
  }
3270
- const speedSkill = new OsuSpeed(this.mods, new osuBase.OsuHitWindow(this.stats.od).hitWindowFor300());
3261
+ const speedSkill = new OsuSpeed(this.mods, this.difficultyStatistics.overallDifficulty);
3271
3262
  this.calculateSkills(speedSkill);
3272
3263
  this.postCalculateSpeed(speedSkill);
3273
3264
  }
@@ -3292,14 +3283,14 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3292
3283
  if (basePerformanceValue > 1e-5) {
3293
3284
  // Document for formula derivation:
3294
3285
  // https://docs.google.com/document/d/10DZGYYSsT_yjz2Mtp6yIJld0Rqx4E-vVHupCqiM4TNI/edit
3295
- this.total = this.attributes.starRating =
3286
+ this.attributes.starRating =
3296
3287
  Math.cbrt(1.14) *
3297
3288
  0.027 *
3298
3289
  (Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformanceValue) +
3299
3290
  4);
3300
3291
  }
3301
3292
  else {
3302
- this.total = this.attributes.starRating = 0;
3293
+ this.attributes.starRating = 0;
3303
3294
  }
3304
3295
  }
3305
3296
  calculateAll() {
@@ -3312,7 +3303,6 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3312
3303
  const flashlightSkill = skills[3];
3313
3304
  this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
3314
3305
  if (isRelax) {
3315
- this.speed = 0;
3316
3306
  this.attributes.speedDifficulty = 0;
3317
3307
  }
3318
3308
  else {
@@ -3332,22 +3322,32 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3332
3322
  this.flashlight.toFixed(2) +
3333
3323
  " flashlight)");
3334
3324
  }
3335
- preProcess() {
3336
- const scale = osuBase.CircleSizeCalculator.standardCSToStandardScale(this.stats.cs);
3337
- for (const object of this.beatmap.hitObjects.objects) {
3338
- object.osuScale = scale;
3325
+ generateDifficultyHitObjects() {
3326
+ const difficultyObjects = [];
3327
+ const { objects } = this.beatmap.hitObjects;
3328
+ for (let i = 0; i < objects.length; ++i) {
3329
+ const difficultyObject = new OsuDifficultyHitObject(objects[i], objects[i - 1] ?? null, objects[i - 2] ?? null, difficultyObjects, this.difficultyStatistics.overallSpeedMultiplier);
3330
+ difficultyObject.computeProperties(this.difficultyStatistics.overallSpeedMultiplier, objects);
3331
+ difficultyObjects.push(difficultyObject);
3339
3332
  }
3340
- const ar = new osuBase.MapStats({
3341
- ar: this.beatmap.difficulty.ar,
3342
- mods: osuBase.ModUtil.removeSpeedChangingMods(this.mods),
3343
- }).calculate().ar;
3344
- osuBase.HitObjectStackEvaluator.applyStandardStacking(this.beatmap.formatVersion, this.beatmap.hitObjects.objects, ar, this.beatmap.general.stackLeniency);
3333
+ return difficultyObjects;
3334
+ }
3335
+ computeDifficultyStatistics(options) {
3336
+ const { difficulty } = this.beatmap;
3337
+ return osuBase.calculateOsuDifficultyStatistics({
3338
+ circleSize: difficulty.cs,
3339
+ approachRate: difficulty.ar ?? difficulty.od,
3340
+ overallDifficulty: difficulty.od,
3341
+ healthDrain: difficulty.hp,
3342
+ mods: options?.mods,
3343
+ customSpeedMultiplier: options?.customSpeedMultiplier,
3344
+ });
3345
3345
  }
3346
3346
  createSkills() {
3347
3347
  return [
3348
3348
  new OsuAim(this.mods, true),
3349
3349
  new OsuAim(this.mods, false),
3350
- new OsuSpeed(this.mods, new osuBase.OsuHitWindow(this.stats.od).hitWindowFor300()),
3350
+ new OsuSpeed(this.mods, this.difficultyStatistics.overallDifficulty),
3351
3351
  new OsuFlashlight(this.mods),
3352
3352
  ];
3353
3353
  }
@@ -3360,19 +3360,18 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3360
3360
  postCalculateAim(aimSkill, aimSkillWithoutSliders) {
3361
3361
  this.strainPeaks.aimWithSliders = aimSkill.strainPeaks;
3362
3362
  this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
3363
- this.aim = this.starValue(aimSkill.difficultyValue());
3363
+ this.attributes.aimDifficulty = this.starValue(aimSkill.difficultyValue());
3364
3364
  if (this.aim) {
3365
3365
  this.attributes.sliderFactor =
3366
3366
  this.starValue(aimSkillWithoutSliders.difficultyValue()) /
3367
3367
  this.aim;
3368
3368
  }
3369
3369
  if (this.mods.some((m) => m instanceof osuBase.ModTouchDevice)) {
3370
- this.aim = Math.pow(this.aim, 0.8);
3370
+ this.attributes.aimDifficulty = Math.pow(this.aim, 0.8);
3371
3371
  }
3372
3372
  if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
3373
- this.aim *= 0.9;
3373
+ this.attributes.aimDifficulty *= 0.9;
3374
3374
  }
3375
- this.attributes.aimDifficulty = this.aim;
3376
3375
  }
3377
3376
  /**
3378
3377
  * Called after speed skill calculation.
@@ -3381,7 +3380,7 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3381
3380
  */
3382
3381
  postCalculateSpeed(speedSkill) {
3383
3382
  this.strainPeaks.speed = speedSkill.strainPeaks;
3384
- this.speed = this.attributes.speedDifficulty = this.starValue(speedSkill.difficultyValue());
3383
+ this.attributes.speedDifficulty = this.starValue(speedSkill.difficultyValue());
3385
3384
  }
3386
3385
  /**
3387
3386
  * Calculates speed-related attributes.
@@ -3400,55 +3399,13 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3400
3399
  */
3401
3400
  postCalculateFlashlight(flashlightSkill) {
3402
3401
  this.strainPeaks.flashlight = flashlightSkill.strainPeaks;
3403
- this.flashlight = this.starValue(flashlightSkill.difficultyValue());
3402
+ this.attributes.flashlightDifficulty = this.starValue(flashlightSkill.difficultyValue());
3404
3403
  if (this.mods.some((m) => m instanceof osuBase.ModTouchDevice)) {
3405
- this.flashlight = Math.pow(this.flashlight, 0.8);
3404
+ this.attributes.flashlightDifficulty = Math.pow(this.flashlight, 0.8);
3406
3405
  }
3407
3406
  if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
3408
- this.flashlight *= 0.7;
3407
+ this.attributes.flashlightDifficulty *= 0.7;
3409
3408
  }
3410
- this.attributes.flashlightDifficulty = this.flashlight;
3411
- }
3412
- }
3413
-
3414
- /**
3415
- * A difficulty calculator that calculates for both osu!droid and osu!standard gamemode.
3416
- */
3417
- class MapStars {
3418
- /**
3419
- * The osu!droid difficulty calculator of the beatmap.
3420
- */
3421
- droid;
3422
- /**
3423
- * The osu!standard difficulty calculator of the beatmap.
3424
- */
3425
- osu;
3426
- /**
3427
- * Constructs this instance and calculates the given beatmap's osu!droid and osu!standard difficulty.
3428
- *
3429
- * @param beatmap The beatmap to calculate.
3430
- * @param options Options for the difficulty calculation.
3431
- */
3432
- constructor(beatmap, options) {
3433
- const stats = new osuBase.MapStats({
3434
- speedMultiplier: options?.stats?.speedMultiplier ?? 1,
3435
- isForceAR: options?.stats?.isForceAR ?? false,
3436
- oldStatistics: options?.stats?.oldStatistics ?? false,
3437
- });
3438
- this.droid = new DroidDifficultyCalculator(beatmap).calculate({
3439
- ...options,
3440
- stats,
3441
- });
3442
- this.osu = new OsuDifficultyCalculator(beatmap).calculate({
3443
- ...options,
3444
- stats,
3445
- });
3446
- }
3447
- /**
3448
- * Returns a string representative of the class.
3449
- */
3450
- toString() {
3451
- return `${this.droid.toString()}\n${this.osu.toString()}`;
3452
3409
  }
3453
3410
  }
3454
3411
 
@@ -3675,7 +3632,6 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
3675
3632
  exports.AimEvaluator = AimEvaluator;
3676
3633
  exports.DifficultyCalculator = DifficultyCalculator;
3677
3634
  exports.DifficultyHitObject = DifficultyHitObject;
3678
- exports.DifficultyHitObjectCreator = DifficultyHitObjectCreator;
3679
3635
  exports.DroidAim = DroidAim;
3680
3636
  exports.DroidAimEvaluator = DroidAimEvaluator;
3681
3637
  exports.DroidDifficultyCalculator = DroidDifficultyCalculator;
@@ -3690,7 +3646,6 @@ exports.DroidTapEvaluator = DroidTapEvaluator;
3690
3646
  exports.DroidVisual = DroidVisual;
3691
3647
  exports.DroidVisualEvaluator = DroidVisualEvaluator;
3692
3648
  exports.FlashlightEvaluator = FlashlightEvaluator;
3693
- exports.MapStars = MapStars;
3694
3649
  exports.OsuAim = OsuAim;
3695
3650
  exports.OsuAimEvaluator = OsuAimEvaluator;
3696
3651
  exports.OsuDifficultyCalculator = OsuDifficultyCalculator;