@rian8337/osu-difficulty-calculator 4.0.0-beta.3 → 4.0.0-beta.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1719 -1498
- package/package.json +9 -6
- package/typings/index.d.ts +571 -473
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -8,10 +8,6 @@ var osuBase = require('@rian8337/osu-base');
|
|
|
8
8
|
* This class should be considered an "evaluating" class and not persisted.
|
|
9
9
|
*/
|
|
10
10
|
class AimEvaluator {
|
|
11
|
-
static wideAngleMultiplier = 1.5;
|
|
12
|
-
static acuteAngleMultiplier = 1.95;
|
|
13
|
-
static sliderMultiplier = 1.35;
|
|
14
|
-
static velocityChangeMultiplier = 0.75;
|
|
15
11
|
/**
|
|
16
12
|
* Calculates the bonus of wide angles.
|
|
17
13
|
*/
|
|
@@ -27,154 +23,244 @@ class AimEvaluator {
|
|
|
27
23
|
return 1 - this.calculateWideAngleBonus(angle);
|
|
28
24
|
}
|
|
29
25
|
}
|
|
26
|
+
AimEvaluator.wideAngleMultiplier = 1.5;
|
|
27
|
+
AimEvaluator.acuteAngleMultiplier = 1.95;
|
|
28
|
+
AimEvaluator.sliderMultiplier = 1.35;
|
|
29
|
+
AimEvaluator.velocityChangeMultiplier = 0.75;
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
32
|
+
* The base of a difficulty calculator.
|
|
33
33
|
*/
|
|
34
|
-
class
|
|
35
|
-
/**
|
|
36
|
-
* The underlying hitobject.
|
|
37
|
-
*/
|
|
38
|
-
object;
|
|
39
|
-
/**
|
|
40
|
-
* The index of this hitobject in the list of all hitobjects.
|
|
41
|
-
*
|
|
42
|
-
* This is one less than the actual index of the hitobject in the beatmap.
|
|
43
|
-
*/
|
|
44
|
-
index = 0;
|
|
45
|
-
/**
|
|
46
|
-
* The preempt time of the hitobject.
|
|
47
|
-
*/
|
|
48
|
-
baseTimePreempt = 600;
|
|
49
|
-
/**
|
|
50
|
-
* Adjusted preempt time of the hitobject, taking speed multiplier into account.
|
|
51
|
-
*/
|
|
52
|
-
timePreempt = 600;
|
|
53
|
-
/**
|
|
54
|
-
* The fade in time of the hitobject.
|
|
55
|
-
*/
|
|
56
|
-
timeFadeIn = 400;
|
|
57
|
-
/**
|
|
58
|
-
* The aim strain generated by the hitobject if sliders are considered.
|
|
59
|
-
*/
|
|
60
|
-
aimStrainWithSliders = 0;
|
|
61
|
-
/**
|
|
62
|
-
* The aim strain generated by the hitobject if sliders are not considered.
|
|
63
|
-
*/
|
|
64
|
-
aimStrainWithoutSliders = 0;
|
|
65
|
-
/**
|
|
66
|
-
* The tap strain generated by the hitobject.
|
|
67
|
-
*
|
|
68
|
-
* This is also used for osu!standard as opposed to "speed strain".
|
|
69
|
-
*/
|
|
70
|
-
tapStrain = 0;
|
|
71
|
-
/**
|
|
72
|
-
* The tap strain generated by the hitobject if `strainTime` isn't modified by
|
|
73
|
-
* OD. This is used in three-finger detection.
|
|
74
|
-
*/
|
|
75
|
-
originalTapStrain = 0;
|
|
76
|
-
/**
|
|
77
|
-
* The rhythm multiplier generated by the hitobject. This is used to alter tap strain.
|
|
78
|
-
*/
|
|
79
|
-
rhythmMultiplier = 0;
|
|
80
|
-
/**
|
|
81
|
-
* The rhythm strain generated by the hitobject.
|
|
82
|
-
*/
|
|
83
|
-
rhythmStrain = 0;
|
|
84
|
-
/**
|
|
85
|
-
* The flashlight strain generated by the hitobject if sliders are considered.
|
|
86
|
-
*/
|
|
87
|
-
flashlightStrainWithSliders = 0;
|
|
88
|
-
/**
|
|
89
|
-
* The flashlight strain generated by the hitobject if sliders are not considered.
|
|
90
|
-
*/
|
|
91
|
-
flashlightStrainWithoutSliders = 0;
|
|
34
|
+
class DifficultyCalculator {
|
|
92
35
|
/**
|
|
93
|
-
* The
|
|
36
|
+
* The difficulty objects of the beatmap.
|
|
94
37
|
*/
|
|
95
|
-
|
|
38
|
+
get objects() {
|
|
39
|
+
return this._objects;
|
|
40
|
+
}
|
|
96
41
|
/**
|
|
97
|
-
* The
|
|
42
|
+
* The total star rating of the beatmap.
|
|
98
43
|
*/
|
|
99
|
-
|
|
44
|
+
get total() {
|
|
45
|
+
return this.attributes.starRating;
|
|
46
|
+
}
|
|
100
47
|
/**
|
|
101
|
-
*
|
|
48
|
+
* Constructs a new instance of the calculator.
|
|
102
49
|
*
|
|
103
|
-
*
|
|
50
|
+
* @param beatmap The beatmap to calculate.
|
|
104
51
|
*/
|
|
105
|
-
|
|
52
|
+
constructor(beatmap) {
|
|
53
|
+
/**
|
|
54
|
+
* The difficulty objects of the beatmap.
|
|
55
|
+
*/
|
|
56
|
+
this._objects = [];
|
|
57
|
+
/**
|
|
58
|
+
* The modifications applied.
|
|
59
|
+
*/
|
|
60
|
+
this.mods = [];
|
|
61
|
+
/**
|
|
62
|
+
* The strain peaks of various calculated difficulties.
|
|
63
|
+
*/
|
|
64
|
+
this.strainPeaks = {
|
|
65
|
+
aimWithSliders: [],
|
|
66
|
+
aimWithoutSliders: [],
|
|
67
|
+
speed: [],
|
|
68
|
+
flashlight: [],
|
|
69
|
+
};
|
|
70
|
+
this.beatmap = beatmap;
|
|
71
|
+
}
|
|
106
72
|
/**
|
|
107
|
-
*
|
|
73
|
+
* Calculates the star rating of the specified beatmap.
|
|
108
74
|
*
|
|
109
|
-
*
|
|
75
|
+
* The beatmap is analyzed in chunks of `sectionLength` duration.
|
|
76
|
+
* For each chunk the highest hitobject strains are added to
|
|
77
|
+
* a list which is then collapsed into a weighted sum, much
|
|
78
|
+
* like scores are weighted on a user's profile.
|
|
110
79
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
80
|
+
* For subsequent chunks, the initial max strain is calculated
|
|
81
|
+
* by decaying the previous hitobject's strain until the
|
|
82
|
+
* beginning of the new chunk.
|
|
114
83
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*/
|
|
118
|
-
minimumJumpDistance = 0;
|
|
119
|
-
/**
|
|
120
|
-
* The time taken to travel through `minimumJumpDistance`, with a minimum value of 25ms.
|
|
121
|
-
*/
|
|
122
|
-
minimumJumpTime = 0;
|
|
123
|
-
/**
|
|
124
|
-
* The normalized distance between the start and end position of this hitobject.
|
|
125
|
-
*/
|
|
126
|
-
travelDistance = 0;
|
|
127
|
-
/**
|
|
128
|
-
* The time taken to travel through `travelDistance`, with a minimum value of 25ms for sliders.
|
|
84
|
+
* @param options Options for the difficulty calculation.
|
|
85
|
+
* @returns The current instance.
|
|
129
86
|
*/
|
|
130
|
-
|
|
87
|
+
calculate(options) {
|
|
88
|
+
var _a;
|
|
89
|
+
this.mods = (_a = options === null || options === void 0 ? void 0 : options.mods) !== null && _a !== void 0 ? _a : [];
|
|
90
|
+
const playableBeatmap = this.beatmap.createPlayableBeatmap({
|
|
91
|
+
mode: this.mode,
|
|
92
|
+
mods: this.mods,
|
|
93
|
+
customSpeedMultiplier: options === null || options === void 0 ? void 0 : options.customSpeedMultiplier,
|
|
94
|
+
});
|
|
95
|
+
const clockRate = this.calculateClockRate(options);
|
|
96
|
+
this.populateDifficultyAttributes(playableBeatmap, clockRate);
|
|
97
|
+
this._objects = this.generateDifficultyHitObjects(playableBeatmap, clockRate);
|
|
98
|
+
this.calculateAll();
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
131
101
|
/**
|
|
132
|
-
*
|
|
102
|
+
* Calculates the skills provided.
|
|
133
103
|
*
|
|
134
|
-
*
|
|
135
|
-
*/
|
|
136
|
-
angle = null;
|
|
137
|
-
/**
|
|
138
|
-
* The amount of milliseconds elapsed between this hitobject and the last hitobject.
|
|
139
|
-
*/
|
|
140
|
-
deltaTime = 0;
|
|
141
|
-
/**
|
|
142
|
-
* The amount of milliseconds elapsed since the start time of the previous hitobject, with a minimum of 25ms.
|
|
143
|
-
*/
|
|
144
|
-
strainTime = 0;
|
|
145
|
-
/**
|
|
146
|
-
* Adjusted start time of the hitobject, taking speed multiplier into account.
|
|
147
|
-
*/
|
|
148
|
-
startTime = 0;
|
|
149
|
-
/**
|
|
150
|
-
* Adjusted end time of the hitobject, taking speed multiplier into account.
|
|
104
|
+
* @param skills The skills to calculate.
|
|
151
105
|
*/
|
|
152
|
-
|
|
106
|
+
calculateSkills(...skills) {
|
|
107
|
+
// The first object doesn't generate a strain, so we begin calculating from the second object.
|
|
108
|
+
for (const object of this.objects.slice(1)) {
|
|
109
|
+
for (const skill of skills) {
|
|
110
|
+
skill.process(object);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
153
114
|
/**
|
|
154
|
-
*
|
|
115
|
+
* Obtains the clock rate of the beatmap.
|
|
116
|
+
*
|
|
117
|
+
* @param options The options to obtain the clock rate with.
|
|
118
|
+
* @returns The clock rate of the beatmap.
|
|
155
119
|
*/
|
|
156
|
-
|
|
120
|
+
calculateClockRate(options) {
|
|
121
|
+
var _a, _b;
|
|
122
|
+
return (osuBase.ModUtil.calculateRateWithMods((_a = options === null || options === void 0 ? void 0 : options.mods) !== null && _a !== void 0 ? _a : []) *
|
|
123
|
+
((_b = options === null || options === void 0 ? void 0 : options.customSpeedMultiplier) !== null && _b !== void 0 ? _b : 1));
|
|
124
|
+
}
|
|
157
125
|
/**
|
|
158
|
-
*
|
|
126
|
+
* Populates the stored difficulty attributes with necessary data.
|
|
159
127
|
*
|
|
160
|
-
*
|
|
128
|
+
* @param beatmap The beatmap to populate the attributes with.
|
|
129
|
+
* @param clockRate The clock rate of the beatmap.
|
|
161
130
|
*/
|
|
162
|
-
|
|
131
|
+
populateDifficultyAttributes(beatmap, clockRate) {
|
|
132
|
+
this.attributes.hitCircleCount = this.beatmap.hitObjects.circles;
|
|
133
|
+
this.attributes.maxCombo = this.beatmap.maxCombo;
|
|
134
|
+
this.attributes.mods = this.mods.slice();
|
|
135
|
+
this.attributes.sliderCount = this.beatmap.hitObjects.sliders;
|
|
136
|
+
this.attributes.spinnerCount = this.beatmap.hitObjects.spinners;
|
|
137
|
+
this.attributes.clockRate = clockRate;
|
|
138
|
+
let greatWindow;
|
|
139
|
+
switch (this.mode) {
|
|
140
|
+
case osuBase.Modes.droid:
|
|
141
|
+
greatWindow = new osuBase.DroidHitWindow(beatmap.difficulty.od).hitWindowFor300(this.mods.some((m) => m instanceof osuBase.ModPrecise));
|
|
142
|
+
break;
|
|
143
|
+
case osuBase.Modes.osu:
|
|
144
|
+
greatWindow = new osuBase.OsuHitWindow(beatmap.difficulty.od).hitWindowFor300();
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
this.attributes.overallDifficulty = osuBase.OsuHitWindow.hitWindow300ToOD(greatWindow / clockRate);
|
|
148
|
+
}
|
|
163
149
|
/**
|
|
164
|
-
*
|
|
150
|
+
* Calculates the star rating value of a difficulty.
|
|
151
|
+
*
|
|
152
|
+
* @param difficulty The difficulty to calculate.
|
|
165
153
|
*/
|
|
166
|
-
|
|
154
|
+
starValue(difficulty) {
|
|
155
|
+
return Math.sqrt(difficulty) * this.difficultyMultiplier;
|
|
156
|
+
}
|
|
167
157
|
/**
|
|
168
|
-
*
|
|
158
|
+
* Calculates the base performance value of a difficulty rating.
|
|
159
|
+
*
|
|
160
|
+
* @param rating The difficulty rating.
|
|
169
161
|
*/
|
|
170
|
-
|
|
162
|
+
basePerformanceValue(rating) {
|
|
163
|
+
return Math.pow(5 * Math.max(1, rating / 0.0675) - 4, 3) / 100000;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Represents a hit object with difficulty calculation values.
|
|
169
|
+
*/
|
|
170
|
+
class DifficultyHitObject {
|
|
171
171
|
/**
|
|
172
|
+
* Note: You **must** call `computeProperties` at some point due to how TypeScript handles
|
|
173
|
+
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue).
|
|
174
|
+
*
|
|
172
175
|
* @param object The underlying hitobject.
|
|
173
|
-
* @param
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
* @param lastObject The hitobject before this hitobject.
|
|
177
|
+
* @param lastLastObject The hitobject before the last hitobject.
|
|
178
|
+
* @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
|
|
179
|
+
* @param clockRate The clock rate of the beatmap.
|
|
180
|
+
* @param greatWindow The great window of the hitobject.
|
|
181
|
+
*/
|
|
182
|
+
constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, greatWindow) {
|
|
183
|
+
/**
|
|
184
|
+
* The aim strain generated by the hitobject if sliders are considered.
|
|
185
|
+
*/
|
|
186
|
+
this.aimStrainWithSliders = 0;
|
|
187
|
+
/**
|
|
188
|
+
* The aim strain generated by the hitobject if sliders are not considered.
|
|
189
|
+
*/
|
|
190
|
+
this.aimStrainWithoutSliders = 0;
|
|
191
|
+
/**
|
|
192
|
+
* The rhythm multiplier generated by the hitobject. This is used to alter tap strain.
|
|
193
|
+
*/
|
|
194
|
+
this.rhythmMultiplier = 0;
|
|
195
|
+
/**
|
|
196
|
+
* The normalized distance from the "lazy" end position of the previous hitobject to the start position of this hitobject.
|
|
197
|
+
*
|
|
198
|
+
* The "lazy" end position is the position at which the cursor ends up if the previous hitobject is followed with as minimal movement as possible (i.e. on the edge of slider follow circles).
|
|
199
|
+
*/
|
|
200
|
+
this.lazyJumpDistance = 0;
|
|
201
|
+
/**
|
|
202
|
+
* The normalized shortest distance to consider for a jump between the previous hitobject and this hitobject.
|
|
203
|
+
*
|
|
204
|
+
* This is bounded from above by `lazyJumpDistance`, and is smaller than the former if a more natural path is able to be taken through the previous hitobject.
|
|
205
|
+
*
|
|
206
|
+
* Suppose a linear slider - circle pattern. Following the slider lazily (see: `lazyJumpDistance`) will result in underestimating the true end position of the slider as being closer towards the start position.
|
|
207
|
+
* As a result, `lazyJumpDistance` overestimates the jump distance because the player is able to take a more natural path by following through the slider to its end,
|
|
208
|
+
* such that the jump is felt as only starting from the slider's true end position.
|
|
209
|
+
*
|
|
210
|
+
* Now consider a slider - circle pattern where the circle is stacked along the path inside the slider.
|
|
211
|
+
* In this case, the lazy end position correctly estimates the true end position of the slider and provides the more natural movement path.
|
|
212
|
+
*/
|
|
213
|
+
this.minimumJumpDistance = 0;
|
|
214
|
+
/**
|
|
215
|
+
* The time taken to travel through `minimumJumpDistance`, with a minimum value of 25ms.
|
|
216
|
+
*/
|
|
217
|
+
this.minimumJumpTime = 0;
|
|
218
|
+
/**
|
|
219
|
+
* The normalized distance between the start and end position of this hitobject.
|
|
220
|
+
*/
|
|
221
|
+
this.travelDistance = 0;
|
|
222
|
+
/**
|
|
223
|
+
* The time taken to travel through `travelDistance`, with a minimum value of 25ms for sliders.
|
|
224
|
+
*/
|
|
225
|
+
this.travelTime = 0;
|
|
226
|
+
/**
|
|
227
|
+
* Angle the player has to take to hit this hitobject.
|
|
228
|
+
*
|
|
229
|
+
* Calculated as the angle between the circles (current-2, current-1, current).
|
|
230
|
+
*/
|
|
231
|
+
this.angle = null;
|
|
232
|
+
this.normalizedRadius = 50;
|
|
233
|
+
this.maximumSliderRadius = this.normalizedRadius * 2.4;
|
|
234
|
+
this.assumedSliderRadius = this.normalizedRadius * 1.8;
|
|
176
235
|
this.object = object;
|
|
177
|
-
this.
|
|
236
|
+
this.lastObject = lastObject;
|
|
237
|
+
this.lastLastObject = lastLastObject;
|
|
238
|
+
this.hitObjects = difficultyHitObjects;
|
|
239
|
+
this.fullGreatWindow = greatWindow * 2;
|
|
240
|
+
this.index = difficultyHitObjects.length - 1;
|
|
241
|
+
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
|
242
|
+
this.startTime = object.startTime / clockRate;
|
|
243
|
+
this.endTime = object.endTime / clockRate;
|
|
244
|
+
if (lastObject) {
|
|
245
|
+
this.deltaTime = this.startTime - lastObject.startTime / clockRate;
|
|
246
|
+
this.strainTime = Math.max(this.deltaTime, DifficultyHitObject.minDeltaTime);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
this.deltaTime = 0;
|
|
250
|
+
this.strainTime = 0;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Computes the properties of this hitobject.
|
|
255
|
+
*
|
|
256
|
+
* @param clockRate The clock rate of the beatmap.
|
|
257
|
+
* @param hitObjects The hitobjects in the beatmap.
|
|
258
|
+
*/
|
|
259
|
+
computeProperties(clockRate,
|
|
260
|
+
// Required for `DroidDifficultyHitObject` override.
|
|
261
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
262
|
+
hitObjects) {
|
|
263
|
+
this.setDistances(clockRate);
|
|
178
264
|
}
|
|
179
265
|
/**
|
|
180
266
|
* Gets the difficulty hitobject at a specific index with respect to the current
|
|
@@ -187,7 +273,8 @@ class DifficultyHitObject {
|
|
|
187
273
|
* difficulty hitobject's index, `null` if the index is out of range.
|
|
188
274
|
*/
|
|
189
275
|
previous(backwardsIndex) {
|
|
190
|
-
|
|
276
|
+
var _a;
|
|
277
|
+
return (_a = this.hitObjects[this.index - backwardsIndex]) !== null && _a !== void 0 ? _a : null;
|
|
191
278
|
}
|
|
192
279
|
/**
|
|
193
280
|
* Gets the difficulty hitobject at a specific index with respect to the current
|
|
@@ -200,285 +287,128 @@ class DifficultyHitObject {
|
|
|
200
287
|
* difficulty hitobject's index, `null` if the index is out of range.
|
|
201
288
|
*/
|
|
202
289
|
next(forwardsIndex) {
|
|
203
|
-
|
|
290
|
+
var _a;
|
|
291
|
+
return ((_a = this.hitObjects[this.index + forwardsIndex + 2]) !== null && _a !== void 0 ? _a : null);
|
|
204
292
|
}
|
|
205
293
|
/**
|
|
206
294
|
* Calculates the opacity of the hitobject at a given time.
|
|
207
295
|
*
|
|
208
296
|
* @param time The time to calculate the hitobject's opacity at.
|
|
209
297
|
* @param isHidden Whether Hidden mod is used.
|
|
210
|
-
* @param mode The gamemode to calculate the opacity for.
|
|
211
298
|
* @returns The opacity of the hitobject at the given time.
|
|
212
299
|
*/
|
|
213
|
-
opacityAt(time, isHidden
|
|
300
|
+
opacityAt(time, isHidden) {
|
|
214
301
|
if (time > this.object.startTime) {
|
|
215
302
|
// Consider a hitobject as being invisible when its start time is passed.
|
|
216
303
|
// In reality the hitobject will be visible beyond its start time up until its hittable window has passed,
|
|
217
304
|
// but this is an approximation and such a case is unlikely to be hit where this function is used.
|
|
218
305
|
return 0;
|
|
219
306
|
}
|
|
220
|
-
const fadeInStartTime = this.object.startTime - this.
|
|
221
|
-
const fadeInDuration = this.timeFadeIn;
|
|
307
|
+
const fadeInStartTime = this.object.startTime - this.object.timePreempt;
|
|
308
|
+
const fadeInDuration = this.object.timeFadeIn;
|
|
222
309
|
if (isHidden) {
|
|
223
310
|
const fadeOutStartTime = fadeInStartTime + fadeInDuration;
|
|
224
|
-
const fadeOutDuration = this.
|
|
225
|
-
(mode === osuBase.Modes.droid
|
|
226
|
-
? 0.35
|
|
227
|
-
: osuBase.ModHidden.fadeOutDurationMultiplier);
|
|
311
|
+
const fadeOutDuration = this.object.timePreempt * osuBase.ModHidden.fadeOutDurationMultiplier;
|
|
228
312
|
return Math.min(osuBase.MathUtils.clamp((time - fadeInStartTime) / fadeInDuration, 0, 1), 1 -
|
|
229
313
|
osuBase.MathUtils.clamp((time - fadeOutStartTime) / fadeOutDuration, 0, 1));
|
|
230
314
|
}
|
|
231
315
|
return osuBase.MathUtils.clamp((time - fadeInStartTime) / fadeInDuration, 0, 1);
|
|
232
316
|
}
|
|
233
317
|
/**
|
|
234
|
-
*
|
|
318
|
+
* How possible is it to doubletap this object together with the next one and get perfect
|
|
319
|
+
* judgement in range from 0 to 1.
|
|
235
320
|
*
|
|
236
|
-
*
|
|
237
|
-
* can be hit with just a single tap in osu!droid.
|
|
238
|
-
*
|
|
239
|
-
* @param considerDistance Whether to consider the distance between both hitobjects.
|
|
240
|
-
* @returns Whether the hitobject is considered overlapping.
|
|
321
|
+
* A value closer to 1 indicates a higher possibility.
|
|
241
322
|
*/
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const previous = this.previous(0);
|
|
247
|
-
if (!previous || previous.object instanceof osuBase.Spinner) {
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
if (this.deltaTime >= 5) {
|
|
251
|
-
return false;
|
|
323
|
+
get doubletapness() {
|
|
324
|
+
const next = this.next(0);
|
|
325
|
+
if (!next) {
|
|
326
|
+
return 0;
|
|
252
327
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
328
|
+
const currentDeltaTime = Math.max(1, this.deltaTime);
|
|
329
|
+
const nextDeltaTime = Math.max(1, next.deltaTime);
|
|
330
|
+
const deltaDifference = Math.abs(nextDeltaTime - currentDeltaTime);
|
|
331
|
+
const speedRatio = currentDeltaTime / Math.max(currentDeltaTime, deltaDifference);
|
|
332
|
+
const windowRatio = Math.pow(Math.min(1, currentDeltaTime / this.fullGreatWindow), 2);
|
|
333
|
+
return 1 - Math.pow(speedRatio, 1 - windowRatio);
|
|
334
|
+
}
|
|
335
|
+
setDistances(clockRate) {
|
|
336
|
+
if (this.object instanceof osuBase.Slider) {
|
|
337
|
+
this.calculateSliderCursorPosition(this.object);
|
|
338
|
+
this.travelDistance = this.object.lazyTravelDistance;
|
|
339
|
+
// Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
|
340
|
+
if (this.mode === osuBase.Modes.droid) {
|
|
341
|
+
this.travelDistance *= Math.pow(1 + this.object.repeatCount / 4, 1 / 4);
|
|
261
342
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
return true;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* A converter used to convert normal hitobjects into difficulty hitobjects.
|
|
270
|
-
*/
|
|
271
|
-
class DifficultyHitObjectCreator {
|
|
272
|
-
/**
|
|
273
|
-
* The threshold for small circle buff for osu!droid.
|
|
274
|
-
*/
|
|
275
|
-
DROID_CIRCLESIZE_BUFF_THRESHOLD = 70;
|
|
276
|
-
/**
|
|
277
|
-
* The threshold for small circle buff for osu!standard.
|
|
278
|
-
*/
|
|
279
|
-
PC_CIRCLESIZE_BUFF_THRESHOLD = 30;
|
|
280
|
-
/**
|
|
281
|
-
* The gamemode this creator is creating for.
|
|
282
|
-
*/
|
|
283
|
-
mode = osuBase.Modes.osu;
|
|
284
|
-
/**
|
|
285
|
-
* The base normalized radius of hitobjects.
|
|
286
|
-
*/
|
|
287
|
-
normalizedRadius = 50;
|
|
288
|
-
maximumSliderRadius = this.normalizedRadius * 2.4;
|
|
289
|
-
assumedSliderRadius = this.normalizedRadius * 1.8;
|
|
290
|
-
minDeltaTime = 25;
|
|
291
|
-
/**
|
|
292
|
-
* Generates difficulty hitobjects for difficulty calculation.
|
|
293
|
-
*/
|
|
294
|
-
generateDifficultyObjects(params) {
|
|
295
|
-
params.preempt ??= 600;
|
|
296
|
-
this.mode = params.mode;
|
|
297
|
-
if (this.mode === osuBase.Modes.droid) {
|
|
298
|
-
this.maximumSliderRadius = this.normalizedRadius * 2;
|
|
299
|
-
}
|
|
300
|
-
const droidCircleSize = new osuBase.MapStats({
|
|
301
|
-
cs: params.circleSize,
|
|
302
|
-
mods: params.mods,
|
|
303
|
-
}).calculate({ mode: osuBase.Modes.droid }).cs;
|
|
304
|
-
const droidScale = (1 - (0.7 * (droidCircleSize - 5)) / 5) / 2;
|
|
305
|
-
const osuCircleSize = new osuBase.MapStats({
|
|
306
|
-
cs: params.circleSize,
|
|
307
|
-
mods: params.mods,
|
|
308
|
-
}).calculate({ mode: osuBase.Modes.osu }).cs;
|
|
309
|
-
const osuScale = (1 - (0.7 * (osuCircleSize - 5)) / 5) / 2;
|
|
310
|
-
params.objects[0].droidScale = droidScale;
|
|
311
|
-
params.objects[0].osuScale = osuScale;
|
|
312
|
-
const scalingFactor = this.getScalingFactor(params.objects[0].getRadius(this.mode));
|
|
313
|
-
const difficultyObjects = [];
|
|
314
|
-
for (let i = 0; i < params.objects.length; ++i) {
|
|
315
|
-
const object = new DifficultyHitObject(params.objects[i], difficultyObjects);
|
|
316
|
-
object.index = difficultyObjects.length - 1;
|
|
317
|
-
object.object.droidScale = droidScale;
|
|
318
|
-
object.object.osuScale = osuScale;
|
|
319
|
-
object.timePreempt = params.preempt;
|
|
320
|
-
object.baseTimePreempt = params.preempt * params.speedMultiplier;
|
|
321
|
-
if (object.object instanceof osuBase.Slider) {
|
|
322
|
-
object.velocity =
|
|
323
|
-
object.object.velocity * params.speedMultiplier;
|
|
324
|
-
object.object.nestedHitObjects.forEach((o) => {
|
|
325
|
-
o.droidScale = droidScale;
|
|
326
|
-
o.osuScale = osuScale;
|
|
327
|
-
});
|
|
328
|
-
this.calculateSliderCursorPosition(object.object);
|
|
329
|
-
object.travelDistance = object.object.lazyTravelDistance;
|
|
330
|
-
// Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
|
331
|
-
if (this.mode === osuBase.Modes.droid) {
|
|
332
|
-
object.travelDistance *= Math.pow(1 + object.object.repeats / 4, 1 / 4);
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
object.travelDistance *= Math.pow(1 + object.object.repeats / 2.5, 1 / 2.5);
|
|
336
|
-
}
|
|
337
|
-
object.travelTime = Math.max(object.object.lazyTravelTime / params.speedMultiplier, this.minDeltaTime);
|
|
338
|
-
}
|
|
339
|
-
const lastObject = difficultyObjects[i - 1];
|
|
340
|
-
const lastLastObject = difficultyObjects[i - 2];
|
|
341
|
-
object.startTime = object.object.startTime / params.speedMultiplier;
|
|
342
|
-
object.endTime = object.object.endTime / params.speedMultiplier;
|
|
343
|
-
if (!lastObject) {
|
|
344
|
-
difficultyObjects.push(object);
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
object.deltaTime =
|
|
348
|
-
(object.object.startTime - lastObject.object.startTime) /
|
|
349
|
-
params.speedMultiplier;
|
|
350
|
-
// Cap to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
|
351
|
-
object.strainTime = Math.max(this.minDeltaTime, object.deltaTime);
|
|
352
|
-
if (object.object instanceof osuBase.Spinner) {
|
|
353
|
-
difficultyObjects.push(object);
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
// We'll have two visible object arrays. The first array contains objects before the current object starts in a reversed order,
|
|
357
|
-
// while the second array contains objects after the current object ends.
|
|
358
|
-
// For overlapping factor, we also need to consider previous visible objects.
|
|
359
|
-
const prevVisibleObjects = [];
|
|
360
|
-
const nextVisibleObjects = [];
|
|
361
|
-
for (let j = i + 1; j < params.objects.length; ++j) {
|
|
362
|
-
const o = params.objects[j];
|
|
363
|
-
if (o instanceof osuBase.Spinner) {
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
if (o.startTime / params.speedMultiplier >
|
|
367
|
-
object.endTime + object.timePreempt) {
|
|
368
|
-
break;
|
|
369
|
-
}
|
|
370
|
-
// Future objects do not have their scales set, so we set them here.
|
|
371
|
-
o.droidScale = droidScale;
|
|
372
|
-
o.osuScale = osuScale;
|
|
373
|
-
nextVisibleObjects.push(o);
|
|
374
|
-
}
|
|
375
|
-
for (let j = 0; j < object.index; ++j) {
|
|
376
|
-
const prev = object.previous(j);
|
|
377
|
-
if (prev.object instanceof osuBase.Spinner) {
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
if (prev.startTime >= object.startTime) {
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
if (prev.startTime < object.startTime - object.timePreempt) {
|
|
384
|
-
break;
|
|
385
|
-
}
|
|
386
|
-
prevVisibleObjects.push(prev.object);
|
|
387
|
-
}
|
|
388
|
-
for (const hitObject of prevVisibleObjects) {
|
|
389
|
-
const distance = object.object
|
|
390
|
-
.getStackedPosition(this.mode)
|
|
391
|
-
.getDistance(hitObject.getStackedEndPosition(this.mode));
|
|
392
|
-
const deltaTime = object.startTime -
|
|
393
|
-
hitObject.endTime / params.speedMultiplier;
|
|
394
|
-
this.applyToOverlappingFactor(object, distance, deltaTime);
|
|
395
|
-
}
|
|
396
|
-
for (const hitObject of nextVisibleObjects) {
|
|
397
|
-
const distance = hitObject
|
|
398
|
-
.getStackedPosition(this.mode)
|
|
399
|
-
.getDistance(object.object.getStackedEndPosition(this.mode));
|
|
400
|
-
const deltaTime = hitObject.startTime / params.speedMultiplier -
|
|
401
|
-
object.endTime;
|
|
402
|
-
if (deltaTime >= 0) {
|
|
403
|
-
object.noteDensity += 1 - deltaTime / object.timePreempt;
|
|
404
|
-
}
|
|
405
|
-
this.applyToOverlappingFactor(object, distance, deltaTime);
|
|
406
|
-
}
|
|
407
|
-
if (lastObject.object instanceof osuBase.Spinner) {
|
|
408
|
-
difficultyObjects.push(object);
|
|
409
|
-
continue;
|
|
343
|
+
else {
|
|
344
|
+
this.travelDistance *= Math.pow(1 + this.object.repeatCount / 2.5, 1 / 2.5);
|
|
410
345
|
}
|
|
411
|
-
|
|
412
|
-
|
|
346
|
+
this.travelTime = Math.max(this.object.lazyTravelTime / clockRate, DifficultyHitObject.minDeltaTime);
|
|
347
|
+
}
|
|
348
|
+
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner.
|
|
349
|
+
if (!this.lastObject ||
|
|
350
|
+
this.object instanceof osuBase.Spinner ||
|
|
351
|
+
this.lastObject instanceof osuBase.Spinner) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
|
355
|
+
const { scalingFactor } = this;
|
|
356
|
+
const lastCursorPosition = this.getEndCursorPosition(this.lastObject);
|
|
357
|
+
this.lazyJumpDistance = this.object
|
|
358
|
+
.getStackedPosition(this.mode)
|
|
359
|
+
.scale(scalingFactor)
|
|
360
|
+
.subtract(lastCursorPosition.scale(scalingFactor)).length;
|
|
361
|
+
this.minimumJumpTime = this.strainTime;
|
|
362
|
+
this.minimumJumpDistance = this.lazyJumpDistance;
|
|
363
|
+
if (this.lastObject instanceof osuBase.Slider) {
|
|
364
|
+
const lastTravelTime = Math.max(this.lastObject.lazyTravelTime / clockRate, DifficultyHitObject.minDeltaTime);
|
|
365
|
+
this.minimumJumpTime = Math.max(this.strainTime - lastTravelTime, DifficultyHitObject.minDeltaTime);
|
|
366
|
+
// There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects.
|
|
367
|
+
//
|
|
368
|
+
// 1. The anti-flow pattern, where players cut the slider short in order to move to the next hitobject.
|
|
369
|
+
//
|
|
370
|
+
// <======o==> ← slider
|
|
371
|
+
// | ← most natural jump path
|
|
372
|
+
// o ← a follow-up hitcircle
|
|
373
|
+
//
|
|
374
|
+
// In this case the most natural jump path is approximated by LazyJumpDistance.
|
|
375
|
+
//
|
|
376
|
+
// 2. The flow pattern, where players follow through the slider to its visual extent into the next hitobject.
|
|
377
|
+
//
|
|
378
|
+
// <======o==>---o
|
|
379
|
+
// ↑
|
|
380
|
+
// most natural jump path
|
|
381
|
+
//
|
|
382
|
+
// In this case the most natural jump path is better approximated by a new distance called "tailJumpDistance" - the distance between the slider's tail and the next hitobject.
|
|
383
|
+
//
|
|
384
|
+
// Thus, the player is assumed to jump the minimum of these two distances in all cases.
|
|
385
|
+
const tailJumpDistance = this.lastObject.tail
|
|
413
386
|
.getStackedPosition(this.mode)
|
|
414
|
-
.
|
|
415
|
-
.
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
// In this case the most natural jump path is approximated by LazyJumpDistance.
|
|
429
|
-
//
|
|
430
|
-
// 2. The flow pattern, where players follow through the slider to its visual extent into the next hitobject.
|
|
431
|
-
//
|
|
432
|
-
// <======o==>---o
|
|
433
|
-
// ↑
|
|
434
|
-
// most natural jump path
|
|
435
|
-
//
|
|
436
|
-
// In this case the most natural jump path is better approximated by a new distance called "tailJumpDistance" - the distance between the slider's tail and the next hitobject.
|
|
437
|
-
//
|
|
438
|
-
// Thus, the player is assumed to jump the minimum of these two distances in all cases.
|
|
439
|
-
const tailJumpDistance = lastObject.object.tail
|
|
440
|
-
.getStackedPosition(this.mode)
|
|
441
|
-
.subtract(object.object.getStackedPosition(this.mode))
|
|
442
|
-
.length * scalingFactor;
|
|
443
|
-
object.minimumJumpDistance = Math.max(0, Math.min(object.lazyJumpDistance -
|
|
444
|
-
(this.maximumSliderRadius -
|
|
445
|
-
this.assumedSliderRadius), tailJumpDistance - this.maximumSliderRadius));
|
|
446
|
-
}
|
|
447
|
-
if (lastLastObject && !(lastLastObject.object instanceof osuBase.Spinner)) {
|
|
448
|
-
const lastLastCursorPosition = this.getEndCursorPosition(lastLastObject.object);
|
|
449
|
-
const v1 = lastLastCursorPosition.subtract(lastObject.object.getStackedPosition(this.mode));
|
|
450
|
-
const v2 = object.object
|
|
451
|
-
.getStackedPosition(this.mode)
|
|
452
|
-
.subtract(lastCursorPosition);
|
|
453
|
-
const dot = v1.dot(v2);
|
|
454
|
-
const det = v1.x * v2.y - v1.y * v2.x;
|
|
455
|
-
object.angle = Math.abs(Math.atan2(det, dot));
|
|
456
|
-
}
|
|
457
|
-
difficultyObjects.push(object);
|
|
387
|
+
.subtract(this.object.getStackedPosition(this.mode))
|
|
388
|
+
.length * scalingFactor;
|
|
389
|
+
this.minimumJumpDistance = Math.max(0, Math.min(this.lazyJumpDistance -
|
|
390
|
+
(this.maximumSliderRadius - this.assumedSliderRadius), tailJumpDistance - this.maximumSliderRadius));
|
|
391
|
+
}
|
|
392
|
+
if (this.lastLastObject && !(this.lastLastObject instanceof osuBase.Spinner)) {
|
|
393
|
+
const lastLastCursorPosition = this.getEndCursorPosition(this.lastLastObject);
|
|
394
|
+
const v1 = lastLastCursorPosition.subtract(this.lastObject.getStackedPosition(this.mode));
|
|
395
|
+
const v2 = this.object
|
|
396
|
+
.getStackedPosition(this.mode)
|
|
397
|
+
.subtract(lastCursorPosition);
|
|
398
|
+
const dot = v1.dot(v2);
|
|
399
|
+
const det = v1.x * v2.y - v1.y * v2.x;
|
|
400
|
+
this.angle = Math.abs(Math.atan2(det, dot));
|
|
458
401
|
}
|
|
459
|
-
return difficultyObjects;
|
|
460
402
|
}
|
|
461
|
-
/**
|
|
462
|
-
* Calculates a slider's cursor position.
|
|
463
|
-
*/
|
|
464
403
|
calculateSliderCursorPosition(slider) {
|
|
465
404
|
if (slider.lazyEndPosition) {
|
|
466
405
|
return;
|
|
467
406
|
}
|
|
468
|
-
// Droid doesn't have a legacy slider tail. Since beatmap parser defaults slider tail
|
|
469
|
-
// to legacy slider tail, it needs to be changed to real slider tail first.
|
|
470
407
|
if (this.mode === osuBase.Modes.droid) {
|
|
471
|
-
slider.tail.startTime += osuBase.Slider.legacyLastTickOffset;
|
|
472
|
-
slider.tail.endTime += osuBase.Slider.legacyLastTickOffset;
|
|
473
|
-
slider.nestedHitObjects.sort((a, b) => a.startTime - b.startTime);
|
|
474
408
|
// Temporary lazy end position until a real result can be derived.
|
|
475
409
|
slider.lazyEndPosition = slider.getStackedPosition(this.mode);
|
|
476
|
-
// Stop here if the slider has too short duration
|
|
477
|
-
//
|
|
478
|
-
// end position heavily, which we do not want to happen.
|
|
479
|
-
//
|
|
480
|
-
// In the real game, this shouldn't happen. Perhaps we need to reinvestigate this
|
|
481
|
-
// in the future.
|
|
410
|
+
// Stop here if the slider has too short duration, allowing the player to essentially
|
|
411
|
+
// complete the slider without movement, making travel distance and time irrelevant.
|
|
482
412
|
if (osuBase.Precision.almostEqualsNumber(slider.startTime, slider.endTime)) {
|
|
483
413
|
return;
|
|
484
414
|
}
|
|
@@ -498,7 +428,7 @@ class DifficultyHitObjectCreator {
|
|
|
498
428
|
.getStackedPosition(this.mode)
|
|
499
429
|
.add(slider.path.positionAt(endTimeMin));
|
|
500
430
|
let currentCursorPosition = slider.getStackedPosition(this.mode);
|
|
501
|
-
const scalingFactor = this.normalizedRadius / slider.
|
|
431
|
+
const scalingFactor = this.normalizedRadius / slider.radius;
|
|
502
432
|
for (let i = 1; i < slider.nestedHitObjects.length; ++i) {
|
|
503
433
|
const currentMovementObject = slider.nestedHitObjects[i];
|
|
504
434
|
let currentMovement = currentMovementObject
|
|
@@ -537,203 +467,25 @@ class DifficultyHitObjectCreator {
|
|
|
537
467
|
}
|
|
538
468
|
}
|
|
539
469
|
}
|
|
540
|
-
/**
|
|
541
|
-
* Gets the scaling factor of a radius.
|
|
542
|
-
*
|
|
543
|
-
* @param radius The radius to get the scaling factor from.
|
|
544
|
-
*/
|
|
545
|
-
getScalingFactor(radius) {
|
|
546
|
-
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
|
547
|
-
let scalingFactor = this.normalizedRadius / radius;
|
|
548
|
-
// High circle size (small CS) bonus
|
|
549
|
-
switch (this.mode) {
|
|
550
|
-
case osuBase.Modes.droid:
|
|
551
|
-
if (radius < this.DROID_CIRCLESIZE_BUFF_THRESHOLD) {
|
|
552
|
-
scalingFactor *=
|
|
553
|
-
1 +
|
|
554
|
-
Math.pow((this.DROID_CIRCLESIZE_BUFF_THRESHOLD - radius) /
|
|
555
|
-
50, 2);
|
|
556
|
-
}
|
|
557
|
-
break;
|
|
558
|
-
case osuBase.Modes.osu:
|
|
559
|
-
if (radius < this.PC_CIRCLESIZE_BUFF_THRESHOLD) {
|
|
560
|
-
scalingFactor *=
|
|
561
|
-
1 +
|
|
562
|
-
Math.min(this.PC_CIRCLESIZE_BUFF_THRESHOLD - radius, 5) /
|
|
563
|
-
50;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
return scalingFactor;
|
|
567
|
-
}
|
|
568
|
-
/**
|
|
569
|
-
* Returns the end cursor position of a hitobject.
|
|
570
|
-
*/
|
|
571
470
|
getEndCursorPosition(object) {
|
|
471
|
+
var _a;
|
|
572
472
|
let pos = object.getStackedPosition(this.mode);
|
|
573
473
|
if (object instanceof osuBase.Slider) {
|
|
574
474
|
this.calculateSliderCursorPosition(object);
|
|
575
|
-
pos = object.lazyEndPosition
|
|
475
|
+
pos = (_a = object.lazyEndPosition) !== null && _a !== void 0 ? _a : pos;
|
|
576
476
|
}
|
|
577
477
|
return pos;
|
|
578
478
|
}
|
|
579
|
-
applyToOverlappingFactor(object, distance, deltaTime) {
|
|
580
|
-
// Penalize objects that are too close to the object in both distance
|
|
581
|
-
// and delta time to prevent stream maps from being overweighted.
|
|
582
|
-
object.overlappingFactor +=
|
|
583
|
-
Math.max(0, 1 - distance / (3 * object.object.getRadius(this.mode))) *
|
|
584
|
-
(7.5 /
|
|
585
|
-
(1 +
|
|
586
|
-
Math.exp(0.15 * (Math.max(deltaTime, this.minDeltaTime) - 75))));
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* The base of difficulty calculators.
|
|
592
|
-
*/
|
|
593
|
-
class DifficultyCalculator {
|
|
594
|
-
/**
|
|
595
|
-
* The calculated beatmap.
|
|
596
|
-
*/
|
|
597
|
-
beatmap;
|
|
598
|
-
/**
|
|
599
|
-
* The difficulty objects of the beatmap.
|
|
600
|
-
*/
|
|
601
|
-
objects = [];
|
|
602
|
-
/**
|
|
603
|
-
* The modifications applied.
|
|
604
|
-
*/
|
|
605
|
-
mods = [];
|
|
606
|
-
/**
|
|
607
|
-
* The total star rating of the beatmap.
|
|
608
|
-
*/
|
|
609
|
-
total = 0;
|
|
610
|
-
/**
|
|
611
|
-
* The map statistics of the beatmap after modifications are applied.
|
|
612
|
-
*/
|
|
613
|
-
stats = new osuBase.MapStats();
|
|
614
|
-
/**
|
|
615
|
-
* The strain peaks of various calculated difficulties.
|
|
616
|
-
*/
|
|
617
|
-
strainPeaks = {
|
|
618
|
-
aimWithSliders: [],
|
|
619
|
-
aimWithoutSliders: [],
|
|
620
|
-
speed: [],
|
|
621
|
-
flashlight: [],
|
|
622
|
-
};
|
|
623
|
-
sectionLength = 400;
|
|
624
|
-
/**
|
|
625
|
-
* Constructs a new instance of the calculator.
|
|
626
|
-
*
|
|
627
|
-
* @param beatmap The beatmap to calculate. This beatmap will be deep-cloned to prevent reference changes.
|
|
628
|
-
*/
|
|
629
|
-
constructor(beatmap) {
|
|
630
|
-
this.beatmap = osuBase.Utils.deepCopy(beatmap);
|
|
631
|
-
}
|
|
632
|
-
/**
|
|
633
|
-
* Calculates the star rating of the specified beatmap.
|
|
634
|
-
*
|
|
635
|
-
* The beatmap is analyzed in chunks of `sectionLength` duration.
|
|
636
|
-
* For each chunk the highest hitobject strains are added to
|
|
637
|
-
* a list which is then collapsed into a weighted sum, much
|
|
638
|
-
* like scores are weighted on a user's profile.
|
|
639
|
-
*
|
|
640
|
-
* For subsequent chunks, the initial max strain is calculated
|
|
641
|
-
* by decaying the previous hitobject's strain until the
|
|
642
|
-
* beginning of the new chunk.
|
|
643
|
-
*
|
|
644
|
-
* @param options Options for the difficulty calculation.
|
|
645
|
-
* @returns The current instance.
|
|
646
|
-
*/
|
|
647
|
-
calculate(options) {
|
|
648
|
-
this.mods = options?.mods ?? [];
|
|
649
|
-
this.stats = new osuBase.MapStats({
|
|
650
|
-
cs: this.beatmap.difficulty.cs,
|
|
651
|
-
ar: this.beatmap.difficulty.ar,
|
|
652
|
-
od: this.beatmap.difficulty.od,
|
|
653
|
-
hp: this.beatmap.difficulty.hp,
|
|
654
|
-
mods: options?.mods,
|
|
655
|
-
speedMultiplier: options?.stats?.speedMultiplier,
|
|
656
|
-
oldStatistics: options?.stats?.oldStatistics,
|
|
657
|
-
}).calculate({ mode: this.mode });
|
|
658
|
-
this.populateDifficultyAttributes();
|
|
659
|
-
this.generateDifficultyHitObjects();
|
|
660
|
-
this.calculateAll();
|
|
661
|
-
return this;
|
|
662
|
-
}
|
|
663
|
-
/**
|
|
664
|
-
* Generates difficulty hitobjects for this calculator.
|
|
665
|
-
*/
|
|
666
|
-
generateDifficultyHitObjects() {
|
|
667
|
-
this.objects.length = 0;
|
|
668
|
-
this.objects.push(...new DifficultyHitObjectCreator().generateDifficultyObjects({
|
|
669
|
-
objects: this.beatmap.hitObjects.objects,
|
|
670
|
-
circleSize: this.beatmap.difficulty.cs,
|
|
671
|
-
mods: this.mods,
|
|
672
|
-
speedMultiplier: this.stats.speedMultiplier,
|
|
673
|
-
mode: this.mode,
|
|
674
|
-
preempt: osuBase.MapStats.arToMS(this.stats.ar),
|
|
675
|
-
}));
|
|
676
|
-
}
|
|
677
|
-
/**
|
|
678
|
-
* Calculates the skills provided.
|
|
679
|
-
*
|
|
680
|
-
* @param skills The skills to calculate.
|
|
681
|
-
*/
|
|
682
|
-
calculateSkills(...skills) {
|
|
683
|
-
// The first object doesn't generate a strain, so we begin calculating from the second object.
|
|
684
|
-
this.objects.slice(1).forEach((h, i) => {
|
|
685
|
-
skills.forEach((skill) => {
|
|
686
|
-
skill.process(h);
|
|
687
|
-
if (i === this.objects.length - 2) {
|
|
688
|
-
// Don't forget to save the last strain peak, which would otherwise be ignored.
|
|
689
|
-
skill.saveCurrentPeak();
|
|
690
|
-
}
|
|
691
|
-
});
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
/**
|
|
695
|
-
* Populates the stored difficulty attributes with necessary data.
|
|
696
|
-
*/
|
|
697
|
-
populateDifficultyAttributes() {
|
|
698
|
-
this.attributes.approachRate = this.stats.ar;
|
|
699
|
-
this.attributes.hitCircleCount = this.beatmap.hitObjects.circles;
|
|
700
|
-
this.attributes.maxCombo = this.beatmap.maxCombo;
|
|
701
|
-
this.attributes.mods = this.mods.slice();
|
|
702
|
-
this.attributes.overallDifficulty = this.stats.od;
|
|
703
|
-
this.attributes.sliderCount = this.beatmap.hitObjects.sliders;
|
|
704
|
-
this.attributes.spinnerCount = this.beatmap.hitObjects.spinners;
|
|
705
|
-
}
|
|
706
|
-
/**
|
|
707
|
-
* Calculates the star rating value of a difficulty.
|
|
708
|
-
*
|
|
709
|
-
* @param difficulty The difficulty to calculate.
|
|
710
|
-
*/
|
|
711
|
-
starValue(difficulty) {
|
|
712
|
-
return Math.sqrt(difficulty) * this.difficultyMultiplier;
|
|
713
|
-
}
|
|
714
|
-
/**
|
|
715
|
-
* Calculates the base performance value of a difficulty rating.
|
|
716
|
-
*
|
|
717
|
-
* @param rating The difficulty rating.
|
|
718
|
-
*/
|
|
719
|
-
basePerformanceValue(rating) {
|
|
720
|
-
return Math.pow(5 * Math.max(1, rating / 0.0675) - 4, 3) / 100000;
|
|
721
|
-
}
|
|
722
479
|
}
|
|
480
|
+
/**
|
|
481
|
+
* The lowest possible delta time value.
|
|
482
|
+
*/
|
|
483
|
+
DifficultyHitObject.minDeltaTime = 25;
|
|
723
484
|
|
|
724
485
|
/**
|
|
725
486
|
* An evaluator for calculating osu!droid Aim skill.
|
|
726
487
|
*/
|
|
727
488
|
class DroidAimEvaluator extends AimEvaluator {
|
|
728
|
-
static wideAngleMultiplier = 1.65;
|
|
729
|
-
static sliderMultiplier = 1.5;
|
|
730
|
-
static velocityChangeMultiplier = 0.85;
|
|
731
|
-
/**
|
|
732
|
-
* Spacing threshold for a single hitobject spacing.
|
|
733
|
-
*/
|
|
734
|
-
static SINGLE_SPACING_THRESHOLD = 175;
|
|
735
|
-
// ~200 1/2 BPM jumps
|
|
736
|
-
static minSpeedBonus = 150;
|
|
737
489
|
/**
|
|
738
490
|
* Evaluates the difficulty of aiming the current object, based on:
|
|
739
491
|
*
|
|
@@ -751,15 +503,16 @@ class DroidAimEvaluator extends AimEvaluator {
|
|
|
751
503
|
current.isOverlapping(true)) {
|
|
752
504
|
return 0;
|
|
753
505
|
}
|
|
754
|
-
return (this.
|
|
755
|
-
this.
|
|
506
|
+
return (this.snapAimStrainOf(current, withSliders) +
|
|
507
|
+
this.flowAimStrainOf(current));
|
|
756
508
|
}
|
|
757
509
|
/**
|
|
758
|
-
* Calculates the aim strain of a hitobject.
|
|
510
|
+
* Calculates the snap aim strain of a hitobject.
|
|
759
511
|
*/
|
|
760
|
-
static
|
|
512
|
+
static snapAimStrainOf(current, withSliders) {
|
|
513
|
+
var _a;
|
|
761
514
|
if (current.index <= 1 ||
|
|
762
|
-
current.previous(0)
|
|
515
|
+
((_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.object) instanceof osuBase.Spinner) {
|
|
763
516
|
return 0;
|
|
764
517
|
}
|
|
765
518
|
const last = current.previous(0);
|
|
@@ -861,28 +614,33 @@ class DroidAimEvaluator extends AimEvaluator {
|
|
|
861
614
|
velocityChangeBonus * this.velocityChangeMultiplier);
|
|
862
615
|
// Add in additional slider velocity bonus.
|
|
863
616
|
if (withSliders) {
|
|
864
|
-
strain +=
|
|
617
|
+
strain +=
|
|
618
|
+
Math.pow(1 + sliderBonus * this.sliderMultiplier, 1.25) - 1;
|
|
865
619
|
}
|
|
866
620
|
return strain;
|
|
867
621
|
}
|
|
868
622
|
/**
|
|
869
|
-
* Calculates the
|
|
623
|
+
* Calculates the flow aim strain of a hitobject.
|
|
870
624
|
*/
|
|
871
|
-
static
|
|
625
|
+
static flowAimStrainOf(current) {
|
|
626
|
+
var _a, _b;
|
|
872
627
|
let speedBonus = 1;
|
|
873
628
|
if (current.strainTime < this.minSpeedBonus) {
|
|
874
629
|
speedBonus +=
|
|
875
630
|
0.75 *
|
|
876
|
-
Math.pow((this.minSpeedBonus - current.strainTime) /
|
|
631
|
+
Math.pow((this.minSpeedBonus - current.strainTime) / 40, 2);
|
|
877
632
|
}
|
|
878
|
-
const travelDistance = current.previous(0)
|
|
879
|
-
const
|
|
880
|
-
return (
|
|
881
|
-
speedBonus *
|
|
882
|
-
Math.pow(distance / this.SINGLE_SPACING_THRESHOLD, 5)) /
|
|
883
|
-
current.strainTime);
|
|
633
|
+
const travelDistance = (_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.travelDistance) !== null && _b !== void 0 ? _b : 0;
|
|
634
|
+
const shortDistancePenalty = Math.pow(Math.min(this.singleSpacingThreshold, travelDistance + current.minimumJumpDistance) / this.singleSpacingThreshold, 3.5);
|
|
635
|
+
return (200 * speedBonus * shortDistancePenalty) / current.strainTime;
|
|
884
636
|
}
|
|
885
637
|
}
|
|
638
|
+
DroidAimEvaluator.wideAngleMultiplier = 1.65;
|
|
639
|
+
DroidAimEvaluator.sliderMultiplier = 1.5;
|
|
640
|
+
DroidAimEvaluator.velocityChangeMultiplier = 0.85;
|
|
641
|
+
DroidAimEvaluator.singleSpacingThreshold = 100;
|
|
642
|
+
// 200 1/4 BPM delta time
|
|
643
|
+
DroidAimEvaluator.minSpeedBonus = 75;
|
|
886
644
|
|
|
887
645
|
/**
|
|
888
646
|
* A bare minimal abstract skill for fully custom skill implementations.
|
|
@@ -890,10 +648,6 @@ class DroidAimEvaluator extends AimEvaluator {
|
|
|
890
648
|
* This class should be considered a "processing" class and not persisted.
|
|
891
649
|
*/
|
|
892
650
|
class Skill {
|
|
893
|
-
/**
|
|
894
|
-
* The mods that this skill processes.
|
|
895
|
-
*/
|
|
896
|
-
mods;
|
|
897
651
|
constructor(mods) {
|
|
898
652
|
this.mods = mods;
|
|
899
653
|
}
|
|
@@ -904,33 +658,21 @@ class Skill {
|
|
|
904
658
|
* and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
|
|
905
659
|
*/
|
|
906
660
|
class StrainSkill extends Skill {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
strainPeaks = [];
|
|
919
|
-
sectionLength = 400;
|
|
920
|
-
currentSectionEnd = 0;
|
|
921
|
-
isFirstObject = true;
|
|
922
|
-
/**
|
|
923
|
-
* Calculates the strain value of a hitobject and stores the value in it. This value is affected by previously processed objects.
|
|
924
|
-
*
|
|
925
|
-
* @param current The hitobject to process.
|
|
926
|
-
*/
|
|
661
|
+
constructor() {
|
|
662
|
+
super(...arguments);
|
|
663
|
+
/**
|
|
664
|
+
* Strain peaks are stored here.
|
|
665
|
+
*/
|
|
666
|
+
this.strainPeaks = [];
|
|
667
|
+
this.sectionLength = 400;
|
|
668
|
+
this.currentStrain = 0;
|
|
669
|
+
this.currentSectionPeak = 0;
|
|
670
|
+
this.currentSectionEnd = 0;
|
|
671
|
+
}
|
|
927
672
|
process(current) {
|
|
928
673
|
// The first object doesn't generate a strain, so we begin with an incremented section end
|
|
929
|
-
if (
|
|
930
|
-
this.currentSectionEnd =
|
|
931
|
-
Math.ceil(current.startTime / this.sectionLength) *
|
|
932
|
-
this.sectionLength;
|
|
933
|
-
this.isFirstObject = false;
|
|
674
|
+
if (current.index === 0) {
|
|
675
|
+
this.currentSectionEnd = this.calculateCurrentSectionStart(current);
|
|
934
676
|
}
|
|
935
677
|
while (current.startTime > this.currentSectionEnd) {
|
|
936
678
|
this.saveCurrentPeak();
|
|
@@ -941,6 +683,10 @@ class StrainSkill extends Skill {
|
|
|
941
683
|
this.currentStrain = this.strainValueAt(current);
|
|
942
684
|
this.saveToHitObject(current);
|
|
943
685
|
this.currentSectionPeak = Math.max(this.currentStrain, this.currentSectionPeak);
|
|
686
|
+
if (!current.next(0)) {
|
|
687
|
+
// Don't forget to save the last strain peak, which would otherwise be ignored.
|
|
688
|
+
this.saveCurrentPeak();
|
|
689
|
+
}
|
|
944
690
|
}
|
|
945
691
|
/**
|
|
946
692
|
* Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty.
|
|
@@ -956,18 +702,26 @@ class StrainSkill extends Skill {
|
|
|
956
702
|
strainDecay(ms) {
|
|
957
703
|
return Math.pow(this.strainDecayBase, ms / 1000);
|
|
958
704
|
}
|
|
705
|
+
/**
|
|
706
|
+
* Calculates the starting time of a strain section at an object.
|
|
707
|
+
*
|
|
708
|
+
* @param current The object at which the strain section starts.
|
|
709
|
+
* @returns The start time of the strain section.
|
|
710
|
+
*/
|
|
711
|
+
calculateCurrentSectionStart(current) {
|
|
712
|
+
return (Math.ceil(current.startTime / this.sectionLength) *
|
|
713
|
+
this.sectionLength);
|
|
714
|
+
}
|
|
959
715
|
/**
|
|
960
716
|
* Sets the initial strain level for a new section.
|
|
961
717
|
*
|
|
962
|
-
* @param
|
|
718
|
+
* @param time The beginning of the new section in milliseconds.
|
|
963
719
|
* @param current The current hitobject.
|
|
964
720
|
*/
|
|
965
|
-
startNewSectionFrom(
|
|
966
|
-
// The maximum strain of the new section is not zero by default
|
|
721
|
+
startNewSectionFrom(time, current) {
|
|
722
|
+
// The maximum strain of the new section is not zero by default.
|
|
967
723
|
// This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level.
|
|
968
|
-
this.currentSectionPeak =
|
|
969
|
-
this.currentStrain *
|
|
970
|
-
this.strainDecay(offset - current.previous(0).startTime);
|
|
724
|
+
this.currentSectionPeak = this.calculateInitialStrain(time, current);
|
|
971
725
|
}
|
|
972
726
|
}
|
|
973
727
|
|
|
@@ -976,6 +730,36 @@ class StrainSkill extends Skill {
|
|
|
976
730
|
* and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
|
|
977
731
|
*/
|
|
978
732
|
class DroidSkill extends StrainSkill {
|
|
733
|
+
constructor() {
|
|
734
|
+
super(...arguments);
|
|
735
|
+
this._objectStrains = [];
|
|
736
|
+
this.difficulty = 0;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* The strains of hitobjects.
|
|
740
|
+
*/
|
|
741
|
+
get objectStrains() {
|
|
742
|
+
return this._objectStrains;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Returns the number of strains weighed against the top strain.
|
|
746
|
+
*
|
|
747
|
+
* The result is scaled by clock rate as it affects the total number of strains.
|
|
748
|
+
*/
|
|
749
|
+
countDifficultStrains() {
|
|
750
|
+
if (this.difficulty === 0) {
|
|
751
|
+
return 0;
|
|
752
|
+
}
|
|
753
|
+
// This is what the top strain is if all strain values were identical.
|
|
754
|
+
const consistentTopStrain = this.difficulty / 10;
|
|
755
|
+
// Use a weighted sum of all strains.
|
|
756
|
+
return this._objectStrains.reduce((total, next) => total +
|
|
757
|
+
1.1 / (1 + Math.exp(-10 * (next / consistentTopStrain - 0.88))), 0);
|
|
758
|
+
}
|
|
759
|
+
process(current) {
|
|
760
|
+
super.process(current);
|
|
761
|
+
this._objectStrains.push(this.getObjectStrain(current));
|
|
762
|
+
}
|
|
979
763
|
difficultyValue() {
|
|
980
764
|
const strains = this.strainPeaks.slice();
|
|
981
765
|
if (this.reducedSectionCount > 0) {
|
|
@@ -988,12 +772,15 @@ class DroidSkill extends StrainSkill {
|
|
|
988
772
|
}
|
|
989
773
|
// Math here preserves the property that two notes of equal difficulty x, we have their summed difficulty = x * starsPerDouble.
|
|
990
774
|
// This also applies to two sets of notes with equal difficulty.
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
775
|
+
this.difficulty = 0;
|
|
776
|
+
for (const strain of strains) {
|
|
777
|
+
this.difficulty += Math.pow(strain, 1 / Math.log2(this.starsPerDouble));
|
|
778
|
+
}
|
|
779
|
+
this.difficulty = Math.pow(this.difficulty, Math.log2(this.starsPerDouble));
|
|
780
|
+
return this.difficulty;
|
|
781
|
+
}
|
|
782
|
+
calculateCurrentSectionStart(current) {
|
|
783
|
+
return current.startTime;
|
|
997
784
|
}
|
|
998
785
|
}
|
|
999
786
|
|
|
@@ -1001,35 +788,40 @@ class DroidSkill extends StrainSkill {
|
|
|
1001
788
|
* Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
|
1002
789
|
*/
|
|
1003
790
|
class DroidAim extends DroidSkill {
|
|
1004
|
-
skillMultiplier = 24.55;
|
|
1005
|
-
strainDecayBase = 0.15;
|
|
1006
|
-
reducedSectionCount = 10;
|
|
1007
|
-
reducedSectionBaseline = 0.75;
|
|
1008
|
-
starsPerDouble = 1.05;
|
|
1009
|
-
withSliders;
|
|
1010
791
|
constructor(mods, withSliders) {
|
|
1011
792
|
super(mods);
|
|
793
|
+
this.strainDecayBase = 0.15;
|
|
794
|
+
this.reducedSectionCount = 10;
|
|
795
|
+
this.reducedSectionBaseline = 0.75;
|
|
796
|
+
this.starsPerDouble = 1.05;
|
|
797
|
+
this.skillMultiplier = 24.55;
|
|
798
|
+
this.currentAimStrain = 0;
|
|
1012
799
|
this.withSliders = withSliders;
|
|
1013
800
|
}
|
|
1014
|
-
/**
|
|
1015
|
-
* @param current The hitobject to calculate.
|
|
1016
|
-
*/
|
|
1017
801
|
strainValueAt(current) {
|
|
1018
|
-
this.
|
|
1019
|
-
this.
|
|
802
|
+
this.currentAimStrain *= this.strainDecay(current.deltaTime);
|
|
803
|
+
this.currentAimStrain +=
|
|
1020
804
|
DroidAimEvaluator.evaluateDifficultyOf(current, this.withSliders) *
|
|
1021
805
|
this.skillMultiplier;
|
|
1022
|
-
return this.
|
|
806
|
+
return this.currentAimStrain;
|
|
807
|
+
}
|
|
808
|
+
calculateInitialStrain(time, current) {
|
|
809
|
+
var _a, _b;
|
|
810
|
+
return (this.currentAimStrain *
|
|
811
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
812
|
+
}
|
|
813
|
+
getObjectStrain() {
|
|
814
|
+
return this.currentAimStrain;
|
|
1023
815
|
}
|
|
1024
816
|
/**
|
|
1025
817
|
* @param current The hitobject to save to.
|
|
1026
818
|
*/
|
|
1027
819
|
saveToHitObject(current) {
|
|
1028
820
|
if (this.withSliders) {
|
|
1029
|
-
current.aimStrainWithSliders = this.
|
|
821
|
+
current.aimStrainWithSliders = this.currentAimStrain;
|
|
1030
822
|
}
|
|
1031
823
|
else {
|
|
1032
|
-
current.aimStrainWithoutSliders = this.
|
|
824
|
+
current.aimStrainWithoutSliders = this.currentAimStrain;
|
|
1033
825
|
}
|
|
1034
826
|
}
|
|
1035
827
|
}
|
|
@@ -1040,9 +832,9 @@ class DroidAim extends DroidSkill {
|
|
|
1040
832
|
* This class should be considered an "evaluating" class and not persisted.
|
|
1041
833
|
*/
|
|
1042
834
|
class SpeedEvaluator {
|
|
1043
|
-
// ~200 1/4 BPM streams
|
|
1044
|
-
static minSpeedBonus = 75;
|
|
1045
835
|
}
|
|
836
|
+
// ~200 1/4 BPM streams
|
|
837
|
+
SpeedEvaluator.minSpeedBonus = 75;
|
|
1046
838
|
|
|
1047
839
|
/**
|
|
1048
840
|
* An evaluator for calculating osu!droid tap skill.
|
|
@@ -1053,43 +845,35 @@ class DroidTapEvaluator extends SpeedEvaluator {
|
|
|
1053
845
|
*
|
|
1054
846
|
* - time between pressing the previous and current object,
|
|
1055
847
|
* - distance between those objects,
|
|
1056
|
-
* -
|
|
848
|
+
* - how easily they can be cheesed,
|
|
849
|
+
* - and the strain time cap.
|
|
1057
850
|
*
|
|
1058
851
|
* @param current The current object.
|
|
1059
852
|
* @param greatWindow The great hit window of the current object.
|
|
1060
853
|
* @param considerCheesability Whether to consider cheesability.
|
|
854
|
+
* @param strainTimeCap The strain time to cap the object's strain time to.
|
|
1061
855
|
*/
|
|
1062
|
-
static evaluateDifficultyOf(current,
|
|
856
|
+
static evaluateDifficultyOf(current, considerCheesability, strainTimeCap) {
|
|
1063
857
|
if (current.object instanceof osuBase.Spinner ||
|
|
1064
858
|
// Exclude overlapping objects that can be tapped at once.
|
|
1065
859
|
current.isOverlapping(false)) {
|
|
1066
860
|
return 0;
|
|
1067
861
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
const nextDeltaTime = Math.max(1, next.deltaTime);
|
|
1077
|
-
const deltaDifference = Math.abs(nextDeltaTime - currentDeltaTime);
|
|
1078
|
-
const speedRatio = currentDeltaTime /
|
|
1079
|
-
Math.max(currentDeltaTime, deltaDifference);
|
|
1080
|
-
const windowRatio = Math.pow(Math.min(1, currentDeltaTime / greatWindowFull), 2);
|
|
1081
|
-
doubletapness = Math.pow(speedRatio, 1 - windowRatio);
|
|
1082
|
-
}
|
|
1083
|
-
// Cap deltatime to the OD 300 hitwindow.
|
|
1084
|
-
// 0.58 is derived from making sure 260 BPM 1/4 OD5 streams aren't nerfed harshly, whilst 0.91 limits the effect of the cap.
|
|
1085
|
-
strainTime /= osuBase.MathUtils.clamp(strainTime / greatWindowFull / 0.58, 0.91, 1);
|
|
1086
|
-
}
|
|
862
|
+
// Nerf doubletappable doubles.
|
|
863
|
+
const doubletapness = considerCheesability
|
|
864
|
+
? 1 - current.doubletapness
|
|
865
|
+
: 1;
|
|
866
|
+
const strainTime = strainTimeCap !== undefined
|
|
867
|
+
? // We cap the strain time to 50 here as the chance of vibro is higher in any BPM higher than 300.
|
|
868
|
+
Math.max(50, strainTimeCap, current.strainTime)
|
|
869
|
+
: current.strainTime;
|
|
1087
870
|
let speedBonus = 1;
|
|
1088
871
|
if (strainTime < this.minSpeedBonus) {
|
|
1089
872
|
speedBonus +=
|
|
1090
|
-
0.75 *
|
|
873
|
+
0.75 *
|
|
874
|
+
Math.pow(osuBase.ErrorFunction.erf((this.minSpeedBonus - strainTime) / 40), 2);
|
|
1091
875
|
}
|
|
1092
|
-
return (speedBonus * doubletapness) / strainTime;
|
|
876
|
+
return (speedBonus * Math.pow(doubletapness, 1.5)) / strainTime;
|
|
1093
877
|
}
|
|
1094
878
|
}
|
|
1095
879
|
|
|
@@ -1097,38 +881,88 @@ class DroidTapEvaluator extends SpeedEvaluator {
|
|
|
1097
881
|
* Represents the skill required to press keys or tap with regards to keeping up with the speed at which objects need to be hit.
|
|
1098
882
|
*/
|
|
1099
883
|
class DroidTap extends DroidSkill {
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
greatWindow;
|
|
1108
|
-
constructor(mods, overallDifficulty) {
|
|
884
|
+
/**
|
|
885
|
+
* The delta time of hitobjects.
|
|
886
|
+
*/
|
|
887
|
+
get objectDeltaTimes() {
|
|
888
|
+
return this._objectDeltaTimes;
|
|
889
|
+
}
|
|
890
|
+
constructor(mods, considerCheesability, strainTimeCap) {
|
|
1109
891
|
super(mods);
|
|
1110
|
-
this.
|
|
892
|
+
this.reducedSectionCount = 10;
|
|
893
|
+
this.reducedSectionBaseline = 0.75;
|
|
894
|
+
this.strainDecayBase = 0.3;
|
|
895
|
+
this.starsPerDouble = 1.1;
|
|
896
|
+
this.currentTapStrain = 0;
|
|
897
|
+
this.currentRhythmMultiplier = 0;
|
|
898
|
+
this.skillMultiplier = 1375;
|
|
899
|
+
this._objectDeltaTimes = [];
|
|
900
|
+
this.considerCheesability = considerCheesability;
|
|
901
|
+
this.strainTimeCap = strainTimeCap;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* The amount of notes that are relevant to the difficulty.
|
|
905
|
+
*/
|
|
906
|
+
relevantNoteCount() {
|
|
907
|
+
if (this._objectStrains.length === 0) {
|
|
908
|
+
return 0;
|
|
909
|
+
}
|
|
910
|
+
const maxStrain = Math.max(...this._objectStrains);
|
|
911
|
+
if (maxStrain === 0) {
|
|
912
|
+
return 0;
|
|
913
|
+
}
|
|
914
|
+
return this._objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
|
|
1111
915
|
}
|
|
1112
916
|
/**
|
|
1113
|
-
*
|
|
917
|
+
* The delta time relevant to the difficulty.
|
|
1114
918
|
*/
|
|
919
|
+
relevantDeltaTime() {
|
|
920
|
+
if (this._objectStrains.length === 0) {
|
|
921
|
+
return 0;
|
|
922
|
+
}
|
|
923
|
+
const maxStrain = Math.max(...this._objectStrains);
|
|
924
|
+
if (maxStrain === 0) {
|
|
925
|
+
return 0;
|
|
926
|
+
}
|
|
927
|
+
return (this._objectDeltaTimes.reduce((total, next, index) => total +
|
|
928
|
+
(next * 1) /
|
|
929
|
+
(1 +
|
|
930
|
+
Math.exp(-((this._objectStrains[index] / maxStrain) *
|
|
931
|
+
25 -
|
|
932
|
+
20))), 0) /
|
|
933
|
+
this._objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 25 - 20))), 0));
|
|
934
|
+
}
|
|
1115
935
|
strainValueAt(current) {
|
|
1116
|
-
|
|
1117
|
-
this.currentTapStrain *= decay;
|
|
936
|
+
this.currentTapStrain *= this.strainDecay(current.strainTime);
|
|
1118
937
|
this.currentTapStrain +=
|
|
1119
|
-
DroidTapEvaluator.evaluateDifficultyOf(current, this.
|
|
1120
|
-
this.
|
|
1121
|
-
this.
|
|
1122
|
-
DroidTapEvaluator.evaluateDifficultyOf(current, this.greatWindow, false) * this.skillMultiplier;
|
|
1123
|
-
this.currentOriginalTapStrain *= current.rhythmMultiplier;
|
|
938
|
+
DroidTapEvaluator.evaluateDifficultyOf(current, this.considerCheesability, this.strainTimeCap) * this.skillMultiplier;
|
|
939
|
+
this.currentRhythmMultiplier = current.rhythmMultiplier;
|
|
940
|
+
this._objectDeltaTimes.push(current.deltaTime);
|
|
1124
941
|
return this.currentTapStrain * current.rhythmMultiplier;
|
|
1125
942
|
}
|
|
943
|
+
calculateInitialStrain(time, current) {
|
|
944
|
+
var _a, _b;
|
|
945
|
+
return (this.currentTapStrain *
|
|
946
|
+
this.currentRhythmMultiplier *
|
|
947
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
948
|
+
}
|
|
949
|
+
getObjectStrain() {
|
|
950
|
+
return this.currentTapStrain * this.currentRhythmMultiplier;
|
|
951
|
+
}
|
|
1126
952
|
/**
|
|
1127
953
|
* @param current The hitobject to save to.
|
|
1128
954
|
*/
|
|
1129
955
|
saveToHitObject(current) {
|
|
1130
|
-
|
|
1131
|
-
|
|
956
|
+
if (this.strainTimeCap !== undefined) {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
const strain = this.currentTapStrain * this.currentRhythmMultiplier;
|
|
960
|
+
if (this.considerCheesability) {
|
|
961
|
+
current.tapStrain = strain;
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
current.originalTapStrain = strain;
|
|
965
|
+
}
|
|
1132
966
|
}
|
|
1133
967
|
}
|
|
1134
968
|
|
|
@@ -1138,12 +972,12 @@ class DroidTap extends DroidSkill {
|
|
|
1138
972
|
* This class should be considered an "evaluating" class and not persisted.
|
|
1139
973
|
*/
|
|
1140
974
|
class FlashlightEvaluator {
|
|
1141
|
-
static maxOpacityBonus = 0.4;
|
|
1142
|
-
static hiddenBonus = 0.2;
|
|
1143
|
-
static minVelocity = 0.5;
|
|
1144
|
-
static sliderMultiplier = 1.3;
|
|
1145
|
-
static minAngleMultiplier = 0.2;
|
|
1146
975
|
}
|
|
976
|
+
FlashlightEvaluator.maxOpacityBonus = 0.4;
|
|
977
|
+
FlashlightEvaluator.hiddenBonus = 0.2;
|
|
978
|
+
FlashlightEvaluator.minVelocity = 0.5;
|
|
979
|
+
FlashlightEvaluator.sliderMultiplier = 1.3;
|
|
980
|
+
FlashlightEvaluator.minAngleMultiplier = 0.2;
|
|
1147
981
|
|
|
1148
982
|
/**
|
|
1149
983
|
* An evaluator for calculating osu!droid Flashlight skill.
|
|
@@ -1168,7 +1002,7 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
1168
1002
|
current.isOverlapping(true)) {
|
|
1169
1003
|
return 0;
|
|
1170
1004
|
}
|
|
1171
|
-
const scalingFactor = 52 / current.object.
|
|
1005
|
+
const scalingFactor = 52 / current.object.radius;
|
|
1172
1006
|
let smallDistNerf = 1;
|
|
1173
1007
|
let cumulativeStrainTime = 0;
|
|
1174
1008
|
let result = 0;
|
|
@@ -1193,7 +1027,7 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
1193
1027
|
const opacityBonus = 1 +
|
|
1194
1028
|
this.maxOpacityBonus *
|
|
1195
1029
|
(1 -
|
|
1196
|
-
current.opacityAt(currentObject.object.startTime, isHiddenMod
|
|
1030
|
+
current.opacityAt(currentObject.object.startTime, isHiddenMod));
|
|
1197
1031
|
result +=
|
|
1198
1032
|
(stackNerf * opacityBonus * scalingFactor * jumpDistance) /
|
|
1199
1033
|
cumulativeStrainTime;
|
|
@@ -1224,8 +1058,8 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
1224
1058
|
// Longer sliders require more memorization.
|
|
1225
1059
|
sliderBonus *= pixelTravelDistance;
|
|
1226
1060
|
// Nerf sliders with repeats, as less memorization is required.
|
|
1227
|
-
if (current.object.
|
|
1228
|
-
sliderBonus /= current.object.
|
|
1061
|
+
if (current.object.repeatCount > 0)
|
|
1062
|
+
sliderBonus /= current.object.repeatCount + 1;
|
|
1229
1063
|
}
|
|
1230
1064
|
result += sliderBonus * this.sliderMultiplier;
|
|
1231
1065
|
return result;
|
|
@@ -1236,72 +1070,97 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
1236
1070
|
* Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
|
|
1237
1071
|
*/
|
|
1238
1072
|
class DroidFlashlight extends DroidSkill {
|
|
1239
|
-
skillMultiplier = 0.125;
|
|
1240
|
-
strainDecayBase = 0.15;
|
|
1241
|
-
reducedSectionCount = 0;
|
|
1242
|
-
reducedSectionBaseline = 1;
|
|
1243
|
-
starsPerDouble = 1.05;
|
|
1244
|
-
isHidden;
|
|
1245
|
-
withSliders;
|
|
1246
1073
|
constructor(mods, withSliders) {
|
|
1247
1074
|
super(mods);
|
|
1075
|
+
this.strainDecayBase = 0.15;
|
|
1076
|
+
this.reducedSectionCount = 0;
|
|
1077
|
+
this.reducedSectionBaseline = 1;
|
|
1078
|
+
this.starsPerDouble = 1.06;
|
|
1079
|
+
this.skillMultiplier = 0.02;
|
|
1080
|
+
this.currentFlashlightStrain = 0;
|
|
1248
1081
|
this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
|
|
1249
1082
|
this.withSliders = withSliders;
|
|
1250
1083
|
}
|
|
1251
|
-
/**
|
|
1252
|
-
* @param current The hitobject to calculate.
|
|
1253
|
-
*/
|
|
1254
1084
|
strainValueAt(current) {
|
|
1255
|
-
this.
|
|
1256
|
-
this.
|
|
1085
|
+
this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
|
|
1086
|
+
this.currentFlashlightStrain +=
|
|
1257
1087
|
DroidFlashlightEvaluator.evaluateDifficultyOf(current, this.isHidden, this.withSliders) * this.skillMultiplier;
|
|
1258
|
-
return this.
|
|
1088
|
+
return this.currentFlashlightStrain;
|
|
1089
|
+
}
|
|
1090
|
+
calculateInitialStrain(time, current) {
|
|
1091
|
+
var _a, _b;
|
|
1092
|
+
return (this.currentFlashlightStrain *
|
|
1093
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
1094
|
+
}
|
|
1095
|
+
getObjectStrain() {
|
|
1096
|
+
return this.currentFlashlightStrain;
|
|
1259
1097
|
}
|
|
1260
1098
|
saveToHitObject(current) {
|
|
1261
1099
|
if (this.withSliders) {
|
|
1262
|
-
current.flashlightStrainWithSliders = this.
|
|
1100
|
+
current.flashlightStrainWithSliders = this.currentFlashlightStrain;
|
|
1263
1101
|
}
|
|
1264
1102
|
else {
|
|
1265
|
-
current.flashlightStrainWithoutSliders =
|
|
1103
|
+
current.flashlightStrainWithoutSliders =
|
|
1104
|
+
this.currentFlashlightStrain;
|
|
1266
1105
|
}
|
|
1267
1106
|
}
|
|
1107
|
+
difficultyValue() {
|
|
1108
|
+
return (this.strainPeaks.reduce((a, v) => a + v, 0) * this.starsPerDouble);
|
|
1109
|
+
}
|
|
1268
1110
|
}
|
|
1269
1111
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1112
|
+
class Island {
|
|
1113
|
+
constructor(delta, deltaDifferenceEpsilon) {
|
|
1114
|
+
this.delta = Number.MAX_SAFE_INTEGER;
|
|
1115
|
+
this.deltaCount = 0;
|
|
1116
|
+
if (deltaDifferenceEpsilon === undefined) {
|
|
1117
|
+
this.deltaDifferenceEpsilon = delta;
|
|
1118
|
+
}
|
|
1119
|
+
else {
|
|
1120
|
+
this.deltaDifferenceEpsilon = deltaDifferenceEpsilon;
|
|
1121
|
+
this.addDelta(delta);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
addDelta(delta) {
|
|
1125
|
+
if (this.delta === Number.MAX_SAFE_INTEGER) {
|
|
1126
|
+
this.delta = Math.max(Math.trunc(delta), DifficultyHitObject.minDeltaTime);
|
|
1127
|
+
}
|
|
1128
|
+
++this.deltaCount;
|
|
1129
|
+
}
|
|
1130
|
+
isSimilarPolarity(other) {
|
|
1131
|
+
// TODO: consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple)
|
|
1132
|
+
// naively adding delta check here breaks _a lot_ of maps because of the flawed ratio calculation
|
|
1133
|
+
return this.deltaCount % 2 == other.deltaCount % 2;
|
|
1134
|
+
}
|
|
1135
|
+
equals(other) {
|
|
1136
|
+
return (Math.abs(this.delta - other.delta) < this.deltaDifferenceEpsilon &&
|
|
1137
|
+
this.deltaCount === other.deltaCount);
|
|
1138
|
+
}
|
|
1278
1139
|
}
|
|
1279
|
-
|
|
1280
1140
|
/**
|
|
1281
1141
|
* An evaluator for calculating osu!droid Rhythm skill.
|
|
1282
1142
|
*/
|
|
1283
|
-
class DroidRhythmEvaluator
|
|
1143
|
+
class DroidRhythmEvaluator {
|
|
1284
1144
|
/**
|
|
1285
1145
|
* Calculates a rhythm multiplier for the difficulty of the tap associated
|
|
1286
1146
|
* with historic data of the current object.
|
|
1287
1147
|
*
|
|
1288
1148
|
* @param current The current object.
|
|
1289
|
-
* @param greatWindow The great hit window of the current object.
|
|
1290
1149
|
*/
|
|
1291
|
-
static evaluateDifficultyOf(current
|
|
1292
|
-
if (current.object instanceof osuBase.Spinner
|
|
1293
|
-
// Exclude overlapping objects that can be tapped at once.
|
|
1294
|
-
current.isOverlapping(false)) {
|
|
1150
|
+
static evaluateDifficultyOf(current) {
|
|
1151
|
+
if (current.object instanceof osuBase.Spinner) {
|
|
1295
1152
|
return 1;
|
|
1296
1153
|
}
|
|
1297
|
-
|
|
1154
|
+
const deltaDifferenceEpsilon = current.fullGreatWindow * 0.3;
|
|
1298
1155
|
let rhythmComplexitySum = 0;
|
|
1299
|
-
let
|
|
1156
|
+
let island = new Island(deltaDifferenceEpsilon);
|
|
1157
|
+
let previousIsland = new Island(deltaDifferenceEpsilon);
|
|
1158
|
+
const islandCounts = new Map();
|
|
1300
1159
|
// Store the ratio of the current start of an island to buff for tighter rhythms.
|
|
1301
1160
|
let startRatio = 0;
|
|
1302
1161
|
let firstDeltaSwitch = false;
|
|
1303
1162
|
let rhythmStart = 0;
|
|
1304
|
-
const historicalNoteCount = Math.min(current.index,
|
|
1163
|
+
const historicalNoteCount = Math.min(current.index, this.historyObjectsMax);
|
|
1305
1164
|
// Exclude overlapping objects that can be tapped at once.
|
|
1306
1165
|
const validPrevious = [];
|
|
1307
1166
|
for (let i = 0; i < historicalNoteCount; ++i) {
|
|
@@ -1319,123 +1178,160 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
|
|
|
1319
1178
|
++rhythmStart;
|
|
1320
1179
|
}
|
|
1321
1180
|
for (let i = rhythmStart; i > 0; --i) {
|
|
1181
|
+
const currentObject = validPrevious[i - 1];
|
|
1182
|
+
const prevObject = validPrevious[i];
|
|
1183
|
+
const lastObject = validPrevious[i + 1];
|
|
1322
1184
|
// Scale note 0 to 1 from history to now.
|
|
1323
|
-
|
|
1324
|
-
(current.startTime -
|
|
1185
|
+
const timeDecay = (this.historyTimeMax -
|
|
1186
|
+
(current.startTime - currentObject.startTime)) /
|
|
1325
1187
|
this.historyTimeMax;
|
|
1188
|
+
const noteDecay = (validPrevious.length - i) / validPrevious.length;
|
|
1326
1189
|
// Either we're limited by time or limited by object count.
|
|
1327
|
-
currentHistoricalDecay = Math.min(
|
|
1328
|
-
const currentDelta =
|
|
1329
|
-
const prevDelta =
|
|
1330
|
-
const lastDelta =
|
|
1190
|
+
const currentHistoricalDecay = Math.min(timeDecay, noteDecay);
|
|
1191
|
+
const currentDelta = currentObject.strainTime;
|
|
1192
|
+
const prevDelta = prevObject.strainTime;
|
|
1193
|
+
const lastDelta = lastObject.strainTime;
|
|
1194
|
+
// Calculate how much current delta difference deserves a rhythm bonus
|
|
1195
|
+
// This function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e. 100 and 200)
|
|
1196
|
+
const deltaDifferenceRatio = Math.min(prevDelta, currentDelta) /
|
|
1197
|
+
Math.max(prevDelta, currentDelta);
|
|
1331
1198
|
const currentRatio = 1 +
|
|
1332
|
-
|
|
1333
|
-
Math.min(0.5, Math.pow(Math.sin(Math.PI /
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
let effectiveRatio = windowPenalty * currentRatio;
|
|
1199
|
+
this.rhythmRatioMultiplier *
|
|
1200
|
+
Math.min(0.5, Math.pow(Math.sin(Math.PI / deltaDifferenceRatio), 2));
|
|
1201
|
+
// Reduce ratio bonus if delta difference is too big
|
|
1202
|
+
const fraction = Math.max(prevDelta / currentDelta, currentDelta / prevDelta);
|
|
1203
|
+
const fractionMultiplier = osuBase.MathUtils.clamp(2 - fraction / 8, 0, 1);
|
|
1204
|
+
const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon);
|
|
1205
|
+
let effectiveRatio = windowPenalty * currentRatio * fractionMultiplier;
|
|
1339
1206
|
if (firstDeltaSwitch) {
|
|
1340
|
-
if (prevDelta
|
|
1341
|
-
prevDelta * 1.25 >= currentDelta) {
|
|
1207
|
+
if (Math.abs(prevDelta - currentDelta) < deltaDifferenceEpsilon) {
|
|
1342
1208
|
// Island is still progressing, count size.
|
|
1343
|
-
|
|
1344
|
-
++islandSize;
|
|
1345
|
-
}
|
|
1209
|
+
island.addDelta(currentDelta);
|
|
1346
1210
|
}
|
|
1347
1211
|
else {
|
|
1348
|
-
|
|
1349
|
-
|
|
1212
|
+
// BPM change is into slider, this is easy acc window.
|
|
1213
|
+
if (currentObject.object instanceof osuBase.Slider) {
|
|
1350
1214
|
effectiveRatio /= 8;
|
|
1351
1215
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
// Repeated island size (ex: triplet -> triplet).
|
|
1358
|
-
effectiveRatio /= 4;
|
|
1216
|
+
// BPM change was from a slider, this is easier typically than circle -> circle.
|
|
1217
|
+
// Unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty
|
|
1218
|
+
// than bursts without sliders.
|
|
1219
|
+
if (prevObject.object instanceof osuBase.Slider) {
|
|
1220
|
+
effectiveRatio *= 0.3;
|
|
1359
1221
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1222
|
+
// Repeated island polarity (2 -> 4, 3 -> 5).
|
|
1223
|
+
if (island.isSimilarPolarity(previousIsland)) {
|
|
1362
1224
|
effectiveRatio /= 2;
|
|
1363
1225
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1226
|
+
// Previous increase happened a note ago.
|
|
1227
|
+
// Albeit this is a 1/1 -> 1/2-1/4 type of transition, we don't want to buff this.
|
|
1228
|
+
if (lastDelta > prevDelta + deltaDifferenceEpsilon &&
|
|
1229
|
+
prevDelta > currentDelta + deltaDifferenceEpsilon) {
|
|
1368
1230
|
effectiveRatio /= 8;
|
|
1369
1231
|
}
|
|
1232
|
+
// Repeated island size (ex: triplet -> triplet).
|
|
1233
|
+
// TODO: remove this nerf since its staying here only for balancing purposes because of the flawed ratio calculation
|
|
1234
|
+
if (previousIsland.deltaCount == island.deltaCount) {
|
|
1235
|
+
effectiveRatio /= 2;
|
|
1236
|
+
}
|
|
1237
|
+
let islandFound = false;
|
|
1238
|
+
for (const [currentIsland, count] of islandCounts) {
|
|
1239
|
+
if (!island.equals(currentIsland)) {
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
islandFound = true;
|
|
1243
|
+
let islandCount = count;
|
|
1244
|
+
if (previousIsland.equals(island)) {
|
|
1245
|
+
// Only add island to island counts if they're going one after another.
|
|
1246
|
+
++islandCount;
|
|
1247
|
+
islandCounts.set(currentIsland, islandCount);
|
|
1248
|
+
}
|
|
1249
|
+
// Repeated island (ex: triplet -> triplet).
|
|
1250
|
+
// Graph: https://www.desmos.com/calculator/pj7an56zwf
|
|
1251
|
+
effectiveRatio *= Math.min(3 / islandCount, Math.pow(1 / islandCount, 2.75 / (1 + Math.exp(14 - 0.24 * island.delta))));
|
|
1252
|
+
break;
|
|
1253
|
+
}
|
|
1254
|
+
if (!islandFound) {
|
|
1255
|
+
islandCounts.set(island, 1);
|
|
1256
|
+
}
|
|
1257
|
+
// Scale down the difficulty if the object is doubletappable.
|
|
1258
|
+
effectiveRatio *= 1 - prevObject.doubletapness * 0.75;
|
|
1370
1259
|
rhythmComplexitySum +=
|
|
1371
|
-
|
|
1372
|
-
currentHistoricalDecay
|
|
1373
|
-
Math.sqrt(4 + islandSize)) /
|
|
1374
|
-
2) *
|
|
1375
|
-
Math.sqrt(4 + previousIslandSize)) /
|
|
1376
|
-
2;
|
|
1260
|
+
Math.sqrt(effectiveRatio * startRatio) *
|
|
1261
|
+
currentHistoricalDecay;
|
|
1377
1262
|
startRatio = effectiveRatio;
|
|
1378
|
-
|
|
1379
|
-
if (prevDelta
|
|
1263
|
+
previousIsland = island;
|
|
1264
|
+
if (prevDelta + deltaDifferenceEpsilon < currentDelta) {
|
|
1380
1265
|
// We're slowing down, stop counting.
|
|
1381
1266
|
// If we're speeding up, this stays as is and we keep counting island size.
|
|
1382
1267
|
firstDeltaSwitch = false;
|
|
1383
1268
|
}
|
|
1384
|
-
|
|
1269
|
+
island = new Island(currentDelta, deltaDifferenceEpsilon);
|
|
1385
1270
|
}
|
|
1386
1271
|
}
|
|
1387
|
-
else if (prevDelta >
|
|
1388
|
-
// We
|
|
1272
|
+
else if (prevDelta > currentDelta + deltaDifferenceEpsilon) {
|
|
1273
|
+
// We are speeding up.
|
|
1389
1274
|
// Begin counting island until we change speed again.
|
|
1390
1275
|
firstDeltaSwitch = true;
|
|
1276
|
+
// BPM change is into slider, this is easy acc window.
|
|
1277
|
+
if (currentObject.object instanceof osuBase.Slider) {
|
|
1278
|
+
effectiveRatio *= 0.6;
|
|
1279
|
+
}
|
|
1280
|
+
// BPM change was from a slider, this is easier typically than circle -> circle
|
|
1281
|
+
// Unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty
|
|
1282
|
+
// than bursts without sliders
|
|
1283
|
+
if (prevObject.object instanceof osuBase.Slider) {
|
|
1284
|
+
effectiveRatio *= 0.6;
|
|
1285
|
+
}
|
|
1391
1286
|
startRatio = effectiveRatio;
|
|
1392
|
-
|
|
1287
|
+
island = new Island(currentDelta, deltaDifferenceEpsilon);
|
|
1393
1288
|
}
|
|
1394
1289
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
let doubletapness = 1;
|
|
1398
|
-
if (next) {
|
|
1399
|
-
const currentDeltaTime = Math.max(1, current.deltaTime);
|
|
1400
|
-
const nextDeltaTime = Math.max(1, next.deltaTime);
|
|
1401
|
-
const deltaDifference = Math.abs(nextDeltaTime - currentDeltaTime);
|
|
1402
|
-
const speedRatio = currentDeltaTime / Math.max(currentDeltaTime, deltaDifference);
|
|
1403
|
-
const windowRatio = Math.pow(Math.min(1, currentDeltaTime / (greatWindow * 2)), 2);
|
|
1404
|
-
doubletapness = Math.pow(speedRatio, 1 - windowRatio);
|
|
1405
|
-
}
|
|
1406
|
-
return (Math.sqrt(4 + rhythmComplexitySum * this.rhythmMultiplier * doubletapness) / 2);
|
|
1290
|
+
return (Math.sqrt(4 + rhythmComplexitySum * this.rhythmOverallMultiplier) /
|
|
1291
|
+
2);
|
|
1407
1292
|
}
|
|
1408
1293
|
}
|
|
1294
|
+
DroidRhythmEvaluator.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
|
|
1295
|
+
DroidRhythmEvaluator.historyObjectsMax = 32;
|
|
1296
|
+
DroidRhythmEvaluator.rhythmOverallMultiplier = 0.95;
|
|
1297
|
+
DroidRhythmEvaluator.rhythmRatioMultiplier = 12;
|
|
1409
1298
|
|
|
1410
1299
|
/**
|
|
1411
1300
|
* Represents the skill required to properly follow a beatmap's rhythm.
|
|
1412
1301
|
*/
|
|
1413
1302
|
class DroidRhythm extends DroidSkill {
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
super(mods);
|
|
1423
|
-
this.hitWindow = new osuBase.OsuHitWindow(overallDifficulty);
|
|
1303
|
+
constructor() {
|
|
1304
|
+
super(...arguments);
|
|
1305
|
+
this.reducedSectionCount = 5;
|
|
1306
|
+
this.reducedSectionBaseline = 0.75;
|
|
1307
|
+
this.strainDecayBase = 0.3;
|
|
1308
|
+
this.starsPerDouble = 1.75;
|
|
1309
|
+
this.currentRhythmStrain = 0;
|
|
1310
|
+
this.currentRhythmMultiplier = 1;
|
|
1424
1311
|
}
|
|
1425
1312
|
strainValueAt(current) {
|
|
1426
|
-
this.
|
|
1427
|
-
|
|
1428
|
-
this.
|
|
1429
|
-
|
|
1313
|
+
this.currentRhythmMultiplier =
|
|
1314
|
+
DroidRhythmEvaluator.evaluateDifficultyOf(current);
|
|
1315
|
+
this.currentRhythmStrain *= this.strainDecay(current.deltaTime);
|
|
1316
|
+
this.currentRhythmStrain += this.currentRhythmMultiplier - 1;
|
|
1317
|
+
return this.currentRhythmStrain;
|
|
1318
|
+
}
|
|
1319
|
+
calculateInitialStrain(time, current) {
|
|
1320
|
+
var _a, _b;
|
|
1321
|
+
return (this.currentRhythmStrain *
|
|
1322
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
1323
|
+
}
|
|
1324
|
+
getObjectStrain() {
|
|
1325
|
+
return this.currentRhythmStrain;
|
|
1430
1326
|
}
|
|
1431
1327
|
saveToHitObject(current) {
|
|
1432
|
-
current.rhythmStrain = this.
|
|
1433
|
-
current.rhythmMultiplier = this.
|
|
1328
|
+
current.rhythmStrain = this.currentRhythmStrain;
|
|
1329
|
+
current.rhythmMultiplier = this.currentRhythmMultiplier;
|
|
1434
1330
|
}
|
|
1435
1331
|
}
|
|
1436
1332
|
|
|
1437
1333
|
/**
|
|
1438
|
-
* An evaluator for calculating osu!droid
|
|
1334
|
+
* An evaluator for calculating osu!droid visual skill.
|
|
1439
1335
|
*/
|
|
1440
1336
|
class DroidVisualEvaluator {
|
|
1441
1337
|
/**
|
|
@@ -1456,7 +1352,8 @@ class DroidVisualEvaluator {
|
|
|
1456
1352
|
static evaluateDifficultyOf(current, isHiddenMod, withSliders) {
|
|
1457
1353
|
if (current.object instanceof osuBase.Spinner ||
|
|
1458
1354
|
// Exclude overlapping objects that can be tapped at once.
|
|
1459
|
-
current.isOverlapping(true)
|
|
1355
|
+
current.isOverlapping(true) ||
|
|
1356
|
+
current.index === 0) {
|
|
1460
1357
|
return 0;
|
|
1461
1358
|
}
|
|
1462
1359
|
// Start with base density and give global bonus for Hidden.
|
|
@@ -1478,28 +1375,31 @@ class DroidVisualEvaluator {
|
|
|
1478
1375
|
}
|
|
1479
1376
|
// Do not consider objects that don't fall under time preempt.
|
|
1480
1377
|
if (current.object.startTime - previous.object.endTime >
|
|
1481
|
-
current.
|
|
1378
|
+
current.object.timePreempt) {
|
|
1482
1379
|
break;
|
|
1483
1380
|
}
|
|
1484
1381
|
strain +=
|
|
1485
1382
|
(1 -
|
|
1486
|
-
current.opacityAt(previous.object.startTime, isHiddenMod
|
|
1383
|
+
current.opacityAt(previous.object.startTime, isHiddenMod)) /
|
|
1487
1384
|
4;
|
|
1488
1385
|
}
|
|
1489
|
-
// Scale the value with overlapping factor.
|
|
1490
|
-
strain /= 10 * (1 + current.overlappingFactor);
|
|
1491
1386
|
if (current.timePreempt < 400) {
|
|
1492
1387
|
// Give bonus for AR higher than 10.33.
|
|
1493
|
-
strain += Math.pow(400 - current.timePreempt, 1.
|
|
1388
|
+
strain += Math.pow(400 - current.timePreempt, 1.35) / 100;
|
|
1494
1389
|
}
|
|
1390
|
+
// Scale the value with overlapping factor.
|
|
1391
|
+
strain /= 10 * (1 + current.overlappingFactor);
|
|
1495
1392
|
if (current.object instanceof osuBase.Slider && withSliders) {
|
|
1496
|
-
const scalingFactor = 50 / current.object.
|
|
1497
|
-
//
|
|
1393
|
+
const scalingFactor = 50 / current.object.radius;
|
|
1394
|
+
// Invert the scaling factor to determine the true travel distance independent of circle size.
|
|
1395
|
+
const pixelTravelDistance = current.object.lazyTravelDistance / scalingFactor;
|
|
1396
|
+
const currentVelocity = pixelTravelDistance / current.travelTime;
|
|
1397
|
+
const spanTravelDistance = pixelTravelDistance / current.object.spanCount;
|
|
1498
1398
|
strain +=
|
|
1499
|
-
//
|
|
1500
|
-
Math.min(6,
|
|
1501
|
-
//
|
|
1502
|
-
|
|
1399
|
+
// Reward sliders based on velocity, while also avoiding overbuffing extremely fast sliders.
|
|
1400
|
+
Math.min(6, currentVelocity * 1.5) *
|
|
1401
|
+
// Longer sliders require more reading.
|
|
1402
|
+
(spanTravelDistance / 100);
|
|
1503
1403
|
let cumulativeStrainTime = 0;
|
|
1504
1404
|
// Reward for velocity changes based on last few sliders.
|
|
1505
1405
|
for (let i = 0; i < Math.min(current.index, 4); ++i) {
|
|
@@ -1510,52 +1410,19 @@ class DroidVisualEvaluator {
|
|
|
1510
1410
|
last.isOverlapping(true)) {
|
|
1511
1411
|
continue;
|
|
1512
1412
|
}
|
|
1413
|
+
// Invert the scaling factor to determine the true travel distance independent of circle size.
|
|
1414
|
+
const lastPixelTravelDistance = last.object.lazyTravelDistance / scalingFactor;
|
|
1415
|
+
const lastVelocity = lastPixelTravelDistance / last.travelTime;
|
|
1416
|
+
const lastSpanTravelDistance = lastPixelTravelDistance / last.object.spanCount;
|
|
1513
1417
|
strain +=
|
|
1514
|
-
//
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
// Reward for rhythm changes.
|
|
1523
|
-
if (current.rhythmMultiplier > 1) {
|
|
1524
|
-
let rhythmBonus = (current.rhythmMultiplier - 1) / 20;
|
|
1525
|
-
// Rhythm changes are harder to read in Hidden.
|
|
1526
|
-
// Add additional bonus for Hidden.
|
|
1527
|
-
if (isHiddenMod) {
|
|
1528
|
-
rhythmBonus += (current.rhythmMultiplier - 1) / 25;
|
|
1529
|
-
}
|
|
1530
|
-
// Rhythm changes are harder to read when objects are stacked together.
|
|
1531
|
-
// Scale rhythm bonus based on the stack of past objects.
|
|
1532
|
-
const diameter = 2 * current.object.getRadius(osuBase.Modes.droid);
|
|
1533
|
-
let cumulativeStrainTime = 0;
|
|
1534
|
-
for (let i = 0; i < Math.min(current.index, 5); ++i) {
|
|
1535
|
-
const previous = current.previous(i);
|
|
1536
|
-
if (previous.object instanceof osuBase.Spinner ||
|
|
1537
|
-
// Exclude overlapping objects that can be tapped at once.
|
|
1538
|
-
previous.isOverlapping(true)) {
|
|
1539
|
-
continue;
|
|
1540
|
-
}
|
|
1541
|
-
const jumpDistance = current.object
|
|
1542
|
-
.getStackedPosition(osuBase.Modes.droid)
|
|
1543
|
-
.getDistance(previous.object.getStackedEndPosition(osuBase.Modes.droid));
|
|
1544
|
-
cumulativeStrainTime += previous.strainTime;
|
|
1545
|
-
rhythmBonus +=
|
|
1546
|
-
// Scale the bonus with diameter.
|
|
1547
|
-
osuBase.MathUtils.clamp((0.5 - jumpDistance / diameter) / 10, 0, 0.05) *
|
|
1548
|
-
// Scale with cumulative strain time to avoid overbuffing past objects.
|
|
1418
|
+
// Reward past sliders based on velocity changes, while also
|
|
1419
|
+
// avoiding overbuffing extremely fast velocity changes.
|
|
1420
|
+
Math.min(10, 2.5 * Math.abs(currentVelocity - lastVelocity)) *
|
|
1421
|
+
// Longer sliders require more reading.
|
|
1422
|
+
(lastSpanTravelDistance / 125) *
|
|
1423
|
+
// Avoid overbuffing past sliders.
|
|
1549
1424
|
Math.min(1, 300 / cumulativeStrainTime);
|
|
1550
|
-
// Give a larger bonus for Hidden.
|
|
1551
|
-
if (isHiddenMod) {
|
|
1552
|
-
rhythmBonus +=
|
|
1553
|
-
(1 -
|
|
1554
|
-
current.opacityAt(previous.object.startTime, isHiddenMod, osuBase.Modes.droid)) /
|
|
1555
|
-
20;
|
|
1556
|
-
}
|
|
1557
1425
|
}
|
|
1558
|
-
strain += rhythmBonus;
|
|
1559
1426
|
}
|
|
1560
1427
|
return strain;
|
|
1561
1428
|
}
|
|
@@ -1565,31 +1432,239 @@ class DroidVisualEvaluator {
|
|
|
1565
1432
|
* Represents the skill required to read every object in the map.
|
|
1566
1433
|
*/
|
|
1567
1434
|
class DroidVisual extends DroidSkill {
|
|
1568
|
-
starsPerDouble = 1.025;
|
|
1569
|
-
reducedSectionCount = 10;
|
|
1570
|
-
reducedSectionBaseline = 0.75;
|
|
1571
|
-
skillMultiplier = 10;
|
|
1572
|
-
strainDecayBase = 0.1;
|
|
1573
|
-
isHidden;
|
|
1574
|
-
withSliders;
|
|
1575
1435
|
constructor(mods, withSliders) {
|
|
1576
1436
|
super(mods);
|
|
1437
|
+
this.starsPerDouble = 1.025;
|
|
1438
|
+
this.reducedSectionCount = 10;
|
|
1439
|
+
this.reducedSectionBaseline = 0.75;
|
|
1440
|
+
this.strainDecayBase = 0.1;
|
|
1441
|
+
this.currentVisualStrain = 0;
|
|
1442
|
+
this.currentRhythmMultiplier = 1;
|
|
1443
|
+
this.skillMultiplier = 10;
|
|
1577
1444
|
this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
|
|
1578
1445
|
this.withSliders = withSliders;
|
|
1579
1446
|
}
|
|
1580
1447
|
strainValueAt(current) {
|
|
1581
|
-
this.
|
|
1582
|
-
this.
|
|
1448
|
+
this.currentVisualStrain *= this.strainDecay(current.deltaTime);
|
|
1449
|
+
this.currentVisualStrain +=
|
|
1583
1450
|
DroidVisualEvaluator.evaluateDifficultyOf(current, this.isHidden, this.withSliders) * this.skillMultiplier;
|
|
1584
|
-
|
|
1451
|
+
this.currentRhythmMultiplier = current.rhythmMultiplier;
|
|
1452
|
+
return this.currentVisualStrain * this.currentRhythmMultiplier;
|
|
1453
|
+
}
|
|
1454
|
+
calculateInitialStrain(time, current) {
|
|
1455
|
+
var _a, _b;
|
|
1456
|
+
return (this.currentVisualStrain *
|
|
1457
|
+
this.currentRhythmMultiplier *
|
|
1458
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
1459
|
+
}
|
|
1460
|
+
getObjectStrain() {
|
|
1461
|
+
return this.currentVisualStrain * this.currentRhythmMultiplier;
|
|
1585
1462
|
}
|
|
1586
1463
|
saveToHitObject(current) {
|
|
1464
|
+
const strain = this.currentVisualStrain * this.currentRhythmMultiplier;
|
|
1587
1465
|
if (this.withSliders) {
|
|
1588
|
-
current.visualStrainWithSliders =
|
|
1466
|
+
current.visualStrainWithSliders = strain;
|
|
1589
1467
|
}
|
|
1590
1468
|
else {
|
|
1591
|
-
current.visualStrainWithoutSliders =
|
|
1469
|
+
current.visualStrainWithoutSliders = strain;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* Represents an osu!droid hit object with difficulty calculation values.
|
|
1476
|
+
*/
|
|
1477
|
+
class DroidDifficultyHitObject extends DifficultyHitObject {
|
|
1478
|
+
get scalingFactor() {
|
|
1479
|
+
const radius = this.object.radius;
|
|
1480
|
+
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
|
1481
|
+
let scalingFactor = this.normalizedRadius / radius;
|
|
1482
|
+
// High circle size (small CS) bonus
|
|
1483
|
+
if (radius < this.radiusBuffThreshold) {
|
|
1484
|
+
scalingFactor *=
|
|
1485
|
+
1 + Math.pow((this.radiusBuffThreshold - radius) / 50, 2);
|
|
1486
|
+
}
|
|
1487
|
+
return scalingFactor;
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Note: You **must** call `computeProperties` at some point due to how TypeScript handles
|
|
1491
|
+
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue).
|
|
1492
|
+
*
|
|
1493
|
+
* @param object The underlying hitobject.
|
|
1494
|
+
* @param lastObject The hitobject before this hitobject.
|
|
1495
|
+
* @param lastLastObject The hitobject before the last hitobject.
|
|
1496
|
+
* @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
|
|
1497
|
+
* @param clockRate The clock rate of the beatmap.
|
|
1498
|
+
* @param greatWindow The great window of the hitobject.
|
|
1499
|
+
*/
|
|
1500
|
+
constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, greatWindow) {
|
|
1501
|
+
super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, greatWindow);
|
|
1502
|
+
/**
|
|
1503
|
+
* The tap strain generated by the hitobject.
|
|
1504
|
+
*/
|
|
1505
|
+
this.tapStrain = 0;
|
|
1506
|
+
/**
|
|
1507
|
+
* The tap strain generated by the hitobject if `strainTime` isn't modified by
|
|
1508
|
+
* OD. This is used in three-finger detection.
|
|
1509
|
+
*/
|
|
1510
|
+
this.originalTapStrain = 0;
|
|
1511
|
+
/**
|
|
1512
|
+
* The rhythm strain generated by the hitobject.
|
|
1513
|
+
*/
|
|
1514
|
+
this.rhythmStrain = 0;
|
|
1515
|
+
/**
|
|
1516
|
+
* The flashlight strain generated by the hitobject if sliders are considered.
|
|
1517
|
+
*/
|
|
1518
|
+
this.flashlightStrainWithSliders = 0;
|
|
1519
|
+
/**
|
|
1520
|
+
* The flashlight strain generated by the hitobject if sliders are not considered.
|
|
1521
|
+
*/
|
|
1522
|
+
this.flashlightStrainWithoutSliders = 0;
|
|
1523
|
+
/**
|
|
1524
|
+
* The visual strain generated by the hitobject if sliders are considered.
|
|
1525
|
+
*/
|
|
1526
|
+
this.visualStrainWithSliders = 0;
|
|
1527
|
+
/**
|
|
1528
|
+
* The visual strain generated by the hitobject if sliders are not considered.
|
|
1529
|
+
*/
|
|
1530
|
+
this.visualStrainWithoutSliders = 0;
|
|
1531
|
+
/**
|
|
1532
|
+
* The note density of the hitobject.
|
|
1533
|
+
*/
|
|
1534
|
+
this.noteDensity = 1;
|
|
1535
|
+
/**
|
|
1536
|
+
* The overlapping factor of the hitobject.
|
|
1537
|
+
*
|
|
1538
|
+
* This is used to scale visual skill.
|
|
1539
|
+
*/
|
|
1540
|
+
this.overlappingFactor = 0;
|
|
1541
|
+
this.radiusBuffThreshold = 70;
|
|
1542
|
+
this.mode = osuBase.Modes.droid;
|
|
1543
|
+
this.maximumSliderRadius = this.normalizedRadius * 2;
|
|
1544
|
+
this.timePreempt = object.timePreempt / clockRate;
|
|
1545
|
+
}
|
|
1546
|
+
computeProperties(clockRate, hitObjects) {
|
|
1547
|
+
super.computeProperties(clockRate, hitObjects);
|
|
1548
|
+
this.setVisuals(clockRate, hitObjects);
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Determines whether this hitobject is considered overlapping with the hitobject before it.
|
|
1552
|
+
*
|
|
1553
|
+
* Keep in mind that "overlapping" in this case is overlapping to the point where both hitobjects
|
|
1554
|
+
* can be hit with just a single tap in osu!droid.
|
|
1555
|
+
*
|
|
1556
|
+
* In the case of sliders, it is considered overlapping if all nested hitobjects can be hit with
|
|
1557
|
+
* one aim motion.
|
|
1558
|
+
*
|
|
1559
|
+
* @param considerDistance Whether to consider the distance between both hitobjects.
|
|
1560
|
+
* @returns Whether the hitobject is considered overlapping.
|
|
1561
|
+
*/
|
|
1562
|
+
isOverlapping(considerDistance) {
|
|
1563
|
+
if (this.object instanceof osuBase.Spinner) {
|
|
1564
|
+
return false;
|
|
1565
|
+
}
|
|
1566
|
+
const prev = this.previous(0);
|
|
1567
|
+
if (!prev || prev.object instanceof osuBase.Spinner) {
|
|
1568
|
+
return false;
|
|
1569
|
+
}
|
|
1570
|
+
if (this.object.startTime !== prev.object.startTime) {
|
|
1571
|
+
return false;
|
|
1572
|
+
}
|
|
1573
|
+
if (!considerDistance) {
|
|
1574
|
+
return true;
|
|
1575
|
+
}
|
|
1576
|
+
const distanceThreshold = 2 * this.object.radius;
|
|
1577
|
+
const startPosition = this.object.getStackedPosition(osuBase.Modes.droid);
|
|
1578
|
+
const prevStartPosition = prev.object.getStackedPosition(osuBase.Modes.droid);
|
|
1579
|
+
// We need to consider two cases:
|
|
1580
|
+
//
|
|
1581
|
+
// Case 1: Current object is a circle, or previous object is a circle.
|
|
1582
|
+
// In this case, we only need to check if their positions are close enough to be tapped together.
|
|
1583
|
+
//
|
|
1584
|
+
// Case 2: Both objects are sliders.
|
|
1585
|
+
// In this case, we need to check if all nested hitobjects can be hit together.
|
|
1586
|
+
// To start with, check if the starting positions can be tapped together.
|
|
1587
|
+
if (startPosition.getDistance(prevStartPosition) > distanceThreshold) {
|
|
1588
|
+
return false;
|
|
1589
|
+
}
|
|
1590
|
+
if (this.object instanceof osuBase.Circle || prev.object instanceof osuBase.Circle) {
|
|
1591
|
+
return true;
|
|
1592
|
+
}
|
|
1593
|
+
// Check if all nested hitobjects can be hit together.
|
|
1594
|
+
for (let i = 1; i < this.object.nestedHitObjects.length; ++i) {
|
|
1595
|
+
const position = this.object.nestedHitObjects[i].getStackedPosition(osuBase.Modes.droid);
|
|
1596
|
+
const prevPosition = prevStartPosition.add(prev.object.curvePositionAt(i / (this.object.nestedHitObjects.length - 1)));
|
|
1597
|
+
if (position.getDistance(prevPosition) > distanceThreshold) {
|
|
1598
|
+
return false;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
// Do the same for the previous slider as well.
|
|
1602
|
+
for (let i = 1; i < prev.object.nestedHitObjects.length; ++i) {
|
|
1603
|
+
const prevPosition = prev.object.nestedHitObjects[i].getStackedPosition(osuBase.Modes.droid);
|
|
1604
|
+
const position = startPosition.add(this.object.curvePositionAt(i / (prev.object.nestedHitObjects.length - 1)));
|
|
1605
|
+
if (prevPosition.getDistance(position) > distanceThreshold) {
|
|
1606
|
+
return false;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
return true;
|
|
1610
|
+
}
|
|
1611
|
+
setVisuals(clockRate, hitObjects) {
|
|
1612
|
+
// We'll have two visible object arrays. The first array contains objects before the current object starts in a reversed order,
|
|
1613
|
+
// while the second array contains objects after the current object ends.
|
|
1614
|
+
// For overlapping factor, we also need to consider previous visible objects.
|
|
1615
|
+
const prevVisibleObjects = [];
|
|
1616
|
+
const nextVisibleObjects = [];
|
|
1617
|
+
for (let j = this.index + 2; j < hitObjects.length; ++j) {
|
|
1618
|
+
const o = hitObjects[j];
|
|
1619
|
+
if (o instanceof osuBase.Spinner) {
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
if (o.startTime / clockRate > this.endTime + this.timePreempt) {
|
|
1623
|
+
break;
|
|
1624
|
+
}
|
|
1625
|
+
nextVisibleObjects.push(o);
|
|
1626
|
+
}
|
|
1627
|
+
for (let j = 0; j < this.index; ++j) {
|
|
1628
|
+
const prev = this.previous(j);
|
|
1629
|
+
if (prev.object instanceof osuBase.Spinner) {
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
if (prev.startTime >= this.startTime) {
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
if (prev.startTime < this.startTime - this.timePreempt) {
|
|
1636
|
+
break;
|
|
1637
|
+
}
|
|
1638
|
+
prevVisibleObjects.push(prev.object);
|
|
1592
1639
|
}
|
|
1640
|
+
for (const hitObject of prevVisibleObjects) {
|
|
1641
|
+
const distance = this.object
|
|
1642
|
+
.getStackedPosition(this.mode)
|
|
1643
|
+
.getDistance(hitObject.getStackedEndPosition(this.mode));
|
|
1644
|
+
const deltaTime = this.startTime - hitObject.endTime / clockRate;
|
|
1645
|
+
this.applyToOverlappingFactor(distance, deltaTime);
|
|
1646
|
+
}
|
|
1647
|
+
for (const hitObject of nextVisibleObjects) {
|
|
1648
|
+
const distance = hitObject
|
|
1649
|
+
.getStackedPosition(this.mode)
|
|
1650
|
+
.getDistance(this.object.getStackedEndPosition(this.mode));
|
|
1651
|
+
const deltaTime = hitObject.startTime / clockRate - this.endTime;
|
|
1652
|
+
if (deltaTime >= 0) {
|
|
1653
|
+
this.noteDensity += 1 - deltaTime / this.timePreempt;
|
|
1654
|
+
}
|
|
1655
|
+
this.applyToOverlappingFactor(distance, deltaTime);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
applyToOverlappingFactor(distance, deltaTime) {
|
|
1659
|
+
// Penalize objects that are too close to the object in both distance
|
|
1660
|
+
// and delta time to prevent stream maps from being overweighted.
|
|
1661
|
+
this.overlappingFactor +=
|
|
1662
|
+
Math.max(0, 1 - distance / (2.5 * this.object.radius)) *
|
|
1663
|
+
(7.5 /
|
|
1664
|
+
(1 +
|
|
1665
|
+
Math.exp(0.15 *
|
|
1666
|
+
(Math.max(deltaTime, DifficultyHitObject.minDeltaTime) -
|
|
1667
|
+
75))));
|
|
1593
1668
|
}
|
|
1594
1669
|
}
|
|
1595
1670
|
|
|
@@ -1597,56 +1672,77 @@ class DroidVisual extends DroidSkill {
|
|
|
1597
1672
|
* A difficulty calculator for osu!droid gamemode.
|
|
1598
1673
|
*/
|
|
1599
1674
|
class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
1675
|
+
constructor() {
|
|
1676
|
+
super(...arguments);
|
|
1677
|
+
this.attributes = {
|
|
1678
|
+
mode: "live",
|
|
1679
|
+
tapDifficulty: 0,
|
|
1680
|
+
rhythmDifficulty: 0,
|
|
1681
|
+
visualDifficulty: 0,
|
|
1682
|
+
aimNoteCount: 0,
|
|
1683
|
+
mods: [],
|
|
1684
|
+
starRating: 0,
|
|
1685
|
+
maxCombo: 0,
|
|
1686
|
+
aimDifficulty: 0,
|
|
1687
|
+
flashlightDifficulty: 0,
|
|
1688
|
+
speedNoteCount: 0,
|
|
1689
|
+
sliderFactor: 0,
|
|
1690
|
+
clockRate: 1,
|
|
1691
|
+
overallDifficulty: 0,
|
|
1692
|
+
hitCircleCount: 0,
|
|
1693
|
+
sliderCount: 0,
|
|
1694
|
+
spinnerCount: 0,
|
|
1695
|
+
aimDifficultStrainCount: 0,
|
|
1696
|
+
tapDifficultStrainCount: 0,
|
|
1697
|
+
flashlightDifficultStrainCount: 0,
|
|
1698
|
+
visualDifficultStrainCount: 0,
|
|
1699
|
+
flashlightSliderFactor: 0,
|
|
1700
|
+
visualSliderFactor: 0,
|
|
1701
|
+
possibleThreeFingeredSections: [],
|
|
1702
|
+
difficultSliders: [],
|
|
1703
|
+
averageSpeedDeltaTime: 0,
|
|
1704
|
+
vibroFactor: 1,
|
|
1705
|
+
};
|
|
1706
|
+
this.difficultyMultiplier = 0.18;
|
|
1707
|
+
this.mode = osuBase.Modes.droid;
|
|
1708
|
+
}
|
|
1600
1709
|
/**
|
|
1601
1710
|
* The aim star rating of the beatmap.
|
|
1602
1711
|
*/
|
|
1603
|
-
aim
|
|
1712
|
+
get aim() {
|
|
1713
|
+
return this.attributes.aimDifficulty;
|
|
1714
|
+
}
|
|
1604
1715
|
/**
|
|
1605
1716
|
* The tap star rating of the beatmap.
|
|
1606
1717
|
*/
|
|
1607
|
-
tap
|
|
1718
|
+
get tap() {
|
|
1719
|
+
return this.attributes.tapDifficulty;
|
|
1720
|
+
}
|
|
1608
1721
|
/**
|
|
1609
1722
|
* The rhythm star rating of the beatmap.
|
|
1610
1723
|
*/
|
|
1611
|
-
rhythm
|
|
1724
|
+
get rhythm() {
|
|
1725
|
+
return this.attributes.rhythmDifficulty;
|
|
1726
|
+
}
|
|
1612
1727
|
/**
|
|
1613
1728
|
* The flashlight star rating of the beatmap.
|
|
1614
1729
|
*/
|
|
1615
|
-
flashlight
|
|
1730
|
+
get flashlight() {
|
|
1731
|
+
return this.attributes.flashlightDifficulty;
|
|
1732
|
+
}
|
|
1616
1733
|
/**
|
|
1617
1734
|
* The visual star rating of the beatmap.
|
|
1618
1735
|
*/
|
|
1619
|
-
visual
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
visualDifficulty: 0,
|
|
1630
|
-
aimNoteCount: 0,
|
|
1631
|
-
mods: [],
|
|
1632
|
-
starRating: 0,
|
|
1633
|
-
maxCombo: 0,
|
|
1634
|
-
aimDifficulty: 0,
|
|
1635
|
-
flashlightDifficulty: 0,
|
|
1636
|
-
speedNoteCount: 0,
|
|
1637
|
-
sliderFactor: 0,
|
|
1638
|
-
approachRate: 0,
|
|
1639
|
-
overallDifficulty: 0,
|
|
1640
|
-
hitCircleCount: 0,
|
|
1641
|
-
sliderCount: 0,
|
|
1642
|
-
spinnerCount: 0,
|
|
1643
|
-
flashlightSliderFactor: 0,
|
|
1644
|
-
visualSliderFactor: 0,
|
|
1645
|
-
possibleThreeFingeredSections: [],
|
|
1646
|
-
difficultSliders: [],
|
|
1647
|
-
};
|
|
1648
|
-
difficultyMultiplier = 0.18;
|
|
1649
|
-
mode = osuBase.Modes.droid;
|
|
1736
|
+
get visual() {
|
|
1737
|
+
return this.attributes.visualDifficulty;
|
|
1738
|
+
}
|
|
1739
|
+
get cacheableAttributes() {
|
|
1740
|
+
return Object.assign(Object.assign({}, this.attributes), { mods: osuBase.ModUtil.modsToOsuString(this.attributes.mods) });
|
|
1741
|
+
}
|
|
1742
|
+
// Override to use DroidDifficultyCalculationOptions
|
|
1743
|
+
calculate(options) {
|
|
1744
|
+
return super.calculate(options);
|
|
1745
|
+
}
|
|
1650
1746
|
/**
|
|
1651
1747
|
* Calculates the aim star rating of the beatmap and stores it in this instance.
|
|
1652
1748
|
*/
|
|
@@ -1660,26 +1756,18 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1660
1756
|
* Calculates the tap star rating of the beatmap and stores it in this instance.
|
|
1661
1757
|
*/
|
|
1662
1758
|
calculateTap() {
|
|
1663
|
-
const
|
|
1664
|
-
this.
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
else {
|
|
1670
|
-
this.postCalculateTap(tapSkill);
|
|
1671
|
-
}
|
|
1672
|
-
this.calculateSpeedAttributes();
|
|
1759
|
+
const tapSkillCheese = new DroidTap(this.mods, true);
|
|
1760
|
+
const tapSkillNoCheese = new DroidTap(this.mods, false);
|
|
1761
|
+
this.calculateSkills(tapSkillCheese, tapSkillNoCheese);
|
|
1762
|
+
const tapSkillVibro = new DroidTap(this.mods, true, tapSkillCheese.relevantDeltaTime());
|
|
1763
|
+
this.calculateSkills(tapSkillVibro);
|
|
1764
|
+
this.postCalculateTap(tapSkillCheese, tapSkillVibro);
|
|
1673
1765
|
}
|
|
1674
1766
|
/**
|
|
1675
1767
|
* Calculates the rhythm star rating of the beatmap and stores it in this instance.
|
|
1676
1768
|
*/
|
|
1677
1769
|
calculateRhythm() {
|
|
1678
|
-
|
|
1679
|
-
this.rhythm = this.attributes.rhythmDifficulty = 0;
|
|
1680
|
-
return;
|
|
1681
|
-
}
|
|
1682
|
-
const rhythmSkill = new DroidRhythm(this.mods, this.stats.od);
|
|
1770
|
+
const rhythmSkill = new DroidRhythm(this.mods);
|
|
1683
1771
|
this.calculateSkills(rhythmSkill);
|
|
1684
1772
|
this.postCalculateRhythm(rhythmSkill);
|
|
1685
1773
|
}
|
|
@@ -1697,7 +1785,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1697
1785
|
*/
|
|
1698
1786
|
calculateVisual() {
|
|
1699
1787
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
1700
|
-
this.
|
|
1788
|
+
this.attributes.visualDifficulty = 0;
|
|
1701
1789
|
return;
|
|
1702
1790
|
}
|
|
1703
1791
|
const visualSkill = new DroidVisual(this.mods, true);
|
|
@@ -1719,55 +1807,35 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1719
1807
|
if (basePerformanceValue > 1e-5) {
|
|
1720
1808
|
// Document for formula derivation:
|
|
1721
1809
|
// https://docs.google.com/document/d/10DZGYYSsT_yjz2Mtp6yIJld0Rqx4E-vVHupCqiM4TNI/edit
|
|
1722
|
-
this.
|
|
1810
|
+
this.attributes.starRating =
|
|
1723
1811
|
0.027 *
|
|
1724
1812
|
(Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformanceValue) +
|
|
1725
1813
|
4);
|
|
1726
1814
|
}
|
|
1727
1815
|
else {
|
|
1728
|
-
this.
|
|
1816
|
+
this.attributes.starRating = 0;
|
|
1729
1817
|
}
|
|
1730
1818
|
}
|
|
1731
1819
|
calculateAll() {
|
|
1732
1820
|
const skills = this.createSkills();
|
|
1733
|
-
const isRelax = this.mods.some((m) => m instanceof osuBase.ModRelax);
|
|
1734
|
-
if (isRelax) {
|
|
1735
|
-
// Remove visual skills to reduce overhead.
|
|
1736
|
-
skills.pop();
|
|
1737
|
-
skills.pop();
|
|
1738
|
-
}
|
|
1739
1821
|
this.calculateSkills(...skills);
|
|
1740
1822
|
const aimSkill = skills[0];
|
|
1741
1823
|
const aimSkillWithoutSliders = skills[1];
|
|
1742
1824
|
const rhythmSkill = skills[2];
|
|
1743
|
-
const
|
|
1744
|
-
const flashlightSkill = skills[
|
|
1745
|
-
const flashlightSkillWithoutSliders = skills[
|
|
1746
|
-
const visualSkill = skills[
|
|
1747
|
-
const visualSkillWithoutSliders = skills[
|
|
1825
|
+
const tapSkillCheese = skills[3];
|
|
1826
|
+
const flashlightSkill = skills[5];
|
|
1827
|
+
const flashlightSkillWithoutSliders = skills[6];
|
|
1828
|
+
const visualSkill = skills[7];
|
|
1829
|
+
const visualSkillWithoutSliders = skills[8];
|
|
1830
|
+
const tapSkillVibro = new DroidTap(this.mods, true, tapSkillCheese.relevantDeltaTime());
|
|
1831
|
+
this.calculateSkills(tapSkillVibro);
|
|
1748
1832
|
this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
}
|
|
1752
|
-
else {
|
|
1753
|
-
this.postCalculateTap(tapSkill);
|
|
1754
|
-
}
|
|
1755
|
-
this.calculateSpeedAttributes();
|
|
1756
|
-
if (!isRelax) {
|
|
1757
|
-
this.postCalculateRhythm(rhythmSkill);
|
|
1758
|
-
}
|
|
1833
|
+
this.postCalculateTap(tapSkillCheese, tapSkillVibro);
|
|
1834
|
+
this.postCalculateRhythm(rhythmSkill);
|
|
1759
1835
|
this.postCalculateFlashlight(flashlightSkill, flashlightSkillWithoutSliders);
|
|
1760
|
-
|
|
1761
|
-
this.postCalculateVisual(visualSkill, visualSkillWithoutSliders);
|
|
1762
|
-
}
|
|
1763
|
-
else {
|
|
1764
|
-
this.visual = this.attributes.visualDifficulty = 0;
|
|
1765
|
-
}
|
|
1836
|
+
this.postCalculateVisual(visualSkill, visualSkillWithoutSliders);
|
|
1766
1837
|
this.calculateTotal();
|
|
1767
1838
|
}
|
|
1768
|
-
/**
|
|
1769
|
-
* Returns a string representative of the class.
|
|
1770
|
-
*/
|
|
1771
1839
|
toString() {
|
|
1772
1840
|
return (this.total.toFixed(2) +
|
|
1773
1841
|
" stars (" +
|
|
@@ -1782,22 +1850,39 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1782
1850
|
this.visual.toFixed(2) +
|
|
1783
1851
|
" visual)");
|
|
1784
1852
|
}
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1853
|
+
generateDifficultyHitObjects(beatmap, clockRate) {
|
|
1854
|
+
var _a, _b;
|
|
1855
|
+
const difficultyObjects = [];
|
|
1856
|
+
const { objects } = beatmap.hitObjects;
|
|
1857
|
+
const isPrecise = this.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
1858
|
+
const greatWindow = new osuBase.DroidHitWindow(beatmap.difficulty.od).hitWindowFor300(isPrecise) / clockRate;
|
|
1859
|
+
for (let i = 0; i < objects.length; ++i) {
|
|
1860
|
+
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, greatWindow);
|
|
1861
|
+
difficultyObject.computeProperties(clockRate, objects);
|
|
1862
|
+
difficultyObjects.push(difficultyObject);
|
|
1863
|
+
}
|
|
1864
|
+
return difficultyObjects;
|
|
1865
|
+
}
|
|
1788
1866
|
createSkills() {
|
|
1789
1867
|
return [
|
|
1790
1868
|
new DroidAim(this.mods, true),
|
|
1791
1869
|
new DroidAim(this.mods, false),
|
|
1792
1870
|
// Tap skill depends on rhythm skill, so we put it first
|
|
1793
|
-
new DroidRhythm(this.mods
|
|
1794
|
-
|
|
1871
|
+
new DroidRhythm(this.mods),
|
|
1872
|
+
// Cheesability tap
|
|
1873
|
+
new DroidTap(this.mods, true),
|
|
1874
|
+
// Non-cheesability tap
|
|
1875
|
+
new DroidTap(this.mods, false),
|
|
1795
1876
|
new DroidFlashlight(this.mods, true),
|
|
1796
1877
|
new DroidFlashlight(this.mods, false),
|
|
1797
1878
|
new DroidVisual(this.mods, true),
|
|
1798
1879
|
new DroidVisual(this.mods, false),
|
|
1799
1880
|
];
|
|
1800
1881
|
}
|
|
1882
|
+
calculateClockRate(options) {
|
|
1883
|
+
var _a, _b;
|
|
1884
|
+
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));
|
|
1885
|
+
}
|
|
1801
1886
|
/**
|
|
1802
1887
|
* Called after aim skill calculation.
|
|
1803
1888
|
*
|
|
@@ -1807,152 +1892,122 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1807
1892
|
postCalculateAim(aimSkill, aimSkillWithoutSliders) {
|
|
1808
1893
|
this.strainPeaks.aimWithSliders = aimSkill.strainPeaks;
|
|
1809
1894
|
this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
|
|
1810
|
-
this.
|
|
1895
|
+
this.attributes.aimDifficulty = this.mods.some((m) => m instanceof osuBase.ModAutopilot)
|
|
1896
|
+
? 0
|
|
1897
|
+
: this.starValue(aimSkill.difficultyValue());
|
|
1811
1898
|
if (this.aim) {
|
|
1812
1899
|
this.attributes.sliderFactor =
|
|
1813
1900
|
this.starValue(aimSkillWithoutSliders.difficultyValue()) /
|
|
1814
1901
|
this.aim;
|
|
1815
1902
|
}
|
|
1816
1903
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
1817
|
-
this.
|
|
1904
|
+
this.attributes.aimDifficulty *= 0.9;
|
|
1818
1905
|
}
|
|
1819
|
-
this.attributes.
|
|
1906
|
+
this.attributes.aimDifficultStrainCount =
|
|
1907
|
+
aimSkill.countDifficultStrains();
|
|
1820
1908
|
this.calculateAimAttributes();
|
|
1821
1909
|
}
|
|
1822
1910
|
/**
|
|
1823
1911
|
* Calculates aim-related attributes.
|
|
1824
1912
|
*/
|
|
1825
1913
|
calculateAimAttributes() {
|
|
1826
|
-
|
|
1827
|
-
let maxStrain = 0;
|
|
1828
|
-
// Take the top 15% most difficult sliders based on velocity.
|
|
1914
|
+
this.attributes.difficultSliders = [];
|
|
1829
1915
|
const topDifficultSliders = [];
|
|
1830
1916
|
for (let i = 0; i < this.objects.length; ++i) {
|
|
1831
1917
|
const object = this.objects[i];
|
|
1832
|
-
objectStrains.push(object.aimStrainWithSliders);
|
|
1833
|
-
maxStrain = Math.max(maxStrain, object.aimStrainWithSliders);
|
|
1834
1918
|
const velocity = object.travelDistance / object.travelTime;
|
|
1835
1919
|
if (velocity > 0) {
|
|
1836
1920
|
topDifficultSliders.push({
|
|
1837
1921
|
index: i,
|
|
1838
1922
|
velocity: velocity,
|
|
1839
1923
|
});
|
|
1840
|
-
topDifficultSliders.sort((a, b) => b.velocity - a.velocity);
|
|
1841
|
-
while (topDifficultSliders.length >
|
|
1842
|
-
Math.ceil(0.15 * this.beatmap.hitObjects.sliders)) {
|
|
1843
|
-
topDifficultSliders.pop();
|
|
1844
|
-
}
|
|
1845
1924
|
}
|
|
1846
1925
|
}
|
|
1847
|
-
if (maxStrain) {
|
|
1848
|
-
this.attributes.aimNoteCount = objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
|
|
1849
|
-
}
|
|
1850
1926
|
const velocitySum = topDifficultSliders.reduce((a, v) => a + v.velocity, 0);
|
|
1851
1927
|
for (const slider of topDifficultSliders) {
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1928
|
+
const difficultyRating = slider.velocity / velocitySum;
|
|
1929
|
+
// Only consider sliders that are fast enough.
|
|
1930
|
+
if (difficultyRating > 0.02) {
|
|
1931
|
+
this.attributes.difficultSliders.push({
|
|
1932
|
+
index: slider.index,
|
|
1933
|
+
difficultyRating: slider.velocity / velocitySum,
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
this.attributes.difficultSliders.sort((a, b) => b.difficultyRating - a.difficultyRating);
|
|
1938
|
+
// Take the top 15% most difficult sliders.
|
|
1939
|
+
while (this.attributes.difficultSliders.length >
|
|
1940
|
+
Math.ceil(0.15 * this.beatmap.hitObjects.sliders)) {
|
|
1941
|
+
this.attributes.difficultSliders.pop();
|
|
1856
1942
|
}
|
|
1857
1943
|
}
|
|
1858
1944
|
/**
|
|
1859
1945
|
* Called after tap skill calculation.
|
|
1860
1946
|
*
|
|
1861
|
-
* @param
|
|
1947
|
+
* @param tapSkillCheese The tap skill that considers cheesing.
|
|
1948
|
+
* @param tapSkillVibro The tap skill that considers vibro.
|
|
1862
1949
|
*/
|
|
1863
|
-
postCalculateTap(
|
|
1864
|
-
this.strainPeaks.speed =
|
|
1865
|
-
this.
|
|
1950
|
+
postCalculateTap(tapSkillCheese, tapSkillVibro) {
|
|
1951
|
+
this.strainPeaks.speed = tapSkillCheese.strainPeaks;
|
|
1952
|
+
this.attributes.tapDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
|
|
1953
|
+
? 0
|
|
1954
|
+
: this.starValue(tapSkillCheese.difficultyValue());
|
|
1955
|
+
if (this.tap) {
|
|
1956
|
+
this.attributes.vibroFactor =
|
|
1957
|
+
this.starValue(tapSkillVibro.difficultyValue()) / this.tap;
|
|
1958
|
+
}
|
|
1959
|
+
this.attributes.speedNoteCount = tapSkillCheese.relevantNoteCount();
|
|
1960
|
+
this.attributes.averageSpeedDeltaTime =
|
|
1961
|
+
tapSkillCheese.relevantDeltaTime();
|
|
1962
|
+
this.attributes.tapDifficultStrainCount =
|
|
1963
|
+
tapSkillCheese.countDifficultStrains();
|
|
1964
|
+
this.calculateTapAttributes();
|
|
1866
1965
|
}
|
|
1867
1966
|
/**
|
|
1868
|
-
* Calculates
|
|
1967
|
+
* Calculates tap-related attributes.
|
|
1869
1968
|
*/
|
|
1870
|
-
|
|
1969
|
+
calculateTapAttributes() {
|
|
1871
1970
|
this.attributes.possibleThreeFingeredSections = [];
|
|
1872
|
-
const
|
|
1873
|
-
const objectStrains = [];
|
|
1874
|
-
let maxStrain = 0;
|
|
1875
|
-
const maxSectionDeltaTime = 2000;
|
|
1971
|
+
const { threeFingerStrainThreshold } = DroidDifficultyCalculator;
|
|
1876
1972
|
const minSectionObjectCount = 5;
|
|
1877
|
-
let
|
|
1878
|
-
|
|
1973
|
+
let inSpeedSection = false;
|
|
1974
|
+
let firstSpeedObjectIndex = 0;
|
|
1975
|
+
for (let i = 2; i < this.objects.length; ++i) {
|
|
1879
1976
|
const current = this.objects[i];
|
|
1880
|
-
const
|
|
1881
|
-
if (
|
|
1882
|
-
|
|
1977
|
+
const prev = this.objects[i - 1];
|
|
1978
|
+
if (!inSpeedSection &&
|
|
1979
|
+
current.originalTapStrain >= threeFingerStrainThreshold) {
|
|
1980
|
+
inSpeedSection = true;
|
|
1981
|
+
firstSpeedObjectIndex = i;
|
|
1982
|
+
continue;
|
|
1883
1983
|
}
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
const
|
|
1887
|
-
|
|
1984
|
+
const currentDelta = current.deltaTime;
|
|
1985
|
+
const prevDelta = prev.deltaTime;
|
|
1986
|
+
const deltaRatio = Math.min(prevDelta, currentDelta) /
|
|
1987
|
+
Math.max(prevDelta, currentDelta);
|
|
1988
|
+
if (inSpeedSection &&
|
|
1989
|
+
(current.originalTapStrain < threeFingerStrainThreshold ||
|
|
1990
|
+
// Stop speed section on slowing down 1/2 rhythm change or anything slower.
|
|
1991
|
+
(prevDelta < currentDelta && deltaRatio <= 0.5) ||
|
|
1992
|
+
// Don't forget to manually add the last section, which would otherwise be ignored.
|
|
1993
|
+
i === this.objects.length - 1)) {
|
|
1994
|
+
const lastSpeedObjectIndex = i - (i === this.objects.length - 1 ? 0 : 1);
|
|
1995
|
+
inSpeedSection = false;
|
|
1888
1996
|
// Ignore sections that don't meet object count requirement.
|
|
1889
|
-
if (i -
|
|
1890
|
-
firstObjectIndex = i + 1;
|
|
1891
|
-
continue;
|
|
1892
|
-
}
|
|
1893
|
-
tempSections.push({
|
|
1894
|
-
firstObjectIndex,
|
|
1895
|
-
lastObjectIndex: i,
|
|
1896
|
-
});
|
|
1897
|
-
firstObjectIndex = i + 1;
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
// Don't forget to manually add the last beatmap section, which would otherwise be ignored.
|
|
1901
|
-
if (this.objects.length - firstObjectIndex > minSectionObjectCount) {
|
|
1902
|
-
tempSections.push({
|
|
1903
|
-
firstObjectIndex,
|
|
1904
|
-
lastObjectIndex: this.objects.length - 1,
|
|
1905
|
-
});
|
|
1906
|
-
}
|
|
1907
|
-
// Refilter with tap strain in mind.
|
|
1908
|
-
const { threeFingerStrainThreshold } = DroidDifficultyCalculator;
|
|
1909
|
-
for (const section of tempSections) {
|
|
1910
|
-
let inSpeedSection = false;
|
|
1911
|
-
let newFirstObjectIndex = section.firstObjectIndex;
|
|
1912
|
-
for (let i = section.firstObjectIndex; i <= section.lastObjectIndex; ++i) {
|
|
1913
|
-
const current = this.objects[i];
|
|
1914
|
-
if (!inSpeedSection &&
|
|
1915
|
-
current.originalTapStrain >= threeFingerStrainThreshold) {
|
|
1916
|
-
inSpeedSection = true;
|
|
1917
|
-
newFirstObjectIndex = i;
|
|
1997
|
+
if (i - firstSpeedObjectIndex < minSectionObjectCount) {
|
|
1918
1998
|
continue;
|
|
1919
1999
|
}
|
|
1920
|
-
if (inSpeedSection &&
|
|
1921
|
-
current.originalTapStrain < threeFingerStrainThreshold) {
|
|
1922
|
-
inSpeedSection = false;
|
|
1923
|
-
this.attributes.possibleThreeFingeredSections.push({
|
|
1924
|
-
firstObjectIndex: newFirstObjectIndex,
|
|
1925
|
-
lastObjectIndex: i,
|
|
1926
|
-
sumStrain: this.calculateThreeFingerSummedStrain(newFirstObjectIndex, i),
|
|
1927
|
-
});
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
// Don't forget to manually add the last beatmap section, which would otherwise be ignored.
|
|
1931
|
-
if (inSpeedSection) {
|
|
1932
2000
|
this.attributes.possibleThreeFingeredSections.push({
|
|
1933
|
-
firstObjectIndex:
|
|
1934
|
-
lastObjectIndex:
|
|
1935
|
-
sumStrain:
|
|
2001
|
+
firstObjectIndex: firstSpeedObjectIndex,
|
|
2002
|
+
lastObjectIndex: lastSpeedObjectIndex,
|
|
2003
|
+
sumStrain: Math.pow(this.objects
|
|
2004
|
+
.slice(firstSpeedObjectIndex, lastSpeedObjectIndex + 1)
|
|
2005
|
+
.reduce((a, v) => a +
|
|
2006
|
+
v.originalTapStrain /
|
|
2007
|
+
threeFingerStrainThreshold, 0), 0.75),
|
|
1936
2008
|
});
|
|
1937
2009
|
}
|
|
1938
2010
|
}
|
|
1939
|
-
if (maxStrain) {
|
|
1940
|
-
this.attributes.speedNoteCount = objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
|
|
1941
|
-
}
|
|
1942
|
-
}
|
|
1943
|
-
/**
|
|
1944
|
-
* Calculates the sum of strains for possible three-fingered sections.
|
|
1945
|
-
*
|
|
1946
|
-
* @param firstObjectIndex The index of the first object in the section.
|
|
1947
|
-
* @param lastObjectIndex The index of the last object in the section.
|
|
1948
|
-
* @returns The summed strain of the section.
|
|
1949
|
-
*/
|
|
1950
|
-
calculateThreeFingerSummedStrain(firstObjectIndex, lastObjectIndex) {
|
|
1951
|
-
return Math.pow(this.objects
|
|
1952
|
-
.slice(firstObjectIndex, lastObjectIndex)
|
|
1953
|
-
.reduce((a, v) => a +
|
|
1954
|
-
v.originalTapStrain /
|
|
1955
|
-
DroidDifficultyCalculator.threeFingerStrainThreshold, 0), 0.75);
|
|
1956
2011
|
}
|
|
1957
2012
|
/**
|
|
1958
2013
|
* Called after rhythm skill calculation.
|
|
@@ -1960,7 +2015,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1960
2015
|
* @param rhythmSkill The rhythm skill.
|
|
1961
2016
|
*/
|
|
1962
2017
|
postCalculateRhythm(rhythmSkill) {
|
|
1963
|
-
this.
|
|
2018
|
+
this.attributes.rhythmDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
|
|
1964
2019
|
? 0
|
|
1965
2020
|
: this.starValue(rhythmSkill.difficultyValue());
|
|
1966
2021
|
}
|
|
@@ -1972,15 +2027,19 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1972
2027
|
*/
|
|
1973
2028
|
postCalculateFlashlight(flashlightSkill, flashlightSkillWithoutSliders) {
|
|
1974
2029
|
this.strainPeaks.flashlight = flashlightSkill.strainPeaks;
|
|
1975
|
-
this.
|
|
2030
|
+
this.attributes.flashlightDifficulty = this.starValue(flashlightSkill.difficultyValue());
|
|
1976
2031
|
if (this.flashlight) {
|
|
1977
2032
|
this.attributes.flashlightSliderFactor =
|
|
1978
2033
|
this.starValue(flashlightSkillWithoutSliders.difficultyValue()) / this.flashlight;
|
|
1979
2034
|
}
|
|
2035
|
+
if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
|
|
2036
|
+
this.attributes.flashlightDifficulty *= 0.3;
|
|
2037
|
+
}
|
|
1980
2038
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
1981
|
-
this.
|
|
2039
|
+
this.attributes.flashlightDifficulty *= 0.7;
|
|
1982
2040
|
}
|
|
1983
|
-
this.attributes.
|
|
2041
|
+
this.attributes.flashlightDifficultStrainCount =
|
|
2042
|
+
flashlightSkill.countDifficultStrains();
|
|
1984
2043
|
}
|
|
1985
2044
|
/**
|
|
1986
2045
|
* Called after visual skill calculation.
|
|
@@ -1989,7 +2048,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1989
2048
|
* @param visualSkillWithoutSliders The visual skill that doesn't consider sliders.
|
|
1990
2049
|
*/
|
|
1991
2050
|
postCalculateVisual(visualSkillWithSliders, visualSkillWithoutSliders) {
|
|
1992
|
-
this.
|
|
2051
|
+
this.attributes.visualDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
|
|
1993
2052
|
? 0
|
|
1994
2053
|
: this.starValue(visualSkillWithSliders.difficultyValue());
|
|
1995
2054
|
if (this.visual) {
|
|
@@ -1997,33 +2056,55 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1997
2056
|
this.starValue(visualSkillWithoutSliders.difficultyValue()) /
|
|
1998
2057
|
this.visual;
|
|
1999
2058
|
}
|
|
2059
|
+
if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
|
|
2060
|
+
this.attributes.visualDifficulty *= 0.8;
|
|
2061
|
+
}
|
|
2062
|
+
this.attributes.visualDifficultStrainCount =
|
|
2063
|
+
visualSkillWithSliders.countDifficultStrains();
|
|
2000
2064
|
}
|
|
2001
2065
|
}
|
|
2066
|
+
/**
|
|
2067
|
+
* The strain threshold to start detecting for possible three-fingered section.
|
|
2068
|
+
*
|
|
2069
|
+
* Increasing this number will result in less sections being flagged.
|
|
2070
|
+
*/
|
|
2071
|
+
DroidDifficultyCalculator.threeFingerStrainThreshold = 175;
|
|
2002
2072
|
|
|
2003
2073
|
/**
|
|
2004
2074
|
* The base class of performance calculators.
|
|
2005
2075
|
*/
|
|
2006
2076
|
class PerformanceCalculator {
|
|
2007
2077
|
/**
|
|
2008
|
-
* The
|
|
2009
|
-
*/
|
|
2010
|
-
total = 0;
|
|
2011
|
-
/**
|
|
2012
|
-
* The calculated accuracy.
|
|
2013
|
-
*/
|
|
2014
|
-
computedAccuracy = new osuBase.Accuracy({});
|
|
2015
|
-
/**
|
|
2016
|
-
* Penalty for combo breaks.
|
|
2017
|
-
*/
|
|
2018
|
-
comboPenalty = 0;
|
|
2019
|
-
/**
|
|
2020
|
-
* The amount of misses that are filtered out from sliderbreaks.
|
|
2021
|
-
*/
|
|
2022
|
-
effectiveMissCount = 0;
|
|
2023
|
-
/**
|
|
2024
|
-
* Nerf factor used for nerfing beatmaps with very likely dropped sliderends.
|
|
2078
|
+
* @param difficultyAttributes The difficulty attributes to calculate.
|
|
2025
2079
|
*/
|
|
2026
|
-
|
|
2080
|
+
constructor(difficultyAttributes) {
|
|
2081
|
+
/**
|
|
2082
|
+
* The overall performance value.
|
|
2083
|
+
*/
|
|
2084
|
+
this.total = 0;
|
|
2085
|
+
/**
|
|
2086
|
+
* The calculated accuracy.
|
|
2087
|
+
*/
|
|
2088
|
+
this.computedAccuracy = new osuBase.Accuracy({});
|
|
2089
|
+
/**
|
|
2090
|
+
* Penalty for combo breaks.
|
|
2091
|
+
*/
|
|
2092
|
+
this.comboPenalty = 0;
|
|
2093
|
+
/**
|
|
2094
|
+
* The amount of misses that are filtered out from sliderbreaks.
|
|
2095
|
+
*/
|
|
2096
|
+
this.effectiveMissCount = 0;
|
|
2097
|
+
/**
|
|
2098
|
+
* Nerf factor used for nerfing beatmaps with very likely dropped sliderends.
|
|
2099
|
+
*/
|
|
2100
|
+
this.sliderNerfFactor = 1;
|
|
2101
|
+
if (this.isCacheableAttribute(difficultyAttributes)) {
|
|
2102
|
+
this.difficultyAttributes = Object.assign(Object.assign({}, difficultyAttributes), { mods: osuBase.ModUtil.pcStringToMods(difficultyAttributes.mods) });
|
|
2103
|
+
}
|
|
2104
|
+
else {
|
|
2105
|
+
this.difficultyAttributes = osuBase.Utils.deepCopy(difficultyAttributes);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2027
2108
|
/**
|
|
2028
2109
|
* Calculates the performance points of the beatmap.
|
|
2029
2110
|
*
|
|
@@ -2033,7 +2114,7 @@ class PerformanceCalculator {
|
|
|
2033
2114
|
calculate(options) {
|
|
2034
2115
|
this.handleOptions(options);
|
|
2035
2116
|
this.calculateValues();
|
|
2036
|
-
this.calculateTotalValue();
|
|
2117
|
+
this.total = this.calculateTotalValue();
|
|
2037
2118
|
return this;
|
|
2038
2119
|
}
|
|
2039
2120
|
/**
|
|
@@ -2064,11 +2145,12 @@ class PerformanceCalculator {
|
|
|
2064
2145
|
* @param options Options for performance calculation.
|
|
2065
2146
|
*/
|
|
2066
2147
|
handleOptions(options) {
|
|
2148
|
+
var _a;
|
|
2067
2149
|
const maxCombo = this.difficultyAttributes.maxCombo;
|
|
2068
2150
|
const miss = this.computedAccuracy.nmiss;
|
|
2069
|
-
const combo = options
|
|
2151
|
+
const combo = (_a = options === null || options === void 0 ? void 0 : options.combo) !== null && _a !== void 0 ? _a : maxCombo - miss;
|
|
2070
2152
|
this.comboPenalty = Math.min(Math.pow(combo / maxCombo, 0.8), 1);
|
|
2071
|
-
if (options
|
|
2153
|
+
if ((options === null || options === void 0 ? void 0 : options.accPercent) instanceof osuBase.Accuracy) {
|
|
2072
2154
|
// Copy into new instance to not modify the original
|
|
2073
2155
|
this.computedAccuracy = new osuBase.Accuracy(options.accPercent);
|
|
2074
2156
|
if (this.computedAccuracy.n300 <= 0) {
|
|
@@ -2083,9 +2165,9 @@ class PerformanceCalculator {
|
|
|
2083
2165
|
}
|
|
2084
2166
|
else {
|
|
2085
2167
|
this.computedAccuracy = new osuBase.Accuracy({
|
|
2086
|
-
percent: options
|
|
2168
|
+
percent: options === null || options === void 0 ? void 0 : options.accPercent,
|
|
2087
2169
|
nobjects: this.totalHits,
|
|
2088
|
-
nmiss: options
|
|
2170
|
+
nmiss: (options === null || options === void 0 ? void 0 : options.miss) || 0,
|
|
2089
2171
|
});
|
|
2090
2172
|
}
|
|
2091
2173
|
this.effectiveMissCount = this.calculateEffectiveMissCount(combo, maxCombo);
|
|
@@ -2119,7 +2201,7 @@ class PerformanceCalculator {
|
|
|
2119
2201
|
if (this.difficultyAttributes.sliderCount > 0) {
|
|
2120
2202
|
// We assume 15% of sliders in a beatmap are difficult since there's no way to tell from the performance calculator.
|
|
2121
2203
|
const estimateDifficultSliders = this.difficultyAttributes.sliderCount * 0.15;
|
|
2122
|
-
const estimateSliderEndsDropped = osuBase.MathUtils.clamp(Math.min(this.computedAccuracy.
|
|
2204
|
+
const estimateSliderEndsDropped = osuBase.MathUtils.clamp(Math.min(this.computedAccuracy.n100 +
|
|
2123
2205
|
this.computedAccuracy.n50 +
|
|
2124
2206
|
this.computedAccuracy.nmiss, maxCombo - combo), 0, estimateDifficultSliders);
|
|
2125
2207
|
this.sliderNerfFactor =
|
|
@@ -2147,32 +2229,52 @@ class PerformanceCalculator {
|
|
|
2147
2229
|
}
|
|
2148
2230
|
return Math.max(this.computedAccuracy.nmiss, comboBasedMissCount);
|
|
2149
2231
|
}
|
|
2232
|
+
/**
|
|
2233
|
+
* Determines whether an attribute is a cacheable attribute.
|
|
2234
|
+
*
|
|
2235
|
+
* @param attributes The attributes to check.
|
|
2236
|
+
* @returns Whether the attributes are cacheable.
|
|
2237
|
+
*/
|
|
2238
|
+
isCacheableAttribute(attributes) {
|
|
2239
|
+
return typeof attributes.mods === "string";
|
|
2240
|
+
}
|
|
2150
2241
|
}
|
|
2151
2242
|
|
|
2152
2243
|
/**
|
|
2153
2244
|
* A performance points calculator that calculates performance points for osu!droid gamemode.
|
|
2154
2245
|
*/
|
|
2155
2246
|
class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2247
|
+
constructor() {
|
|
2248
|
+
super(...arguments);
|
|
2249
|
+
/**
|
|
2250
|
+
* The aim performance value.
|
|
2251
|
+
*/
|
|
2252
|
+
this.aim = 0;
|
|
2253
|
+
/**
|
|
2254
|
+
* The tap performance value.
|
|
2255
|
+
*/
|
|
2256
|
+
this.tap = 0;
|
|
2257
|
+
/**
|
|
2258
|
+
* The accuracy performance value.
|
|
2259
|
+
*/
|
|
2260
|
+
this.accuracy = 0;
|
|
2261
|
+
/**
|
|
2262
|
+
* The flashlight performance value.
|
|
2263
|
+
*/
|
|
2264
|
+
this.flashlight = 0;
|
|
2265
|
+
/**
|
|
2266
|
+
* The visual performance value.
|
|
2267
|
+
*/
|
|
2268
|
+
this.visual = 0;
|
|
2269
|
+
this.finalMultiplier = 1.24;
|
|
2270
|
+
this.mode = osuBase.Modes.droid;
|
|
2271
|
+
this._aimSliderCheesePenalty = 1;
|
|
2272
|
+
this._flashlightSliderCheesePenalty = 1;
|
|
2273
|
+
this._visualSliderCheesePenalty = 1;
|
|
2274
|
+
this._tapPenalty = 1;
|
|
2275
|
+
this._deviation = 0;
|
|
2276
|
+
this._tapDeviation = 0;
|
|
2277
|
+
}
|
|
2176
2278
|
/**
|
|
2177
2279
|
* The penalty used to penalize the tap performance value.
|
|
2178
2280
|
*
|
|
@@ -2217,22 +2319,6 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2217
2319
|
get visualSliderCheesePenalty() {
|
|
2218
2320
|
return this._visualSliderCheesePenalty;
|
|
2219
2321
|
}
|
|
2220
|
-
difficultyAttributes;
|
|
2221
|
-
finalMultiplier = 1.24;
|
|
2222
|
-
mode = osuBase.Modes.droid;
|
|
2223
|
-
_aimSliderCheesePenalty = 1;
|
|
2224
|
-
_flashlightSliderCheesePenalty = 1;
|
|
2225
|
-
_visualSliderCheesePenalty = 1;
|
|
2226
|
-
_tapPenalty = 1;
|
|
2227
|
-
_deviation = 0;
|
|
2228
|
-
_tapDeviation = 0;
|
|
2229
|
-
/**
|
|
2230
|
-
* @param difficultyAttributes The difficulty attributes to calculate.
|
|
2231
|
-
*/
|
|
2232
|
-
constructor(difficultyAttributes) {
|
|
2233
|
-
super();
|
|
2234
|
-
this.difficultyAttributes = osuBase.Utils.deepCopy(difficultyAttributes);
|
|
2235
|
-
}
|
|
2236
2322
|
/**
|
|
2237
2323
|
* Applies a tap penalty value to this calculator.
|
|
2238
2324
|
*
|
|
@@ -2247,9 +2333,9 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2247
2333
|
if (value === this._tapPenalty) {
|
|
2248
2334
|
return;
|
|
2249
2335
|
}
|
|
2250
|
-
this.tap *= this._tapPenalty / value;
|
|
2251
2336
|
this._tapPenalty = value;
|
|
2252
|
-
this.
|
|
2337
|
+
this.tap = this.calculateTapValue();
|
|
2338
|
+
this.total = this.calculateTotalValue();
|
|
2253
2339
|
}
|
|
2254
2340
|
/**
|
|
2255
2341
|
* Applies an aim slider cheese penalty value to this calculator.
|
|
@@ -2269,8 +2355,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2269
2355
|
return;
|
|
2270
2356
|
}
|
|
2271
2357
|
this._aimSliderCheesePenalty = value;
|
|
2272
|
-
this.calculateAimValue();
|
|
2273
|
-
this.calculateTotalValue();
|
|
2358
|
+
this.aim = this.calculateAimValue();
|
|
2359
|
+
this.total = this.calculateTotalValue();
|
|
2274
2360
|
}
|
|
2275
2361
|
/**
|
|
2276
2362
|
* Applies a flashlight slider cheese penalty value to this calculator.
|
|
@@ -2290,8 +2376,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2290
2376
|
return;
|
|
2291
2377
|
}
|
|
2292
2378
|
this._flashlightSliderCheesePenalty = value;
|
|
2293
|
-
this.calculateFlashlightValue();
|
|
2294
|
-
this.calculateTotalValue();
|
|
2379
|
+
this.flashlight = this.calculateFlashlightValue();
|
|
2380
|
+
this.total = this.calculateTotalValue();
|
|
2295
2381
|
}
|
|
2296
2382
|
/**
|
|
2297
2383
|
* Applies a visual slider cheese penalty value to this calculator.
|
|
@@ -2311,80 +2397,96 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2311
2397
|
return;
|
|
2312
2398
|
}
|
|
2313
2399
|
this._visualSliderCheesePenalty = value;
|
|
2314
|
-
this.calculateVisualValue();
|
|
2315
|
-
this.calculateTotalValue();
|
|
2400
|
+
this.visual = this.calculateVisualValue();
|
|
2401
|
+
this.total = this.calculateTotalValue();
|
|
2316
2402
|
}
|
|
2317
2403
|
calculateValues() {
|
|
2318
2404
|
this._deviation = this.calculateDeviation();
|
|
2319
2405
|
this._tapDeviation = this.calculateTapDeviation();
|
|
2320
|
-
this.calculateAimValue();
|
|
2321
|
-
this.calculateTapValue();
|
|
2322
|
-
this.calculateAccuracyValue();
|
|
2323
|
-
this.calculateFlashlightValue();
|
|
2324
|
-
this.calculateVisualValue();
|
|
2406
|
+
this.aim = this.calculateAimValue();
|
|
2407
|
+
this.tap = this.calculateTapValue();
|
|
2408
|
+
this.accuracy = this.calculateAccuracyValue();
|
|
2409
|
+
this.flashlight = this.calculateFlashlightValue();
|
|
2410
|
+
this.visual = this.calculateVisualValue();
|
|
2325
2411
|
}
|
|
2326
2412
|
calculateTotalValue() {
|
|
2327
|
-
this.
|
|
2328
|
-
Math.pow(
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
Math.pow(this.visual, 1.1), 1 / 1.1) * this.finalMultiplier;
|
|
2413
|
+
return (Math.pow(Math.pow(this.aim, 1.1) +
|
|
2414
|
+
Math.pow(this.tap, 1.1) +
|
|
2415
|
+
Math.pow(this.accuracy, 1.1) +
|
|
2416
|
+
Math.pow(this.flashlight, 1.1) +
|
|
2417
|
+
Math.pow(this.visual, 1.1), 1 / 1.1) * this.finalMultiplier);
|
|
2333
2418
|
}
|
|
2334
2419
|
handleOptions(options) {
|
|
2335
|
-
|
|
2336
|
-
this.
|
|
2420
|
+
var _a, _b, _c, _d;
|
|
2421
|
+
this._tapPenalty = (_a = options === null || options === void 0 ? void 0 : options.tapPenalty) !== null && _a !== void 0 ? _a : 1;
|
|
2422
|
+
this._aimSliderCheesePenalty = (_b = options === null || options === void 0 ? void 0 : options.aimSliderCheesePenalty) !== null && _b !== void 0 ? _b : 1;
|
|
2337
2423
|
this._flashlightSliderCheesePenalty =
|
|
2338
|
-
options
|
|
2424
|
+
(_c = options === null || options === void 0 ? void 0 : options.flashlightSliderCheesePenalty) !== null && _c !== void 0 ? _c : 1;
|
|
2339
2425
|
this._visualSliderCheesePenalty =
|
|
2340
|
-
options
|
|
2426
|
+
(_d = options === null || options === void 0 ? void 0 : options.visualSliderCheesePenalty) !== null && _d !== void 0 ? _d : 1;
|
|
2341
2427
|
super.handleOptions(options);
|
|
2342
2428
|
}
|
|
2343
2429
|
/**
|
|
2344
2430
|
* Calculates the aim performance value of the beatmap.
|
|
2345
2431
|
*/
|
|
2346
2432
|
calculateAimValue() {
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
this.aim *=
|
|
2352
|
-
0.97 *
|
|
2353
|
-
Math.pow(1 -
|
|
2354
|
-
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
|
|
2355
|
-
}
|
|
2356
|
-
// Combo scaling
|
|
2357
|
-
this.aim *= this.comboPenalty;
|
|
2433
|
+
let aimValue = this.baseValue(Math.pow(this.difficultyAttributes.aimDifficulty, 0.8));
|
|
2434
|
+
aimValue *= Math.min(this.calculateStrainBasedMissPenalty(this.difficultyAttributes.aimDifficultStrainCount), this.proportionalMissPenalty);
|
|
2435
|
+
// Scale the aim value with estimated full combo deviation.
|
|
2436
|
+
aimValue *= this.calculateDeviationBasedLengthScaling();
|
|
2358
2437
|
// Scale the aim value with slider factor to nerf very likely dropped sliderends.
|
|
2359
|
-
|
|
2438
|
+
aimValue *= this.sliderNerfFactor;
|
|
2360
2439
|
// Scale the aim value with slider cheese penalty.
|
|
2361
|
-
|
|
2440
|
+
aimValue *= this._aimSliderCheesePenalty;
|
|
2362
2441
|
// Scale the aim value with deviation.
|
|
2363
|
-
|
|
2364
|
-
1.
|
|
2365
|
-
Math.pow(osuBase.ErrorFunction.erf(
|
|
2442
|
+
aimValue *=
|
|
2443
|
+
1.025 *
|
|
2444
|
+
Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * this._deviation)), 0.475);
|
|
2445
|
+
// OD 7 SS stays the same.
|
|
2446
|
+
aimValue *= 0.98 + Math.pow(7, 2) / 2500;
|
|
2447
|
+
return aimValue;
|
|
2366
2448
|
}
|
|
2367
2449
|
/**
|
|
2368
2450
|
* Calculates the tap performance value of the beatmap.
|
|
2369
2451
|
*/
|
|
2370
2452
|
calculateTapValue() {
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
//
|
|
2381
|
-
this
|
|
2453
|
+
let tapValue = this.baseValue(this.difficultyAttributes.tapDifficulty);
|
|
2454
|
+
tapValue *= this.calculateStrainBasedMissPenalty(this.difficultyAttributes.tapDifficultStrainCount);
|
|
2455
|
+
// Scale the tap value with estimated full combo deviation.
|
|
2456
|
+
// Consider notes that are difficult to tap with respect to other notes, but
|
|
2457
|
+
// also cap the note count to prevent buffing filler patterns.
|
|
2458
|
+
tapValue *= this.calculateDeviationBasedLengthScaling(Math.min(this.difficultyAttributes.speedNoteCount, this.totalHits / 1.45));
|
|
2459
|
+
// Normalize the deviation to 300 BPM.
|
|
2460
|
+
const normalizedDeviation = this.tapDeviation *
|
|
2461
|
+
Math.max(1, 50 / this.difficultyAttributes.averageSpeedDeltaTime);
|
|
2462
|
+
// We expect the player to get 7500/x deviation when doubletapping x BPM.
|
|
2463
|
+
// Using this expectation, we penalize scores with deviation above 25.
|
|
2464
|
+
const averageBPM = 60000 / 4 / this.difficultyAttributes.averageSpeedDeltaTime;
|
|
2465
|
+
const adjustedDeviation = normalizedDeviation *
|
|
2466
|
+
(1 +
|
|
2467
|
+
1 /
|
|
2468
|
+
(1 +
|
|
2469
|
+
Math.exp(-(normalizedDeviation - 7500 / averageBPM) /
|
|
2470
|
+
((2 * 300) / averageBPM))));
|
|
2382
2471
|
// Scale the tap value with tap deviation.
|
|
2383
|
-
|
|
2384
|
-
1.
|
|
2385
|
-
Math.pow(osuBase.ErrorFunction.erf(
|
|
2472
|
+
tapValue *=
|
|
2473
|
+
1.05 *
|
|
2474
|
+
Math.pow(osuBase.ErrorFunction.erf(20 / (Math.SQRT2 * adjustedDeviation)), 0.6);
|
|
2475
|
+
// Additional scaling for tap value based on average BPM and how "vibroable" the beatmap is.
|
|
2476
|
+
// Higher BPMs require more precise tapping. When the deviation is too high,
|
|
2477
|
+
// it can be assumed that the player taps invariant to rhythm.
|
|
2478
|
+
// We harshen the punishment for such scenario.
|
|
2479
|
+
tapValue *=
|
|
2480
|
+
(1 - Math.pow(this.difficultyAttributes.vibroFactor, 6)) /
|
|
2481
|
+
(1 +
|
|
2482
|
+
Math.exp((this._tapDeviation - 7500 / averageBPM) /
|
|
2483
|
+
((2 * 300) / averageBPM))) +
|
|
2484
|
+
Math.pow(this.difficultyAttributes.vibroFactor, 6);
|
|
2386
2485
|
// Scale the tap value with three-fingered penalty.
|
|
2387
|
-
|
|
2486
|
+
tapValue /= this._tapPenalty;
|
|
2487
|
+
// OD 8 SS stays the same.
|
|
2488
|
+
tapValue *= 0.95 + Math.pow(8, 2) / 750;
|
|
2489
|
+
return tapValue;
|
|
2388
2490
|
}
|
|
2389
2491
|
/**
|
|
2390
2492
|
* Calculates the accuracy performance value of the beatmap.
|
|
@@ -2392,84 +2494,128 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2392
2494
|
calculateAccuracyValue() {
|
|
2393
2495
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax) ||
|
|
2394
2496
|
this.totalSuccessfulHits === 0) {
|
|
2395
|
-
|
|
2396
|
-
return;
|
|
2497
|
+
return 0;
|
|
2397
2498
|
}
|
|
2398
|
-
this.
|
|
2399
|
-
650 *
|
|
2400
|
-
Math.exp(-0.125 * this._deviation) *
|
|
2401
|
-
// The following function is to give higher reward for deviations lower than 25 (250 UR).
|
|
2402
|
-
(15 / (this._deviation + 15) + 0.65);
|
|
2403
|
-
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
|
|
2499
|
+
let accuracyValue = 650 * Math.exp(-0.1 * this._deviation);
|
|
2404
2500
|
const ncircles = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModScoreV2)
|
|
2405
2501
|
? this.totalHits - this.difficultyAttributes.spinnerCount
|
|
2406
2502
|
: this.difficultyAttributes.hitCircleCount;
|
|
2407
|
-
|
|
2503
|
+
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
|
|
2504
|
+
accuracyValue *= Math.min(1.15, Math.sqrt(Math.log(1 + ((Math.E - 1) * ncircles) / 1000)));
|
|
2408
2505
|
// Scale the accuracy value with rhythm complexity.
|
|
2409
|
-
|
|
2506
|
+
accuracyValue *=
|
|
2410
2507
|
1.5 /
|
|
2411
2508
|
(1 +
|
|
2412
2509
|
Math.exp(-(this.difficultyAttributes.rhythmDifficulty - 1) / 2));
|
|
2510
|
+
// Penalize accuracy pp after the first miss.
|
|
2511
|
+
accuracyValue *= Math.pow(0.97, Math.max(0, this.effectiveMissCount - 1));
|
|
2413
2512
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
|
|
2414
|
-
|
|
2513
|
+
accuracyValue *= 1.02;
|
|
2415
2514
|
}
|
|
2515
|
+
return accuracyValue;
|
|
2416
2516
|
}
|
|
2417
2517
|
/**
|
|
2418
2518
|
* Calculates the flashlight performance value of the beatmap.
|
|
2419
2519
|
*/
|
|
2420
2520
|
calculateFlashlightValue() {
|
|
2421
2521
|
if (!this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
|
|
2422
|
-
|
|
2423
|
-
return;
|
|
2424
|
-
}
|
|
2425
|
-
this.flashlight =
|
|
2426
|
-
Math.pow(this.difficultyAttributes.flashlightDifficulty, 1.6) * 25;
|
|
2427
|
-
// Combo scaling
|
|
2428
|
-
this.flashlight *= this.comboPenalty;
|
|
2429
|
-
if (this.effectiveMissCount > 0) {
|
|
2430
|
-
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
2431
|
-
this.flashlight *=
|
|
2432
|
-
0.97 *
|
|
2433
|
-
Math.pow(1 -
|
|
2434
|
-
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
|
|
2522
|
+
return 0;
|
|
2435
2523
|
}
|
|
2524
|
+
let flashlightValue = Math.pow(this.difficultyAttributes.flashlightDifficulty, 1.6) * 25;
|
|
2525
|
+
flashlightValue *= Math.min(this.calculateStrainBasedMissPenalty(this.difficultyAttributes.flashlightDifficultStrainCount), this.proportionalMissPenalty);
|
|
2436
2526
|
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
2437
|
-
|
|
2527
|
+
flashlightValue *=
|
|
2438
2528
|
0.7 +
|
|
2439
2529
|
0.1 * Math.min(1, this.totalHits / 200) +
|
|
2440
2530
|
(this.totalHits > 200
|
|
2441
2531
|
? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
|
|
2442
2532
|
: 0);
|
|
2443
2533
|
// Scale the flashlight value with slider cheese penalty.
|
|
2444
|
-
|
|
2534
|
+
flashlightValue *= this._flashlightSliderCheesePenalty;
|
|
2445
2535
|
// Scale the flashlight value with deviation.
|
|
2446
|
-
|
|
2536
|
+
flashlightValue *= osuBase.ErrorFunction.erf(50 / (Math.SQRT2 * this._deviation));
|
|
2537
|
+
return flashlightValue;
|
|
2447
2538
|
}
|
|
2448
2539
|
/**
|
|
2449
2540
|
* Calculates the visual performance value of the beatmap.
|
|
2450
2541
|
*/
|
|
2451
2542
|
calculateVisualValue() {
|
|
2452
|
-
this.
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
0.97 *
|
|
2458
|
-
Math.pow(1 -
|
|
2459
|
-
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
|
|
2460
|
-
}
|
|
2461
|
-
// Combo scaling
|
|
2462
|
-
this.visual *= this.comboPenalty;
|
|
2463
|
-
// Scale the visual value with object count to penalize short maps.
|
|
2464
|
-
this.visual *= Math.min(1, 1.650668 +
|
|
2465
|
-
(0.4845796 - 1.650668) /
|
|
2466
|
-
(1 + Math.pow(this.totalHits / 817.9306, 1.147469)));
|
|
2543
|
+
let visualValue = Math.pow(this.difficultyAttributes.visualDifficulty, 1.6) * 22.5;
|
|
2544
|
+
visualValue *= Math.min(this.calculateStrainBasedMissPenalty(this.difficultyAttributes.visualDifficultStrainCount), this.proportionalMissPenalty);
|
|
2545
|
+
// Scale the visual value with estimated full combo deviation.
|
|
2546
|
+
// As visual is easily "bypassable" with memorization, punish for memorization.
|
|
2547
|
+
visualValue *= this.calculateDeviationBasedLengthScaling(undefined, true);
|
|
2467
2548
|
// Scale the visual value with slider cheese penalty.
|
|
2468
|
-
|
|
2549
|
+
visualValue *= this._visualSliderCheesePenalty;
|
|
2469
2550
|
// Scale the visual value with deviation.
|
|
2470
|
-
|
|
2471
|
-
1.
|
|
2472
|
-
Math.pow(osuBase.ErrorFunction.erf(
|
|
2551
|
+
visualValue *=
|
|
2552
|
+
1.05 *
|
|
2553
|
+
Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * this._deviation)), 0.775);
|
|
2554
|
+
// OD 5 SS stays the same.
|
|
2555
|
+
visualValue *= 0.98 + Math.pow(5, 2) / 2500;
|
|
2556
|
+
return visualValue;
|
|
2557
|
+
}
|
|
2558
|
+
/**
|
|
2559
|
+
* Calculates a strain-based miss penalty.
|
|
2560
|
+
*
|
|
2561
|
+
* Strain-based miss penalty assumes that a player will miss on the hardest parts of a map,
|
|
2562
|
+
* so we use the amount of relatively difficult sections to adjust miss penalty
|
|
2563
|
+
* to make it more punishing on maps with lower amount of hard sections.
|
|
2564
|
+
*/
|
|
2565
|
+
calculateStrainBasedMissPenalty(difficultStrainCount) {
|
|
2566
|
+
if (this.effectiveMissCount === 0) {
|
|
2567
|
+
return 1;
|
|
2568
|
+
}
|
|
2569
|
+
return (0.96 /
|
|
2570
|
+
(this.effectiveMissCount /
|
|
2571
|
+
(4 * Math.pow(Math.log(difficultStrainCount), 0.94)) +
|
|
2572
|
+
1));
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* The object-based proportional miss penalty.
|
|
2576
|
+
*/
|
|
2577
|
+
get proportionalMissPenalty() {
|
|
2578
|
+
if (this.effectiveMissCount === 0) {
|
|
2579
|
+
return 1;
|
|
2580
|
+
}
|
|
2581
|
+
const missProportion = (this.totalHits - this.effectiveMissCount) / (this.totalHits + 1);
|
|
2582
|
+
const noMissProportion = this.totalHits / (this.totalHits + 1);
|
|
2583
|
+
return (
|
|
2584
|
+
// Aim deviation-based scale.
|
|
2585
|
+
(osuBase.ErrorFunction.erfInv(missProportion) /
|
|
2586
|
+
osuBase.ErrorFunction.erfInv(noMissProportion)) *
|
|
2587
|
+
// Cheesing-based scale (i.e. 50% misses is deliberately only hitting each other
|
|
2588
|
+
// note, 90% misses is deliberately only hitting 1 note every 10 notes).
|
|
2589
|
+
Math.pow(missProportion, 8));
|
|
2590
|
+
}
|
|
2591
|
+
/**
|
|
2592
|
+
* Calculates the object-based length scaling based on the deviation of a player for a full
|
|
2593
|
+
* combo in this beatmap, taking retries into account.
|
|
2594
|
+
*
|
|
2595
|
+
* @param objectCount The amount of objects to be considered. Defaults to the amount of
|
|
2596
|
+
* objects in this beatmap.
|
|
2597
|
+
* @param punishForMemorization Whether to punish the deviation for memorization. Defaults to `false`.
|
|
2598
|
+
*/
|
|
2599
|
+
calculateDeviationBasedLengthScaling(objectCount = this.totalHits, punishForMemorization = false) {
|
|
2600
|
+
// Assume a sample proportion of hits for a full combo to be `(n - 0.5) / n` due to
|
|
2601
|
+
// continuity correction, where `n` is the object count.
|
|
2602
|
+
const calculateProportion = (notes) => (notes - 0.5) / notes;
|
|
2603
|
+
// Keeping `x` notes as the benchmark, assume that a player will retry a beatmap
|
|
2604
|
+
// `max(1, x/n)` times relative to an `x`-note beatmap.
|
|
2605
|
+
const benchmarkNotes = 700;
|
|
2606
|
+
// Calculate the proportion equivalent to the bottom half of retry count percentile of
|
|
2607
|
+
// scores and take it as the player's "real" proportion.
|
|
2608
|
+
const retryProportion = (proportion, notes, tries) => proportion +
|
|
2609
|
+
Math.sqrt((2 * proportion * (1 - proportion)) / notes) *
|
|
2610
|
+
osuBase.ErrorFunction.erfInv(1 / tries - 1);
|
|
2611
|
+
// Using the proportion, we calculate the deviation based off that proportion and again
|
|
2612
|
+
// compared to the hit deviation for proportion `(n - 0.5) / n`.
|
|
2613
|
+
let multiplier = Math.max(0, osuBase.ErrorFunction.erfInv(retryProportion(calculateProportion(objectCount), objectCount, Math.max(1, benchmarkNotes / objectCount))) / osuBase.ErrorFunction.erfInv(calculateProportion(benchmarkNotes)) || 0);
|
|
2614
|
+
// Punish for memorization if needed.
|
|
2615
|
+
if (punishForMemorization) {
|
|
2616
|
+
multiplier *= Math.min(1, Math.sqrt(objectCount / benchmarkNotes));
|
|
2617
|
+
}
|
|
2618
|
+
return multiplier;
|
|
2473
2619
|
}
|
|
2474
2620
|
/**
|
|
2475
2621
|
* Estimates the player's tap deviation based on the OD, number of circles and sliders,
|
|
@@ -2490,40 +2636,65 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2490
2636
|
return Number.POSITIVE_INFINITY;
|
|
2491
2637
|
}
|
|
2492
2638
|
const hitWindow300 = new osuBase.OsuHitWindow(this.difficultyAttributes.overallDifficulty).hitWindowFor300();
|
|
2493
|
-
// Obtain the 50 hit window for droid.
|
|
2494
|
-
const
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
const
|
|
2499
|
-
|
|
2500
|
-
const
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
//
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2639
|
+
// Obtain the 50 and 100 hit window for droid.
|
|
2640
|
+
const isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
2641
|
+
const droidHitWindow = new osuBase.DroidHitWindow(osuBase.DroidHitWindow.hitWindow300ToOD(hitWindow300 * this.difficultyAttributes.clockRate, isPrecise));
|
|
2642
|
+
const hitWindow50 = droidHitWindow.hitWindowFor50(isPrecise) /
|
|
2643
|
+
this.difficultyAttributes.clockRate;
|
|
2644
|
+
const hitWindow100 = droidHitWindow.hitWindowFor100(isPrecise) /
|
|
2645
|
+
this.difficultyAttributes.clockRate;
|
|
2646
|
+
const { n100, n50, nmiss } = this.computedAccuracy;
|
|
2647
|
+
const circleCount = this.difficultyAttributes.hitCircleCount;
|
|
2648
|
+
const missCountCircles = Math.min(nmiss, circleCount);
|
|
2649
|
+
const mehCountCircles = Math.min(n50, circleCount - missCountCircles);
|
|
2650
|
+
const okCountCircles = Math.min(n100, circleCount - missCountCircles - mehCountCircles);
|
|
2651
|
+
const greatCountCircles = Math.max(0, circleCount - missCountCircles - mehCountCircles - okCountCircles);
|
|
2652
|
+
// Assume 100s, 50s, and misses happen on circles. If there are less non-300s on circles than 300s,
|
|
2653
|
+
// compute the deviation on circles.
|
|
2654
|
+
if (greatCountCircles > 0) {
|
|
2655
|
+
// The probability that a player hits a circle is unknown, but we can estimate it to be
|
|
2656
|
+
// the number of greats on circles divided by the number of circles, and then add one
|
|
2657
|
+
// to the number of circles as a bias correction.
|
|
2658
|
+
const greatProbabilityCircle = greatCountCircles /
|
|
2659
|
+
(circleCount - missCountCircles - mehCountCircles + 1);
|
|
2660
|
+
// Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed.
|
|
2661
|
+
// Begin with the normal distribution first.
|
|
2662
|
+
let deviationOnCircles = hitWindow300 /
|
|
2663
|
+
(Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbabilityCircle));
|
|
2664
|
+
deviationOnCircles *= Math.sqrt(1 -
|
|
2665
|
+
(Math.sqrt(2 / Math.PI) *
|
|
2666
|
+
hitWindow100 *
|
|
2667
|
+
Math.exp(-0.5 *
|
|
2668
|
+
Math.pow(hitWindow100 / deviationOnCircles, 2))) /
|
|
2669
|
+
(deviationOnCircles *
|
|
2670
|
+
osuBase.ErrorFunction.erf(hitWindow100 /
|
|
2671
|
+
(Math.SQRT2 * deviationOnCircles))));
|
|
2672
|
+
// Then compute the variance for 50s.
|
|
2673
|
+
const mehVariance = (hitWindow50 * hitWindow50 +
|
|
2674
|
+
hitWindow100 * hitWindow50 +
|
|
2675
|
+
hitWindow100 * hitWindow100) /
|
|
2676
|
+
3;
|
|
2677
|
+
// Find the total deviation.
|
|
2678
|
+
deviationOnCircles = Math.sqrt(((greatCountCircles + okCountCircles) *
|
|
2679
|
+
Math.pow(deviationOnCircles, 2) +
|
|
2680
|
+
mehCountCircles * mehVariance) /
|
|
2681
|
+
(greatCountCircles + okCountCircles + mehCountCircles));
|
|
2682
|
+
return deviationOnCircles;
|
|
2683
|
+
}
|
|
2684
|
+
// If there are more non-300s than there are circles, compute the deviation on sliders instead.
|
|
2685
|
+
// Here, all that matters is whether or not the slider was missed, since it is impossible
|
|
2686
|
+
// to get a 100 or 50 on a slider by mis-tapping it.
|
|
2687
|
+
const sliderCount = this.difficultyAttributes.sliderCount;
|
|
2688
|
+
const missCountSliders = Math.min(sliderCount, nmiss - missCountCircles);
|
|
2689
|
+
const greatCountSliders = sliderCount - missCountSliders;
|
|
2690
|
+
// We only get here if nothing was hit. In this case, there is no estimate for deviation.
|
|
2691
|
+
// Note that this is never negative, so checking if this is only equal to 0 makes sense.
|
|
2692
|
+
if (greatCountSliders === 0) {
|
|
2520
2693
|
return Number.POSITIVE_INFINITY;
|
|
2521
2694
|
}
|
|
2522
|
-
const
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
(Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbabilitySlider));
|
|
2526
|
-
return Math.min(deviationOnCircles, deviationOnSliders);
|
|
2695
|
+
const greatProbabilitySlider = greatCountSliders / (sliderCount + 1);
|
|
2696
|
+
return (hitWindow50 /
|
|
2697
|
+
(Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbabilitySlider)));
|
|
2527
2698
|
}
|
|
2528
2699
|
/**
|
|
2529
2700
|
* Does the same as {@link calculateDeviation}, but only for notes and inaccuracies that are relevant to tap difficulty.
|
|
@@ -2535,14 +2706,59 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2535
2706
|
if (this.totalSuccessfulHits === 0) {
|
|
2536
2707
|
return Number.POSITIVE_INFINITY;
|
|
2537
2708
|
}
|
|
2538
|
-
const
|
|
2539
|
-
const
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
const
|
|
2545
|
-
|
|
2709
|
+
const { speedNoteCount, clockRate, overallDifficulty } = this.difficultyAttributes;
|
|
2710
|
+
const hitWindow300 = new osuBase.OsuHitWindow(overallDifficulty).hitWindowFor300();
|
|
2711
|
+
// Obtain the 50 and 100 hit window for droid.
|
|
2712
|
+
const isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
2713
|
+
const droidHitWindow = new osuBase.DroidHitWindow(osuBase.DroidHitWindow.hitWindow300ToOD(hitWindow300 * clockRate, isPrecise));
|
|
2714
|
+
const hitWindow50 = droidHitWindow.hitWindowFor50(isPrecise) / clockRate;
|
|
2715
|
+
const hitWindow100 = droidHitWindow.hitWindowFor100(isPrecise) / clockRate;
|
|
2716
|
+
const { n100, n50, nmiss } = this.computedAccuracy;
|
|
2717
|
+
// Assume a fixed ratio of non-300s hit in speed notes based on speed note count ratio and OD.
|
|
2718
|
+
// Graph: https://www.desmos.com/calculator/iskvgjkxr4
|
|
2719
|
+
const speedNoteRatio = speedNoteCount / this.totalHits;
|
|
2720
|
+
const nonGreatCount = n100 + n50 + nmiss;
|
|
2721
|
+
const nonGreatRatio = 1 -
|
|
2722
|
+
(Math.pow(Math.exp(Math.sqrt(hitWindow300)) + 1, 1 - speedNoteRatio) -
|
|
2723
|
+
1) /
|
|
2724
|
+
Math.exp(Math.sqrt(hitWindow300));
|
|
2725
|
+
const relevantCountGreat = Math.max(0, speedNoteCount - nonGreatCount * nonGreatRatio);
|
|
2726
|
+
const relevantCountOk = n100 * nonGreatRatio;
|
|
2727
|
+
const relevantCountMeh = n50 * nonGreatRatio;
|
|
2728
|
+
const relevantCountMiss = nmiss * nonGreatRatio;
|
|
2729
|
+
// Assume 100s, 50s, and misses happen on circles. If there are less non-300s on circles than 300s,
|
|
2730
|
+
// compute the deviation on circles.
|
|
2731
|
+
if (relevantCountGreat > 0) {
|
|
2732
|
+
// The probability that a player hits a circle is unknown, but we can estimate it to be
|
|
2733
|
+
// the number of greats on circles divided by the number of circles, and then add one
|
|
2734
|
+
// to the number of circles as a bias correction.
|
|
2735
|
+
const greatProbabilityCircle = relevantCountGreat /
|
|
2736
|
+
(speedNoteCount - relevantCountMiss - relevantCountMeh + 1);
|
|
2737
|
+
// Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed.
|
|
2738
|
+
// Begin with the normal distribution first.
|
|
2739
|
+
let deviationOnCircles = hitWindow300 /
|
|
2740
|
+
(Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbabilityCircle));
|
|
2741
|
+
deviationOnCircles *= Math.sqrt(1 -
|
|
2742
|
+
(Math.sqrt(2 / Math.PI) *
|
|
2743
|
+
hitWindow100 *
|
|
2744
|
+
Math.exp(-0.5 *
|
|
2745
|
+
Math.pow(hitWindow100 / deviationOnCircles, 2))) /
|
|
2746
|
+
(deviationOnCircles *
|
|
2747
|
+
osuBase.ErrorFunction.erf(hitWindow100 /
|
|
2748
|
+
(Math.SQRT2 * deviationOnCircles))));
|
|
2749
|
+
// Then compute the variance for 50s.
|
|
2750
|
+
const mehVariance = (hitWindow50 * hitWindow50 +
|
|
2751
|
+
hitWindow100 * hitWindow50 +
|
|
2752
|
+
hitWindow100 * hitWindow100) /
|
|
2753
|
+
3;
|
|
2754
|
+
// Find the total deviation.
|
|
2755
|
+
deviationOnCircles = Math.sqrt(((relevantCountGreat + relevantCountOk) *
|
|
2756
|
+
Math.pow(deviationOnCircles, 2) +
|
|
2757
|
+
relevantCountMeh * mehVariance) /
|
|
2758
|
+
(relevantCountGreat + relevantCountOk + relevantCountMeh));
|
|
2759
|
+
return deviationOnCircles;
|
|
2760
|
+
}
|
|
2761
|
+
return Number.POSITIVE_INFINITY;
|
|
2546
2762
|
}
|
|
2547
2763
|
toString() {
|
|
2548
2764
|
return (this.total.toFixed(2) +
|
|
@@ -2565,16 +2781,13 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2565
2781
|
* and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
|
|
2566
2782
|
*/
|
|
2567
2783
|
class OsuSkill extends StrainSkill {
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
* The final multiplier to be applied to the final difficulty value after all other calculations.
|
|
2576
|
-
*/
|
|
2577
|
-
difficultyMultiplier = OsuSkill.defaultDifficultyMultiplier;
|
|
2784
|
+
constructor() {
|
|
2785
|
+
super(...arguments);
|
|
2786
|
+
/**
|
|
2787
|
+
* The final multiplier to be applied to the final difficulty value after all other calculations.
|
|
2788
|
+
*/
|
|
2789
|
+
this.difficultyMultiplier = OsuSkill.defaultDifficultyMultiplier;
|
|
2790
|
+
}
|
|
2578
2791
|
difficultyValue() {
|
|
2579
2792
|
const strains = this.strainPeaks
|
|
2580
2793
|
.slice()
|
|
@@ -2602,6 +2815,12 @@ class OsuSkill extends StrainSkill {
|
|
|
2602
2815
|
return difficulty * this.difficultyMultiplier;
|
|
2603
2816
|
}
|
|
2604
2817
|
}
|
|
2818
|
+
/**
|
|
2819
|
+
* The default multiplier applied to the final difficulty value after all other calculations.
|
|
2820
|
+
*
|
|
2821
|
+
* May be overridden via {@link difficultyMultiplier}.
|
|
2822
|
+
*/
|
|
2823
|
+
OsuSkill.defaultDifficultyMultiplier = 1.06;
|
|
2605
2824
|
|
|
2606
2825
|
/**
|
|
2607
2826
|
* An evaluator for calculating osu!standard Aim skill.
|
|
@@ -2622,7 +2841,7 @@ class OsuAimEvaluator extends AimEvaluator {
|
|
|
2622
2841
|
const last = current.previous(0);
|
|
2623
2842
|
if (current.object instanceof osuBase.Spinner ||
|
|
2624
2843
|
current.index <= 1 ||
|
|
2625
|
-
last
|
|
2844
|
+
(last === null || last === void 0 ? void 0 : last.object) instanceof osuBase.Spinner) {
|
|
2626
2845
|
return 0;
|
|
2627
2846
|
}
|
|
2628
2847
|
const lastLast = current.previous(1);
|
|
@@ -2729,36 +2948,37 @@ class OsuAimEvaluator extends AimEvaluator {
|
|
|
2729
2948
|
* Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
|
2730
2949
|
*/
|
|
2731
2950
|
class OsuAim extends OsuSkill {
|
|
2732
|
-
skillMultiplier = 23.55;
|
|
2733
|
-
strainDecayBase = 0.15;
|
|
2734
|
-
reducedSectionCount = 10;
|
|
2735
|
-
reducedSectionBaseline = 0.75;
|
|
2736
|
-
difficultyMultiplier = 1.06;
|
|
2737
|
-
decayWeight = 0.9;
|
|
2738
|
-
withSliders;
|
|
2739
2951
|
constructor(mods, withSliders) {
|
|
2740
2952
|
super(mods);
|
|
2953
|
+
this.strainDecayBase = 0.15;
|
|
2954
|
+
this.reducedSectionCount = 10;
|
|
2955
|
+
this.reducedSectionBaseline = 0.75;
|
|
2956
|
+
this.decayWeight = 0.9;
|
|
2957
|
+
this.currentAimStrain = 0;
|
|
2958
|
+
this.skillMultiplier = 23.55;
|
|
2741
2959
|
this.withSliders = withSliders;
|
|
2742
2960
|
}
|
|
2743
|
-
/**
|
|
2744
|
-
* @param current The hitobject to calculate.
|
|
2745
|
-
*/
|
|
2746
2961
|
strainValueAt(current) {
|
|
2747
|
-
this.
|
|
2748
|
-
this.
|
|
2962
|
+
this.currentAimStrain *= this.strainDecay(current.deltaTime);
|
|
2963
|
+
this.currentAimStrain +=
|
|
2749
2964
|
OsuAimEvaluator.evaluateDifficultyOf(current, this.withSliders) *
|
|
2750
2965
|
this.skillMultiplier;
|
|
2751
|
-
return this.
|
|
2966
|
+
return this.currentAimStrain;
|
|
2967
|
+
}
|
|
2968
|
+
calculateInitialStrain(time, current) {
|
|
2969
|
+
var _a, _b;
|
|
2970
|
+
return (this.currentAimStrain *
|
|
2971
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
2752
2972
|
}
|
|
2753
2973
|
/**
|
|
2754
2974
|
* @param current The hitobject to save to.
|
|
2755
2975
|
*/
|
|
2756
2976
|
saveToHitObject(current) {
|
|
2757
2977
|
if (this.withSliders) {
|
|
2758
|
-
current.aimStrainWithSliders = this.
|
|
2978
|
+
current.aimStrainWithSliders = this.currentAimStrain;
|
|
2759
2979
|
}
|
|
2760
2980
|
else {
|
|
2761
|
-
current.aimStrainWithoutSliders = this.
|
|
2981
|
+
current.aimStrainWithoutSliders = this.currentAimStrain;
|
|
2762
2982
|
}
|
|
2763
2983
|
}
|
|
2764
2984
|
}
|
|
@@ -2767,10 +2987,6 @@ class OsuAim extends OsuSkill {
|
|
|
2767
2987
|
* An evaluator for calculating osu!standard speed skill.
|
|
2768
2988
|
*/
|
|
2769
2989
|
class OsuSpeedEvaluator extends SpeedEvaluator {
|
|
2770
|
-
/**
|
|
2771
|
-
* Spacing threshold for a single hitobject spacing.
|
|
2772
|
-
*/
|
|
2773
|
-
static SINGLE_SPACING_THRESHOLD = 125;
|
|
2774
2990
|
/**
|
|
2775
2991
|
* Evaluates the difficulty of tapping the current object, based on:
|
|
2776
2992
|
*
|
|
@@ -2779,35 +2995,25 @@ class OsuSpeedEvaluator extends SpeedEvaluator {
|
|
|
2779
2995
|
* - and how easily they can be cheesed.
|
|
2780
2996
|
*
|
|
2781
2997
|
* @param current The current object.
|
|
2782
|
-
* @param greatWindow The great hit window of the current object.
|
|
2783
2998
|
*/
|
|
2784
|
-
static evaluateDifficultyOf(current
|
|
2999
|
+
static evaluateDifficultyOf(current) {
|
|
3000
|
+
var _a;
|
|
2785
3001
|
if (current.object instanceof osuBase.Spinner) {
|
|
2786
3002
|
return 0;
|
|
2787
3003
|
}
|
|
2788
3004
|
const prev = current.previous(0);
|
|
2789
3005
|
let strainTime = current.strainTime;
|
|
2790
|
-
const greatWindowFull = greatWindow * 2;
|
|
2791
3006
|
// Nerf doubletappable doubles.
|
|
2792
|
-
const
|
|
2793
|
-
let doubletapness = 1;
|
|
2794
|
-
if (next) {
|
|
2795
|
-
const currentDeltaTime = Math.max(1, current.deltaTime);
|
|
2796
|
-
const nextDeltaTime = Math.max(1, next.deltaTime);
|
|
2797
|
-
const deltaDifference = Math.abs(nextDeltaTime - currentDeltaTime);
|
|
2798
|
-
const speedRatio = currentDeltaTime / Math.max(currentDeltaTime, deltaDifference);
|
|
2799
|
-
const windowRatio = Math.pow(Math.min(1, currentDeltaTime / greatWindowFull), 2);
|
|
2800
|
-
doubletapness = Math.pow(speedRatio, 1 - windowRatio);
|
|
2801
|
-
}
|
|
3007
|
+
const doubletapness = 1 - current.doubletapness;
|
|
2802
3008
|
// Cap deltatime to the OD 300 hitwindow.
|
|
2803
3009
|
// 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.
|
|
2804
|
-
strainTime /= osuBase.MathUtils.clamp(strainTime /
|
|
3010
|
+
strainTime /= osuBase.MathUtils.clamp(strainTime / current.fullGreatWindow / 0.93, 0.92, 1);
|
|
2805
3011
|
let speedBonus = 1;
|
|
2806
3012
|
if (strainTime < this.minSpeedBonus) {
|
|
2807
3013
|
speedBonus +=
|
|
2808
3014
|
0.75 * Math.pow((this.minSpeedBonus - strainTime) / 40, 2);
|
|
2809
3015
|
}
|
|
2810
|
-
const travelDistance = prev
|
|
3016
|
+
const travelDistance = (_a = prev === null || prev === void 0 ? void 0 : prev.travelDistance) !== null && _a !== void 0 ? _a : 0;
|
|
2811
3017
|
const distance = Math.min(this.SINGLE_SPACING_THRESHOLD, travelDistance + current.minimumJumpDistance);
|
|
2812
3018
|
return (((speedBonus +
|
|
2813
3019
|
speedBonus *
|
|
@@ -2816,11 +3022,15 @@ class OsuSpeedEvaluator extends SpeedEvaluator {
|
|
|
2816
3022
|
strainTime);
|
|
2817
3023
|
}
|
|
2818
3024
|
}
|
|
3025
|
+
/**
|
|
3026
|
+
* Spacing threshold for a single hitobject spacing.
|
|
3027
|
+
*/
|
|
3028
|
+
OsuSpeedEvaluator.SINGLE_SPACING_THRESHOLD = 125;
|
|
2819
3029
|
|
|
2820
3030
|
/**
|
|
2821
3031
|
* An evaluator for calculating osu!standard Rhythm skill.
|
|
2822
3032
|
*/
|
|
2823
|
-
class OsuRhythmEvaluator
|
|
3033
|
+
class OsuRhythmEvaluator {
|
|
2824
3034
|
/**
|
|
2825
3035
|
* Calculates a rhythm multiplier for the difficulty of the tap associated
|
|
2826
3036
|
* with historic data of the current object.
|
|
@@ -2828,7 +3038,7 @@ class OsuRhythmEvaluator extends RhythmEvaluator {
|
|
|
2828
3038
|
* @param current The current object.
|
|
2829
3039
|
* @param greatWindow The great hit window of the current object.
|
|
2830
3040
|
*/
|
|
2831
|
-
static evaluateDifficultyOf(current
|
|
3041
|
+
static evaluateDifficultyOf(current) {
|
|
2832
3042
|
if (current.object instanceof osuBase.Spinner) {
|
|
2833
3043
|
return 0;
|
|
2834
3044
|
}
|
|
@@ -2863,8 +3073,9 @@ class OsuRhythmEvaluator extends RhythmEvaluator {
|
|
|
2863
3073
|
Math.min(0.5, Math.pow(Math.sin(Math.PI /
|
|
2864
3074
|
(Math.min(prevDelta, currentDelta) /
|
|
2865
3075
|
Math.max(prevDelta, currentDelta))), 2));
|
|
2866
|
-
const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) -
|
|
2867
|
-
|
|
3076
|
+
const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) -
|
|
3077
|
+
current.fullGreatWindow * 0.3) /
|
|
3078
|
+
(current.fullGreatWindow * 0.3));
|
|
2868
3079
|
let effectiveRatio = windowPenalty * currentRatio;
|
|
2869
3080
|
if (firstDeltaSwitch) {
|
|
2870
3081
|
if (prevDelta <= 1.25 * currentDelta &&
|
|
@@ -2925,23 +3136,23 @@ class OsuRhythmEvaluator extends RhythmEvaluator {
|
|
|
2925
3136
|
return Math.sqrt(4 + rhythmComplexitySum * this.rhythmMultiplier) / 2;
|
|
2926
3137
|
}
|
|
2927
3138
|
}
|
|
3139
|
+
OsuRhythmEvaluator.rhythmMultiplier = 0.75;
|
|
3140
|
+
OsuRhythmEvaluator.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
|
|
2928
3141
|
|
|
2929
3142
|
/**
|
|
2930
3143
|
* Represents the skill required to press keys or tap with regards to keeping up with the speed at which objects need to be hit.
|
|
2931
3144
|
*/
|
|
2932
3145
|
class OsuSpeed extends OsuSkill {
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
super(mods);
|
|
2944
|
-
this.greatWindow = greatWindow;
|
|
3146
|
+
constructor() {
|
|
3147
|
+
super(...arguments);
|
|
3148
|
+
this.strainDecayBase = 0.3;
|
|
3149
|
+
this.reducedSectionCount = 5;
|
|
3150
|
+
this.reducedSectionBaseline = 0.75;
|
|
3151
|
+
this.difficultyMultiplier = 1.04;
|
|
3152
|
+
this.decayWeight = 0.9;
|
|
3153
|
+
this.currentSpeedStrain = 0;
|
|
3154
|
+
this.currentRhythm = 0;
|
|
3155
|
+
this.skillMultiplier = 1375;
|
|
2945
3156
|
}
|
|
2946
3157
|
/**
|
|
2947
3158
|
* @param current The hitobject to calculate.
|
|
@@ -2949,16 +3160,22 @@ class OsuSpeed extends OsuSkill {
|
|
|
2949
3160
|
strainValueAt(current) {
|
|
2950
3161
|
this.currentSpeedStrain *= this.strainDecay(current.strainTime);
|
|
2951
3162
|
this.currentSpeedStrain +=
|
|
2952
|
-
OsuSpeedEvaluator.evaluateDifficultyOf(current
|
|
3163
|
+
OsuSpeedEvaluator.evaluateDifficultyOf(current) *
|
|
2953
3164
|
this.skillMultiplier;
|
|
2954
|
-
this.currentRhythm = OsuRhythmEvaluator.evaluateDifficultyOf(current
|
|
3165
|
+
this.currentRhythm = OsuRhythmEvaluator.evaluateDifficultyOf(current);
|
|
2955
3166
|
return this.currentSpeedStrain * this.currentRhythm;
|
|
2956
3167
|
}
|
|
3168
|
+
calculateInitialStrain(time, current) {
|
|
3169
|
+
var _a, _b;
|
|
3170
|
+
return (this.currentSpeedStrain *
|
|
3171
|
+
this.currentRhythm *
|
|
3172
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
3173
|
+
}
|
|
2957
3174
|
/**
|
|
2958
3175
|
* @param current The hitobject to save to.
|
|
2959
3176
|
*/
|
|
2960
3177
|
saveToHitObject(current) {
|
|
2961
|
-
current.
|
|
3178
|
+
current.speedStrain = this.currentSpeedStrain * this.currentRhythm;
|
|
2962
3179
|
current.rhythmMultiplier = this.currentRhythm;
|
|
2963
3180
|
}
|
|
2964
3181
|
}
|
|
@@ -2983,7 +3200,7 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
2983
3200
|
if (current.object instanceof osuBase.Spinner) {
|
|
2984
3201
|
return 0;
|
|
2985
3202
|
}
|
|
2986
|
-
const scalingFactor = 52 / current.object.
|
|
3203
|
+
const scalingFactor = 52 / current.object.radius;
|
|
2987
3204
|
let smallDistNerf = 1;
|
|
2988
3205
|
let cumulativeStrainTime = 0;
|
|
2989
3206
|
let result = 0;
|
|
@@ -3006,7 +3223,7 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
3006
3223
|
const opacityBonus = 1 +
|
|
3007
3224
|
this.maxOpacityBonus *
|
|
3008
3225
|
(1 -
|
|
3009
|
-
current.opacityAt(currentObject.object.startTime, isHiddenMod
|
|
3226
|
+
current.opacityAt(currentObject.object.startTime, isHiddenMod));
|
|
3010
3227
|
result +=
|
|
3011
3228
|
(stackNerf * opacityBonus * scalingFactor * jumpDistance) /
|
|
3012
3229
|
cumulativeStrainTime;
|
|
@@ -3037,8 +3254,8 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
3037
3254
|
// Longer sliders require more memorization.
|
|
3038
3255
|
sliderBonus *= pixelTravelDistance;
|
|
3039
3256
|
// Nerf sliders with repeats, as less memorization is required.
|
|
3040
|
-
if (current.object.
|
|
3041
|
-
sliderBonus /= current.object.
|
|
3257
|
+
if (current.object.repeatCount > 0)
|
|
3258
|
+
sliderBonus /= current.object.repeatCount + 1;
|
|
3042
3259
|
}
|
|
3043
3260
|
result += sliderBonus * this.sliderMultiplier;
|
|
3044
3261
|
return result;
|
|
@@ -3049,27 +3266,59 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
3049
3266
|
* Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
|
|
3050
3267
|
*/
|
|
3051
3268
|
class OsuFlashlight extends OsuSkill {
|
|
3052
|
-
skillMultiplier = 0.052;
|
|
3053
|
-
strainDecayBase = 0.15;
|
|
3054
|
-
reducedSectionCount = 0;
|
|
3055
|
-
reducedSectionBaseline = 1;
|
|
3056
|
-
decayWeight = 1;
|
|
3057
|
-
isHidden;
|
|
3058
3269
|
constructor(mods) {
|
|
3059
3270
|
super(mods);
|
|
3271
|
+
this.strainDecayBase = 0.15;
|
|
3272
|
+
this.reducedSectionCount = 0;
|
|
3273
|
+
this.reducedSectionBaseline = 1;
|
|
3274
|
+
this.decayWeight = 1;
|
|
3275
|
+
this.currentFlashlightStrain = 0;
|
|
3276
|
+
this.skillMultiplier = 0.052;
|
|
3060
3277
|
this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
|
|
3061
3278
|
}
|
|
3062
|
-
/**
|
|
3063
|
-
* @param current The hitobject to calculate.
|
|
3064
|
-
*/
|
|
3065
3279
|
strainValueAt(current) {
|
|
3066
|
-
this.
|
|
3067
|
-
this.
|
|
3280
|
+
this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
|
|
3281
|
+
this.currentFlashlightStrain +=
|
|
3068
3282
|
OsuFlashlightEvaluator.evaluateDifficultyOf(current, this.isHidden) * this.skillMultiplier;
|
|
3069
|
-
return this.
|
|
3283
|
+
return this.currentFlashlightStrain;
|
|
3284
|
+
}
|
|
3285
|
+
calculateInitialStrain(time, current) {
|
|
3286
|
+
var _a, _b;
|
|
3287
|
+
return (this.currentFlashlightStrain *
|
|
3288
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
3070
3289
|
}
|
|
3071
3290
|
saveToHitObject(current) {
|
|
3072
|
-
current.
|
|
3291
|
+
current.flashlightStrain = this.currentFlashlightStrain;
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
/**
|
|
3296
|
+
* Represents an osu!standard hit object with difficulty calculation values.
|
|
3297
|
+
*/
|
|
3298
|
+
class OsuDifficultyHitObject extends DifficultyHitObject {
|
|
3299
|
+
constructor() {
|
|
3300
|
+
super(...arguments);
|
|
3301
|
+
/**
|
|
3302
|
+
* The speed strain generated by the hitobject.
|
|
3303
|
+
*/
|
|
3304
|
+
this.speedStrain = 0;
|
|
3305
|
+
/**
|
|
3306
|
+
* The flashlight strain generated by this hitobject.
|
|
3307
|
+
*/
|
|
3308
|
+
this.flashlightStrain = 0;
|
|
3309
|
+
this.radiusBuffThreshold = 30;
|
|
3310
|
+
this.mode = osuBase.Modes.osu;
|
|
3311
|
+
}
|
|
3312
|
+
get scalingFactor() {
|
|
3313
|
+
const radius = this.object.radius;
|
|
3314
|
+
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
|
3315
|
+
let scalingFactor = this.normalizedRadius / radius;
|
|
3316
|
+
// High circle size (small CS) bonus
|
|
3317
|
+
if (radius < this.radiusBuffThreshold) {
|
|
3318
|
+
scalingFactor *=
|
|
3319
|
+
1 + Math.min(this.radiusBuffThreshold - radius, 5) / 50;
|
|
3320
|
+
}
|
|
3321
|
+
return scalingFactor;
|
|
3073
3322
|
}
|
|
3074
3323
|
}
|
|
3075
3324
|
|
|
@@ -3077,35 +3326,48 @@ class OsuFlashlight extends OsuSkill {
|
|
|
3077
3326
|
* A difficulty calculator for osu!standard gamemode.
|
|
3078
3327
|
*/
|
|
3079
3328
|
class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
3329
|
+
constructor() {
|
|
3330
|
+
super(...arguments);
|
|
3331
|
+
this.attributes = {
|
|
3332
|
+
speedDifficulty: 0,
|
|
3333
|
+
mods: [],
|
|
3334
|
+
starRating: 0,
|
|
3335
|
+
maxCombo: 0,
|
|
3336
|
+
aimDifficulty: 0,
|
|
3337
|
+
flashlightDifficulty: 0,
|
|
3338
|
+
speedNoteCount: 0,
|
|
3339
|
+
sliderFactor: 0,
|
|
3340
|
+
clockRate: 1,
|
|
3341
|
+
approachRate: 0,
|
|
3342
|
+
overallDifficulty: 0,
|
|
3343
|
+
hitCircleCount: 0,
|
|
3344
|
+
sliderCount: 0,
|
|
3345
|
+
spinnerCount: 0,
|
|
3346
|
+
};
|
|
3347
|
+
this.difficultyMultiplier = 0.0675;
|
|
3348
|
+
this.mode = osuBase.Modes.osu;
|
|
3349
|
+
}
|
|
3080
3350
|
/**
|
|
3081
3351
|
* The aim star rating of the beatmap.
|
|
3082
3352
|
*/
|
|
3083
|
-
aim
|
|
3353
|
+
get aim() {
|
|
3354
|
+
return this.attributes.aimDifficulty;
|
|
3355
|
+
}
|
|
3084
3356
|
/**
|
|
3085
3357
|
* The speed star rating of the beatmap.
|
|
3086
3358
|
*/
|
|
3087
|
-
speed
|
|
3359
|
+
get speed() {
|
|
3360
|
+
return this.attributes.speedDifficulty;
|
|
3361
|
+
}
|
|
3088
3362
|
/**
|
|
3089
3363
|
* The flashlight star rating of the beatmap.
|
|
3090
3364
|
*/
|
|
3091
|
-
flashlight
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
aimDifficulty: 0,
|
|
3098
|
-
flashlightDifficulty: 0,
|
|
3099
|
-
speedNoteCount: 0,
|
|
3100
|
-
sliderFactor: 0,
|
|
3101
|
-
approachRate: 0,
|
|
3102
|
-
overallDifficulty: 0,
|
|
3103
|
-
hitCircleCount: 0,
|
|
3104
|
-
sliderCount: 0,
|
|
3105
|
-
spinnerCount: 0,
|
|
3106
|
-
};
|
|
3107
|
-
difficultyMultiplier = 0.0675;
|
|
3108
|
-
mode = osuBase.Modes.osu;
|
|
3365
|
+
get flashlight() {
|
|
3366
|
+
return this.attributes.flashlightDifficulty;
|
|
3367
|
+
}
|
|
3368
|
+
get cacheableAttributes() {
|
|
3369
|
+
return Object.assign(Object.assign({}, this.attributes), { mods: osuBase.ModUtil.modsToOsuString(this.attributes.mods) });
|
|
3370
|
+
}
|
|
3109
3371
|
/**
|
|
3110
3372
|
* Calculates the aim star rating of the beatmap and stores it in this instance.
|
|
3111
3373
|
*/
|
|
@@ -3120,10 +3382,10 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3120
3382
|
*/
|
|
3121
3383
|
calculateSpeed() {
|
|
3122
3384
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3123
|
-
this.
|
|
3385
|
+
this.attributes.speedDifficulty = 0;
|
|
3124
3386
|
return;
|
|
3125
3387
|
}
|
|
3126
|
-
const speedSkill = new OsuSpeed(this.mods
|
|
3388
|
+
const speedSkill = new OsuSpeed(this.mods);
|
|
3127
3389
|
this.calculateSkills(speedSkill);
|
|
3128
3390
|
this.postCalculateSpeed(speedSkill);
|
|
3129
3391
|
}
|
|
@@ -3148,14 +3410,14 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3148
3410
|
if (basePerformanceValue > 1e-5) {
|
|
3149
3411
|
// Document for formula derivation:
|
|
3150
3412
|
// https://docs.google.com/document/d/10DZGYYSsT_yjz2Mtp6yIJld0Rqx4E-vVHupCqiM4TNI/edit
|
|
3151
|
-
this.
|
|
3413
|
+
this.attributes.starRating =
|
|
3152
3414
|
Math.cbrt(1.14) *
|
|
3153
3415
|
0.027 *
|
|
3154
3416
|
(Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformanceValue) +
|
|
3155
3417
|
4);
|
|
3156
3418
|
}
|
|
3157
3419
|
else {
|
|
3158
|
-
this.
|
|
3420
|
+
this.attributes.starRating = 0;
|
|
3159
3421
|
}
|
|
3160
3422
|
}
|
|
3161
3423
|
calculateAll() {
|
|
@@ -3168,7 +3430,6 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3168
3430
|
const flashlightSkill = skills[3];
|
|
3169
3431
|
this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
|
|
3170
3432
|
if (isRelax) {
|
|
3171
|
-
this.speed = 0;
|
|
3172
3433
|
this.attributes.speedDifficulty = 0;
|
|
3173
3434
|
}
|
|
3174
3435
|
else {
|
|
@@ -3178,9 +3439,6 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3178
3439
|
this.postCalculateFlashlight(flashlightSkill);
|
|
3179
3440
|
this.calculateTotal();
|
|
3180
3441
|
}
|
|
3181
|
-
/**
|
|
3182
|
-
* Returns a string representative of the class.
|
|
3183
|
-
*/
|
|
3184
3442
|
toString() {
|
|
3185
3443
|
return (this.total.toFixed(2) +
|
|
3186
3444
|
" stars (" +
|
|
@@ -3191,17 +3449,32 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3191
3449
|
this.flashlight.toFixed(2) +
|
|
3192
3450
|
" flashlight)");
|
|
3193
3451
|
}
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3452
|
+
generateDifficultyHitObjects(beatmap, clockRate) {
|
|
3453
|
+
var _a, _b;
|
|
3454
|
+
const difficultyObjects = [];
|
|
3455
|
+
const { objects } = beatmap.hitObjects;
|
|
3456
|
+
const greatWindow = new osuBase.OsuHitWindow(beatmap.difficulty.od).hitWindowFor300() /
|
|
3457
|
+
clockRate;
|
|
3458
|
+
for (let i = 0; i < objects.length; ++i) {
|
|
3459
|
+
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, greatWindow);
|
|
3460
|
+
difficultyObject.computeProperties(clockRate, objects);
|
|
3461
|
+
difficultyObjects.push(difficultyObject);
|
|
3462
|
+
}
|
|
3463
|
+
return difficultyObjects;
|
|
3464
|
+
}
|
|
3197
3465
|
createSkills() {
|
|
3198
3466
|
return [
|
|
3199
3467
|
new OsuAim(this.mods, true),
|
|
3200
3468
|
new OsuAim(this.mods, false),
|
|
3201
|
-
new OsuSpeed(this.mods
|
|
3469
|
+
new OsuSpeed(this.mods),
|
|
3202
3470
|
new OsuFlashlight(this.mods),
|
|
3203
3471
|
];
|
|
3204
3472
|
}
|
|
3473
|
+
populateDifficultyAttributes(beatmap, clockRate) {
|
|
3474
|
+
super.populateDifficultyAttributes(beatmap, clockRate);
|
|
3475
|
+
const preempt = osuBase.BeatmapDifficulty.difficultyRange(beatmap.difficulty.ar, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin) / clockRate;
|
|
3476
|
+
this.attributes.approachRate = osuBase.BeatmapDifficulty.inverseDifficultyRange(preempt, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin);
|
|
3477
|
+
}
|
|
3205
3478
|
/**
|
|
3206
3479
|
* Called after aim skill calculation.
|
|
3207
3480
|
*
|
|
@@ -3211,19 +3484,18 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3211
3484
|
postCalculateAim(aimSkill, aimSkillWithoutSliders) {
|
|
3212
3485
|
this.strainPeaks.aimWithSliders = aimSkill.strainPeaks;
|
|
3213
3486
|
this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
|
|
3214
|
-
this.
|
|
3487
|
+
this.attributes.aimDifficulty = this.starValue(aimSkill.difficultyValue());
|
|
3215
3488
|
if (this.aim) {
|
|
3216
3489
|
this.attributes.sliderFactor =
|
|
3217
3490
|
this.starValue(aimSkillWithoutSliders.difficultyValue()) /
|
|
3218
3491
|
this.aim;
|
|
3219
3492
|
}
|
|
3220
3493
|
if (this.mods.some((m) => m instanceof osuBase.ModTouchDevice)) {
|
|
3221
|
-
this.
|
|
3494
|
+
this.attributes.aimDifficulty = Math.pow(this.aim, 0.8);
|
|
3222
3495
|
}
|
|
3223
3496
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3224
|
-
this.
|
|
3497
|
+
this.attributes.aimDifficulty *= 0.9;
|
|
3225
3498
|
}
|
|
3226
|
-
this.attributes.aimDifficulty = this.aim;
|
|
3227
3499
|
}
|
|
3228
3500
|
/**
|
|
3229
3501
|
* Called after speed skill calculation.
|
|
@@ -3232,13 +3504,13 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3232
3504
|
*/
|
|
3233
3505
|
postCalculateSpeed(speedSkill) {
|
|
3234
3506
|
this.strainPeaks.speed = speedSkill.strainPeaks;
|
|
3235
|
-
this.
|
|
3507
|
+
this.attributes.speedDifficulty = this.starValue(speedSkill.difficultyValue());
|
|
3236
3508
|
}
|
|
3237
3509
|
/**
|
|
3238
3510
|
* Calculates speed-related attributes.
|
|
3239
3511
|
*/
|
|
3240
3512
|
calculateSpeedAttributes() {
|
|
3241
|
-
const objectStrains = this.objects.map((v) => v.
|
|
3513
|
+
const objectStrains = this.objects.map((v) => v.speedStrain);
|
|
3242
3514
|
const maxStrain = Math.max(...objectStrains);
|
|
3243
3515
|
if (maxStrain) {
|
|
3244
3516
|
this.attributes.speedNoteCount = objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
|
|
@@ -3251,55 +3523,13 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3251
3523
|
*/
|
|
3252
3524
|
postCalculateFlashlight(flashlightSkill) {
|
|
3253
3525
|
this.strainPeaks.flashlight = flashlightSkill.strainPeaks;
|
|
3254
|
-
this.
|
|
3526
|
+
this.attributes.flashlightDifficulty = this.starValue(flashlightSkill.difficultyValue());
|
|
3255
3527
|
if (this.mods.some((m) => m instanceof osuBase.ModTouchDevice)) {
|
|
3256
|
-
this.
|
|
3528
|
+
this.attributes.flashlightDifficulty = Math.pow(this.flashlight, 0.8);
|
|
3257
3529
|
}
|
|
3258
3530
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3259
|
-
this.
|
|
3531
|
+
this.attributes.flashlightDifficulty *= 0.7;
|
|
3260
3532
|
}
|
|
3261
|
-
this.attributes.flashlightDifficulty = this.flashlight;
|
|
3262
|
-
}
|
|
3263
|
-
}
|
|
3264
|
-
|
|
3265
|
-
/**
|
|
3266
|
-
* A difficulty calculator that calculates for both osu!droid and osu!standard gamemode.
|
|
3267
|
-
*/
|
|
3268
|
-
class MapStars {
|
|
3269
|
-
/**
|
|
3270
|
-
* The osu!droid difficulty calculator of the beatmap.
|
|
3271
|
-
*/
|
|
3272
|
-
droid;
|
|
3273
|
-
/**
|
|
3274
|
-
* The osu!standard difficulty calculator of the beatmap.
|
|
3275
|
-
*/
|
|
3276
|
-
osu;
|
|
3277
|
-
/**
|
|
3278
|
-
* Constructs this instance and calculates the given beatmap's osu!droid and osu!standard difficulty.
|
|
3279
|
-
*
|
|
3280
|
-
* @param beatmap The beatmap to calculate.
|
|
3281
|
-
* @param options Options for the difficulty calculation.
|
|
3282
|
-
*/
|
|
3283
|
-
constructor(beatmap, options) {
|
|
3284
|
-
const stats = new osuBase.MapStats({
|
|
3285
|
-
speedMultiplier: options?.stats?.speedMultiplier ?? 1,
|
|
3286
|
-
isForceAR: options?.stats?.isForceAR ?? false,
|
|
3287
|
-
oldStatistics: options?.stats?.oldStatistics ?? false,
|
|
3288
|
-
});
|
|
3289
|
-
this.droid = new DroidDifficultyCalculator(beatmap).calculate({
|
|
3290
|
-
...options,
|
|
3291
|
-
stats,
|
|
3292
|
-
});
|
|
3293
|
-
this.osu = new OsuDifficultyCalculator(beatmap).calculate({
|
|
3294
|
-
...options,
|
|
3295
|
-
stats,
|
|
3296
|
-
});
|
|
3297
|
-
}
|
|
3298
|
-
/**
|
|
3299
|
-
* Returns a string representative of the class.
|
|
3300
|
-
*/
|
|
3301
|
-
toString() {
|
|
3302
|
-
return `${this.droid.toString()}\n${this.osu.toString()}`;
|
|
3303
3533
|
}
|
|
3304
3534
|
}
|
|
3305
3535
|
|
|
@@ -3307,65 +3537,60 @@ class MapStars {
|
|
|
3307
3537
|
* A performance points calculator that calculates performance points for osu!standard gamemode.
|
|
3308
3538
|
*/
|
|
3309
3539
|
class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
* @param difficultyAttributes The difficulty attributes to calculate.
|
|
3331
|
-
*/
|
|
3332
|
-
constructor(difficultyAttributes) {
|
|
3333
|
-
super();
|
|
3334
|
-
this.difficultyAttributes = osuBase.Utils.deepCopy(difficultyAttributes);
|
|
3540
|
+
constructor() {
|
|
3541
|
+
super(...arguments);
|
|
3542
|
+
/**
|
|
3543
|
+
* The aim performance value.
|
|
3544
|
+
*/
|
|
3545
|
+
this.aim = 0;
|
|
3546
|
+
/**
|
|
3547
|
+
* The speed performance value.
|
|
3548
|
+
*/
|
|
3549
|
+
this.speed = 0;
|
|
3550
|
+
/**
|
|
3551
|
+
* The accuracy performance value.
|
|
3552
|
+
*/
|
|
3553
|
+
this.accuracy = 0;
|
|
3554
|
+
/**
|
|
3555
|
+
* The flashlight performance value.
|
|
3556
|
+
*/
|
|
3557
|
+
this.flashlight = 0;
|
|
3558
|
+
this.finalMultiplier = 1.14;
|
|
3559
|
+
this.mode = osuBase.Modes.osu;
|
|
3335
3560
|
}
|
|
3336
3561
|
calculateValues() {
|
|
3337
|
-
this.calculateAimValue();
|
|
3338
|
-
this.calculateSpeedValue();
|
|
3339
|
-
this.calculateAccuracyValue();
|
|
3340
|
-
this.calculateFlashlightValue();
|
|
3562
|
+
this.aim = this.calculateAimValue();
|
|
3563
|
+
this.speed = this.calculateSpeedValue();
|
|
3564
|
+
this.accuracy = this.calculateAccuracyValue();
|
|
3565
|
+
this.flashlight = this.calculateFlashlightValue();
|
|
3341
3566
|
}
|
|
3342
3567
|
calculateTotalValue() {
|
|
3343
|
-
this.
|
|
3344
|
-
Math.pow(
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
Math.pow(this.flashlight, 1.1), 1 / 1.1) * this.finalMultiplier;
|
|
3568
|
+
return (Math.pow(Math.pow(this.aim, 1.1) +
|
|
3569
|
+
Math.pow(this.speed, 1.1) +
|
|
3570
|
+
Math.pow(this.accuracy, 1.1) +
|
|
3571
|
+
Math.pow(this.flashlight, 1.1), 1 / 1.1) * this.finalMultiplier);
|
|
3348
3572
|
}
|
|
3349
3573
|
/**
|
|
3350
3574
|
* Calculates the aim performance value of the beatmap.
|
|
3351
3575
|
*/
|
|
3352
3576
|
calculateAimValue() {
|
|
3353
|
-
|
|
3577
|
+
let aimValue = this.baseValue(this.difficultyAttributes.aimDifficulty);
|
|
3354
3578
|
// Longer maps are worth more
|
|
3355
3579
|
let lengthBonus = 0.95 + 0.4 * Math.min(1, this.totalHits / 2000);
|
|
3356
3580
|
if (this.totalHits > 2000) {
|
|
3357
3581
|
lengthBonus += Math.log10(this.totalHits / 2000) * 0.5;
|
|
3358
3582
|
}
|
|
3359
|
-
|
|
3583
|
+
aimValue *= lengthBonus;
|
|
3360
3584
|
if (this.effectiveMissCount > 0) {
|
|
3361
|
-
// Penalize misses by assessing # of misses relative to the total # of objects.
|
|
3362
|
-
|
|
3585
|
+
// Penalize misses by assessing # of misses relative to the total # of objects.
|
|
3586
|
+
// Default a 3% reduction for any # of misses.
|
|
3587
|
+
aimValue *=
|
|
3363
3588
|
0.97 *
|
|
3364
3589
|
Math.pow(1 -
|
|
3365
3590
|
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
|
|
3366
3591
|
}
|
|
3367
3592
|
// Combo scaling
|
|
3368
|
-
|
|
3593
|
+
aimValue *= this.comboPenalty;
|
|
3369
3594
|
const calculatedAR = this.difficultyAttributes.approachRate;
|
|
3370
3595
|
if (!this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3371
3596
|
// AR scaling
|
|
@@ -3377,145 +3602,142 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3377
3602
|
arFactor += 0.05 * (8 - calculatedAR);
|
|
3378
3603
|
}
|
|
3379
3604
|
// Buff for longer maps with high AR.
|
|
3380
|
-
|
|
3605
|
+
aimValue *= 1 + arFactor * lengthBonus;
|
|
3381
3606
|
}
|
|
3382
3607
|
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
|
3383
3608
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHidden)) {
|
|
3384
|
-
|
|
3609
|
+
aimValue *= 1 + 0.04 * (12 - calculatedAR);
|
|
3385
3610
|
}
|
|
3386
3611
|
// Scale the aim value with slider factor to nerf very likely dropped sliderends.
|
|
3387
|
-
|
|
3612
|
+
aimValue *= this.sliderNerfFactor;
|
|
3388
3613
|
// Scale the aim value with accuracy.
|
|
3389
|
-
|
|
3614
|
+
aimValue *= this.computedAccuracy.value();
|
|
3390
3615
|
// It is also important to consider accuracy difficulty when doing that.
|
|
3391
3616
|
const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
|
|
3392
|
-
|
|
3617
|
+
aimValue *= 0.98 + odScaling;
|
|
3618
|
+
return aimValue;
|
|
3393
3619
|
}
|
|
3394
3620
|
/**
|
|
3395
3621
|
* Calculates the speed performance value of the beatmap.
|
|
3396
3622
|
*/
|
|
3397
3623
|
calculateSpeedValue() {
|
|
3398
3624
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3399
|
-
|
|
3400
|
-
return;
|
|
3625
|
+
return 0;
|
|
3401
3626
|
}
|
|
3402
|
-
|
|
3403
|
-
this.speed = this.baseValue(this.difficultyAttributes.speedDifficulty);
|
|
3627
|
+
let speedValue = this.baseValue(this.difficultyAttributes.speedDifficulty);
|
|
3404
3628
|
// Longer maps are worth more
|
|
3405
3629
|
let lengthBonus = 0.95 + 0.4 * Math.min(1, this.totalHits / 2000);
|
|
3406
3630
|
if (this.totalHits > 2000) {
|
|
3407
3631
|
lengthBonus += Math.log10(this.totalHits / 2000) * 0.5;
|
|
3408
3632
|
}
|
|
3409
|
-
|
|
3633
|
+
speedValue *= lengthBonus;
|
|
3410
3634
|
if (this.effectiveMissCount > 0) {
|
|
3411
|
-
// Penalize misses by assessing # of misses relative to the total # of objects.
|
|
3412
|
-
|
|
3635
|
+
// Penalize misses by assessing # of misses relative to the total # of objects.
|
|
3636
|
+
// Default a 3% reduction for any # of misses.
|
|
3637
|
+
speedValue *=
|
|
3413
3638
|
0.97 *
|
|
3414
3639
|
Math.pow(1 -
|
|
3415
3640
|
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
|
|
3416
3641
|
}
|
|
3417
3642
|
// Combo scaling
|
|
3418
|
-
|
|
3643
|
+
speedValue *= this.comboPenalty;
|
|
3419
3644
|
// AR scaling
|
|
3420
3645
|
const calculatedAR = this.difficultyAttributes.approachRate;
|
|
3421
3646
|
if (calculatedAR > 10.33) {
|
|
3422
3647
|
// Buff for longer maps with high AR.
|
|
3423
|
-
|
|
3648
|
+
speedValue *= 1 + 0.3 * (calculatedAR - 10.33) * lengthBonus;
|
|
3424
3649
|
}
|
|
3425
3650
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHidden)) {
|
|
3426
|
-
|
|
3651
|
+
speedValue *= 1 + 0.04 * (12 - calculatedAR);
|
|
3427
3652
|
}
|
|
3428
3653
|
// Calculate accuracy assuming the worst case scenario.
|
|
3429
3654
|
const countGreat = this.computedAccuracy.n300;
|
|
3430
3655
|
const countOk = this.computedAccuracy.n100;
|
|
3431
3656
|
const countMeh = this.computedAccuracy.n50;
|
|
3432
3657
|
const relevantTotalDiff = this.totalHits - this.difficultyAttributes.speedNoteCount;
|
|
3433
|
-
const relevantAccuracy = new osuBase.Accuracy(
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3658
|
+
const relevantAccuracy = new osuBase.Accuracy(this.difficultyAttributes.speedNoteCount > 0
|
|
3659
|
+
? {
|
|
3660
|
+
n300: Math.max(0, countGreat - relevantTotalDiff),
|
|
3661
|
+
n100: Math.max(0, countOk - Math.max(0, relevantTotalDiff - countGreat)),
|
|
3662
|
+
n50: Math.max(0, countMeh -
|
|
3663
|
+
Math.max(0, relevantTotalDiff - countGreat - countOk)),
|
|
3664
|
+
}
|
|
3665
|
+
: // Set accuracy to 0.
|
|
3666
|
+
{ n300: 0, nobjects: 1 });
|
|
3439
3667
|
// Scale the speed value with accuracy and OD.
|
|
3440
|
-
|
|
3668
|
+
speedValue *=
|
|
3441
3669
|
(0.95 +
|
|
3442
3670
|
Math.pow(this.difficultyAttributes.overallDifficulty, 2) /
|
|
3443
3671
|
750) *
|
|
3444
|
-
Math.pow((this.computedAccuracy.value(
|
|
3445
|
-
relevantAccuracy.value()) /
|
|
3672
|
+
Math.pow((this.computedAccuracy.value() +
|
|
3673
|
+
relevantAccuracy.value(this.difficultyAttributes.speedNoteCount)) /
|
|
3446
3674
|
2, (14.5 -
|
|
3447
3675
|
Math.max(this.difficultyAttributes.overallDifficulty, 8)) /
|
|
3448
3676
|
2);
|
|
3449
3677
|
// Scale the speed value with # of 50s to punish doubletapping.
|
|
3450
|
-
|
|
3678
|
+
speedValue *= Math.pow(0.99, Math.max(0, this.computedAccuracy.n50 - this.totalHits / 500));
|
|
3679
|
+
return speedValue;
|
|
3451
3680
|
}
|
|
3452
3681
|
/**
|
|
3453
3682
|
* Calculates the accuracy performance value of the beatmap.
|
|
3454
3683
|
*/
|
|
3455
3684
|
calculateAccuracyValue() {
|
|
3456
3685
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3457
|
-
|
|
3458
|
-
return;
|
|
3686
|
+
return 0;
|
|
3459
3687
|
}
|
|
3460
3688
|
const ncircles = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModScoreV2)
|
|
3461
3689
|
? this.totalHits - this.difficultyAttributes.spinnerCount
|
|
3462
3690
|
: this.difficultyAttributes.hitCircleCount;
|
|
3463
3691
|
if (ncircles === 0) {
|
|
3464
|
-
|
|
3465
|
-
return;
|
|
3692
|
+
return 0;
|
|
3466
3693
|
}
|
|
3467
|
-
const realAccuracy = new osuBase.Accuracy({
|
|
3468
|
-
...this.computedAccuracy,
|
|
3469
|
-
n300: this.computedAccuracy.n300 - (this.totalHits - ncircles),
|
|
3470
|
-
});
|
|
3694
|
+
const realAccuracy = new osuBase.Accuracy(Object.assign(Object.assign({}, this.computedAccuracy), { n300: this.computedAccuracy.n300 - (this.totalHits - ncircles) }));
|
|
3471
3695
|
// Lots of arbitrary values from testing.
|
|
3472
3696
|
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
|
|
3473
|
-
this.
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3697
|
+
let accuracyValue = Math.pow(1.52163, this.difficultyAttributes.overallDifficulty) *
|
|
3698
|
+
// It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points.
|
|
3699
|
+
Math.pow(realAccuracy.n300 < 0 ? 0 : realAccuracy.value(), 24) *
|
|
3700
|
+
2.83;
|
|
3477
3701
|
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
|
|
3478
|
-
|
|
3702
|
+
accuracyValue *= Math.min(1.15, Math.pow(ncircles / 1000, 0.3));
|
|
3479
3703
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHidden)) {
|
|
3480
|
-
|
|
3704
|
+
accuracyValue *= 1.08;
|
|
3481
3705
|
}
|
|
3482
3706
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
|
|
3483
|
-
|
|
3707
|
+
accuracyValue *= 1.02;
|
|
3484
3708
|
}
|
|
3709
|
+
return accuracyValue;
|
|
3485
3710
|
}
|
|
3486
3711
|
/**
|
|
3487
3712
|
* Calculates the flashlight performance value of the beatmap.
|
|
3488
3713
|
*/
|
|
3489
3714
|
calculateFlashlightValue() {
|
|
3490
3715
|
if (!this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
|
|
3491
|
-
|
|
3492
|
-
return;
|
|
3716
|
+
return 0;
|
|
3493
3717
|
}
|
|
3494
|
-
|
|
3495
|
-
this.flashlight =
|
|
3496
|
-
Math.pow(this.difficultyAttributes.flashlightDifficulty, 2) * 25;
|
|
3718
|
+
let flashlightValue = Math.pow(this.difficultyAttributes.flashlightDifficulty, 2) * 25;
|
|
3497
3719
|
// Combo scaling
|
|
3498
|
-
|
|
3720
|
+
flashlightValue *= this.comboPenalty;
|
|
3499
3721
|
if (this.effectiveMissCount > 0) {
|
|
3500
3722
|
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
3501
|
-
|
|
3723
|
+
flashlightValue *=
|
|
3502
3724
|
0.97 *
|
|
3503
3725
|
Math.pow(1 -
|
|
3504
3726
|
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
|
|
3505
3727
|
}
|
|
3506
3728
|
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
3507
|
-
|
|
3729
|
+
flashlightValue *=
|
|
3508
3730
|
0.7 +
|
|
3509
3731
|
0.1 * Math.min(1, this.totalHits / 200) +
|
|
3510
3732
|
(this.totalHits > 200
|
|
3511
3733
|
? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
|
|
3512
3734
|
: 0);
|
|
3513
3735
|
// Scale the flashlight value with accuracy slightly.
|
|
3514
|
-
this.
|
|
3515
|
-
0.5 + this.computedAccuracy.value(this.totalHits) / 2;
|
|
3736
|
+
flashlightValue *= 0.5 + this.computedAccuracy.value() / 2;
|
|
3516
3737
|
// It is also important to consider accuracy difficulty when doing that.
|
|
3517
3738
|
const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
|
|
3518
|
-
|
|
3739
|
+
flashlightValue *= 0.98 + odScaling;
|
|
3740
|
+
return flashlightValue;
|
|
3519
3741
|
}
|
|
3520
3742
|
toString() {
|
|
3521
3743
|
return (this.total.toFixed(2) +
|
|
@@ -3534,10 +3756,10 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3534
3756
|
exports.AimEvaluator = AimEvaluator;
|
|
3535
3757
|
exports.DifficultyCalculator = DifficultyCalculator;
|
|
3536
3758
|
exports.DifficultyHitObject = DifficultyHitObject;
|
|
3537
|
-
exports.DifficultyHitObjectCreator = DifficultyHitObjectCreator;
|
|
3538
3759
|
exports.DroidAim = DroidAim;
|
|
3539
3760
|
exports.DroidAimEvaluator = DroidAimEvaluator;
|
|
3540
3761
|
exports.DroidDifficultyCalculator = DroidDifficultyCalculator;
|
|
3762
|
+
exports.DroidDifficultyHitObject = DroidDifficultyHitObject;
|
|
3541
3763
|
exports.DroidFlashlight = DroidFlashlight;
|
|
3542
3764
|
exports.DroidFlashlightEvaluator = DroidFlashlightEvaluator;
|
|
3543
3765
|
exports.DroidPerformanceCalculator = DroidPerformanceCalculator;
|
|
@@ -3548,10 +3770,10 @@ exports.DroidTapEvaluator = DroidTapEvaluator;
|
|
|
3548
3770
|
exports.DroidVisual = DroidVisual;
|
|
3549
3771
|
exports.DroidVisualEvaluator = DroidVisualEvaluator;
|
|
3550
3772
|
exports.FlashlightEvaluator = FlashlightEvaluator;
|
|
3551
|
-
exports.MapStars = MapStars;
|
|
3552
3773
|
exports.OsuAim = OsuAim;
|
|
3553
3774
|
exports.OsuAimEvaluator = OsuAimEvaluator;
|
|
3554
3775
|
exports.OsuDifficultyCalculator = OsuDifficultyCalculator;
|
|
3776
|
+
exports.OsuDifficultyHitObject = OsuDifficultyHitObject;
|
|
3555
3777
|
exports.OsuFlashlight = OsuFlashlight;
|
|
3556
3778
|
exports.OsuFlashlightEvaluator = OsuFlashlightEvaluator;
|
|
3557
3779
|
exports.OsuPerformanceCalculator = OsuPerformanceCalculator;
|
|
@@ -3559,6 +3781,5 @@ exports.OsuRhythmEvaluator = OsuRhythmEvaluator;
|
|
|
3559
3781
|
exports.OsuSpeed = OsuSpeed;
|
|
3560
3782
|
exports.OsuSpeedEvaluator = OsuSpeedEvaluator;
|
|
3561
3783
|
exports.PerformanceCalculator = PerformanceCalculator;
|
|
3562
|
-
exports.RhythmEvaluator = RhythmEvaluator;
|
|
3563
3784
|
exports.SpeedEvaluator = SpeedEvaluator;
|
|
3564
3785
|
//# sourceMappingURL=index.js.map
|