@rian8337/osu-difficulty-calculator 4.0.0-beta.53 → 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 +783 -986
  2. package/package.json +3 -3
  3. package/typings/index.d.ts +256 -356
package/dist/index.js CHANGED
@@ -3,146 +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
- * Retains `Mod`s that adjust a beatmap's difficulty from the specified mods.
45
+ * Converts this `DifficultyAttributes` instance to an attribute structure that can be cached.
48
46
  *
49
- * @param mods The mods to retain the difficulty adjustment mods from.
50
- * @returns The retained difficulty adjustment mods.
47
+ * @returns The cacheable attributes.
51
48
  */
52
- static retainDifficultyAdjustmentMods(mods) {
53
- return mods.filter((mod) => this.difficultyAdjustmentMods.has(mod.constructor));
49
+ toCacheableAttributes() {
50
+ return Object.assign(Object.assign({}, this), { mods: osuBase.ModUtil.serializeMods(this.mods) });
54
51
  }
55
52
  /**
56
- * Calculates the star rating of the specified beatmap.
57
- *
58
- * The beatmap is analyzed in chunks of `sectionLength` duration.
59
- * For each chunk the highest hitobject strains are added to
60
- * a list which is then collapsed into a weighted sum, much
61
- * like scores are weighted on a user's profile.
62
- *
63
- * For subsequent chunks, the initial max strain is calculated
64
- * by decaying the previous hitobject's strain until the
65
- * beginning of the new chunk.
66
- *
67
- * @param options Options for the difficulty calculation.
68
- * @returns The current instance.
53
+ * Returns a string representation of the difficulty attributes.
69
54
  */
70
- calculate(options) {
71
- var _a;
72
- this.mods = (_a = options === null || options === void 0 ? void 0 : options.mods) !== null && _a !== void 0 ? _a : [];
73
- const playableBeatmap = this.beatmap.createPlayableBeatmap({
74
- mode: this.mode,
75
- mods: this.mods,
76
- customSpeedMultiplier: options === null || options === void 0 ? void 0 : options.customSpeedMultiplier,
77
- });
78
- const clockRate = this.calculateClockRate(options);
79
- this.populateDifficultyAttributes(playableBeatmap, clockRate);
80
- this._objects = this.generateDifficultyHitObjects(playableBeatmap, clockRate);
81
- this.calculateAll();
82
- return this;
55
+ toString() {
56
+ return `${this.starRating.toFixed(2)} stars`;
83
57
  }
84
- /**
85
- * Calculates the skills provided.
86
- *
87
- * @param skills The skills to calculate.
88
- */
89
- calculateSkills(...skills) {
90
- // The first object doesn't generate a strain, so we begin calculating from the second object.
91
- for (const object of this.objects.slice(1)) {
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) {
92
89
  for (const skill of skills) {
93
90
  skill.process(object);
94
91
  }
95
92
  }
93
+ return this.createDifficultyAttributes(playableBeatmap, skills, objects);
96
94
  }
97
- /**
98
- * Obtains the clock rate of the beatmap.
99
- *
100
- * @param options The options to obtain the clock rate with.
101
- * @returns The clock rate of the beatmap.
102
- */
103
- calculateClockRate(options) {
104
- var _a, _b;
105
- return (osuBase.ModUtil.calculateRateWithMods((_a = options === null || options === void 0 ? void 0 : options.mods) !== null && _a !== void 0 ? _a : []) *
106
- ((_b = options === null || options === void 0 ? void 0 : options.customSpeedMultiplier) !== null && _b !== void 0 ? _b : 1));
107
- }
108
- /**
109
- * Populates the stored difficulty attributes with necessary data.
110
- *
111
- * @param beatmap The beatmap to populate the attributes with.
112
- * @param clockRate The clock rate of the beatmap.
113
- */
114
- populateDifficultyAttributes(beatmap, clockRate) {
115
- this.attributes.hitCircleCount = this.beatmap.hitObjects.circles;
116
- this.attributes.maxCombo = this.beatmap.maxCombo;
117
- this.attributes.mods = this.mods.slice();
118
- this.attributes.sliderCount = this.beatmap.hitObjects.sliders;
119
- this.attributes.spinnerCount = this.beatmap.hitObjects.spinners;
120
- this.attributes.clockRate = clockRate;
121
- let greatWindow;
122
- switch (this.mode) {
123
- case osuBase.Modes.droid:
124
- if (this.mods.some((m) => m instanceof osuBase.ModPrecise)) {
125
- greatWindow = new osuBase.PreciseDroidHitWindow(beatmap.difficulty.od).greatWindow;
126
- }
127
- else {
128
- greatWindow = new osuBase.DroidHitWindow(beatmap.difficulty.od)
129
- .greatWindow;
130
- }
131
- break;
132
- case osuBase.Modes.osu:
133
- greatWindow = new osuBase.OsuHitWindow(beatmap.difficulty.od)
134
- .greatWindow;
135
- 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
+ }
136
105
  }
137
- 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
+ };
138
112
  }
139
113
  /**
140
- * Calculates the star rating value of a difficulty.
114
+ * Calculates the base rating of a `Skill`.
141
115
  *
142
- * @param difficulty The difficulty to calculate.
116
+ * @param skill The `Skill` to calculate the rating of.
117
+ * @returns The rating of the `Skill`.
143
118
  */
144
- starValue(difficulty) {
145
- return Math.sqrt(difficulty) * this.difficultyMultiplier;
119
+ calculateRating(skill) {
120
+ return Math.sqrt(skill.difficultyValue()) * this.difficultyMultiplier;
146
121
  }
147
122
  /**
148
123
  * Calculates the base performance value of a difficulty rating.
@@ -153,10 +128,6 @@ class DifficultyCalculator {
153
128
  return Math.pow(5 * Math.max(1, rating / 0.0675) - 4, 3) / 100000;
154
129
  }
155
130
  }
156
- /**
157
- * `Mod`s that adjust the difficulty of a beatmap.
158
- */
159
- DifficultyCalculator.difficultyAdjustmentMods = new Set();
160
131
 
161
132
  /**
162
133
  * Represents a hit object with difficulty calculation values.
@@ -178,7 +149,7 @@ class DifficultyHitObject {
178
149
  * @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
179
150
  * @param clockRate The clock rate of the beatmap.
180
151
  */
181
- constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate) {
152
+ constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, index) {
182
153
  var _a, _b, _c, _d;
183
154
  /**
184
155
  * The aim strain generated by the hitobject if sliders are considered.
@@ -243,7 +214,7 @@ class DifficultyHitObject {
243
214
  this.fullGreatWindow = ((_d = (_c = object.hitWindow) === null || _c === void 0 ? void 0 : _c.greatWindow) !== null && _d !== void 0 ? _d : 1200) * 2;
244
215
  }
245
216
  this.fullGreatWindow /= clockRate;
246
- this.index = difficultyHitObjects.length - 1;
217
+ this.index = index;
247
218
  // Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
248
219
  this.startTime = object.startTime / clockRate;
249
220
  this.endTime = object.endTime / clockRate;
@@ -280,7 +251,7 @@ class DifficultyHitObject {
280
251
  */
281
252
  previous(backwardsIndex) {
282
253
  var _a;
283
- 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);
284
255
  }
285
256
  /**
286
257
  * Gets the difficulty hitobject at a specific index with respect to the current
@@ -294,7 +265,7 @@ class DifficultyHitObject {
294
265
  */
295
266
  next(forwardsIndex) {
296
267
  var _a;
297
- 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);
298
269
  }
299
270
  /**
300
271
  * Calculates the opacity of the hitobject at a given time.
@@ -545,8 +516,8 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
545
516
  * @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
546
517
  * @param clockRate The clock rate of the beatmap.
547
518
  */
