@rian8337/osu-difficulty-calculator 4.0.0-beta.52 → 4.0.0-beta.55

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 +788 -950
  2. package/package.json +3 -3
  3. package/typings/index.d.ts +256 -343
package/dist/index.js CHANGED
@@ -3,137 +3,121 @@
3
3
  var osuBase = require('@rian8337/osu-base');
4
4
 
5
5
  /**
6
- * The base of a difficulty calculator.
6
+ * Holds data that can be used to calculate performance points.
7
7
  */
8
- class DifficultyCalculator {
9
- /**
10
- * The difficulty objects of the beatmap.
11
- */
12
- get objects() {
13
- return this._objects;
14
- }
15
- /**
16
- * The total star rating of the beatmap.
17
- */
18
- get total() {
19
- return this.attributes.starRating;
20
- }
21
- /**
22
- * Constructs a new instance of the calculator.
23
- *
24
- * @param beatmap The beatmap to calculate.
25
- */
26
- constructor(beatmap) {
27
- /**
28
- * The difficulty objects of the beatmap.
29
- */
30
- this._objects = [];
31
- /**
32
- * The modifications applied.
33
- */
8
+ class DifficultyAttributes {
9
+ constructor(cacheableAttributes) {
34
10
  this.mods = [];
35
- /**
36
- * The strain peaks of various calculated difficulties.
37
- */
38
- this.strainPeaks = {
39
- aimWithSliders: [],
40
- aimWithoutSliders: [],
41
- speed: [],
42
- flashlight: [],
43
- };
44
- this.beatmap = beatmap;
11
+ this.starRating = 0;
12
+ this.maxCombo = 0;
13
+ this.aimDifficulty = 0;
14
+ this.flashlightDifficulty = 0;
15
+ this.speedNoteCount = 0;
16
+ this.sliderFactor = 1;
17
+ this.clockRate = 1;
18
+ this.overallDifficulty = 0;
19
+ this.hitCircleCount = 0;
20
+ this.sliderCount = 0;
21
+ this.spinnerCount = 0;
22
+ this.aimDifficultSliderCount = 0;
23
+ this.aimDifficultStrainCount = 0;
24
+ if (!cacheableAttributes) {
25
+ return;
26
+ }
27
+ this.mods = osuBase.ModUtil.deserializeMods(cacheableAttributes.mods);
28
+ this.starRating = cacheableAttributes.starRating;
29
+ this.maxCombo = cacheableAttributes.maxCombo;
30
+ this.aimDifficulty = cacheableAttributes.aimDifficulty;
31
+ this.flashlightDifficulty = cacheableAttributes.flashlightDifficulty;
32
+ this.speedNoteCount = cacheableAttributes.speedNoteCount;
33
+ this.sliderFactor = cacheableAttributes.sliderFactor;
34
+ this.clockRate = cacheableAttributes.clockRate;
35
+ this.overallDifficulty = cacheableAttributes.overallDifficulty;
36
+ this.hitCircleCount = cacheableAttributes.hitCircleCount;
37
+ this.sliderCount = cacheableAttributes.sliderCount;
38
+ this.spinnerCount = cacheableAttributes.spinnerCount;
39
+ this.aimDifficultSliderCount =
40
+ cacheableAttributes.aimDifficultSliderCount;
41
+ this.aimDifficultStrainCount =
42
+ cacheableAttributes.aimDifficultStrainCount;
45
43
  }
46
44
  /**
47
- * Calculates the star rating of the specified beatmap.
45
+ * Converts this `DifficultyAttributes` instance to an attribute structure that can be cached.
48
46
  *
49
- * The beatmap is analyzed in chunks of `sectionLength` duration.
50
- * For each chunk the highest hitobject strains are added to
51
- * a list which is then collapsed into a weighted sum, much
52
- * like scores are weighted on a user's profile.
53
- *
54
- * For subsequent chunks, the initial max strain is calculated
55
- * by decaying the previous hitobject's strain until the
56
- * beginning of the new chunk.
57
- *
58
- * @param options Options for the difficulty calculation.
59
- * @returns The current instance.
47
+ * @returns The cacheable attributes.
60
48
  */
61
- calculate(options) {
62
- var _a;
63
- this.mods = (_a = options === null || options === void 0 ? void 0 : options.mods) !== null && _a !== void 0 ? _a : [];
64
- const playableBeatmap = this.beatmap.createPlayableBeatmap({
65
- mode: this.mode,
66
- mods: this.mods,
67
- customSpeedMultiplier: options === null || options === void 0 ? void 0 : options.customSpeedMultiplier,
68
- });
69
- const clockRate = this.calculateClockRate(options);
70
- this.populateDifficultyAttributes(playableBeatmap, clockRate);
71
- this._objects = this.generateDifficultyHitObjects(playableBeatmap, clockRate);
72
- this.calculateAll();
73
- return this;
49
+ toCacheableAttributes() {
50
+ return Object.assign(Object.assign({}, this), { mods: osuBase.ModUtil.serializeMods(this.mods) });
74
51
  }
75
52
  /**
76
- * Calculates the skills provided.
77
- *
78
- * @param skills The skills to calculate.
53
+ * Returns a string representation of the difficulty attributes.
79
54
  */
80
- calculateSkills(...skills) {
81
- // The first object doesn't generate a strain, so we begin calculating from the second object.
82
- for (const object of this.objects.slice(1)) {
55
+ toString() {
56
+ return `${this.starRating.toFixed(2)} stars`;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * The base of a difficulty calculator.
62
+ */
63
+ class DifficultyCalculator {
64
+ constructor() {
65
+ /**
66
+ * `Mod`s that adjust the difficulty of a beatmap.
67
+ */
68
+ this.difficultyAdjustmentMods = new Set([
69
+ osuBase.ModDoubleTime,
70
+ osuBase.ModNightCore,
71
+ osuBase.ModDifficultyAdjust,
72
+ osuBase.ModCustomSpeed,
73
+ osuBase.ModHalfTime,
74
+ osuBase.ModEasy,
75
+ osuBase.ModHardRock,
76
+ osuBase.ModFlashlight,
77
+ osuBase.ModHidden,
78
+ osuBase.ModRelax,
79
+ osuBase.ModAutopilot,
80
+ ]);
81
+ }
82
+ calculate(beatmap, mods = []) {
83
+ const playableBeatmap = beatmap instanceof osuBase.PlayableBeatmap
84
+ ? beatmap
85
+ : this.createPlayableBeatmap(beatmap, mods);
86
+ const skills = this.createSkills(playableBeatmap);
87
+ const objects = this.createDifficultyHitObjects(playableBeatmap);
88
+ for (const object of objects) {
83
89
  for (const skill of skills) {
84
90
  skill.process(object);
85
91
  }
86
92
  }
93
+ return this.createDifficultyAttributes(playableBeatmap, skills, objects);
87
94
  }
88
- /**
89
- * Obtains the clock rate of the beatmap.
90
- *
91
- * @param options The options to obtain the clock rate with.
92
- * @returns The clock rate of the beatmap.
93
- */
94
- calculateClockRate(options) {
95
- var _a, _b;
96
- return (osuBase.ModUtil.calculateRateWithMods((_a = options === null || options === void 0 ? void 0 : options.mods) !== null && _a !== void 0 ? _a : []) *
97
- ((_b = options === null || options === void 0 ? void 0 : options.customSpeedMultiplier) !== null && _b !== void 0 ? _b : 1));
98
- }
99
- /**
100
- * Populates the stored difficulty attributes with necessary data.
101
- *
102
- * @param beatmap The beatmap to populate the attributes with.
103
- * @param clockRate The clock rate of the beatmap.
104
- */
105
- populateDifficultyAttributes(beatmap, clockRate) {
106
- this.attributes.hitCircleCount = this.beatmap.hitObjects.circles;
107
- this.attributes.maxCombo = this.beatmap.maxCombo;
108
- this.attributes.mods = this.mods.slice();
109
- this.attributes.sliderCount = this.beatmap.hitObjects.sliders;
110
- this.attributes.spinnerCount = this.beatmap.hitObjects.spinners;
111
- this.attributes.clockRate = clockRate;
112
- let greatWindow;
113
- switch (this.mode) {
114
- case osuBase.Modes.droid:
115
- if (this.mods.some((m) => m instanceof osuBase.ModPrecise)) {
116
- greatWindow = new osuBase.PreciseDroidHitWindow(beatmap.difficulty.od).greatWindow;
117
- }
118
- else {
119
- greatWindow = new osuBase.DroidHitWindow(beatmap.difficulty.od)
120
- .greatWindow;
121
- }
122
- break;
123
- case osuBase.Modes.osu:
124
- greatWindow = new osuBase.OsuHitWindow(beatmap.difficulty.od)
125
- .greatWindow;
126
- break;
95
+ calculateStrainPeaks(beatmap, mods = []) {
96
+ const playableBeatmap = beatmap instanceof osuBase.PlayableBeatmap
97
+ ? beatmap
98
+ : this.createPlayableBeatmap(beatmap, mods);
99
+ const skills = this.createStrainPeakSkills(playableBeatmap);
100
+ const objects = this.createDifficultyHitObjects(playableBeatmap);
101
+ for (const object of objects) {
102
+ for (const skill of skills) {
103
+ skill.process(object);
104
+ }
127
105
  }
128
- this.attributes.overallDifficulty = osuBase.OsuHitWindow.greatWindowToOD(greatWindow / clockRate);
106
+ return {
107
+ aimWithSliders: skills[0].strainPeaks,
108
+ aimWithoutSliders: skills[1].strainPeaks,
109
+ speed: skills[2].strainPeaks,
110
+ flashlight: skills[3].strainPeaks,
111
+ };
129
112
  }
130
113
  /**
131
- * Calculates the star rating value of a difficulty.
114
+ * Calculates the base rating of a `Skill`.
132
115
  *
133
- * @param difficulty The difficulty to calculate.
116
+ * @param skill The `Skill` to calculate the rating of.
117
+ * @returns The rating of the `Skill`.
134
118
  */
135
- starValue(difficulty) {
136
- return Math.sqrt(difficulty) * this.difficultyMultiplier;
119
+ calculateRating(skill) {
120
+ return Math.sqrt(skill.difficultyValue()) * this.difficultyMultiplier;
137
121
  }
138
122
  /**
139
123
  * Calculates the base performance value of a difficulty rating.
@@ -165,7 +149,7 @@ class DifficultyHitObject {
165
149
  * @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
166
150
  * @param clockRate The clock rate of the beatmap.
167
151
  */
168
- constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate) {
152
+ constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, index) {
169
153
  var _a, _b, _c, _d;
170
154
  /**
171
155
  * The aim strain generated by the hitobject if sliders are considered.
@@ -230,7 +214,7 @@ class DifficultyHitObject {
230
214
  this.fullGreatWindow = ((_d = (_c = object.hitWindow) === null || _c === void 0 ? void 0 : _c.greatWindow) !== null && _d !== void 0 ? _d : 1200) * 2;
231
215
  }
232
216
  this.fullGreatWindow /= clockRate;
233
- this.index = difficultyHitObjects.length - 1;
217
+ this.index = index;
234
218
  // Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
235
219
  this.startTime = object.startTime / clockRate;
236
220
  this.endTime = object.endTime / clockRate;
@@ -267,7 +251,7 @@ class DifficultyHitObject {
267
251
  */
268
252
  previous(backwardsIndex) {
269
253
  var _a;
270
- return (_a = this.hitObjects[this.index - backwardsIndex]) !== null && _a !== void 0 ? _a : null;
254
+ return ((_a = this.hitObjects[this.index - backwardsIndex - 1]) !== null && _a !== void 0 ? _a : null);
271
255
  }
272
256
  /**
273
257
  * Gets the difficulty hitobject at a specific index with respect to the current
@@ -281,7 +265,7 @@ class DifficultyHitObject {
281
265
  */
282
266
  next(forwardsIndex) {
283
267
  var _a;
284
- return ((_a = this.hitObjects[this.index + forwardsIndex + 2]) !== null && _a !== void 0 ? _a : null);
268
+ return ((_a = this.hitObjects[this.index + forwardsIndex + 1]) !== null && _a !== void 0 ? _a : null);
285
269
  }
286
270
  /**
287
271
  * Calculates the opacity of the hitobject at a given time.
@@ -532,8 +516,8 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
532
516
  * @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
533
517
  * @param clockRate The clock rate of the beatmap.
534
518
  */
535
- constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate) {
536
- super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate);
519
+ constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, index) {
520
+ super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, index);
537
521
  /**
538
522
  * The tap strain generated by the hitobject.
539
523
  */
@@ -590,6 +574,14 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
590
574
  }
591
575
  return super.opacityAt(time, mods);
592
576
  }
577
+ previous(backwardsIndex) {
578
+ var _a;
579
+ return (_a = this.hitObjects[this.index - backwardsIndex]) !== null && _a !== void 0 ? _a : null;
580
+ }
581
+ next(forwardsIndex) {
582
+ var _a;
583
+ return ((_a = this.hitObjects[this.index + forwardsIndex + 2]) !== null && _a !== void 0 ? _a : null);
584
+ }
593
585
  /**
594
586
  * Determines whether this hitobject is considered overlapping with the hitobject before it.
595
587
  *
@@ -994,6 +986,9 @@ class StrainSkill extends Skill {
994
986
  */
995
987
  class DroidSkill extends StrainSkill {
996
988
  process(current) {
989
+ if (current.index < 0) {
990
+ return;
991
+ }
997
992
  super.process(current);
998
993
  this._objectStrains.push(this.getObjectStrain(current));
999
994
  }
@@ -1082,134 +1077,41 @@ class DroidAim extends DroidSkill {
1082
1077
  }
1083
1078
 
1084
1079
  /**
1085
- * An evaluator for calculating osu!droid tap skill.
1086
- */
1087
- class DroidTapEvaluator {
1088
- /**
1089
- * Evaluates the difficulty of tapping the current object, based on:
1090
- *
1091
- * - time between pressing the previous and current object,
1092
- * - distance between those objects,
1093
- * - how easily they can be cheesed,
1094
- * - and the strain time cap.
1095
- *
1096
- * @param current The current object.
1097
- * @param greatWindow The great hit window of the current object.
1098
- * @param considerCheesability Whether to consider cheesability.
1099
- * @param strainTimeCap The strain time to cap the object's strain time to.
1100
- */
1101
- static evaluateDifficultyOf(current, considerCheesability, strainTimeCap) {
1102
- if (current.object instanceof osuBase.Spinner ||
1103
- // Exclude overlapping objects that can be tapped at once.
1104
- current.isOverlapping(false)) {
1105
- return 0;
1106
- }
1107
- // Nerf doubletappable doubles.
1108
- const doubletapness = considerCheesability
1109
- ? 1 - current.doubletapness
1110
- : 1;
1111
- const strainTime = strainTimeCap !== undefined
1112
- ? // We cap the strain time to 50 here as the chance of vibro is higher in any BPM higher than 300.
1113
- Math.max(50, strainTimeCap, current.strainTime)
1114
- : current.strainTime;
1115
- let speedBonus = 1;
1116
- if (strainTime < this.minSpeedBonus) {
1117
- speedBonus +=
1118
- 0.75 *
1119
- Math.pow(osuBase.ErrorFunction.erf((this.minSpeedBonus - strainTime) / 40), 2);
1120
- }
1121
- return (speedBonus * Math.pow(doubletapness, 1.5) * 1000) / strainTime;
1122
- }
1123
- }
1124
- // ~200 1/4 BPM streams
1125
- DroidTapEvaluator.minSpeedBonus = 75;
1126
-
1127
- /**
1128
- * Represents the skill required to press keys or tap with regards to keeping up with the speed at which objects need to be hit.
1080
+ * Holds data that can be used to calculate osu!droid performance points.
1129
1081
  */
1130
- class DroidTap extends DroidSkill {
1131
- /**
1132
- * The delta time of hitobjects.
1133
- */
1134
- get objectDeltaTimes() {
1135
- return this._objectDeltaTimes;
1136
- }
1137
- constructor(mods, considerCheesability, strainTimeCap) {
1138
- super(mods);
1139
- this.reducedSectionCount = 10;
1140
- this.reducedSectionBaseline = 0.75;
1141
- this.strainDecayBase = 0.3;
1142
- this.starsPerDouble = 1.1;
1143
- this.currentTapStrain = 0;
1144
- this.currentRhythmMultiplier = 0;
1145
- this.skillMultiplier = 1.375;
1146
- this._objectDeltaTimes = [];
1147
- this.considerCheesability = considerCheesability;
1148
- this.strainTimeCap = strainTimeCap;
1149
- }
1150
- /**
1151
- * The amount of notes that are relevant to the difficulty.
1152
- */
1153
- relevantNoteCount() {
1154
- if (this._objectStrains.length === 0) {
1155
- return 0;
1156
- }
1157
- const maxStrain = osuBase.MathUtils.max(this._objectStrains);
1158
- if (maxStrain === 0) {
1159
- return 0;
1160
- }
1161
- return this._objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
1162
- }
1163
- /**
1164
- * The delta time relevant to the difficulty.
1165
- */
1166
- relevantDeltaTime() {
1167
- if (this._objectStrains.length === 0) {
1168
- return 0;
1169
- }
1170
- const maxStrain = osuBase.MathUtils.max(this._objectStrains);
1171
- if (maxStrain === 0) {
1172
- return 0;
1173
- }
1174
- return (this._objectDeltaTimes.reduce((total, next, index) => total +
1175
- (next * 1) /
1176
- (1 +
1177
- Math.exp(-((this._objectStrains[index] / maxStrain) *
1178
- 25 -
1179
- 20))), 0) /
1180
- this._objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 25 - 20))), 0));
1181
- }
1182
- strainValueAt(current) {
1183
- this.currentTapStrain *= this.strainDecay(current.strainTime);
1184
- this.currentTapStrain +=
1185
- DroidTapEvaluator.evaluateDifficultyOf(current, this.considerCheesability, this.strainTimeCap) * this.skillMultiplier;
1186
- this.currentRhythmMultiplier = current.rhythmMultiplier;
1187
- this._objectDeltaTimes.push(current.deltaTime);
1188
- return this.currentTapStrain * current.rhythmMultiplier;
1189
- }
1190
- calculateInitialStrain(time, current) {
1191
- var _a, _b;
1192
- return (this.currentTapStrain *
1193
- this.currentRhythmMultiplier *
1194
- this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
1195
- }
1196
- getObjectStrain() {
1197
- return this.currentTapStrain * this.currentRhythmMultiplier;
1198
- }
1199
- /**
1200
- * @param current The hitobject to save to.
1201
- */
1202
- saveToHitObject(current) {
1203
- if (this.strainTimeCap !== undefined) {
1082
+ class DroidDifficultyAttributes extends DifficultyAttributes {
1083
+ constructor(cacheableAttributes) {
1084
+ super(cacheableAttributes);
1085
+ this.tapDifficulty = 0;
1086
+ this.rhythmDifficulty = 0;
1087
+ this.visualDifficulty = 0;
1088
+ this.tapDifficultStrainCount = 0;
1089
+ this.flashlightDifficultStrainCount = 0;
1090
+ this.visualDifficultStrainCount = 0;
1091
+ this.averageSpeedDeltaTime = 0;
1092
+ this.vibroFactor = 1;
1093
+ if (!cacheableAttributes) {
1204
1094
  return;
1205
1095
  }
1206
- const strain = this.currentTapStrain * this.currentRhythmMultiplier;
1207
- if (this.considerCheesability) {
1208
- current.tapStrain = strain;
1209
- }
1210
- else {
1211
- current.originalTapStrain = strain;
1212
- }
1096
+ this.tapDifficulty = cacheableAttributes.tapDifficulty;
1097
+ this.rhythmDifficulty = cacheableAttributes.rhythmDifficulty;
1098
+ this.visualDifficulty = cacheableAttributes.visualDifficulty;
1099
+ this.tapDifficultStrainCount =
1100
+ cacheableAttributes.tapDifficultStrainCount;
1101
+ this.flashlightDifficultStrainCount =
1102
+ cacheableAttributes.flashlightDifficultStrainCount;
1103
+ this.visualDifficultStrainCount =
1104
+ cacheableAttributes.visualDifficultStrainCount;
1105
+ this.averageSpeedDeltaTime = cacheableAttributes.averageSpeedDeltaTime;
1106
+ this.vibroFactor = cacheableAttributes.vibroFactor;
1107
+ }
1108
+ toString() {
1109
+ return (super.toString() +
1110
+ ` (${this.aimDifficulty.toFixed(2)} aim, ` +
1111
+ `${this.tapDifficulty.toFixed(2)} tap, ` +
1112
+ `${this.rhythmDifficulty.toFixed(2)} rhythm, ` +
1113
+ `${this.flashlightDifficulty.toFixed(2)} flashlight, ` +
1114
+ `${this.visualDifficulty.toFixed(2)} visual)`);
1213
1115
  }
1214
1116
  }
1215
1117
 
@@ -1587,52 +1489,184 @@ class DroidRhythm extends DroidSkill {
1587
1489
  }
1588
1490
 
1589
1491
  /**
1590
- * An evaluator for calculating osu!droid visual skill.
1492
+ * An evaluator for calculating osu!droid tap skill.
1591
1493
  */
1592
- class DroidVisualEvaluator {
1494
+ class DroidTapEvaluator {
1593
1495
  /**
1594
- * Evaluates the difficulty of reading the current object, based on:
1496
+ * Evaluates the difficulty of tapping the current object, based on:
1595
1497
  *
1596
- * - note density of the current object,
1597
- * - overlapping factor of the current object,
1598
- * - the preempt time of the current object,
1599
- * - the visual opacity of the current object,
1600
- * - the velocity of the current object if it's a slider,
1601
- * - past objects' velocity if they are sliders,
1602
- * - and whether the Hidden mod is enabled.
1498
+ * - time between pressing the previous and current object,
1499
+ * - distance between those objects,
1500
+ * - how easily they can be cheesed,
1501
+ * - and the strain time cap.
1603
1502
  *
1604
1503
  * @param current The current object.
1605
- * @param mods The mods used.
1606
- * @param withSliders Whether to take slider difficulty into account.
1504
+ * @param greatWindow The great hit window of the current object.
1505
+ * @param considerCheesability Whether to consider cheesability.
1506
+ * @param strainTimeCap The strain time to cap the object's strain time to.
1607
1507
  */
1608
- static evaluateDifficultyOf(current, mods, withSliders) {
1508
+ static evaluateDifficultyOf(current, considerCheesability, strainTimeCap) {
1609
1509
  if (current.object instanceof osuBase.Spinner ||
1610
1510
  // Exclude overlapping objects that can be tapped at once.
1611
- current.isOverlapping(true) ||
1612
- current.index === 0) {
1511
+ current.isOverlapping(false)) {
1613
1512
  return 0;
1614
1513
  }
1615
- // Start with base density and give global bonus for Hidden and Traceable.
1616
- // Add density caps for sanity.
1617
- let strain;
1618
- if (mods.some((m) => m instanceof osuBase.ModHidden)) {
1619
- strain = Math.min(30, Math.pow(current.noteDensity, 3));
1620
- }
1621
- else if (mods.some((m) => m instanceof osuBase.ModTraceable)) {
1622
- // Give more bonus for hit circles due to there being no circle piece.
1623
- if (current.object instanceof osuBase.Circle) {
1624
- strain = Math.min(25, Math.pow(current.noteDensity, 2.5));
1625
- }
1626
- else {
1627
- strain = Math.min(22.5, Math.pow(current.noteDensity, 2.25));
1628
- }
1629
- }
1630
- else {
1631
- strain = Math.min(20, Math.pow(current.noteDensity, 2));
1514
+ // Nerf doubletappable doubles.
1515
+ const doubletapness = considerCheesability
1516
+ ? 1 - current.doubletapness
1517
+ : 1;
1518
+ const strainTime = strainTimeCap !== undefined
1519
+ ? // We cap the strain time to 50 here as the chance of vibro is higher in any BPM higher than 300.
1520
+ Math.max(50, strainTimeCap, current.strainTime)
1521
+ : current.strainTime;
1522
+ let speedBonus = 1;
1523
+ if (strainTime < this.minSpeedBonus) {
1524
+ speedBonus +=
1525
+ 0.75 *
1526
+ Math.pow(osuBase.ErrorFunction.erf((this.minSpeedBonus - strainTime) / 40), 2);
1632
1527
  }
1633
- // Bonus based on how visible the object is.
1634
- for (let i = 0; i < Math.min(current.index, 10); ++i) {
1635
- const previous = current.previous(i);
1528
+ return (speedBonus * Math.pow(doubletapness, 1.5) * 1000) / strainTime;
1529
+ }
1530
+ }
1531
+ // ~200 1/4 BPM streams
1532
+ DroidTapEvaluator.minSpeedBonus = 75;
1533
+
1534
+ /**
1535
+ * Represents the skill required to press keys or tap with regards to keeping up with the speed at which objects need to be hit.
1536
+ */
1537
+ class DroidTap extends DroidSkill {
1538
+ /**
1539
+ * The delta time of hitobjects.
1540
+ */
1541
+ get objectDeltaTimes() {
1542
+ return this._objectDeltaTimes;
1543
+ }
1544
+ constructor(mods, considerCheesability, strainTimeCap) {
1545
+ super(mods);
1546
+ this.reducedSectionCount = 10;
1547
+ this.reducedSectionBaseline = 0.75;
1548
+ this.strainDecayBase = 0.3;
1549
+ this.starsPerDouble = 1.1;
1550
+ this.currentTapStrain = 0;
1551
+ this.currentRhythmMultiplier = 0;
1552
+ this.skillMultiplier = 1.375;
1553
+ this._objectDeltaTimes = [];
1554
+ this.considerCheesability = considerCheesability;
1555
+ this.strainTimeCap = strainTimeCap;
1556
+ }
1557
+ /**
1558
+ * The amount of notes that are relevant to the difficulty.
1559
+ */
1560
+ relevantNoteCount() {
1561
+ if (this._objectStrains.length === 0) {
1562
+ return 0;
1563
+ }
1564
+ const maxStrain = osuBase.MathUtils.max(this._objectStrains);
1565
+ if (maxStrain === 0) {
1566
+ return 0;
1567
+ }
1568
+ return this._objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
1569
+ }
1570
+ /**
1571
+ * The delta time relevant to the difficulty.
1572
+ */
1573
+ relevantDeltaTime() {
1574
+ if (this._objectStrains.length === 0) {
1575
+ return 0;
1576
+ }
1577
+ const maxStrain = osuBase.MathUtils.max(this._objectStrains);
1578
+ if (maxStrain === 0) {
1579
+ return 0;
1580
+ }
1581
+ return (this._objectDeltaTimes.reduce((total, next, index) => total +
1582
+ (next * 1) /
1583
+ (1 +
1584
+ Math.exp(-((this._objectStrains[index] / maxStrain) *
1585
+ 25 -
1586
+ 20))), 0) /
1587
+ this._objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 25 - 20))), 0));
1588
+ }
1589
+ strainValueAt(current) {
1590
+ this.currentTapStrain *= this.strainDecay(current.strainTime);
1591
+ this.currentTapStrain +=
1592
+ DroidTapEvaluator.evaluateDifficultyOf(current, this.considerCheesability, this.strainTimeCap) * this.skillMultiplier;
1593
+ this.currentRhythmMultiplier = current.rhythmMultiplier;
1594
+ this._objectDeltaTimes.push(current.deltaTime);
1595
+ return this.currentTapStrain * current.rhythmMultiplier;
1596
+ }
1597
+ calculateInitialStrain(time, current) {
1598
+ var _a, _b;
1599
+ return (this.currentTapStrain *
1600
+ this.currentRhythmMultiplier *
1601
+ this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
1602
+ }
1603
+ getObjectStrain() {
1604
+ return this.currentTapStrain * this.currentRhythmMultiplier;
1605
+ }
1606
+ /**
1607
+ * @param current The hitobject to save to.
1608
+ */
1609
+ saveToHitObject(current) {
1610
+ if (this.strainTimeCap !== undefined) {
1611
+ return;
1612
+ }
1613
+ const strain = this.currentTapStrain * this.currentRhythmMultiplier;
1614
+ if (this.considerCheesability) {
1615
+ current.tapStrain = strain;
1616
+ }
1617
+ else {
1618
+ current.originalTapStrain = strain;
1619
+ }
1620
+ }
1621
+ }
1622
+
1623
+ /**
1624
+ * An evaluator for calculating osu!droid visual skill.
1625
+ */
1626
+ class DroidVisualEvaluator {
1627
+ /**
1628
+ * Evaluates the difficulty of reading the current object, based on:
1629
+ *
1630
+ * - note density of the current object,
1631
+ * - overlapping factor of the current object,
1632
+ * - the preempt time of the current object,
1633
+ * - the visual opacity of the current object,
1634
+ * - the velocity of the current object if it's a slider,
1635
+ * - past objects' velocity if they are sliders,
1636
+ * - and whether the Hidden mod is enabled.
1637
+ *
1638
+ * @param current The current object.
1639
+ * @param mods The mods used.
1640
+ * @param withSliders Whether to take slider difficulty into account.
1641
+ */
1642
+ static evaluateDifficultyOf(current, mods, withSliders) {
1643
+ if (current.object instanceof osuBase.Spinner ||
1644
+ // Exclude overlapping objects that can be tapped at once.
1645
+ current.isOverlapping(true) ||
1646
+ current.index === 0) {
1647
+ return 0;
1648
+ }
1649
+ // Start with base density and give global bonus for Hidden and Traceable.
1650
+ // Add density caps for sanity.
1651
+ let strain;
1652
+ if (mods.some((m) => m instanceof osuBase.ModHidden)) {
1653
+ strain = Math.min(30, Math.pow(current.noteDensity, 3));
1654
+ }
1655
+ else if (mods.some((m) => m instanceof osuBase.ModTraceable)) {
1656
+ // Give more bonus for hit circles due to there being no circle piece.
1657
+ if (current.object instanceof osuBase.Circle) {
1658
+ strain = Math.min(25, Math.pow(current.noteDensity, 2.5));
1659
+ }
1660
+ else {
1661
+ strain = Math.min(22.5, Math.pow(current.noteDensity, 2.25));
1662
+ }
1663
+ }
1664
+ else {
1665
+ strain = Math.min(20, Math.pow(current.noteDensity, 2));
1666
+ }
1667
+ // Bonus based on how visible the object is.
1668
+ for (let i = 0; i < Math.min(current.index, 10); ++i) {
1669
+ const previous = current.previous(i);
1636
1670
  if (previous.object instanceof osuBase.Spinner ||
1637
1671
  // Exclude overlapping objects that can be tapped at once.
1638
1672
  previous.isOverlapping(true)) {
@@ -1733,151 +1767,78 @@ class DroidVisual extends DroidSkill {
1733
1767
  }
1734
1768
  }
1735
1769
 
1770
+ /**
1771
+ * Holds data that can be used to calculate osu!droid performance points as well
1772
+ * as doing some analysis using the replay of a score.
1773
+ */
1774
+ class ExtendedDroidDifficultyAttributes extends DroidDifficultyAttributes {
1775
+ constructor(cacheableAttributes) {
1776
+ super(cacheableAttributes);
1777
+ this.mode = "live";
1778
+ this.possibleThreeFingeredSections = [];
1779
+ this.difficultSliders = [];
1780
+ this.aimNoteCount = 0;
1781
+ this.flashlightSliderFactor = 1;
1782
+ this.visualSliderFactor = 1;
1783
+ if (!cacheableAttributes) {
1784
+ return;
1785
+ }
1786
+ this.possibleThreeFingeredSections =
1787
+ cacheableAttributes.possibleThreeFingeredSections;
1788
+ this.difficultSliders = cacheableAttributes.difficultSliders;
1789
+ this.aimNoteCount = cacheableAttributes.aimNoteCount;
1790
+ this.flashlightSliderFactor =
1791
+ cacheableAttributes.flashlightSliderFactor;
1792
+ this.visualSliderFactor = cacheableAttributes.visualSliderFactor;
1793
+ }
1794
+ }
1795
+
1736
1796
  /**
1737
1797
  * A difficulty calculator for osu!droid gamemode.
1738
1798
  */
1739
1799
  class DroidDifficultyCalculator extends DifficultyCalculator {
1740
1800
  constructor() {
1741
- super(...arguments);
1742
- this.attributes = {
1743
- mode: "live",
1744
- aimDifficultSliderCount: 0,
1745
- tapDifficulty: 0,
1746
- rhythmDifficulty: 0,
1747
- visualDifficulty: 0,
1748
- aimNoteCount: 0,
1749
- mods: [],
1750
- starRating: 0,
1751
- maxCombo: 0,
1752
- aimDifficulty: 0,
1753
- flashlightDifficulty: 0,
1754
- speedNoteCount: 0,
1755
- sliderFactor: 0,
1756
- clockRate: 1,
1757
- overallDifficulty: 0,
1758
- hitCircleCount: 0,
1759
- sliderCount: 0,
1760
- spinnerCount: 0,
1761
- aimDifficultStrainCount: 0,
1762
- tapDifficultStrainCount: 0,
1763
- flashlightDifficultStrainCount: 0,
1764
- visualDifficultStrainCount: 0,
1765
- flashlightSliderFactor: 0,
1766
- visualSliderFactor: 0,
1767
- possibleThreeFingeredSections: [],
1768
- difficultSliders: [],
1769
- averageSpeedDeltaTime: 0,
1770
- vibroFactor: 1,
1771
- };
1801
+ super();
1772
1802
  this.difficultyMultiplier = 0.18;
1773
- this.mode = osuBase.Modes.droid;
1774
- }
1775
- /**
1776
- * The aim star rating of the beatmap.
1777
- */
1778
- get aim() {
1779
- return this.attributes.aimDifficulty;
1780
- }
1781
- /**
1782
- * The tap star rating of the beatmap.
1783
- */
1784
- get tap() {
1785
- return this.attributes.tapDifficulty;
1786
- }
1787
- /**
1788
- * The rhythm star rating of the beatmap.
1789
- */
1790
- get rhythm() {
1791
- return this.attributes.rhythmDifficulty;
1792
- }
1793
- /**
1794
- * The flashlight star rating of the beatmap.
1795
- */
1796
- get flashlight() {
1797
- return this.attributes.flashlightDifficulty;
1798
- }
1799
- /**
1800
- * The visual star rating of the beatmap.
1801
- */
1802
- get visual() {
1803
- return this.attributes.visualDifficulty;
1804
- }
1805
- get cacheableAttributes() {
1806
- return Object.assign(Object.assign({}, this.attributes), { mods: osuBase.ModUtil.modsToOsuString(this.attributes.mods) });
1807
- }
1808
- // Override to use DroidDifficultyCalculationOptions
1809
- calculate(options) {
1810
- return super.calculate(options);
1811
- }
1812
- /**
1813
- * Calculates the aim star rating of the beatmap and stores it in this instance.
1814
- */
1815
- calculateAim() {
1816
- if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
1817
- this.attributes.aimDifficulty = 0;
1818
- return;
1819
- }
1820
- const aimSkill = new DroidAim(this.mods, true);
1821
- const aimSkillWithoutSliders = new DroidAim(this.mods, false);
1822
- this.calculateSkills(aimSkill, aimSkillWithoutSliders);
1823
- this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
1824
- }
1825
- /**
1826
- * Calculates the tap star rating of the beatmap and stores it in this instance.
1827
- */
1828
- calculateTap() {
1829
- if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
1830
- this.attributes.tapDifficulty = 0;
1831
- return;
1832
- }
1833
- const tapSkillCheese = new DroidTap(this.mods, true);
1834
- const tapSkillNoCheese = new DroidTap(this.mods, false);
1835
- this.calculateSkills(tapSkillCheese, tapSkillNoCheese);
1836
- const tapSkillVibro = new DroidTap(this.mods, true, tapSkillCheese.relevantDeltaTime());
1837
- this.calculateSkills(tapSkillVibro);
1838
- this.postCalculateTap(tapSkillCheese, tapSkillVibro);
1839
- }
1840
- /**
1841
- * Calculates the rhythm star rating of the beatmap and stores it in this instance.
1842
- */
1843
- calculateRhythm() {
1844
- const rhythmSkill = new DroidRhythm(this.mods);
1845
- this.calculateSkills(rhythmSkill);
1846
- this.postCalculateRhythm(rhythmSkill);
1847
- }
1848
- /**
1849
- * Calculates the flashlight star rating of the beatmap and stores it in this instance.
1850
- */
1851
- calculateFlashlight() {
1852
- if (!this.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
1853
- this.attributes.flashlightDifficulty = 0;
1854
- return;
1855
- }
1856
- const flashlightSkill = new DroidFlashlight(this.mods, true);
1857
- const flashlightSkillWithoutSliders = new DroidFlashlight(this.mods, false);
1858
- this.calculateSkills(flashlightSkill, flashlightSkillWithoutSliders);
1859
- this.postCalculateFlashlight(flashlightSkill, flashlightSkillWithoutSliders);
1860
- }
1861
- /**
1862
- * Calculates the visual star rating of the beatmap and stores it in this instance.
1863
- */
1864
- calculateVisual() {
1865
- if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
1866
- this.attributes.visualDifficulty = 0;
1867
- return;
1868
- }
1869
- const visualSkill = new DroidVisual(this.mods, true);
1870
- const visualSkillWithoutSliders = new DroidVisual(this.mods, false);
1871
- this.calculateSkills(visualSkill, visualSkillWithoutSliders);
1872
- this.postCalculateVisual(visualSkill, visualSkillWithoutSliders);
1873
- }
1874
- calculateTotal() {
1875
- const aimPerformanceValue = this.basePerformanceValue(Math.pow(this.aim, 0.8));
1876
- const tapPerformanceValue = this.basePerformanceValue(this.tap);
1877
- const flashlightPerformanceValue = this.mods.some((m) => m instanceof osuBase.ModFlashlight)
1878
- ? Math.pow(this.flashlight, 1.6) * 25
1879
- : 0;
1880
- const visualPerformanceValue = Math.pow(this.visual, 1.6) * 22.5;
1803
+ this.difficultyAdjustmentMods
1804
+ .add(osuBase.ModPrecise)
1805
+ .add(osuBase.ModScoreV2)
1806
+ .add(osuBase.ModTraceable);
1807
+ }
1808
+ retainDifficultyAdjustmentMods(mods) {
1809
+ return mods.filter((mod) => mod.isApplicableToDroid() &&
1810
+ this.difficultyAdjustmentMods.has(mod.constructor) &&
1811
+ mod.isDroidRelevant);
1812
+ }
1813
+ createDifficultyAttributes(beatmap, skills, objects) {
1814
+ const attributes = new ExtendedDroidDifficultyAttributes();
1815
+ attributes.mods = beatmap.mods.slice();
1816
+ attributes.maxCombo = beatmap.maxCombo;
1817
+ attributes.clockRate = beatmap.speedMultiplier;
1818
+ attributes.hitCircleCount = beatmap.hitObjects.circles;
1819
+ attributes.sliderCount = beatmap.hitObjects.sliders;
1820
+ attributes.spinnerCount = beatmap.hitObjects.spinners;
1821
+ this.populateAimAttributes(attributes, skills, objects);
1822
+ this.populateTapAttributes(attributes, skills, objects);
1823
+ this.populateRhythmAttributes(attributes, skills);
1824
+ this.populateFlashlightAttributes(attributes, skills);
1825
+ this.populateVisualAttributes(attributes, skills);
1826
+ if (attributes.mods.some((m) => m instanceof osuBase.ModRelax)) {
1827
+ attributes.aimDifficulty *= 0.9;
1828
+ attributes.tapDifficulty = 0;
1829
+ attributes.rhythmDifficulty = 0;
1830
+ attributes.flashlightDifficulty *= 0.7;
1831
+ attributes.visualDifficulty = 0;
1832
+ }
1833
+ else if (attributes.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
1834
+ attributes.aimDifficulty = 0;
1835
+ attributes.flashlightDifficulty *= 0.3;
1836
+ attributes.visualDifficulty *= 0.8;
1837
+ }
1838
+ const aimPerformanceValue = this.basePerformanceValue(Math.pow(attributes.aimDifficulty, 0.8));
1839
+ const tapPerformanceValue = this.basePerformanceValue(attributes.tapDifficulty);
1840
+ const flashlightPerformanceValue = Math.pow(attributes.flashlightDifficulty, 1.6) * 25;
1841
+ const visualPerformanceValue = Math.pow(attributes.visualDifficulty, 1.6) * 22.5;
1881
1842
  const basePerformanceValue = Math.pow(Math.pow(aimPerformanceValue, 1.1) +
1882
1843
  Math.pow(tapPerformanceValue, 1.1) +
1883
1844
  Math.pow(flashlightPerformanceValue, 1.1) +
@@ -1885,126 +1846,82 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1885
1846
  if (basePerformanceValue > 1e-5) {
1886
1847
  // Document for formula derivation:
1887
1848
  // https://docs.google.com/document/d/10DZGYYSsT_yjz2Mtp6yIJld0Rqx4E-vVHupCqiM4TNI/edit
1888
- this.attributes.starRating =
1849
+ attributes.starRating =
1889
1850
  0.027 *
1890
1851
  (Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformanceValue) +
1891
1852
  4);
1892
1853
  }
1893
1854
  else {
1894
- this.attributes.starRating = 0;
1895
- }
1896
- }
1897
- calculateAll() {
1898
- const skills = this.createSkills();
1899
- this.calculateSkills(...skills);
1900
- const aimSkill = skills.find((s) => s instanceof DroidAim && s.withSliders);
1901
- const aimSkillWithoutSliders = skills.find((s) => s instanceof DroidAim && !s.withSliders);
1902
- const rhythmSkill = skills.find((s) => s instanceof DroidRhythm);
1903
- const tapSkillCheese = skills.find((s) => s instanceof DroidTap && s.considerCheesability);
1904
- const flashlightSkill = skills.find((s) => s instanceof DroidFlashlight && s.withSliders);
1905
- const flashlightSkillWithoutSliders = skills.find((s) => s instanceof DroidFlashlight && !s.withSliders);
1906
- const visualSkill = skills.find((s) => s instanceof DroidVisual && s.withSliders);
1907
- const visualSkillWithoutSliders = skills.find((s) => s instanceof DroidVisual && !s.withSliders);
1908
- if (aimSkill && aimSkillWithoutSliders) {
1909
- this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
1910
- }
1911
- if (tapSkillCheese) {
1912
- const tapSkillVibro = new DroidTap(this.mods, true, tapSkillCheese.relevantDeltaTime());
1913
- this.calculateSkills(tapSkillVibro);
1914
- this.postCalculateTap(tapSkillCheese, tapSkillVibro);
1855
+ attributes.starRating = 0;
1915
1856
  }
1916
- this.postCalculateRhythm(rhythmSkill);
1917
- if (flashlightSkill && flashlightSkillWithoutSliders) {
1918
- this.postCalculateFlashlight(flashlightSkill, flashlightSkillWithoutSliders);
1857
+ let greatWindow;
1858
+ if (attributes.mods.some((m) => m instanceof osuBase.ModPrecise)) {
1859
+ greatWindow = new osuBase.PreciseDroidHitWindow(beatmap.difficulty.od)
1860
+ .greatWindow;
1919
1861
  }
1920
- if (visualSkill && visualSkillWithoutSliders) {
1921
- this.postCalculateVisual(visualSkill, visualSkillWithoutSliders);
1862
+ else {
1863
+ greatWindow = new osuBase.DroidHitWindow(beatmap.difficulty.od).greatWindow;
1922
1864
  }
1923
- this.calculateTotal();
1865
+ attributes.overallDifficulty = osuBase.OsuHitWindow.greatWindowToOD(greatWindow / attributes.clockRate);
1866
+ return attributes;
1924
1867
  }
1925
- toString() {
1926
- return (this.total.toFixed(2) +
1927
- " stars (" +
1928
- this.aim.toFixed(2) +
1929
- " aim, " +
1930
- this.tap.toFixed(2) +
1931
- " tap, " +
1932
- this.rhythm.toFixed(2) +
1933
- " rhythm, " +
1934
- this.flashlight.toFixed(2) +
1935
- " flashlight, " +
1936
- this.visual.toFixed(2) +
1937
- " visual)");
1868
+ createPlayableBeatmap(beatmap, mods) {
1869
+ return beatmap.createDroidPlayableBeatmap(mods);
1938
1870
  }
1939
- generateDifficultyHitObjects(beatmap, clockRate) {
1871
+ createDifficultyHitObjects(beatmap) {
1940
1872
  var _a, _b;
1873
+ const clockRate = beatmap.speedMultiplier;
1941
1874
  const difficultyObjects = [];
1942
1875
  const { objects } = beatmap.hitObjects;
1943
1876
  for (let i = 0; i < objects.length; ++i) {
1944
- const difficultyObject = new DroidDifficultyHitObject(objects[i], (_a = objects[i - 1]) !== null && _a !== void 0 ? _a : null, (_b = objects[i - 2]) !== null && _b !== void 0 ? _b : null, difficultyObjects, clockRate);
1877
+ const difficultyObject = new DroidDifficultyHitObject(objects[i], (_a = objects[i - 1]) !== null && _a !== void 0 ? _a : null, (_b = objects[i - 2]) !== null && _b !== void 0 ? _b : null, difficultyObjects, clockRate, i - 1);
1945
1878
  difficultyObject.computeProperties(clockRate, objects);
1946
1879
  difficultyObjects.push(difficultyObject);
1947
1880
  }
1948
1881
  return difficultyObjects;
1949
1882
  }
1950
- createSkills() {
1883
+ createSkills(beatmap) {
1884
+ const { mods } = beatmap;
1951
1885
  const skills = [];
1952
- if (!this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
1953
- skills.push(new DroidAim(this.mods, true));
1954
- skills.push(new DroidAim(this.mods, false));
1886
+ if (!mods.some((m) => m instanceof osuBase.ModAutopilot)) {
1887
+ skills.push(new DroidAim(mods, true));
1888
+ skills.push(new DroidAim(mods, false));
1955
1889
  }
1956
- if (!this.mods.some((m) => m instanceof osuBase.ModRelax)) {
1890
+ if (!mods.some((m) => m instanceof osuBase.ModRelax)) {
1957
1891
  // Tap and visual skills depend on rhythm skill, so we put it first
1958
- skills.push(new DroidRhythm(this.mods));
1959
- skills.push(new DroidTap(this.mods, true));
1960
- skills.push(new DroidTap(this.mods, false));
1961
- skills.push(new DroidVisual(this.mods, true));
1962
- skills.push(new DroidVisual(this.mods, false));
1892
+ skills.push(new DroidRhythm(mods));
1893
+ skills.push(new DroidTap(mods, true));
1894
+ skills.push(new DroidTap(mods, false));
1895
+ skills.push(new DroidVisual(mods, true));
1896
+ skills.push(new DroidVisual(mods, false));
1963
1897
  }
1964
- if (this.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
1965
- skills.push(new DroidFlashlight(this.mods, true));
1966
- skills.push(new DroidFlashlight(this.mods, false));
1898
+ if (mods.some((m) => m instanceof osuBase.ModFlashlight)) {
1899
+ skills.push(new DroidFlashlight(mods, true));
1900
+ skills.push(new DroidFlashlight(mods, false));
1967
1901
  }
1968
1902
  return skills;
1969
1903
  }
1970
- calculateClockRate(options) {
1971
- var _a, _b;
1972
- return (osuBase.ModUtil.calculateRateWithMods((_a = options === null || options === void 0 ? void 0 : options.mods) !== null && _a !== void 0 ? _a : [], options === null || options === void 0 ? void 0 : options.oldStatistics) * ((_b = options === null || options === void 0 ? void 0 : options.customSpeedMultiplier) !== null && _b !== void 0 ? _b : 1));
1973
- }
1974
- /**
1975
- * Called after aim skill calculation.
1976
- *
1977
- * @param aimSkill The aim skill that considers sliders.
1978
- * @param aimSkillWithoutSliders The aim skill that doesn't consider sliders.
1979
- */
1980
- postCalculateAim(aimSkill, aimSkillWithoutSliders) {
1981
- this.strainPeaks.aimWithSliders = aimSkill.strainPeaks;
1982
- this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
1983
- this.attributes.aimDifficulty = this.mods.some((m) => m instanceof osuBase.ModAutopilot)
1984
- ? 0
1985
- : this.starValue(aimSkill.difficultyValue());
1986
- if (this.aim) {
1987
- this.attributes.sliderFactor =
1988
- this.starValue(aimSkillWithoutSliders.difficultyValue()) /
1989
- this.aim;
1990
- }
1991
- if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
1992
- this.attributes.aimDifficulty *= 0.9;
1904
+ createStrainPeakSkills(beatmap) {
1905
+ const { mods } = beatmap;
1906
+ return [
1907
+ new DroidAim(mods, true),
1908
+ new DroidAim(mods, false),
1909
+ new DroidTap(mods, true),
1910
+ new DroidFlashlight(mods, true),
1911
+ ];
1912
+ }
1913
+ populateAimAttributes(attributes, skills, objects) {
1914
+ const aim = skills.find((s) => s instanceof DroidAim && s.withSliders);
1915
+ const aimNoSlider = skills.find((s) => s instanceof DroidAim && !s.withSliders);
1916
+ if (!aim || !aimNoSlider) {
1917
+ return;
1993
1918
  }
1994
- this.attributes.aimDifficultStrainCount =
1995
- aimSkill.countDifficultStrains();
1996
- this.attributes.aimDifficultSliderCount =
1997
- aimSkill.countDifficultSliders();
1998
- this.calculateAimAttributes();
1999
- }
2000
- /**
2001
- * Calculates aim-related attributes.
2002
- */
2003
- calculateAimAttributes() {
2004
- this.attributes.difficultSliders = [];
1919
+ attributes.aimDifficulty = this.calculateRating(aim);
1920
+ attributes.aimDifficultSliderCount = aim.countDifficultSliders();
1921
+ attributes.aimDifficultStrainCount = aim.countDifficultStrains();
2005
1922
  const topDifficultSliders = [];
2006
- for (let i = 0; i < this.objects.length; ++i) {
2007
- const object = this.objects[i];
1923
+ for (let i = 0; i < objects.length; ++i) {
1924
+ const object = objects[i];
2008
1925
  const velocity = object.travelDistance / object.travelTime;
2009
1926
  if (velocity > 0) {
2010
1927
  topDifficultSliders.push({
@@ -2018,53 +1935,50 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
2018
1935
  const difficultyRating = slider.velocity / velocitySum;
2019
1936
  // Only consider sliders that are fast enough.
2020
1937
  if (difficultyRating > 0.02) {
2021
- this.attributes.difficultSliders.push({
1938
+ attributes.difficultSliders.push({
2022
1939
  index: slider.index,
2023
1940
  difficultyRating: slider.velocity / velocitySum,
2024
1941
  });
2025
1942
  }
2026
1943
  }
2027
- this.attributes.difficultSliders.sort((a, b) => b.difficultyRating - a.difficultyRating);
1944
+ attributes.difficultSliders.sort((a, b) => b.difficultyRating - a.difficultyRating);
2028
1945
  // Take the top 15% most difficult sliders.
2029
- while (this.attributes.difficultSliders.length >
2030
- Math.ceil(0.15 * this.beatmap.hitObjects.sliders)) {
2031
- this.attributes.difficultSliders.pop();
1946
+ while (attributes.difficultSliders.length >
1947
+ Math.ceil(0.15 * attributes.sliderCount)) {
1948
+ attributes.difficultSliders.pop();
1949
+ }
1950
+ if (attributes.aimDifficulty > 0) {
1951
+ attributes.sliderFactor =
1952
+ this.calculateRating(aimNoSlider) / attributes.aimDifficulty;
1953
+ }
1954
+ else {
1955
+ attributes.sliderFactor = 1;
2032
1956
  }
2033
1957
  }
2034
- /**
2035
- * Called after tap skill calculation.
2036
- *
2037
- * @param tapSkillCheese The tap skill that considers cheesing.
2038
- * @param tapSkillVibro The tap skill that considers vibro.
2039
- */
2040
- postCalculateTap(tapSkillCheese, tapSkillVibro) {
2041
- this.strainPeaks.speed = tapSkillCheese.strainPeaks;
2042
- this.attributes.tapDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
2043
- ? 0
2044
- : this.starValue(tapSkillCheese.difficultyValue());
2045
- if (this.tap) {
2046
- this.attributes.vibroFactor =
2047
- this.starValue(tapSkillVibro.difficultyValue()) / this.tap;
2048
- }
2049
- this.attributes.speedNoteCount = tapSkillCheese.relevantNoteCount();
2050
- this.attributes.averageSpeedDeltaTime =
2051
- tapSkillCheese.relevantDeltaTime();
2052
- this.attributes.tapDifficultStrainCount =
2053
- tapSkillCheese.countDifficultStrains();
2054
- this.calculateTapAttributes();
2055
- }
2056
- /**
2057
- * Calculates tap-related attributes.
2058
- */
2059
- calculateTapAttributes() {
2060
- this.attributes.possibleThreeFingeredSections = [];
1958
+ populateTapAttributes(attributes, skills, objects) {
1959
+ const tap = skills.find((s) => s instanceof DroidTap && s.considerCheesability);
1960
+ if (!tap) {
1961
+ return;
1962
+ }
1963
+ attributes.tapDifficulty = this.calculateRating(tap);
1964
+ attributes.tapDifficultStrainCount = tap.countDifficultStrains();
1965
+ attributes.speedNoteCount = tap.relevantNoteCount();
1966
+ attributes.averageSpeedDeltaTime = tap.relevantDeltaTime();
1967
+ if (attributes.tapDifficulty > 0) {
1968
+ const tapVibro = new DroidTap(attributes.mods, true, attributes.averageSpeedDeltaTime);
1969
+ for (const object of objects) {
1970
+ tapVibro.process(object);
1971
+ }
1972
+ attributes.vibroFactor =
1973
+ this.calculateRating(tapVibro) / attributes.tapDifficulty;
1974
+ }
2061
1975
  const { threeFingerStrainThreshold } = DroidDifficultyCalculator;
2062
1976
  const minSectionObjectCount = 5;
2063
1977
  let inSpeedSection = false;
2064
1978
  let firstSpeedObjectIndex = 0;
2065
- for (let i = 2; i < this.objects.length; ++i) {
2066
- const current = this.objects[i];
2067
- const prev = this.objects[i - 1];
1979
+ for (let i = 2; i < objects.length; ++i) {
1980
+ const current = objects[i];
1981
+ const prev = objects[i - 1];
2068
1982
  if (!inSpeedSection &&
2069
1983
  current.originalTapStrain >= threeFingerStrainThreshold) {
2070
1984
  inSpeedSection = true;
@@ -2080,17 +1994,17 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
2080
1994
  // Stop speed section on slowing down 1/2 rhythm change or anything slower.
2081
1995
  (prevDelta < currentDelta && deltaRatio <= 0.5) ||
2082
1996
  // Don't forget to manually add the last section, which would otherwise be ignored.
2083
- i === this.objects.length - 1)) {
2084
- const lastSpeedObjectIndex = i - (i === this.objects.length - 1 ? 0 : 1);
1997
+ i === objects.length - 1)) {
1998
+ const lastSpeedObjectIndex = i - (i === objects.length - 1 ? 0 : 1);
2085
1999
  inSpeedSection = false;
2086
2000
  // Ignore sections that don't meet object count requirement.
2087
2001
  if (i - firstSpeedObjectIndex < minSectionObjectCount) {
2088
2002
  continue;
2089
2003
  }
2090
- this.attributes.possibleThreeFingeredSections.push({
2004
+ attributes.possibleThreeFingeredSections.push({
2091
2005
  firstObjectIndex: firstSpeedObjectIndex,
2092
2006
  lastObjectIndex: lastSpeedObjectIndex,
2093
- sumStrain: Math.pow(this.objects
2007
+ sumStrain: Math.pow(objects
2094
2008
  .slice(firstSpeedObjectIndex, lastSpeedObjectIndex + 1)
2095
2009
  .reduce((a, v) => a +
2096
2010
  v.originalTapStrain /
@@ -2099,61 +2013,47 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
2099
2013
  }
2100
2014
  }
2101
2015
  }
2102
- /**
2103
- * Called after rhythm skill calculation.
2104
- *
2105
- * @param rhythmSkill The rhythm skill.
2106
- */
2107
- postCalculateRhythm(rhythmSkill) {
2108
- this.attributes.rhythmDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
2109
- ? 0
2110
- : this.starValue(rhythmSkill.difficultyValue());
2111
- }
2112
- /**
2113
- * Called after flashlight skill calculation.
2114
- *
2115
- * @param flashlightSkill The flashlight skill that considers sliders.
2116
- * @param flashlightSkillWithoutSliders The flashlight skill that doesn't consider sliders.
2117
- */
2118
- postCalculateFlashlight(flashlightSkill, flashlightSkillWithoutSliders) {
2119
- this.strainPeaks.flashlight = flashlightSkill.strainPeaks;
2120
- this.attributes.flashlightDifficulty = this.starValue(flashlightSkill.difficultyValue());
2121
- if (this.flashlight) {
2122
- this.attributes.flashlightSliderFactor =
2123
- this.starValue(flashlightSkillWithoutSliders.difficultyValue()) / this.flashlight;
2016
+ populateRhythmAttributes(attributes, skills) {
2017
+ const rhythm = skills.find((s) => s instanceof DroidRhythm);
2018
+ if (!rhythm) {
2019
+ return;
2124
2020
  }
2125
- if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
2126
- this.attributes.flashlightDifficulty *= 0.3;
2021
+ attributes.rhythmDifficulty = this.calculateRating(rhythm);
2022
+ }
2023
+ populateFlashlightAttributes(attributes, skills) {
2024
+ const flashlight = skills.find((s) => s instanceof DroidFlashlight && s.withSliders);
2025
+ const flashlightNoSliders = skills.find((s) => s instanceof DroidFlashlight && !s.withSliders);
2026
+ if (!flashlight || !flashlightNoSliders) {
2027
+ return;
2127
2028
  }
2128
- if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
2129
- this.attributes.flashlightDifficulty *= 0.7;
2029
+ attributes.flashlightDifficulty = this.calculateRating(flashlight);
2030
+ attributes.flashlightDifficultStrainCount =
2031
+ flashlight.countDifficultStrains();
2032
+ if (attributes.flashlightDifficulty > 0) {
2033
+ attributes.flashlightSliderFactor =
2034
+ this.calculateRating(flashlightNoSliders) /
2035
+ attributes.flashlightDifficulty;
2130
2036
  }
2131
- else if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
2132
- this.attributes.flashlightDifficulty *= 0.4;
2037
+ else {
2038
+ attributes.flashlightSliderFactor = 1;
2133
2039
  }
2134
- this.attributes.flashlightDifficultStrainCount =
2135
- flashlightSkill.countDifficultStrains();
2136
2040
  }
2137
- /**
2138
- * Called after visual skill calculation.
2139
- *
2140
- * @param visualSkillWithSliders The visual skill that considers sliders.
2141
- * @param visualSkillWithoutSliders The visual skill that doesn't consider sliders.
2142
- */
2143
- postCalculateVisual(visualSkillWithSliders, visualSkillWithoutSliders) {
2144
- this.attributes.visualDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
2145
- ? 0
2146
- : this.starValue(visualSkillWithSliders.difficultyValue());
2147
- if (this.visual) {
2148
- this.attributes.visualSliderFactor =
2149
- this.starValue(visualSkillWithoutSliders.difficultyValue()) /
2150
- this.visual;
2041
+ populateVisualAttributes(attributes, skills) {
2042
+ const visual = skills.find((s) => s instanceof DroidVisual && s.withSliders);
2043
+ const visualNoSliders = skills.find((s) => s instanceof DroidVisual && !s.withSliders);
2044
+ if (!visual || !visualNoSliders) {
2045
+ return;
2151
2046
  }
2152
- if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
2153
- this.attributes.visualDifficulty *= 0.8;
2047
+ attributes.visualDifficulty = this.calculateRating(visual);
2048
+ attributes.visualDifficultStrainCount = visual.countDifficultStrains();
2049
+ if (attributes.visualDifficulty > 0) {
2050
+ attributes.visualSliderFactor =
2051
+ this.calculateRating(visualNoSliders) /
2052
+ attributes.visualDifficulty;
2053
+ }
2054
+ else {
2055
+ attributes.visualSliderFactor = 1;
2154
2056
  }
2155
- this.attributes.visualDifficultStrainCount =
2156
- visualSkillWithSliders.countDifficultStrains();
2157
2057
  }
2158
2058
  }
2159
2059
  /**
@@ -2189,7 +2089,7 @@ class PerformanceCalculator {
2189
2089
  this.sliderNerfFactor = 1;
2190
2090
  this.difficultyAttributes = difficultyAttributes;
2191
2091
  this.mods = this.isCacheableAttribute(difficultyAttributes)
2192
- ? osuBase.ModUtil.pcStringToMods(difficultyAttributes.mods)
2092
+ ? osuBase.ModUtil.deserializeMods(difficultyAttributes.mods)
2193
2093
  : difficultyAttributes.mods;
2194
2094
  }
2195
2095
  /**
@@ -3142,62 +3042,149 @@ class OsuAim extends OsuSkill {
3142
3042
  }
3143
3043
 
3144
3044
  /**
3145
- * An evaluator for calculating osu!standard speed skill.
3045
+ * Holds data that can be used to calculate osu!standard performance points.
3146
3046
  */
3147
- class OsuSpeedEvaluator {
3047
+ class OsuDifficultyAttributes extends DifficultyAttributes {
3048
+ constructor(cacheableAttributes) {
3049
+ super(cacheableAttributes);
3050
+ this.approachRate = 0;
3051
+ this.speedDifficulty = 0;
3052
+ this.speedDifficultStrainCount = 0;
3053
+ if (!cacheableAttributes) {
3054
+ return;
3055
+ }
3056
+ this.approachRate = cacheableAttributes.approachRate;
3057
+ this.speedDifficulty = cacheableAttributes.speedDifficulty;
3058
+ this.speedDifficultStrainCount =
3059
+ cacheableAttributes.speedDifficultStrainCount;
3060
+ }
3061
+ toString() {
3062
+ return (super.toString() +
3063
+ ` (${this.aimDifficulty.toFixed(2)} aim, ` +
3064
+ `${this.speedDifficulty.toFixed(2)} speed, ` +
3065
+ `${this.flashlightDifficulty.toFixed(2)} flashlight)`);
3066
+ }
3067
+ }
3068
+
3069
+ /**
3070
+ * An evaluator for calculating osu!standard Flashlight skill.
3071
+ */
3072
+ class OsuFlashlightEvaluator {
3148
3073
  /**
3149
- * Evaluates the difficulty of tapping the current object, based on:
3074
+ * Evaluates the difficulty of memorizing and hitting the current object, based on:
3150
3075
  *
3151
- * - time between pressing the previous and current object,
3152
- * - distance between those objects,
3153
- * - and how easily they can be cheesed.
3076
+ * - distance between a number of previous objects and the current object,
3077
+ * - the visual opacity of the current object,
3078
+ * - the angle made by the current object,
3079
+ * - length and speed of the current object (for sliders),
3080
+ * - and whether Hidden mod is enabled.
3154
3081
  *
3155
3082
  * @param current The current object.
3156
- * @param mods The mods applied.
3083
+ * @param mods The mods used.
3157
3084
  */
3158
3085
  static evaluateDifficultyOf(current, mods) {
3159
- var _a;
3160
3086
  if (current.object instanceof osuBase.Spinner) {
3161
3087
  return 0;
3162
3088
  }
3163
- const prev = current.previous(0);
3164
- let strainTime = current.strainTime;
3165
- // Nerf doubletappable doubles.
3166
- const doubletapness = 1 - current.doubletapness;
3167
- // Cap deltatime to the OD 300 hitwindow.
3168
- // 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.
3169
- strainTime /= osuBase.MathUtils.clamp(strainTime / current.fullGreatWindow / 0.93, 0.92, 1);
3170
- // speedBonus will be 0.0 for BPM < 200
3171
- let speedBonus = 0;
3172
- // Add additional scaling bonus for streams/bursts higher than 200bpm
3173
- if (strainTime < this.minSpeedBonus) {
3174
- speedBonus =
3175
- 0.75 * Math.pow((this.minSpeedBonus - strainTime) / 40, 2);
3089
+ const scalingFactor = 52 / current.object.radius;
3090
+ let smallDistNerf = 1;
3091
+ let cumulativeStrainTime = 0;
3092
+ let result = 0;
3093
+ let last = current;
3094
+ let angleRepeatCount = 0;
3095
+ for (let i = 0; i < Math.min(current.index, 10); ++i) {
3096
+ const currentObject = current.previous(i);
3097
+ cumulativeStrainTime += last.strainTime;
3098
+ if (!(currentObject.object instanceof osuBase.Spinner)) {
3099
+ const jumpDistance = current.object
3100
+ .getStackedPosition(osuBase.Modes.osu)
3101
+ .subtract(currentObject.object.getStackedEndPosition(osuBase.Modes.osu)).length;
3102
+ // We want to nerf objects that can be easily seen within the Flashlight circle radius.
3103
+ if (i === 0) {
3104
+ smallDistNerf = Math.min(1, jumpDistance / 75);
3105
+ }
3106
+ // We also want to nerf stacks so that only the first object of the stack is accounted for.
3107
+ const stackNerf = Math.min(1, currentObject.lazyJumpDistance / scalingFactor / 25);
3108
+ // Bonus based on how visible the object is.
3109
+ const opacityBonus = 1 +
3110
+ this.maxOpacityBonus *
3111
+ (1 -
3112
+ current.opacityAt(currentObject.object.startTime, mods));
3113
+ result +=
3114
+ (stackNerf * opacityBonus * scalingFactor * jumpDistance) /
3115
+ cumulativeStrainTime;
3116
+ if (currentObject.angle !== null && current.angle !== null) {
3117
+ // Objects further back in time should count less for the nerf.
3118
+ if (Math.abs(currentObject.angle - current.angle) < 0.02) {
3119
+ angleRepeatCount += Math.max(0, 1 - 0.1 * i);
3120
+ }
3121
+ }
3122
+ }
3123
+ last = currentObject;
3176
3124
  }
3177
- const travelDistance = (_a = prev === null || prev === void 0 ? void 0 : prev.travelDistance) !== null && _a !== void 0 ? _a : 0;
3178
- // Cap distance at spacing threshold
3179
- const distance = Math.min(this.SINGLE_SPACING_THRESHOLD, travelDistance + current.minimumJumpDistance);
3180
- // Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
3181
- let distanceBonus = Math.pow(distance / this.SINGLE_SPACING_THRESHOLD, 3.95) *
3182
- this.DISTANCE_MULTIPLIER;
3183
- if (mods.some((m) => m instanceof osuBase.ModAutopilot)) {
3184
- distanceBonus = 0;
3125
+ result = Math.pow(smallDistNerf * result, 2);
3126
+ // Additional bonus for Hidden due to there being no approach circles.
3127
+ if (mods.some((m) => m instanceof osuBase.ModHidden)) {
3128
+ result *= 1 + this.hiddenBonus;
3185
3129
  }
3186
- // Base difficulty with all bonuses
3187
- const difficulty = ((1 + speedBonus + distanceBonus) * 1000) / strainTime;
3188
- // Apply penalty if there's doubletappable doubles
3189
- return difficulty * doubletapness;
3130
+ // Nerf patterns with repeated angles.
3131
+ result *=
3132
+ this.minAngleMultiplier +
3133
+ (1 - this.minAngleMultiplier) / (angleRepeatCount + 1);
3134
+ let sliderBonus = 0;
3135
+ if (current.object instanceof osuBase.Slider) {
3136
+ // Invert the scaling factor to determine the true travel distance independent of circle size.
3137
+ const pixelTravelDistance = current.object.lazyTravelDistance / scalingFactor;
3138
+ // Reward sliders based on velocity.
3139
+ sliderBonus = Math.pow(Math.max(0, pixelTravelDistance / current.travelTime - this.minVelocity), 0.5);
3140
+ // Longer sliders require more memorization.
3141
+ sliderBonus *= pixelTravelDistance;
3142
+ // Nerf sliders with repeats, as less memorization is required.
3143
+ if (current.object.repeatCount > 0)
3144
+ sliderBonus /= current.object.repeatCount + 1;
3145
+ }
3146
+ result += sliderBonus * this.sliderMultiplier;
3147
+ return result;
3190
3148
  }
3191
3149
  }
3150
+ OsuFlashlightEvaluator.maxOpacityBonus = 0.4;
3151
+ OsuFlashlightEvaluator.hiddenBonus = 0.2;
3152
+ OsuFlashlightEvaluator.minVelocity = 0.5;
3153
+ OsuFlashlightEvaluator.sliderMultiplier = 1.3;
3154
+ OsuFlashlightEvaluator.minAngleMultiplier = 0.2;
3155
+
3192
3156
  /**
3193
- * Spacing threshold for a single hitobject spacing.
3194
- *
3195
- * About 1.25 circles distance between hitobject centers.
3157
+ * Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
3196
3158
  */
3197
- OsuSpeedEvaluator.SINGLE_SPACING_THRESHOLD = 125;
3198
- // ~200 1/4 BPM streams
3199
- OsuSpeedEvaluator.minSpeedBonus = 75;
3200
- OsuSpeedEvaluator.DISTANCE_MULTIPLIER = 0.9;
3159
+ class OsuFlashlight extends OsuSkill {
3160
+ constructor() {
3161
+ super(...arguments);
3162
+ this.strainDecayBase = 0.15;
3163
+ this.reducedSectionCount = 0;
3164
+ this.reducedSectionBaseline = 1;
3165
+ this.decayWeight = 1;
3166
+ this.currentFlashlightStrain = 0;
3167
+ this.skillMultiplier = 0.05512;
3168
+ }
3169
+ difficultyValue() {
3170
+ return this.strainPeaks.reduce((a, b) => a + b, 0);
3171
+ }
3172
+ strainValueAt(current) {
3173
+ this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
3174
+ this.currentFlashlightStrain +=
3175
+ OsuFlashlightEvaluator.evaluateDifficultyOf(current, this.mods) *
3176
+ this.skillMultiplier;
3177
+ return this.currentFlashlightStrain;
3178
+ }
3179
+ calculateInitialStrain(time, current) {
3180
+ var _a, _b;
3181
+ return (this.currentFlashlightStrain *
3182
+ this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
3183
+ }
3184
+ saveToHitObject(current) {
3185
+ current.flashlightStrain = this.currentFlashlightStrain;
3186
+ }
3187
+ }
3201
3188
 
3202
3189
  /**
3203
3190
  * An evaluator for calculating osu!standard Rhythm skill.
@@ -3347,6 +3334,64 @@ OsuRhythmEvaluator.historyObjectsMax = 32;
3347
3334
  OsuRhythmEvaluator.rhythmOverallMultiplier = 0.95;
3348
3335
  OsuRhythmEvaluator.rhythmRatioMultiplier = 12;
3349
3336
 
3337
+ /**
3338
+ * An evaluator for calculating osu!standard speed skill.
3339
+ */
3340
+ class OsuSpeedEvaluator {
3341
+ /**
3342
+ * Evaluates the difficulty of tapping the current object, based on:
3343
+ *
3344
+ * - time between pressing the previous and current object,
3345
+ * - distance between those objects,
3346
+ * - and how easily they can be cheesed.
3347
+ *
3348
+ * @param current The current object.
3349
+ * @param mods The mods applied.
3350
+ */
3351
+ static evaluateDifficultyOf(current, mods) {
3352
+ var _a;
3353
+ if (current.object instanceof osuBase.Spinner) {
3354
+ return 0;
3355
+ }
3356
+ const prev = current.previous(0);
3357
+ let strainTime = current.strainTime;
3358
+ // Nerf doubletappable doubles.
3359
+ const doubletapness = 1 - current.doubletapness;
3360
+ // Cap deltatime to the OD 300 hitwindow.
3361
+ // 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.
3362
+ strainTime /= osuBase.MathUtils.clamp(strainTime / current.fullGreatWindow / 0.93, 0.92, 1);
3363
+ // speedBonus will be 0.0 for BPM < 200
3364
+ let speedBonus = 0;
3365
+ // Add additional scaling bonus for streams/bursts higher than 200bpm
3366
+ if (strainTime < this.minSpeedBonus) {
3367
+ speedBonus =
3368
+ 0.75 * Math.pow((this.minSpeedBonus - strainTime) / 40, 2);
3369
+ }
3370
+ const travelDistance = (_a = prev === null || prev === void 0 ? void 0 : prev.travelDistance) !== null && _a !== void 0 ? _a : 0;
3371
+ // Cap distance at spacing threshold
3372
+ const distance = Math.min(this.SINGLE_SPACING_THRESHOLD, travelDistance + current.minimumJumpDistance);
3373
+ // Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
3374
+ let distanceBonus = Math.pow(distance / this.SINGLE_SPACING_THRESHOLD, 3.95) *
3375
+ this.DISTANCE_MULTIPLIER;
3376
+ if (mods.some((m) => m instanceof osuBase.ModAutopilot)) {
3377
+ distanceBonus = 0;
3378
+ }
3379
+ // Base difficulty with all bonuses
3380
+ const difficulty = ((1 + speedBonus + distanceBonus) * 1000) / strainTime;
3381
+ // Apply penalty if there's doubletappable doubles
3382
+ return difficulty * doubletapness;
3383
+ }
3384
+ }
3385
+ /**
3386
+ * Spacing threshold for a single hitobject spacing.
3387
+ *
3388
+ * About 1.25 circles distance between hitobject centers.
3389
+ */
3390
+ OsuSpeedEvaluator.SINGLE_SPACING_THRESHOLD = 125;
3391
+ // ~200 1/4 BPM streams
3392
+ OsuSpeedEvaluator.minSpeedBonus = 75;
3393
+ OsuSpeedEvaluator.DISTANCE_MULTIPLIER = 0.9;
3394
+
3350
3395
  /**
3351
3396
  * Represents the skill required to press keys or tap with regards to keeping up with the speed at which objects need to be hit.
3352
3397
  */
@@ -3361,6 +3406,19 @@ class OsuSpeed extends OsuSkill {
3361
3406
  this.currentRhythm = 0;
3362
3407
  this.skillMultiplier = 1.46;
3363
3408
  }
3409
+ /**
3410
+ * The amount of notes that are relevant to the difficulty.
3411
+ */
3412
+ relevantNoteCount() {
3413
+ if (this._objectStrains.length === 0) {
3414
+ return 0;
3415
+ }
3416
+ const maxStrain = osuBase.MathUtils.max(this._objectStrains);
3417
+ if (maxStrain === 0) {
3418
+ return 0;
3419
+ }
3420
+ return this._objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
3421
+ }
3364
3422
  /**
3365
3423
  * @param current The hitobject to calculate.
3366
3424
  */
@@ -3389,360 +3447,136 @@ class OsuSpeed extends OsuSkill {
3389
3447
  }
3390
3448
  }
3391
3449
 
3392
- /**
3393
- * An evaluator for calculating osu!standard Flashlight skill.
3394
- */
3395
- class OsuFlashlightEvaluator {
3396
- /**
3397
- * Evaluates the difficulty of memorizing and hitting the current object, based on:
3398
- *
3399
- * - distance between a number of previous objects and the current object,
3400
- * - the visual opacity of the current object,
3401
- * - the angle made by the current object,
3402
- * - length and speed of the current object (for sliders),
3403
- * - and whether Hidden mod is enabled.
3404
- *
3405
- * @param current The current object.
3406
- * @param mods The mods used.
3407
- */
3408
- static evaluateDifficultyOf(current, mods) {
3409
- if (current.object instanceof osuBase.Spinner) {
3410
- return 0;
3411
- }
3412
- const scalingFactor = 52 / current.object.radius;
3413
- let smallDistNerf = 1;
3414
- let cumulativeStrainTime = 0;
3415
- let result = 0;
3416
- let last = current;
3417
- let angleRepeatCount = 0;
3418
- for (let i = 0; i < Math.min(current.index, 10); ++i) {
3419
- const currentObject = current.previous(i);
3420
- cumulativeStrainTime += last.strainTime;
3421
- if (!(currentObject.object instanceof osuBase.Spinner)) {
3422
- const jumpDistance = current.object
3423
- .getStackedPosition(osuBase.Modes.osu)
3424
- .subtract(currentObject.object.getStackedEndPosition(osuBase.Modes.osu)).length;
3425
- // We want to nerf objects that can be easily seen within the Flashlight circle radius.
3426
- if (i === 0) {
3427
- smallDistNerf = Math.min(1, jumpDistance / 75);
3428
- }
3429
- // We also want to nerf stacks so that only the first object of the stack is accounted for.
3430
- const stackNerf = Math.min(1, currentObject.lazyJumpDistance / scalingFactor / 25);
3431
- // Bonus based on how visible the object is.
3432
- const opacityBonus = 1 +
3433
- this.maxOpacityBonus *
3434
- (1 -
3435
- current.opacityAt(currentObject.object.startTime, mods));
3436
- result +=
3437
- (stackNerf * opacityBonus * scalingFactor * jumpDistance) /
3438
- cumulativeStrainTime;
3439
- if (currentObject.angle !== null && current.angle !== null) {
3440
- // Objects further back in time should count less for the nerf.
3441
- if (Math.abs(currentObject.angle - current.angle) < 0.02) {
3442
- angleRepeatCount += Math.max(0, 1 - 0.1 * i);
3443
- }
3444
- }
3445
- }
3446
- last = currentObject;
3447
- }
3448
- result = Math.pow(smallDistNerf * result, 2);
3449
- // Additional bonus for Hidden due to there being no approach circles.
3450
- if (mods.some((m) => m instanceof osuBase.ModHidden)) {
3451
- result *= 1 + this.hiddenBonus;
3452
- }
3453
- // Nerf patterns with repeated angles.
3454
- result *=
3455
- this.minAngleMultiplier +
3456
- (1 - this.minAngleMultiplier) / (angleRepeatCount + 1);
3457
- let sliderBonus = 0;
3458
- if (current.object instanceof osuBase.Slider) {
3459
- // Invert the scaling factor to determine the true travel distance independent of circle size.
3460
- const pixelTravelDistance = current.object.lazyTravelDistance / scalingFactor;
3461
- // Reward sliders based on velocity.
3462
- sliderBonus = Math.pow(Math.max(0, pixelTravelDistance / current.travelTime - this.minVelocity), 0.5);
3463
- // Longer sliders require more memorization.
3464
- sliderBonus *= pixelTravelDistance;
3465
- // Nerf sliders with repeats, as less memorization is required.
3466
- if (current.object.repeatCount > 0)
3467
- sliderBonus /= current.object.repeatCount + 1;
3468
- }
3469
- result += sliderBonus * this.sliderMultiplier;
3470
- return result;
3471
- }
3472
- }
3473
- OsuFlashlightEvaluator.maxOpacityBonus = 0.4;
3474
- OsuFlashlightEvaluator.hiddenBonus = 0.2;
3475
- OsuFlashlightEvaluator.minVelocity = 0.5;
3476
- OsuFlashlightEvaluator.sliderMultiplier = 1.3;
3477
- OsuFlashlightEvaluator.minAngleMultiplier = 0.2;
3478
-
3479
- /**
3480
- * Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
3481
- */
3482
- class OsuFlashlight extends OsuSkill {
3483
- constructor() {
3484
- super(...arguments);
3485
- this.strainDecayBase = 0.15;
3486
- this.reducedSectionCount = 0;
3487
- this.reducedSectionBaseline = 1;
3488
- this.decayWeight = 1;
3489
- this.currentFlashlightStrain = 0;
3490
- this.skillMultiplier = 0.05512;
3491
- }
3492
- difficultyValue() {
3493
- return this.strainPeaks.reduce((a, b) => a + b, 0);
3494
- }
3495
- strainValueAt(current) {
3496
- this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
3497
- this.currentFlashlightStrain +=
3498
- OsuFlashlightEvaluator.evaluateDifficultyOf(current, this.mods) *
3499
- this.skillMultiplier;
3500
- return this.currentFlashlightStrain;
3501
- }
3502
- calculateInitialStrain(time, current) {
3503
- var _a, _b;
3504
- return (this.currentFlashlightStrain *
3505
- this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
3506
- }
3507
- saveToHitObject(current) {
3508
- current.flashlightStrain = this.currentFlashlightStrain;
3509
- }
3510
- }
3511
-
3512
3450
  /**
3513
3451
  * A difficulty calculator for osu!standard gamemode.
3514
3452
  */
3515
3453
  class OsuDifficultyCalculator extends DifficultyCalculator {
3516
3454
  constructor() {
3517
- super(...arguments);
3518
- this.attributes = {
3519
- speedDifficulty: 0,
3520
- mods: [],
3521
- starRating: 0,
3522
- maxCombo: 0,
3523
- aimDifficulty: 0,
3524
- flashlightDifficulty: 0,
3525
- speedNoteCount: 0,
3526
- sliderFactor: 0,
3527
- clockRate: 1,
3528
- approachRate: 0,
3529
- overallDifficulty: 0,
3530
- hitCircleCount: 0,
3531
- sliderCount: 0,
3532
- spinnerCount: 0,
3533
- aimDifficultSliderCount: 0,
3534
- aimDifficultStrainCount: 0,
3535
- speedDifficultStrainCount: 0,
3536
- };
3455
+ super();
3537
3456
  this.difficultyMultiplier = 0.0675;
3538
- this.mode = osuBase.Modes.osu;
3539
- }
3540
- /**
3541
- * The aim star rating of the beatmap.
3542
- */
3543
- get aim() {
3544
- return this.attributes.aimDifficulty;
3545
- }
3546
- /**
3547
- * The speed star rating of the beatmap.
3548
- */
3549
- get speed() {
3550
- return this.attributes.speedDifficulty;
3551
- }
3552
- /**
3553
- * The flashlight star rating of the beatmap.
3554
- */
3555
- get flashlight() {
3556
- return this.attributes.flashlightDifficulty;
3557
- }
3558
- get cacheableAttributes() {
3559
- return Object.assign(Object.assign({}, this.attributes), { mods: osuBase.ModUtil.modsToOsuString(this.attributes.mods) });
3560
- }
3561
- /**
3562
- * Calculates the aim star rating of the beatmap and stores it in this instance.
3563
- */
3564
- calculateAim() {
3565
- if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
3566
- this.attributes.aimDifficulty = 0;
3567
- return;
3568
- }
3569
- const aimSkill = new OsuAim(this.mods, true);
3570
- const aimSkillWithoutSliders = new OsuAim(this.mods, false);
3571
- this.calculateSkills(aimSkill, aimSkillWithoutSliders);
3572
- this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
3573
- }
3574
- /**
3575
- * Calculates the speed star rating of the beatmap and stores it in this instance.
3576
- */
3577
- calculateSpeed() {
3578
- if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
3579
- this.attributes.speedDifficulty = 0;
3580
- return;
3581
- }
3582
- const speedSkill = new OsuSpeed(this.mods);
3583
- this.calculateSkills(speedSkill);
3584
- this.postCalculateSpeed(speedSkill);
3585
- }
3586
- /**
3587
- * Calculates the flashlight star rating of the beatmap and stores it in this instance.
3588
- */
3589
- calculateFlashlight() {
3590
- if (!this.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
3591
- this.attributes.flashlightDifficulty = 0;
3592
- return;
3593
- }
3594
- const flashlightSkill = new OsuFlashlight(this.mods);
3595
- this.calculateSkills(flashlightSkill);
3596
- this.postCalculateFlashlight(flashlightSkill);
3597
- }
3598
- calculateTotal() {
3599
- const aimPerformanceValue = this.basePerformanceValue(this.aim);
3600
- const speedPerformanceValue = this.basePerformanceValue(this.speed);
3601
- let flashlightPerformanceValue = 0;
3602
- if (this.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
3603
- flashlightPerformanceValue = Math.pow(this.flashlight, 2) * 25;
3604
- }
3457
+ this.difficultyAdjustmentMods.add(osuBase.ModTouchDevice);
3458
+ }
3459
+ retainDifficultyAdjustmentMods(mods) {
3460
+ return mods.filter((mod) => mod.isApplicableToOsu() &&
3461
+ this.difficultyAdjustmentMods.has(mod.constructor) &&
3462
+ mod.isOsuRelevant);
3463
+ }
3464
+ createDifficultyAttributes(beatmap, skills) {
3465
+ const attributes = new OsuDifficultyAttributes();
3466
+ attributes.mods = beatmap.mods.slice();
3467
+ attributes.maxCombo = beatmap.maxCombo;
3468
+ attributes.clockRate = beatmap.speedMultiplier;
3469
+ attributes.hitCircleCount = beatmap.hitObjects.circles;
3470
+ attributes.sliderCount = beatmap.hitObjects.sliders;
3471
+ attributes.spinnerCount = beatmap.hitObjects.spinners;
3472
+ this.populateAimAttributes(attributes, skills);
3473
+ this.populateSpeedAttributes(attributes, skills);
3474
+ this.populateFlashlightAttributes(attributes, skills);
3475
+ if (attributes.mods.some((m) => m instanceof osuBase.ModRelax)) {
3476
+ attributes.aimDifficulty *= 0.9;
3477
+ attributes.speedDifficulty = 0;
3478
+ attributes.flashlightDifficulty *= 0.7;
3479
+ }
3480
+ else if (attributes.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
3481
+ attributes.aimDifficulty = 0;
3482
+ attributes.speedDifficulty *= 0.5;
3483
+ attributes.flashlightDifficulty *= 0.4;
3484
+ }
3485
+ const aimPerformanceValue = this.basePerformanceValue(attributes.aimDifficulty);
3486
+ const speedPerformanceValue = this.basePerformanceValue(attributes.speedDifficulty);
3487
+ const flashlightPerformanceValue = Math.pow(attributes.flashlightDifficulty, 2) * 25;
3605
3488
  const basePerformanceValue = Math.pow(Math.pow(aimPerformanceValue, 1.1) +
3606
3489
  Math.pow(speedPerformanceValue, 1.1) +
3607
3490
  Math.pow(flashlightPerformanceValue, 1.1), 1 / 1.1);
3608
3491
  if (basePerformanceValue > 1e-5) {
3609
3492
  // Document for formula derivation:
3610
3493
  // https://docs.google.com/document/d/10DZGYYSsT_yjz2Mtp6yIJld0Rqx4E-vVHupCqiM4TNI/edit
3611
- this.attributes.starRating =
3494
+ attributes.starRating =
3612
3495
  Math.cbrt(1.15) *
3613
3496
  0.027 *
3614
3497
  (Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformanceValue) +
3615
3498
  4);
3616
3499
  }
3617
3500
  else {
3618
- this.attributes.starRating = 0;
3501
+ attributes.starRating = 0;
3619
3502
  }
3503
+ const preempt = osuBase.BeatmapDifficulty.difficultyRange(beatmap.difficulty.ar, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin) / attributes.clockRate;
3504
+ attributes.approachRate = osuBase.BeatmapDifficulty.inverseDifficultyRange(preempt, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin);
3505
+ const { greatWindow } = new osuBase.OsuHitWindow(beatmap.difficulty.od);
3506
+ attributes.overallDifficulty = osuBase.OsuHitWindow.greatWindowToOD(greatWindow / attributes.clockRate);
3507
+ return attributes;
3620
3508
  }
3621
- calculateAll() {
3622
- const skills = this.createSkills();
3623
- this.calculateSkills(...skills);
3624
- const aimSkill = skills.find((s) => s instanceof OsuAim && s.withSliders);
3625
- const aimSkillWithoutSliders = skills.find((s) => s instanceof OsuAim && !s.withSliders);
3626
- const speedSkill = skills.find((s) => s instanceof OsuSpeed);
3627
- const flashlightSkill = skills.find((s) => s instanceof OsuFlashlight);
3628
- if (aimSkill && aimSkillWithoutSliders) {
3629
- this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
3630
- }
3631
- if (speedSkill) {
3632
- this.postCalculateSpeed(speedSkill);
3633
- }
3634
- if (flashlightSkill) {
3635
- this.postCalculateFlashlight(flashlightSkill);
3636
- }
3637
- this.calculateTotal();
3638
- }
3639
- toString() {
3640
- return (this.total.toFixed(2) +
3641
- " stars (" +
3642
- this.aim.toFixed(2) +
3643
- " aim, " +
3644
- this.speed.toFixed(2) +
3645
- " speed, " +
3646
- this.flashlight.toFixed(2) +
3647
- " flashlight)");
3509
+ createPlayableBeatmap(beatmap, mods) {
3510
+ return beatmap.createOsuPlayableBeatmap(mods);
3648
3511
  }
3649
- generateDifficultyHitObjects(beatmap, clockRate) {
3650
- var _a, _b;
3512
+ createDifficultyHitObjects(beatmap) {
3513
+ var _a;
3514
+ const clockRate = beatmap.speedMultiplier;
3651
3515
  const difficultyObjects = [];
3652
3516
  const { objects } = beatmap.hitObjects;
3653
- for (let i = 0; i < objects.length; ++i) {
3654
- const difficultyObject = new OsuDifficultyHitObject(objects[i], (_a = objects[i - 1]) !== null && _a !== void 0 ? _a : null, (_b = objects[i - 2]) !== null && _b !== void 0 ? _b : null, difficultyObjects, clockRate);
3517
+ for (let i = 1; i < objects.length; ++i) {
3518
+ const difficultyObject = new OsuDifficultyHitObject(objects[i], objects[i - 1], (_a = objects[i - 2]) !== null && _a !== void 0 ? _a : null, difficultyObjects, clockRate, i - 1);
3655
3519
  difficultyObject.computeProperties(clockRate, objects);
3656
3520
  difficultyObjects.push(difficultyObject);
3657
3521
  }
3658
3522
  return difficultyObjects;
3659
3523
  }
3660
- createSkills() {
3524
+ createSkills(beatmap) {
3525
+ const { mods } = beatmap;
3661
3526
  const skills = [];
3662
- if (!this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
3663
- skills.push(new OsuAim(this.mods, true));
3664
- skills.push(new OsuAim(this.mods, false));
3527
+ if (!mods.some((m) => m instanceof osuBase.ModAutopilot)) {
3528
+ skills.push(new OsuAim(mods, true));
3529
+ skills.push(new OsuAim(mods, false));
3665
3530
  }
3666
- if (!this.mods.some((m) => m instanceof osuBase.ModRelax)) {
3667
- skills.push(new OsuSpeed(this.mods));
3531
+ if (!mods.some((m) => m instanceof osuBase.ModRelax)) {
3532
+ skills.push(new OsuSpeed(mods));
3668
3533
  }
3669
- if (this.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
3670
- skills.push(new OsuFlashlight(this.mods));
3534
+ if (mods.some((m) => m instanceof osuBase.ModFlashlight)) {
3535
+ skills.push(new OsuFlashlight(mods));
3671
3536
  }
3672
3537
  return skills;
3673
3538
  }
3674
- populateDifficultyAttributes(beatmap, clockRate) {
3675
- super.populateDifficultyAttributes(beatmap, clockRate);
3676
- const preempt = osuBase.BeatmapDifficulty.difficultyRange(beatmap.difficulty.ar, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin) / clockRate;
3677
- this.attributes.approachRate = osuBase.BeatmapDifficulty.inverseDifficultyRange(preempt, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin);
3678
- }
3679
- /**
3680
- * Called after aim skill calculation.
3681
- *
3682
- * @param aimSkill The aim skill that considers sliders.
3683
- * @param aimSkillWithoutSliders The aim skill that doesn't consider sliders.
3684
- */
3685
- postCalculateAim(aimSkill, aimSkillWithoutSliders) {
3686
- this.strainPeaks.aimWithSliders = aimSkill.strainPeaks;
3687
- this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
3688
- this.attributes.aimDifficulty = this.starValue(aimSkill.difficultyValue());
3689
- if (this.aim) {
3690
- this.attributes.sliderFactor =
3691
- this.starValue(aimSkillWithoutSliders.difficultyValue()) /
3692
- this.aim;
3693
- }
3694
- if (this.mods.some((m) => m instanceof osuBase.ModTouchDevice)) {
3695
- this.attributes.aimDifficulty = Math.pow(this.aim, 0.8);
3539
+ createStrainPeakSkills(beatmap) {
3540
+ const { mods } = beatmap;
3541
+ return [
3542
+ new OsuAim(mods, true),
3543
+ new OsuAim(mods, false),
3544
+ new OsuSpeed(mods),
3545
+ new OsuFlashlight(mods),
3546
+ ];
3547
+ }
3548
+ populateAimAttributes(attributes, skills) {
3549
+ const aim = skills.find((s) => s instanceof OsuAim && s.withSliders);
3550
+ const aimNoSlider = skills.find((s) => s instanceof OsuAim && !s.withSliders);
3551
+ if (!aim || !aimNoSlider) {
3552
+ return;
3696
3553
  }
3697
- if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
3698
- this.attributes.aimDifficulty *= 0.9;
3554
+ attributes.aimDifficulty = this.calculateRating(aim);
3555
+ attributes.aimDifficultSliderCount = aim.countDifficultSliders();
3556
+ attributes.aimDifficultStrainCount = aim.countDifficultStrains();
3557
+ if (attributes.aimDifficulty > 0) {
3558
+ attributes.sliderFactor =
3559
+ this.calculateRating(aimNoSlider) / attributes.aimDifficulty;
3699
3560
  }
3700
- else if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
3701
- this.attributes.aimDifficulty = 0;
3561
+ else {
3562
+ attributes.sliderFactor = 1;
3702
3563
  }
3703
- this.attributes.aimDifficultStrainCount =
3704
- aimSkill.countDifficultStrains();
3705
- this.attributes.aimDifficultSliderCount =
3706
- aimSkill.countDifficultSliders();
3707
3564
  }
3708
- /**
3709
- * Called after speed skill calculation.
3710
- *
3711
- * @param speedSkill The speed skill.
3712
- */
3713
- postCalculateSpeed(speedSkill) {
3714
- this.strainPeaks.speed = speedSkill.strainPeaks;
3715
- this.attributes.speedDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
3716
- ? 0
3717
- : this.starValue(speedSkill.difficultyValue());
3718
- if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
3719
- this.attributes.speedDifficulty *= 0.5;
3720
- }
3721
- this.attributes.speedDifficultStrainCount =
3722
- speedSkill.countDifficultStrains();
3723
- const objectStrains = this.objects.map((v) => v.speedStrain);
3724
- const maxStrain = osuBase.MathUtils.max(objectStrains);
3725
- if (maxStrain) {
3726
- this.attributes.speedNoteCount = objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
3565
+ populateSpeedAttributes(attributes, skills) {
3566
+ const speed = skills.find((s) => s instanceof OsuSpeed);
3567
+ if (!speed) {
3568
+ return;
3727
3569
  }
3570
+ attributes.speedDifficulty = this.calculateRating(speed);
3571
+ attributes.speedNoteCount = speed.relevantNoteCount();
3572
+ attributes.speedDifficultStrainCount = speed.countDifficultStrains();
3728
3573
  }
3729
- /**
3730
- * Called after flashlight skill calculation.
3731
- *
3732
- * @param flashlightSkill The flashlight skill.
3733
- */
3734
- postCalculateFlashlight(flashlightSkill) {
3735
- this.strainPeaks.flashlight = flashlightSkill.strainPeaks;
3736
- this.attributes.flashlightDifficulty = this.starValue(flashlightSkill.difficultyValue());
3737
- if (this.mods.some((m) => m instanceof osuBase.ModTouchDevice)) {
3738
- this.attributes.flashlightDifficulty = Math.pow(this.flashlight, 0.8);
3739
- }
3740
- if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
3741
- this.attributes.flashlightDifficulty *= 0.7;
3742
- }
3743
- else if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
3744
- this.attributes.flashlightDifficulty *= 0.4;
3574
+ populateFlashlightAttributes(attributes, skills) {
3575
+ const flashlight = skills.find((s) => s instanceof OsuFlashlight);
3576
+ if (!flashlight) {
3577
+ return;
3745
3578
  }
3579
+ attributes.flashlightDifficulty = this.calculateRating(flashlight);
3746
3580
  }
3747
3581
  }
3748
3582
 
@@ -4076,10 +3910,12 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
4076
3910
  }
4077
3911
  }
4078
3912
 
3913
+ exports.DifficultyAttributes = DifficultyAttributes;
4079
3914
  exports.DifficultyCalculator = DifficultyCalculator;
4080
3915
  exports.DifficultyHitObject = DifficultyHitObject;
4081
3916
  exports.DroidAim = DroidAim;
4082
3917
  exports.DroidAimEvaluator = DroidAimEvaluator;
3918
+ exports.DroidDifficultyAttributes = DroidDifficultyAttributes;
4083
3919
  exports.DroidDifficultyCalculator = DroidDifficultyCalculator;
4084
3920
  exports.DroidDifficultyHitObject = DroidDifficultyHitObject;
4085
3921
  exports.DroidFlashlight = DroidFlashlight;
@@ -4091,8 +3927,10 @@ exports.DroidTap = DroidTap;
4091
3927
  exports.DroidTapEvaluator = DroidTapEvaluator;
4092
3928
  exports.DroidVisual = DroidVisual;
4093
3929
  exports.DroidVisualEvaluator = DroidVisualEvaluator;
3930
+ exports.ExtendedDroidDifficultyAttributes = ExtendedDroidDifficultyAttributes;
4094
3931
  exports.OsuAim = OsuAim;
4095
3932
  exports.OsuAimEvaluator = OsuAimEvaluator;
3933
+ exports.OsuDifficultyAttributes = OsuDifficultyAttributes;
4096
3934
  exports.OsuDifficultyCalculator = OsuDifficultyCalculator;
4097
3935
  exports.OsuDifficultyHitObject = OsuDifficultyHitObject;
4098
3936
  exports.OsuFlashlight = OsuFlashlight;