548
- constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate) {
549
- super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate);
519
+ constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, index) {
520
+ super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, index);
550
521
  /**
551
522
  * The tap strain generated by the hitobject.
552
523
  */
@@ -603,6 +574,14 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
603
574
  }
604
575
  return super.opacityAt(time, mods);
605
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
+ }
606
585
  /**
607
586
  * Determines whether this hitobject is considered overlapping with the hitobject before it.
608
587
  *
@@ -1007,6 +986,9 @@ class StrainSkill extends Skill {
1007
986
  */
1008
987
  class DroidSkill extends StrainSkill {
1009
988
  process(current) {
989
+ if (current.index < 0) {
990
+ return;
991
+ }
1010
992
  super.process(current);
1011
993
  this._objectStrains.push(this.getObjectStrain(current));
1012
994
  }
@@ -1095,134 +1077,41 @@ class DroidAim extends DroidSkill {
1095
1077
  }
1096
1078
 
1097
1079
  /**
1098
- * An evaluator for calculating osu!droid tap skill.
1099
- */
1100
- class DroidTapEvaluator {
1101
- /**
1102
- * Evaluates the difficulty of tapping the current object, based on:
1103
- *
1104
- * - time between pressing the previous and current object,
1105
- * - distance between those objects,
1106
- * - how easily they can be cheesed,
1107
- * - and the strain time cap.
1108
- *
1109
- * @param current The current object.
1110
- * @param greatWindow The great hit window of the current object.
1111
- * @param considerCheesability Whether to consider cheesability.
1112
- * @param strainTimeCap The strain time to cap the object's strain time to.
1113
- */
1114
- static evaluateDifficultyOf(current, considerCheesability, strainTimeCap) {
1115
- if (current.object instanceof osuBase.Spinner ||
1116
- // Exclude overlapping objects that can be tapped at once.
1117
- current.isOverlapping(false)) {
1118
- return 0;
1119
- }
1120
- // Nerf doubletappable doubles.
1121
- const doubletapness = considerCheesability
1122
- ? 1 - current.doubletapness
1123
- : 1;
1124
- const strainTime = strainTimeCap !== undefined
1125
- ? // We cap the strain time to 50 here as the chance of vibro is higher in any BPM higher than 300.
1126
- Math.max(50, strainTimeCap, current.strainTime)
1127
- : current.strainTime;
1128
- let speedBonus = 1;
1129
- if (strainTime < this.minSpeedBonus) {
1130
- speedBonus +=
1131
- 0.75 *
1132
- Math.pow(osuBase.ErrorFunction.erf((this.minSpeedBonus - strainTime) / 40), 2);
1133
- }
1134
- return (speedBonus * Math.pow(doubletapness, 1.5) * 1000) / strainTime;
1135
- }
1136
- }
1137
- // ~200 1/4 BPM streams
1138
- DroidTapEvaluator.minSpeedBonus = 75;
1139
-
1140
- /**
1141
- * 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.
1142
1081
  */
1143
- class DroidTap extends DroidSkill {
1144
- /**
1145
- * The delta time of hitobjects.
1146
- */
1147
- get objectDeltaTimes() {
1148
- return this._objectDeltaTimes;
1149
- }
1150
- constructor(mods, considerCheesability, strainTimeCap) {
1151
- super(mods);
1152
- this.reducedSectionCount = 10;
1153
- this.reducedSectionBaseline = 0.75;
1154
- this.strainDecayBase = 0.3;
1155
- this.starsPerDouble = 1.1;
1156
- this.currentTapStrain = 0;
1157
- this.currentRhythmMultiplier = 0;
1158
- this.skillMultiplier = 1.375;
1159
- this._objectDeltaTimes = [];
1160
- this.considerCheesability = considerCheesability;
1161
- this.strainTimeCap = strainTimeCap;
1162
- }
1163
- /**
1164
- * The amount of notes that are relevant to the difficulty.
1165
- */
1166
- relevantNoteCount() {
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._objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
1175
- }
1176
- /**
1177
- * The delta time relevant to the difficulty.
1178
- */
1179
- relevantDeltaTime() {
1180
- if (this._objectStrains.length === 0) {
1181
- return 0;
1182
- }
1183
- const maxStrain = osuBase.MathUtils.max(this._objectStrains);
1184
- if (maxStrain === 0) {
1185
- return 0;
1186
- }
1187
- return (this._objectDeltaTimes.reduce((total, next, index) => total +
1188
- (next * 1) /
1189
- (1 +
1190
- Math.exp(-((this._objectStrains[index] / maxStrain) *
1191
- 25 -
1192
- 20))), 0) /
1193
- this._objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 25 - 20))), 0));
1194
- }
1195
- strainValueAt(current) {
1196
- this.currentTapStrain *= this.strainDecay(current.strainTime);
1197
- this.currentTapStrain +=
1198
- DroidTapEvaluator.evaluateDifficultyOf(current, this.considerCheesability, this.strainTimeCap) * this.skillMultiplier;
1199
- this.currentRhythmMultiplier = current.rhythmMultiplier;
1200
- this._objectDeltaTimes.push(current.deltaTime);
1201
- return this.currentTapStrain * current.rhythmMultiplier;
1202
- }
1203
- calculateInitialStrain(time, current) {
1204
- var _a, _b;
1205
- return (this.currentTapStrain *
1206
- this.currentRhythmMultiplier *
1207
- this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
1208
- }
1209
- getObjectStrain() {
1210
- return this.currentTapStrain * this.currentRhythmMultiplier;
1211
- }
1212
- /**
1213
- * @param current The hitobject to save to.
1214
- */
1215
- saveToHitObject(current) {
1216
- 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) {
1217
1094
  return;
1218
1095
  }
1219
- const strain = this.currentTapStrain * this.currentRhythmMultiplier;
1220
- if (this.considerCheesability) {
1221
- current.tapStrain = strain;
1222
- }
1223
- else {
1224
- current.originalTapStrain = strain;
1225
- }
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)`);
1226
1115
  }
1227
1116
  }
1228
1117
 
@@ -1600,48 +1489,180 @@ class DroidRhythm extends DroidSkill {
1600
1489
  }
1601
1490
 
1602
1491
  /**
1603
- * An evaluator for calculating osu!droid visual skill.
1492
+ * An evaluator for calculating osu!droid tap skill.
1604
1493
  */
1605
- class DroidVisualEvaluator {
1494
+ class DroidTapEvaluator {
1606
1495
  /**
1607
- * Evaluates the difficulty of reading the current object, based on:
1496
+ * Evaluates the difficulty of tapping the current object, based on:
1608
1497
  *
1609
- * - note density of the current object,
1610
- * - overlapping factor of the current object,
1611
- * - the preempt time of the current object,
1612
- * - the visual opacity of the current object,
1613
- * - the velocity of the current object if it's a slider,
1614
- * - past objects' velocity if they are sliders,
1615
- * - 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.
1616
1502
  *
1617
1503
  * @param current The current object.
1618
- * @param mods The mods used.
1619
- * @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.
1620
1507
  */
1621
- static evaluateDifficultyOf(current, mods, withSliders) {
1508
+ static evaluateDifficultyOf(current, considerCheesability, strainTimeCap) {
1622
1509
  if (current.object instanceof osuBase.Spinner ||
1623
1510
  // Exclude overlapping objects that can be tapped at once.
1624
- current.isOverlapping(true) ||
1625
- current.index === 0) {
1511
+ current.isOverlapping(false)) {
1626
1512
  return 0;
1627
1513
  }
1628
- // Start with base density and give global bonus for Hidden and Traceable.
1629
- // Add density caps for sanity.
1630
- let strain;
1631
- if (mods.some((m) => m instanceof osuBase.ModHidden)) {
1632
- strain = Math.min(30, Math.pow(current.noteDensity, 3));
1633
- }
1634
- else if (mods.some((m) => m instanceof osuBase.ModTraceable)) {
1635
- // Give more bonus for hit circles due to there being no circle piece.
1636
- if (current.object instanceof osuBase.Circle) {
1637
- strain = Math.min(25, Math.pow(current.noteDensity, 2.5));
1638
- }
1639
- else {
1640
- strain = Math.min(22.5, Math.pow(current.noteDensity, 2.25));
1641
- }
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);
1642
1527
  }
1643
- else {
1644
- strain = Math.min(20, Math.pow(current.noteDensity, 2));
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));
1645
1666
  }
1646
1667
  // Bonus based on how visible the object is.
1647
1668
  for (let i = 0; i < Math.min(current.index, 10); ++i) {
@@ -1746,151 +1767,78 @@ class DroidVisual extends DroidSkill {
1746
1767
  }
1747
1768
  }
1748
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
+
1749
1796
  /**
1750
1797
  * A difficulty calculator for osu!droid gamemode.
1751
1798
  */
1752
1799
  class DroidDifficultyCalculator extends DifficultyCalculator {
1753
1800
  constructor() {
1754
- super(...arguments);
1755
- this.attributes = {
1756
- mode: "live",
1757
- aimDifficultSliderCount: 0,
1758
- tapDifficulty: 0,
1759
- rhythmDifficulty: 0,
1760
- visualDifficulty: 0,
1761
- aimNoteCount: 0,
1762
- mods: [],
1763
- starRating: 0,
1764
- maxCombo: 0,
1765
- aimDifficulty: 0,
1766
- flashlightDifficulty: 0,
1767
- speedNoteCount: 0,
1768
- sliderFactor: 0,
1769
- clockRate: 1,
1770
- overallDifficulty: 0,
1771
- hitCircleCount: 0,
1772
- sliderCount: 0,
1773
- spinnerCount: 0,
1774
- aimDifficultStrainCount: 0,
1775
- tapDifficultStrainCount: 0,
1776
- flashlightDifficultStrainCount: 0,
1777
- visualDifficultStrainCount: 0,
1778
- flashlightSliderFactor: 0,
1779
- visualSliderFactor: 0,
1780
- possibleThreeFingeredSections: [],
1781
- difficultSliders: [],
1782
- averageSpeedDeltaTime: 0,
1783
- vibroFactor: 1,
1784
- };
1801
+ super();
1785
1802
  this.difficultyMultiplier = 0.18;
1786
- this.mode = osuBase.Modes.droid;
1787
- }
1788
- /**
1789
- * The aim star rating of the beatmap.
1790
- */
1791
- get aim() {
1792
- return this.attributes.aimDifficulty;
1793
- }
1794
- /**
1795
- * The tap star rating of the beatmap.
1796
- */
1797
- get tap() {
1798
- return this.attributes.tapDifficulty;
1799
- }
1800
- /**
1801
- * The rhythm star rating of the beatmap.
1802
- */
1803
- get rhythm() {
1804
- return this.attributes.rhythmDifficulty;
1805
- }
1806
- /**
1807
- * The flashlight star rating of the beatmap.
1808
- */
1809
- get flashlight() {
1810
- return this.attributes.flashlightDifficulty;
1811
- }
1812
- /**
1813
- * The visual star rating of the beatmap.
1814
- */
1815
- get visual() {
1816
- return this.attributes.visualDifficulty;
1817
- }
1818
- get cacheableAttributes() {
1819
- return Object.assign(Object.assign({}, this.attributes), { mods: osuBase.ModUtil.modsToOsuString(this.attributes.mods) });
1820
- }
1821
- // Override to use DroidDifficultyCalculationOptions
1822
- calculate(options) {
1823
- return super.calculate(options);
1824
- }
1825
- /**
1826
- * Calculates the aim star rating of the beatmap and stores it in this instance.
1827
- */
1828
- calculateAim() {
1829
- if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
1830
- this.attributes.aimDifficulty = 0;
1831
- return;
1832
- }
1833
- const aimSkill = new DroidAim(this.mods, true);
1834
- const aimSkillWithoutSliders = new DroidAim(this.mods, false);
1835
- this.calculateSkills(aimSkill, aimSkillWithoutSliders);
1836
- this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
1837
- }
1838
- /**
1839
- * Calculates the tap star rating of the beatmap and stores it in this instance.
1840
- */
1841
- calculateTap() {
1842
- if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
1843
- this.attributes.tapDifficulty = 0;
1844
- return;
1845
- }
1846
- const tapSkillCheese = new DroidTap(this.mods, true);
1847
- const tapSkillNoCheese = new DroidTap(this.mods, false);
1848
- this.calculateSkills(tapSkillCheese, tapSkillNoCheese);
1849
- const tapSkillVibro = new DroidTap(this.mods, true, tapSkillCheese.relevantDeltaTime());
1850
- this.calculateSkills(tapSkillVibro);
1851
- this.postCalculateTap(tapSkillCheese, tapSkillVibro);
1852
- }
1853
- /**
1854
- * Calculates the rhythm star rating of the beatmap and stores it in this instance.
1855
- */
1856
- calculateRhythm() {
1857
- const rhythmSkill = new DroidRhythm(this.mods);
1858
- this.calculateSkills(rhythmSkill);
1859
- this.postCalculateRhythm(rhythmSkill);
1860
- }
1861
- /**
1862
- * Calculates the flashlight star rating of the beatmap and stores it in this instance.
1863
- */
1864
- calculateFlashlight() {
1865
- if (!this.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
1866
- this.attributes.flashlightDifficulty = 0;
1867
- return;
1868
- }
1869
- const flashlightSkill = new DroidFlashlight(this.mods, true);
1870
- const flashlightSkillWithoutSliders = new DroidFlashlight(this.mods, false);
1871
- this.calculateSkills(flashlightSkill, flashlightSkillWithoutSliders);
1872
- this.postCalculateFlashlight(flashlightSkill, flashlightSkillWithoutSliders);
1873
- }
1874
- /**
1875
- * Calculates the visual star rating of the beatmap and stores it in this instance.
1876
- */
1877
- calculateVisual() {
1878
- if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
1879
- this.attributes.visualDifficulty = 0;
1880
- return;
1881
- }
1882
- const visualSkill = new DroidVisual(this.mods, true);
1883
- const visualSkillWithoutSliders = new DroidVisual(this.mods, false);
1884
- this.calculateSkills(visualSkill, visualSkillWithoutSliders);
1885
- this.postCalculateVisual(visualSkill, visualSkillWithoutSliders);
1886
- }
1887
- calculateTotal() {
1888
- const aimPerformanceValue = this.basePerformanceValue(Math.pow(this.aim, 0.8));
1889
- const tapPerformanceValue = this.basePerformanceValue(this.tap);
1890
- const flashlightPerformanceValue = this.mods.some((m) => m instanceof osuBase.ModFlashlight)
1891
- ? Math.pow(this.flashlight, 1.6) * 25
1892
- : 0;
1893
- 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;
1894
1842
  const basePerformanceValue = Math.pow(Math.pow(aimPerformanceValue, 1.1) +
1895
1843
  Math.pow(tapPerformanceValue, 1.1) +
1896
1844
  Math.pow(flashlightPerformanceValue, 1.1) +
@@ -1898,126 +1846,82 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1898
1846
  if (basePerformanceValue > 1e-5) {
1899
1847
  // Document for formula derivation:
1900
1848
  // https://docs.google.com/document/d/10DZGYYSsT_yjz2Mtp6yIJld0Rqx4E-vVHupCqiM4TNI/edit
1901
- this.attributes.starRating =
1849
+ attributes.starRating =
1902
1850
  0.027 *
1903
1851
  (Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformanceValue) +
1904
1852
  4);
1905
1853
  }
1906
1854
  else {
1907
- this.attributes.starRating = 0;
1908
- }
1909
- }
1910
- calculateAll() {
1911
- const skills = this.createSkills();
1912
- this.calculateSkills(...skills);
1913
- const aimSkill = skills.find((s) => s instanceof DroidAim && s.withSliders);
1914
- const aimSkillWithoutSliders = skills.find((s) => s instanceof DroidAim && !s.withSliders);
1915
- const rhythmSkill = skills.find((s) => s instanceof DroidRhythm);
1916
- const tapSkillCheese = skills.find((s) => s instanceof DroidTap && s.considerCheesability);
1917
- const flashlightSkill = skills.find((s) => s instanceof DroidFlashlight && s.withSliders);
1918
- const flashlightSkillWithoutSliders = skills.find((s) => s instanceof DroidFlashlight && !s.withSliders);
1919
- const visualSkill = skills.find((s) => s instanceof DroidVisual && s.withSliders);
1920
- const visualSkillWithoutSliders = skills.find((s) => s instanceof DroidVisual && !s.withSliders);
1921
- if (aimSkill && aimSkillWithoutSliders) {
1922
- this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
1923
- }
1924
- if (tapSkillCheese) {
1925
- const tapSkillVibro = new DroidTap(this.mods, true, tapSkillCheese.relevantDeltaTime());
1926
- this.calculateSkills(tapSkillVibro);
1927
- this.postCalculateTap(tapSkillCheese, tapSkillVibro);
1855
+ attributes.starRating = 0;
1928
1856
  }
1929
- this.postCalculateRhythm(rhythmSkill);
1930
- if (flashlightSkill && flashlightSkillWithoutSliders) {
1931
- 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;
1932
1861
  }
1933
- if (visualSkill && visualSkillWithoutSliders) {
1934
- this.postCalculateVisual(visualSkill, visualSkillWithoutSliders);
1862
+ else {
1863
+ greatWindow = new osuBase.DroidHitWindow(beatmap.difficulty.od).greatWindow;
1935
1864
  }
1936
- this.calculateTotal();
1865
+ attributes.overallDifficulty = osuBase.OsuHitWindow.greatWindowToOD(greatWindow / attributes.clockRate);
1866
+ return attributes;
1937
1867
  }
1938
- toString() {
1939
- return (this.total.toFixed(2) +
1940
- " stars (" +
1941
- this.aim.toFixed(2) +
1942
- " aim, " +
1943
- this.tap.toFixed(2) +
1944
- " tap, " +
1945
- this.rhythm.toFixed(2) +
1946
- " rhythm, " +
1947
- this.flashlight.toFixed(2) +
1948
- " flashlight, " +
1949
- this.visual.toFixed(2) +
1950
- " visual)");
1868
+ createPlayableBeatmap(beatmap, mods) {
1869
+ return beatmap.createDroidPlayableBeatmap(mods);
1951
1870
  }
1952
- generateDifficultyHitObjects(beatmap, clockRate) {
1871
+ createDifficultyHitObjects(beatmap) {
1953
1872
  var _a, _b;
1873
+ const clockRate = beatmap.speedMultiplier;
1954
1874
  const difficultyObjects = [];
1955
1875
  const { objects } = beatmap.hitObjects;
1956
1876
  for (let i = 0; i < objects.length; ++i) {
1957
- 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);
1958
1878
  difficultyObject.computeProperties(clockRate, objects);
1959
1879
  difficultyObjects.push(difficultyObject);
1960
1880
  }
1961
1881
  return difficultyObjects;
1962
1882
  }
1963
- createSkills() {
1883
+ createSkills(beatmap) {
1884
+ const { mods } = beatmap;
1964
1885
  const skills = [];
1965
- if (!this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
1966
- skills.push(new DroidAim(this.mods, true));
1967
- 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));
1968
1889
  }
1969
- if (!this.mods.some((m) => m instanceof osuBase.ModRelax)) {
1890
+ if (!mods.some((m) => m instanceof osuBase.ModRelax)) {
1970
1891
  // Tap and visual skills depend on rhythm skill, so we put it first
1971
- skills.push(new DroidRhythm(this.mods));
1972
- skills.push(new DroidTap(this.mods, true));
1973
- skills.push(new DroidTap(this.mods, false));
1974
- skills.push(new DroidVisual(this.mods, true));
1975
- 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));
1976
1897
  }
1977
- if (this.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
1978
- skills.push(new DroidFlashlight(this.mods, true));
1979
- 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));
1980
1901
  }
1981
1902
  return skills;
1982
1903
  }
1983
- calculateClockRate(options) {
1984
- var _a, _b;
1985
- 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));
1986
- }
1987
- /**
1988
- * Called after aim skill calculation.
1989
- *
1990
- * @param aimSkill The aim skill that considers sliders.
1991
- * @param aimSkillWithoutSliders The aim skill that doesn't consider sliders.
1992
- */
1993
- postCalculateAim(aimSkill, aimSkillWithoutSliders) {
1994
- this.strainPeaks.aimWithSliders = aimSkill.strainPeaks;
1995
- this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
1996
- this.attributes.aimDifficulty = this.mods.some((m) => m instanceof osuBase.ModAutopilot)
1997
- ? 0
1998
- : this.starValue(aimSkill.difficultyValue());
1999
- if (this.aim) {
2000
- this.attributes.sliderFactor =
2001
- this.starValue(aimSkillWithoutSliders.difficultyValue()) /
2002
- this.aim;
2003
- }
2004
- if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
2005
- 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;
2006
1918
  }
2007
- this.attributes.aimDifficultStrainCount =
2008
- aimSkill.countDifficultStrains();
2009
- this.attributes.aimDifficultSliderCount =
2010
- aimSkill.countDifficultSliders();
2011
- this.calculateAimAttributes();
2012
- }
2013
- /**
2014
- * Calculates aim-related attributes.
2015
- */
2016
- calculateAimAttributes() {
2017
- this.attributes.difficultSliders = [];
1919
+ attributes.aimDifficulty = this.calculateRating(aim);
1920
+ attributes.aimDifficultSliderCount = aim.countDifficultSliders();
1921
+ attributes.aimDifficultStrainCount = aim.countDifficultStrains();
2018
1922
  const topDifficultSliders = [];
2019
- for (let i = 0; i < this.objects.length; ++i) {
2020
- const object = this.objects[i];
1923
+ for (let i = 0; i < objects.length; ++i) {
1924
+ const object = objects[i];
2021
1925
  const velocity = object.travelDistance / object.travelTime;
2022
1926
  if (velocity > 0) {
2023
1927
  topDifficultSliders.push({
@@ -2031,53 +1935,50 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
2031
1935
  const difficultyRating = slider.velocity / velocitySum;
2032
1936
  // Only consider sliders that are fast enough.
2033
1937
  if (difficultyRating > 0.02) {
2034
- this.attributes.difficultSliders.push({
1938
+ attributes.difficultSliders.push({
2035
1939
  index: slider.index,
2036
1940
  difficultyRating: slider.velocity / velocitySum,
2037
1941
  });
2038
1942
  }
2039
1943
  }
2040
- this.attributes.difficultSliders.sort((a, b) => b.difficultyRating - a.difficultyRating);
1944
+ attributes.difficultSliders.sort((a, b) => b.difficultyRating - a.difficultyRating);
2041
1945
  // Take the top 15% most difficult sliders.
2042
- while (this.attributes.difficultSliders.length >
2043
- Math.ceil(0.15 * this.beatmap.hitObjects.sliders)) {
2044
- 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;
2045
1956
  }
2046
1957
  }
2047
- /**
2048
- * Called after tap skill calculation.
2049
- *
2050
- * @param tapSkillCheese The tap skill that considers cheesing.
2051
- * @param tapSkillVibro The tap skill that considers vibro.
2052
- */
2053
- postCalculateTap(tapSkillCheese, tapSkillVibro) {
2054
- this.strainPeaks.speed = tapSkillCheese.strainPeaks;
2055
- this.attributes.tapDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
2056
- ? 0
2057
- : this.starValue(tapSkillCheese.difficultyValue());
2058
- if (this.tap) {
2059
- this.attributes.vibroFactor =
2060
- this.starValue(tapSkillVibro.difficultyValue()) / this.tap;
2061
- }
2062
- this.attributes.speedNoteCount = tapSkillCheese.relevantNoteCount();
2063
- this.attributes.averageSpeedDeltaTime =
2064
- tapSkillCheese.relevantDeltaTime();
2065
- this.attributes.tapDifficultStrainCount =
2066
- tapSkillCheese.countDifficultStrains();
2067
- this.calculateTapAttributes();
2068
- }
2069
- /**
2070
- * Calculates tap-related attributes.
2071
- */
2072
- calculateTapAttributes() {
2073
- 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
+ }
2074
1975
  const { threeFingerStrainThreshold } = DroidDifficultyCalculator;
2075
1976
  const minSectionObjectCount = 5;
2076
1977
  let inSpeedSection = false;
2077
1978
  let firstSpeedObjectIndex = 0;
2078
- for (let i = 2; i < this.objects.length; ++i) {
2079
- const current = this.objects[i];
2080
- 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];
2081
1982
  if (!inSpeedSection &&
2082
1983
  current.originalTapStrain >= threeFingerStrainThreshold) {
2083
1984
  inSpeedSection = true;
@@ -2093,17 +1994,17 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
2093
1994
  // Stop speed section on slowing down 1/2 rhythm change or anything slower.
2094
1995
  (prevDelta < currentDelta && deltaRatio <= 0.5) ||
2095
1996
  // Don't forget to manually add the last section, which would otherwise be ignored.
2096
- i === this.objects.length - 1)) {
2097
- 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);
2098
1999
  inSpeedSection = false;
2099
2000
  // Ignore sections that don't meet object count requirement.
2100
2001
  if (i - firstSpeedObjectIndex < minSectionObjectCount) {
2101
2002
  continue;
2102
2003
  }
2103
- this.attributes.possibleThreeFingeredSections.push({
2004
+ attributes.possibleThreeFingeredSections.push({
2104
2005
  firstObjectIndex: firstSpeedObjectIndex,
2105
2006
  lastObjectIndex: lastSpeedObjectIndex,
2106
- sumStrain: Math.pow(this.objects
2007
+ sumStrain: Math.pow(objects
2107
2008
  .slice(firstSpeedObjectIndex, lastSpeedObjectIndex + 1)
2108
2009
  .reduce((a, v) => a +
2109
2010
  v.originalTapStrain /
@@ -2112,61 +2013,47 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
2112
2013
  }
2113
2014
  }
2114
2015
  }
2115
- /**
2116
- * Called after rhythm skill calculation.
2117
- *
2118
- * @param rhythmSkill The rhythm skill.
2119
- */
2120
- postCalculateRhythm(rhythmSkill) {
2121
- this.attributes.rhythmDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
2122
- ? 0
2123
- : this.starValue(rhythmSkill.difficultyValue());
2124
- }
2125
- /**
2126
- * Called after flashlight skill calculation.
2127
- *
2128
- * @param flashlightSkill The flashlight skill that considers sliders.
2129
- * @param flashlightSkillWithoutSliders The flashlight skill that doesn't consider sliders.
2130
- */
2131
- postCalculateFlashlight(flashlightSkill, flashlightSkillWithoutSliders) {
2132
- this.strainPeaks.flashlight = flashlightSkill.strainPeaks;
2133
- this.attributes.flashlightDifficulty = this.starValue(flashlightSkill.difficultyValue());
2134
- if (this.flashlight) {
2135
- this.attributes.flashlightSliderFactor =
2136
- this.starValue(flashlightSkillWithoutSliders.difficultyValue()) / this.flashlight;
2016
+ populateRhythmAttributes(attributes, skills) {
2017
+ const rhythm = skills.find((s) => s instanceof DroidRhythm);
2018
+ if (!rhythm) {
2019
+ return;
2137
2020
  }
2138
- if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
2139
- 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;
2140
2028
  }
2141
- if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
2142
- 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;
2143
2036
  }
2144
- else if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
2145
- this.attributes.flashlightDifficulty *= 0.4;
2037
+ else {
2038
+ attributes.flashlightSliderFactor = 1;
2146
2039
  }
2147
- this.attributes.flashlightDifficultStrainCount =
2148
- flashlightSkill.countDifficultStrains();
2149
2040
  }
2150
- /**
2151
- * Called after visual skill calculation.
2152
- *
2153
- * @param visualSkillWithSliders The visual skill that considers sliders.
2154
- * @param visualSkillWithoutSliders The visual skill that doesn't consider sliders.
2155
- */
2156
- postCalculateVisual(visualSkillWithSliders, visualSkillWithoutSliders) {
2157
- this.attributes.visualDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
2158
- ? 0
2159
- : this.starValue(visualSkillWithSliders.difficultyValue());
2160
- if (this.visual) {
2161
- this.attributes.visualSliderFactor =
2162
- this.starValue(visualSkillWithoutSliders.difficultyValue()) /
2163
- 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;
2164
2046
  }
2165
- if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
2166
- 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;
2167
2056
  }
2168
- this.attributes.visualDifficultStrainCount =
2169
- visualSkillWithSliders.countDifficultStrains();
2170
2057
  }
2171
2058
  }
2172
2059
  /**
@@ -2175,21 +2062,6 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
2175
2062
  * Increasing this number will result in less sections being flagged.
2176
2063
  */
2177
2064
  DroidDifficultyCalculator.threeFingerStrainThreshold = 175;
2178
- DroidDifficultyCalculator.difficultyAdjustmentMods = new Set([
2179
- osuBase.ModDoubleTime,
2180
- osuBase.ModNightCore,
2181
- osuBase.ModDifficultyAdjust,
2182
- osuBase.ModHalfTime,
2183
- osuBase.ModEasy,
2184
- osuBase.ModHardRock,
2185
- osuBase.ModFlashlight,
2186
- osuBase.ModHidden,
2187
- osuBase.ModRelax,
2188
- osuBase.ModAutopilot,
2189
- osuBase.ModPrecise,
2190
- osuBase.ModScoreV2,
2191
- osuBase.ModTraceable,
2192
- ]);
2193
2065
 
2194
2066
  /**
2195
2067
  * The base class of performance calculators.
@@ -2217,7 +2089,7 @@ class PerformanceCalculator {
2217
2089
  this.sliderNerfFactor = 1;
2218
2090
  this.difficultyAttributes = difficultyAttributes;
2219
2091
  this.mods = this.isCacheableAttribute(difficultyAttributes)
2220
- ? osuBase.ModUtil.pcStringToMods(difficultyAttributes.mods)
2092
+ ? osuBase.ModUtil.deserializeMods(difficultyAttributes.mods)
2221
2093
  : difficultyAttributes.mods;
2222
2094
  }
2223
2095
  /**
@@ -3170,62 +3042,149 @@ class OsuAim extends OsuSkill {
3170
3042
  }
3171
3043
 
3172
3044
  /**
3173
- * An evaluator for calculating osu!standard speed skill.
3045
+ * Holds data that can be used to calculate osu!standard performance points.
3174
3046
  */
3175
- 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 {
3176
3073
  /**
3177
- * Evaluates the difficulty of tapping the current object, based on:
3074
+ * Evaluates the difficulty of memorizing and hitting the current object, based on:
3178
3075
  *
3179
- * - time between pressing the previous and current object,
3180
- * - distance between those objects,
3181
- * - 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.
3182
3081
  *
3183
3082
  * @param current The current object.
3184
- * @param mods The mods applied.
3083
+ * @param mods The mods used.
3185
3084
  */
3186
3085
  static evaluateDifficultyOf(current, mods) {
3187
- var _a;
3188
3086
  if (current.object instanceof osuBase.Spinner) {
3189
3087
  return 0;
3190
3088
  }
3191
- const prev = current.previous(0);
3192
- let strainTime = current.strainTime;
3193
- // Nerf doubletappable doubles.
3194
- const doubletapness = 1 - current.doubletapness;
3195
- // Cap deltatime to the OD 300 hitwindow.
3196
- // 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.
3197
- strainTime /= osuBase.MathUtils.clamp(strainTime / current.fullGreatWindow / 0.93, 0.92, 1);
3198
- // speedBonus will be 0.0 for BPM < 200
3199
- let speedBonus = 0;
3200
- // Add additional scaling bonus for streams/bursts higher than 200bpm
3201
- if (strainTime < this.minSpeedBonus) {
3202
- speedBonus =
3203
- 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;
3204
3124
  }
3205
- const travelDistance = (_a = prev === null || prev === void 0 ? void 0 : prev.travelDistance) !== null && _a !== void 0 ? _a : 0;
3206
- // Cap distance at spacing threshold
3207
- const distance = Math.min(this.SINGLE_SPACING_THRESHOLD, travelDistance + current.minimumJumpDistance);
3208
- // Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
3209
- let distanceBonus = Math.pow(distance / this.SINGLE_SPACING_THRESHOLD, 3.95) *
3210
- this.DISTANCE_MULTIPLIER;
3211
- if (mods.some((m) => m instanceof osuBase.ModAutopilot)) {
3212
- 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;
3213
3129
  }
3214
- // Base difficulty with all bonuses
3215
- const difficulty = ((1 + speedBonus + distanceBonus) * 1000) / strainTime;
3216
- // Apply penalty if there's doubletappable doubles
3217
- 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;
3218
3148
  }
3219
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
+
3220
3156
  /**
3221
- * Spacing threshold for a single hitobject spacing.
3222
- *
3223
- * 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.
3224
3158
  */
3225
- OsuSpeedEvaluator.SINGLE_SPACING_THRESHOLD = 125;
3226
- // ~200 1/4 BPM streams
3227
- OsuSpeedEvaluator.minSpeedBonus = 75;
3228
- 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
+ }
3229
3188
 
3230
3189
  /**
3231
3190
  * An evaluator for calculating osu!standard Rhythm skill.
@@ -3375,6 +3334,64 @@ OsuRhythmEvaluator.historyObjectsMax = 32;
3375
3334
  OsuRhythmEvaluator.rhythmOverallMultiplier = 0.95;
3376
3335
  OsuRhythmEvaluator.rhythmRatioMultiplier = 12;
3377
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
+
3378
3395
  /**
3379
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.
3380
3397
  */
@@ -3389,6 +3406,19 @@ class OsuSpeed extends OsuSkill {
3389
3406
  this.currentRhythm = 0;
3390
3407
  this.skillMultiplier = 1.46;
3391
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
+ }
3392
3422
  /**
3393
3423
  * @param current The hitobject to calculate.
3394
3424
  */
@@ -3417,375 +3447,138 @@ class OsuSpeed extends OsuSkill {
3417
3447
  }
3418
3448
  }
3419
3449
 
3420
- /**
3421
- * An evaluator for calculating osu!standard Flashlight skill.
3422
- */
3423
- class OsuFlashlightEvaluator {
3424
- /**
3425
- * Evaluates the difficulty of memorizing and hitting the current object, based on:
3426
- *
3427
- * - distance between a number of previous objects and the current object,
3428
- * - the visual opacity of the current object,
3429
- * - the angle made by the current object,
3430
- * - length and speed of the current object (for sliders),
3431
- * - and whether Hidden mod is enabled.
3432
- *
3433
- * @param current The current object.
3434
- * @param mods The mods used.
3435
- */
3436
- static evaluateDifficultyOf(current, mods) {
3437
- if (current.object instanceof osuBase.Spinner) {
3438
- return 0;
3439
- }
3440
- const scalingFactor = 52 / current.object.radius;
3441
- let smallDistNerf = 1;
3442
- let cumulativeStrainTime = 0;
3443
- let result = 0;
3444
- let last = current;
3445
- let angleRepeatCount = 0;
3446
- for (let i = 0; i < Math.min(current.index, 10); ++i) {
3447
- const currentObject = current.previous(i);
3448
- cumulativeStrainTime += last.strainTime;
3449
- if (!(currentObject.object instanceof osuBase.Spinner)) {
3450
- const jumpDistance = current.object
3451
- .getStackedPosition(osuBase.Modes.osu)
3452
- .subtract(currentObject.object.getStackedEndPosition(osuBase.Modes.osu)).length;
3453
- // We want to nerf objects that can be easily seen within the Flashlight circle radius.
3454
- if (i === 0) {
3455
- smallDistNerf = Math.min(1, jumpDistance / 75);
3456
- }
3457
- // We also want to nerf stacks so that only the first object of the stack is accounted for.
3458
- const stackNerf = Math.min(1, currentObject.lazyJumpDistance / scalingFactor / 25);
3459
- // Bonus based on how visible the object is.
3460
- const opacityBonus = 1 +
3461
- this.maxOpacityBonus *
3462
- (1 -
3463
- current.opacityAt(currentObject.object.startTime, mods));
3464
- result +=
3465
- (stackNerf * opacityBonus * scalingFactor * jumpDistance) /
3466
- cumulativeStrainTime;
3467
- if (currentObject.angle !== null && current.angle !== null) {
3468
- // Objects further back in time should count less for the nerf.
3469
- if (Math.abs(currentObject.angle - current.angle) < 0.02) {
3470
- angleRepeatCount += Math.max(0, 1 - 0.1 * i);
3471
- }
3472
- }
3473
- }
3474
- last = currentObject;
3475
- }
3476
- result = Math.pow(smallDistNerf * result, 2);
3477
- // Additional bonus for Hidden due to there being no approach circles.
3478
- if (mods.some((m) => m instanceof osuBase.ModHidden)) {
3479
- result *= 1 + this.hiddenBonus;
3480
- }
3481
- // Nerf patterns with repeated angles.
3482
- result *=
3483
- this.minAngleMultiplier +
3484
- (1 - this.minAngleMultiplier) / (angleRepeatCount + 1);
3485
- let sliderBonus = 0;
3486
- if (current.object instanceof osuBase.Slider) {
3487
- // Invert the scaling factor to determine the true travel distance independent of circle size.
3488
- const pixelTravelDistance = current.object.lazyTravelDistance / scalingFactor;
3489
- // Reward sliders based on velocity.
3490
- sliderBonus = Math.pow(Math.max(0, pixelTravelDistance / current.travelTime - this.minVelocity), 0.5);
3491
- // Longer sliders require more memorization.
3492
- sliderBonus *= pixelTravelDistance;
3493
- // Nerf sliders with repeats, as less memorization is required.
3494
- if (current.object.repeatCount > 0)
3495
- sliderBonus /= current.object.repeatCount + 1;
3496
- }
3497
- result += sliderBonus * this.sliderMultiplier;
3498
- return result;
3499
- }
3500
- }
3501
- OsuFlashlightEvaluator.maxOpacityBonus = 0.4;
3502
- OsuFlashlightEvaluator.hiddenBonus = 0.2;
3503
- OsuFlashlightEvaluator.minVelocity = 0.5;
3504
- OsuFlashlightEvaluator.sliderMultiplier = 1.3;
3505
- OsuFlashlightEvaluator.minAngleMultiplier = 0.2;
3506
-
3507
- /**
3508
- * Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
3509
- */
3510
- class OsuFlashlight extends OsuSkill {
3511
- constructor() {
3512
- super(...arguments);
3513
- this.strainDecayBase = 0.15;
3514
- this.reducedSectionCount = 0;
3515
- this.reducedSectionBaseline = 1;
3516
- this.decayWeight = 1;
3517
- this.currentFlashlightStrain = 0;
3518
- this.skillMultiplier = 0.05512;
3519
- }
3520
- difficultyValue() {
3521
- return this.strainPeaks.reduce((a, b) => a + b, 0);
3522
- }
3523
- strainValueAt(current) {
3524
- this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
3525
- this.currentFlashlightStrain +=
3526
- OsuFlashlightEvaluator.evaluateDifficultyOf(current, this.mods) *
3527
- this.skillMultiplier;
3528
- return this.currentFlashlightStrain;
3529
- }
3530
- calculateInitialStrain(time, current) {
3531
- var _a, _b;
3532
- return (this.currentFlashlightStrain *
3533
- this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
3534
- }
3535
- saveToHitObject(current) {
3536
- current.flashlightStrain = this.currentFlashlightStrain;
3537
- }
3538
- }
3539
-
3540
3450
  /**
3541
3451
  * A difficulty calculator for osu!standard gamemode.
3542
3452
  */
3543
3453
  class OsuDifficultyCalculator extends DifficultyCalculator {
3544
3454
  constructor() {
3545
- super(...arguments);
3546
- this.attributes = {
3547
- speedDifficulty: 0,
3548
- mods: [],
3549
- starRating: 0,
3550
- maxCombo: 0,
3551
- aimDifficulty: 0,
3552
- flashlightDifficulty: 0,
3553
- speedNoteCount: 0,
3554
- sliderFactor: 0,
3555
- clockRate: 1,
3556
- approachRate: 0,
3557
- overallDifficulty: 0,
3558
- hitCircleCount: 0,
3559
- sliderCount: 0,
3560
- spinnerCount: 0,
3561
- aimDifficultSliderCount: 0,
3562
- aimDifficultStrainCount: 0,
3563
- speedDifficultStrainCount: 0,
3564
- };
3455
+ super();
3565
3456
  this.difficultyMultiplier = 0.0675;
3566
- this.mode = osuBase.Modes.osu;
3567
- }
3568
- /**
3569
- * The aim star rating of the beatmap.
3570
- */
3571
- get aim() {
3572
- return this.attributes.aimDifficulty;
3573
- }
3574
- /**
3575
- * The speed star rating of the beatmap.
3576
- */
3577
- get speed() {
3578
- return this.attributes.speedDifficulty;
3579
- }
3580
- /**
3581
- * The flashlight star rating of the beatmap.
3582
- */
3583
- get flashlight() {
3584
- return this.attributes.flashlightDifficulty;
3585
- }
3586
- get cacheableAttributes() {
3587
- return Object.assign(Object.assign({}, this.attributes), { mods: osuBase.ModUtil.modsToOsuString(this.attributes.mods) });
3588
- }
3589
- /**
3590
- * Calculates the aim star rating of the beatmap and stores it in this instance.
3591
- */
3592
- calculateAim() {
3593
- if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
3594
- this.attributes.aimDifficulty = 0;
3595
- return;
3596
- }
3597
- const aimSkill = new OsuAim(this.mods, true);
3598
- const aimSkillWithoutSliders = new OsuAim(this.mods, false);
3599
- this.calculateSkills(aimSkill, aimSkillWithoutSliders);
3600
- this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
3601
- }
3602
- /**
3603
- * Calculates the speed star rating of the beatmap and stores it in this instance.
3604
- */
3605
- calculateSpeed() {
3606
- if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
3607
- this.attributes.speedDifficulty = 0;
3608
- return;
3609
- }
3610
- const speedSkill = new OsuSpeed(this.mods);
3611
- this.calculateSkills(speedSkill);
3612
- this.postCalculateSpeed(speedSkill);
3613
- }
3614
- /**
3615
- * Calculates the flashlight star rating of the beatmap and stores it in this instance.
3616
- */
3617
- calculateFlashlight() {
3618
- if (!this.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
3619
- this.attributes.flashlightDifficulty = 0;
3620
- return;
3621
- }
3622
- const flashlightSkill = new OsuFlashlight(this.mods);
3623
- this.calculateSkills(flashlightSkill);
3624
- this.postCalculateFlashlight(flashlightSkill);
3625
- }
3626
- calculateTotal() {
3627
- const aimPerformanceValue = this.basePerformanceValue(this.aim);
3628
- const speedPerformanceValue = this.basePerformanceValue(this.speed);
3629
- let flashlightPerformanceValue = 0;
3630
- if (this.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
3631
- flashlightPerformanceValue = Math.pow(this.flashlight, 2) * 25;
3632
- }
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;
3633
3488
  const basePerformanceValue = Math.pow(Math.pow(aimPerformanceValue, 1.1) +
3634
3489
  Math.pow(speedPerformanceValue, 1.1) +
3635
3490
  Math.pow(flashlightPerformanceValue, 1.1), 1 / 1.1);
3636
3491
  if (basePerformanceValue > 1e-5) {
3637
3492
  // Document for formula derivation:
3638
3493
  // https://docs.google.com/document/d/10DZGYYSsT_yjz2Mtp6yIJld0Rqx4E-vVHupCqiM4TNI/edit
3639
- this.attributes.starRating =
3494
+ attributes.starRating =
3640
3495
  Math.cbrt(1.15) *
3641
3496
  0.027 *
3642
3497
  (Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformanceValue) +
3643
3498
  4);
3644
3499
  }
3645
3500
  else {
3646
- this.attributes.starRating = 0;
3501
+ attributes.starRating = 0;
3647
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;
3648
3508
  }
3649
- calculateAll() {
3650
- const skills = this.createSkills();
3651
- this.calculateSkills(...skills);
3652
- const aimSkill = skills.find((s) => s instanceof OsuAim && s.withSliders);
3653
- const aimSkillWithoutSliders = skills.find((s) => s instanceof OsuAim && !s.withSliders);
3654
- const speedSkill = skills.find((s) => s instanceof OsuSpeed);
3655
- const flashlightSkill = skills.find((s) => s instanceof OsuFlashlight);
3656
- if (aimSkill && aimSkillWithoutSliders) {
3657
- this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
3658
- }
3659
- if (speedSkill) {
3660
- this.postCalculateSpeed(speedSkill);
3661
- }
3662
- if (flashlightSkill) {
3663
- this.postCalculateFlashlight(flashlightSkill);
3664
- }
3665
- this.calculateTotal();
3666
- }
3667
- toString() {
3668
- return (this.total.toFixed(2) +
3669
- " stars (" +
3670
- this.aim.toFixed(2) +
3671
- " aim, " +
3672
- this.speed.toFixed(2) +
3673
- " speed, " +
3674
- this.flashlight.toFixed(2) +
3675
- " flashlight)");
3509
+ createPlayableBeatmap(beatmap, mods) {
3510
+ return beatmap.createOsuPlayableBeatmap(mods);
3676
3511
  }
3677
- generateDifficultyHitObjects(beatmap, clockRate) {
3678
- var _a, _b;
3512
+ createDifficultyHitObjects(beatmap) {
3513
+ var _a;
3514
+ const clockRate = beatmap.speedMultiplier;
3679
3515
  const difficultyObjects = [];
3680
3516
  const { objects } = beatmap.hitObjects;
3681
- for (let i = 0; i < objects.length; ++i) {
3682
- 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);
3683
3519
  difficultyObject.computeProperties(clockRate, objects);
3684
3520
  difficultyObjects.push(difficultyObject);
3685
3521
  }
3686
3522
  return difficultyObjects;
3687
3523
  }
3688
- createSkills() {
3524
+ createSkills(beatmap) {
3525
+ const { mods } = beatmap;
3689
3526
  const skills = [];
3690
- if (!this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
3691
- skills.push(new OsuAim(this.mods, true));
3692
- 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));
3693
3530
  }
3694
- if (!this.mods.some((m) => m instanceof osuBase.ModRelax)) {
3695
- skills.push(new OsuSpeed(this.mods));
3531
+ if (!mods.some((m) => m instanceof osuBase.ModRelax)) {
3532
+ skills.push(new OsuSpeed(mods));
3696
3533
  }
3697
- if (this.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
3698
- skills.push(new OsuFlashlight(this.mods));
3534
+ if (mods.some((m) => m instanceof osuBase.ModFlashlight)) {
3535
+ skills.push(new OsuFlashlight(mods));
3699
3536
  }
3700
3537
  return skills;
3701
3538
  }
3702
- populateDifficultyAttributes(beatmap, clockRate) {
3703
- super.populateDifficultyAttributes(beatmap, clockRate);
3704
- const preempt = osuBase.BeatmapDifficulty.difficultyRange(beatmap.difficulty.ar, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin) / clockRate;
3705
- this.attributes.approachRate = osuBase.BeatmapDifficulty.inverseDifficultyRange(preempt, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin);
3706
- }
3707
- /**
3708
- * Called after aim skill calculation.
3709
- *
3710
- * @param aimSkill The aim skill that considers sliders.
3711
- * @param aimSkillWithoutSliders The aim skill that doesn't consider sliders.
3712
- */
3713
- postCalculateAim(aimSkill, aimSkillWithoutSliders) {
3714
- this.strainPeaks.aimWithSliders = aimSkill.strainPeaks;
3715
- this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
3716
- this.attributes.aimDifficulty = this.starValue(aimSkill.difficultyValue());
3717
- if (this.aim) {
3718
- this.attributes.sliderFactor =
3719
- this.starValue(aimSkillWithoutSliders.difficultyValue()) /
3720
- this.aim;
3721
- }
3722
- if (this.mods.some((m) => m instanceof osuBase.ModTouchDevice)) {
3723
- 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;
3724
3553
  }
3725
- if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
3726
- 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;
3727
3560
  }
3728
- else if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
3729
- this.attributes.aimDifficulty = 0;
3561
+ else {
3562
+ attributes.sliderFactor = 1;
3730
3563
  }
3731
- this.attributes.aimDifficultStrainCount =
3732
- aimSkill.countDifficultStrains();
3733
- this.attributes.aimDifficultSliderCount =
3734
- aimSkill.countDifficultSliders();
3735
3564
  }
3736
- /**
3737
- * Called after speed skill calculation.
3738
- *
3739
- * @param speedSkill The speed skill.
3740
- */
3741
- postCalculateSpeed(speedSkill) {
3742
- this.strainPeaks.speed = speedSkill.strainPeaks;
3743
- this.attributes.speedDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
3744
- ? 0
3745
- : this.starValue(speedSkill.difficultyValue());
3746
- if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
3747
- this.attributes.speedDifficulty *= 0.5;
3748
- }
3749
- this.attributes.speedDifficultStrainCount =
3750
- speedSkill.countDifficultStrains();
3751
- const objectStrains = this.objects.map((v) => v.speedStrain);
3752
- const maxStrain = osuBase.MathUtils.max(objectStrains);
3753
- if (maxStrain) {
3754
- 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;
3755
3569
  }
3570
+ attributes.speedDifficulty = this.calculateRating(speed);
3571
+ attributes.speedNoteCount = speed.relevantNoteCount();
3572
+ attributes.speedDifficultStrainCount = speed.countDifficultStrains();
3756
3573
  }
3757
- /**
3758
- * Called after flashlight skill calculation.
3759
- *
3760
- * @param flashlightSkill The flashlight skill.
3761
- */
3762
- postCalculateFlashlight(flashlightSkill) {
3763
- this.strainPeaks.flashlight = flashlightSkill.strainPeaks;
3764
- this.attributes.flashlightDifficulty = this.starValue(flashlightSkill.difficultyValue());
3765
- if (this.mods.some((m) => m instanceof osuBase.ModTouchDevice)) {
3766
- this.attributes.flashlightDifficulty = Math.pow(this.flashlight, 0.8);
3767
- }
3768
- if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
3769
- this.attributes.flashlightDifficulty *= 0.7;
3770
- }
3771
- else if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
3772
- this.attributes.flashlightDifficulty *= 0.4;
3574
+ populateFlashlightAttributes(attributes, skills) {
3575
+ const flashlight = skills.find((s) => s instanceof OsuFlashlight);
3576
+ if (!flashlight) {
3577
+ return;
3773
3578
  }
3579
+ attributes.flashlightDifficulty = this.calculateRating(flashlight);
3774
3580
  }
3775
3581
  }
3776
- OsuDifficultyCalculator.difficultyAdjustmentMods = new Set([
3777
- osuBase.ModTouchDevice,
3778
- osuBase.ModDoubleTime,
3779
- osuBase.ModNightCore,
3780
- osuBase.ModDifficultyAdjust,
3781
- osuBase.ModHalfTime,
3782
- osuBase.ModEasy,
3783
- osuBase.ModHardRock,
3784
- osuBase.ModFlashlight,
3785
- osuBase.ModHidden,
3786
- osuBase.ModRelax,
3787
- osuBase.ModAutopilot,
3788
- ]);
3789
3582
 
3790
3583
  /**
3791
3584
  * A performance points calculator that calculates performance points for osu!standard gamemode.
@@ -4117,10 +3910,12 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
4117
3910
  }
4118
3911
  }
4119
3912
 
3913
+ exports.DifficultyAttributes = DifficultyAttributes;
4120
3914
  exports.DifficultyCalculator = DifficultyCalculator;
4121
3915
  exports.DifficultyHitObject = DifficultyHitObject;
4122
3916
  exports.DroidAim = DroidAim;
4123
3917
  exports.DroidAimEvaluator = DroidAimEvaluator;
3918
+ exports.DroidDifficultyAttributes = DroidDifficultyAttributes;
4124
3919
  exports.DroidDifficultyCalculator = DroidDifficultyCalculator;
4125
3920
  exports.DroidDifficultyHitObject = DroidDifficultyHitObject;
4126
3921
  exports.DroidFlashlight = DroidFlashlight;
@@ -4132,8 +3927,10 @@ exports.DroidTap = DroidTap;
4132
3927
  exports.DroidTapEvaluator = DroidTapEvaluator;
4133
3928
  exports.DroidVisual = DroidVisual;
4134
3929
  exports.DroidVisualEvaluator = DroidVisualEvaluator;
3930
+ exports.ExtendedDroidDifficultyAttributes = ExtendedDroidDifficultyAttributes;
4135
3931
  exports.OsuAim = OsuAim;
4136
3932
  exports.OsuAimEvaluator = OsuAimEvaluator;
3933
+ exports.OsuDifficultyAttributes = OsuDifficultyAttributes;
4137
3934
  exports.OsuDifficultyCalculator = OsuDifficultyCalculator;
4138
3935
  exports.OsuDifficultyHitObject = OsuDifficultyHitObject;
4139
3936
  exports.OsuFlashlight = OsuFlashlight;