@rian8337/osu-difficulty-calculator 4.0.0-beta.2 → 4.0.0-beta.20
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 +1545 -1393
- package/package.json +9 -6
- package/typings/index.d.ts +552 -456
- 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,225 @@ 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;
|
|
92
|
-
/**
|
|
93
|
-
* The visual strain generated by the hitobject if sliders are considered.
|
|
94
|
-
*/
|
|
95
|
-
visualStrainWithSliders = 0;
|
|
34
|
+
class DifficultyCalculator {
|
|
96
35
|
/**
|
|
97
|
-
* The
|
|
36
|
+
* The total star rating of the beatmap.
|
|
98
37
|
*/
|
|
99
|
-
|
|
38
|
+
get total() {
|
|
39
|
+
return this.attributes.starRating;
|
|
40
|
+
}
|
|
100
41
|
/**
|
|
101
|
-
*
|
|
42
|
+
* Constructs a new instance of the calculator.
|
|
102
43
|
*
|
|
103
|
-
*
|
|
44
|
+
* @param beatmap The beatmap to calculate. This beatmap will be deep-cloned to prevent reference changes.
|
|
104
45
|
*/
|
|
105
|
-
|
|
46
|
+
constructor(beatmap) {
|
|
47
|
+
var _a;
|
|
48
|
+
/**
|
|
49
|
+
* The difficulty objects of the beatmap.
|
|
50
|
+
*/
|
|
51
|
+
this.objects = [];
|
|
52
|
+
/**
|
|
53
|
+
* The modifications applied.
|
|
54
|
+
*/
|
|
55
|
+
this.mods = [];
|
|
56
|
+
/**
|
|
57
|
+
* The strain peaks of various calculated difficulties.
|
|
58
|
+
*/
|
|
59
|
+
this.strainPeaks = {
|
|
60
|
+
aimWithSliders: [],
|
|
61
|
+
aimWithoutSliders: [],
|
|
62
|
+
speed: [],
|
|
63
|
+
flashlight: [],
|
|
64
|
+
};
|
|
65
|
+
this.beatmap = beatmap;
|
|
66
|
+
this.difficultyStatistics = {
|
|
67
|
+
circleSize: beatmap.difficulty.cs,
|
|
68
|
+
approachRate: (_a = beatmap.difficulty.ar) !== null && _a !== void 0 ? _a : beatmap.difficulty.od,
|
|
69
|
+
overallDifficulty: beatmap.difficulty.od,
|
|
70
|
+
healthDrain: beatmap.difficulty.hp,
|
|
71
|
+
overallSpeedMultiplier: 1,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
106
74
|
/**
|
|
107
|
-
*
|
|
75
|
+
* Calculates the star rating of the specified beatmap.
|
|
108
76
|
*
|
|
109
|
-
*
|
|
77
|
+
* The beatmap is analyzed in chunks of `sectionLength` duration.
|
|
78
|
+
* For each chunk the highest hitobject strains are added to
|
|
79
|
+
* a list which is then collapsed into a weighted sum, much
|
|
80
|
+
* like scores are weighted on a user's profile.
|
|
110
81
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
82
|
+
* For subsequent chunks, the initial max strain is calculated
|
|
83
|
+
* by decaying the previous hitobject's strain until the
|
|
84
|
+
* beginning of the new chunk.
|
|
114
85
|
*
|
|
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.
|
|
86
|
+
* @param options Options for the difficulty calculation.
|
|
87
|
+
* @returns The current instance.
|
|
129
88
|
*/
|
|
130
|
-
|
|
89
|
+
calculate(options) {
|
|
90
|
+
var _a;
|
|
91
|
+
this.mods = (_a = options === null || options === void 0 ? void 0 : options.mods) !== null && _a !== void 0 ? _a : [];
|
|
92
|
+
const converted = new osuBase.BeatmapConverter(this.beatmap).convert({
|
|
93
|
+
mode: this.mode,
|
|
94
|
+
mods: this.mods,
|
|
95
|
+
customSpeedMultiplier: options === null || options === void 0 ? void 0 : options.customSpeedMultiplier,
|
|
96
|
+
});
|
|
97
|
+
this.difficultyStatistics = Object.seal(this.computeDifficultyStatistics(options));
|
|
98
|
+
this.populateDifficultyAttributes();
|
|
99
|
+
this.objects.push(...this.generateDifficultyHitObjects(converted));
|
|
100
|
+
this.calculateAll();
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
131
103
|
/**
|
|
132
|
-
*
|
|
104
|
+
* Calculates the skills provided.
|
|
133
105
|
*
|
|
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.
|
|
106
|
+
* @param skills The skills to calculate.
|
|
151
107
|
*/
|
|
152
|
-
|
|
108
|
+
calculateSkills(...skills) {
|
|
109
|
+
// The first object doesn't generate a strain, so we begin calculating from the second object.
|
|
110
|
+
for (const object of this.objects.slice(1)) {
|
|
111
|
+
for (const skill of skills) {
|
|
112
|
+
skill.process(object);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
153
116
|
/**
|
|
154
|
-
*
|
|
117
|
+
* Populates the stored difficulty attributes with necessary data.
|
|
155
118
|
*/
|
|
156
|
-
|
|
119
|
+
populateDifficultyAttributes() {
|
|
120
|
+
this.attributes.approachRate = this.difficultyStatistics.approachRate;
|
|
121
|
+
this.attributes.hitCircleCount = this.beatmap.hitObjects.circles;
|
|
122
|
+
this.attributes.maxCombo = this.beatmap.maxCombo;
|
|
123
|
+
this.attributes.mods = this.mods.slice();
|
|
124
|
+
this.attributes.overallDifficulty =
|
|
125
|
+
this.difficultyStatistics.overallDifficulty;
|
|
126
|
+
this.attributes.sliderCount = this.beatmap.hitObjects.sliders;
|
|
127
|
+
this.attributes.spinnerCount = this.beatmap.hitObjects.spinners;
|
|
128
|
+
this.attributes.clockRate =
|
|
129
|
+
this.difficultyStatistics.overallSpeedMultiplier;
|
|
130
|
+
}
|
|
157
131
|
/**
|
|
158
|
-
*
|
|
132
|
+
* Calculates the star rating value of a difficulty.
|
|
159
133
|
*
|
|
160
|
-
*
|
|
161
|
-
*/
|
|
162
|
-
overlappingFactor = 0;
|
|
163
|
-
/**
|
|
164
|
-
* Adjusted velocity of the hitobject, taking speed multiplier into account.
|
|
134
|
+
* @param difficulty The difficulty to calculate.
|
|
165
135
|
*/
|
|
166
|
-
|
|
136
|
+
starValue(difficulty) {
|
|
137
|
+
return Math.sqrt(difficulty) * this.difficultyMultiplier;
|
|
138
|
+
}
|
|
167
139
|
/**
|
|
168
|
-
*
|
|
140
|
+
* Calculates the base performance value of a difficulty rating.
|
|
141
|
+
*
|
|
142
|
+
* @param rating The difficulty rating.
|
|
169
143
|
*/
|
|
170
|
-
|
|
144
|
+
basePerformanceValue(rating) {
|
|
145
|
+
return Math.pow(5 * Math.max(1, rating / 0.0675) - 4, 3) / 100000;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Represents a hit object with difficulty calculation values.
|
|
151
|
+
*/
|
|
152
|
+
class DifficultyHitObject {
|
|
171
153
|
/**
|
|
154
|
+
* Note: You **must** call `computeProperties` at some point due to how TypeScript handles
|
|
155
|
+
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue).
|
|
156
|
+
*
|
|
172
157
|
* @param object The underlying hitobject.
|
|
173
|
-
* @param
|
|
174
|
-
|
|
175
|
-
|
|
158
|
+
* @param lastObject The hitobject before this hitobject.
|
|
159
|
+
* @param lastLastObject The hitobject before the last hitobject.
|
|
160
|
+
* @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
|
|
161
|
+
* @param clockRate The clock rate of the beatmap.
|
|
162
|
+
*/
|
|
163
|
+
constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate) {
|
|
164
|
+
/**
|
|
165
|
+
* The aim strain generated by the hitobject if sliders are considered.
|
|
166
|
+
*/
|
|
167
|
+
this.aimStrainWithSliders = 0;
|
|
168
|
+
/**
|
|
169
|
+
* The aim strain generated by the hitobject if sliders are not considered.
|
|
170
|
+
*/
|
|
171
|
+
this.aimStrainWithoutSliders = 0;
|
|
172
|
+
/**
|
|
173
|
+
* The rhythm multiplier generated by the hitobject. This is used to alter tap strain.
|
|
174
|
+
*/
|
|
175
|
+
this.rhythmMultiplier = 0;
|
|
176
|
+
/**
|
|
177
|
+
* The normalized distance from the "lazy" end position of the previous hitobject to the start position of this hitobject.
|
|
178
|
+
*
|
|
179
|
+
* 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).
|
|
180
|
+
*/
|
|
181
|
+
this.lazyJumpDistance = 0;
|
|
182
|
+
/**
|
|
183
|
+
* The normalized shortest distance to consider for a jump between the previous hitobject and this hitobject.
|
|
184
|
+
*
|
|
185
|
+
* 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.
|
|
186
|
+
*
|
|
187
|
+
* 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.
|
|
188
|
+
* 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,
|
|
189
|
+
* such that the jump is felt as only starting from the slider's true end position.
|
|
190
|
+
*
|
|
191
|
+
* Now consider a slider - circle pattern where the circle is stacked along the path inside the slider.
|
|
192
|
+
* In this case, the lazy end position correctly estimates the true end position of the slider and provides the more natural movement path.
|
|
193
|
+
*/
|
|
194
|
+
this.minimumJumpDistance = 0;
|
|
195
|
+
/**
|
|
196
|
+
* The time taken to travel through `minimumJumpDistance`, with a minimum value of 25ms.
|
|
197
|
+
*/
|
|
198
|
+
this.minimumJumpTime = 0;
|
|
199
|
+
/**
|
|
200
|
+
* The normalized distance between the start and end position of this hitobject.
|
|
201
|
+
*/
|
|
202
|
+
this.travelDistance = 0;
|
|
203
|
+
/**
|
|
204
|
+
* The time taken to travel through `travelDistance`, with a minimum value of 25ms for sliders.
|
|
205
|
+
*/
|
|
206
|
+
this.travelTime = 0;
|
|
207
|
+
/**
|
|
208
|
+
* Angle the player has to take to hit this hitobject.
|
|
209
|
+
*
|
|
210
|
+
* Calculated as the angle between the circles (current-2, current-1, current).
|
|
211
|
+
*/
|
|
212
|
+
this.angle = null;
|
|
213
|
+
this.normalizedRadius = 50;
|
|
214
|
+
this.maximumSliderRadius = this.normalizedRadius * 2.4;
|
|
215
|
+
this.assumedSliderRadius = this.normalizedRadius * 1.8;
|
|
216
|
+
this.minDeltaTime = 25;
|
|
176
217
|
this.object = object;
|
|
177
|
-
this.
|
|
218
|
+
this.lastObject = lastObject;
|
|
219
|
+
this.lastLastObject = lastLastObject;
|
|
220
|
+
this.hitObjects = difficultyHitObjects;
|
|
221
|
+
this.index = difficultyHitObjects.length - 1;
|
|
222
|
+
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
|
223
|
+
this.startTime = object.startTime / clockRate;
|
|
224
|
+
this.endTime = object.endTime / clockRate;
|
|
225
|
+
if (lastObject) {
|
|
226
|
+
this.deltaTime = this.startTime - lastObject.startTime / clockRate;
|
|
227
|
+
this.strainTime = Math.max(this.deltaTime, this.minDeltaTime);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
this.deltaTime = 0;
|
|
231
|
+
this.strainTime = 0;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Computes the properties of this hitobject.
|
|
236
|
+
*
|
|
237
|
+
* @param clockRate The clock rate of the beatmap.
|
|
238
|
+
* @param hitObjects The hitobjects in the beatmap.
|
|
239
|
+
*/
|
|
240
|
+
computeProperties(clockRate,
|
|
241
|
+
// Required for `DroidDifficultyHitObject` override.
|
|
242
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
243
|
+
hitObjects) {
|
|
244
|
+
this.setDistances(clockRate);
|
|
178
245
|
}
|
|
179
246
|
/**
|
|
180
247
|
* Gets the difficulty hitobject at a specific index with respect to the current
|
|
@@ -187,7 +254,8 @@ class DifficultyHitObject {
|
|
|
187
254
|
* difficulty hitobject's index, `null` if the index is out of range.
|
|
188
255
|
*/
|
|
189
256
|
previous(backwardsIndex) {
|
|
190
|
-
|
|
257
|
+
var _a;
|
|
258
|
+
return (_a = this.hitObjects[this.index - backwardsIndex]) !== null && _a !== void 0 ? _a : null;
|
|
191
259
|
}
|
|
192
260
|
/**
|
|
193
261
|
* Gets the difficulty hitobject at a specific index with respect to the current
|
|
@@ -200,289 +268,105 @@ class DifficultyHitObject {
|
|
|
200
268
|
* difficulty hitobject's index, `null` if the index is out of range.
|
|
201
269
|
*/
|
|
202
270
|
next(forwardsIndex) {
|
|
203
|
-
|
|
271
|
+
var _a;
|
|
272
|
+
return ((_a = this.hitObjects[this.index + forwardsIndex + 2]) !== null && _a !== void 0 ? _a : null);
|
|
204
273
|
}
|
|
205
274
|
/**
|
|
206
275
|
* Calculates the opacity of the hitobject at a given time.
|
|
207
276
|
*
|
|
208
277
|
* @param time The time to calculate the hitobject's opacity at.
|
|
209
278
|
* @param isHidden Whether Hidden mod is used.
|
|
210
|
-
* @param mode The gamemode to calculate the opacity for.
|
|
211
279
|
* @returns The opacity of the hitobject at the given time.
|
|
212
280
|
*/
|
|
213
|
-
opacityAt(time, isHidden
|
|
281
|
+
opacityAt(time, isHidden) {
|
|
214
282
|
if (time > this.object.startTime) {
|
|
215
283
|
// Consider a hitobject as being invisible when its start time is passed.
|
|
216
284
|
// In reality the hitobject will be visible beyond its start time up until its hittable window has passed,
|
|
217
285
|
// but this is an approximation and such a case is unlikely to be hit where this function is used.
|
|
218
286
|
return 0;
|
|
219
287
|
}
|
|
220
|
-
const fadeInStartTime = this.object.startTime - this.
|
|
221
|
-
const fadeInDuration = this.timeFadeIn;
|
|
288
|
+
const fadeInStartTime = this.object.startTime - this.object.timePreempt;
|
|
289
|
+
const fadeInDuration = this.object.timeFadeIn;
|
|
222
290
|
if (isHidden) {
|
|
223
291
|
const fadeOutStartTime = fadeInStartTime + fadeInDuration;
|
|
224
|
-
const fadeOutDuration = this.
|
|
225
|
-
(mode === osuBase.Modes.droid
|
|
226
|
-
? 0.35
|
|
227
|
-
: osuBase.ModHidden.fadeOutDurationMultiplier);
|
|
292
|
+
const fadeOutDuration = this.object.timePreempt * osuBase.ModHidden.fadeOutDurationMultiplier;
|
|
228
293
|
return Math.min(osuBase.MathUtils.clamp((time - fadeInStartTime) / fadeInDuration, 0, 1), 1 -
|
|
229
294
|
osuBase.MathUtils.clamp((time - fadeOutStartTime) / fadeOutDuration, 0, 1));
|
|
230
295
|
}
|
|
231
296
|
return osuBase.MathUtils.clamp((time - fadeInStartTime) / fadeInDuration, 0, 1);
|
|
232
297
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (this.object instanceof osuBase.Spinner) {
|
|
244
|
-
return false;
|
|
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;
|
|
252
|
-
}
|
|
253
|
-
if (considerDistance) {
|
|
254
|
-
const endPosition = this.object.getStackedPosition(osuBase.Modes.droid);
|
|
255
|
-
let distance = previous.object
|
|
256
|
-
.getStackedEndPosition(osuBase.Modes.droid)
|
|
257
|
-
.getDistance(endPosition);
|
|
258
|
-
if (previous.object instanceof osuBase.Slider &&
|
|
259
|
-
previous.object.lazyEndPosition) {
|
|
260
|
-
distance = Math.min(distance, previous.object.lazyEndPosition.getDistance(endPosition));
|
|
298
|
+
setDistances(clockRate) {
|
|
299
|
+
if (this.object instanceof osuBase.Slider) {
|
|
300
|
+
this.calculateSliderCursorPosition(this.object);
|
|
301
|
+
this.travelDistance = this.object.lazyTravelDistance;
|
|
302
|
+
// Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
|
303
|
+
if (this.mode === osuBase.Modes.droid) {
|
|
304
|
+
this.travelDistance *= Math.pow(1 + this.object.repeatCount / 4, 1 / 4);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
this.travelDistance *= Math.pow(1 + this.object.repeatCount / 2.5, 1 / 2.5);
|
|
261
308
|
}
|
|
262
|
-
|
|
309
|
+
this.travelTime = Math.max(this.object.lazyTravelTime / clockRate, this.minDeltaTime);
|
|
263
310
|
}
|
|
264
|
-
|
|
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;
|
|
311
|
+
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner.
|
|
312
|
+
if (!this.lastObject ||
|
|
313
|
+
this.object instanceof osuBase.Spinner ||
|
|
314
|
+
this.lastObject instanceof osuBase.Spinner) {
|
|
315
|
+
return;
|
|
299
316
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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;
|
|
410
|
-
}
|
|
411
|
-
const lastCursorPosition = this.getEndCursorPosition(lastObject.object);
|
|
412
|
-
object.lazyJumpDistance = object.object
|
|
317
|
+
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
|
318
|
+
const { scalingFactor } = this;
|
|
319
|
+
const lastCursorPosition = this.getEndCursorPosition(this.lastObject);
|
|
320
|
+
this.lazyJumpDistance = this.object
|
|
321
|
+
.getStackedPosition(this.mode)
|
|
322
|
+
.scale(scalingFactor)
|
|
323
|
+
.subtract(lastCursorPosition.scale(scalingFactor)).length;
|
|
324
|
+
this.minimumJumpTime = this.strainTime;
|
|
325
|
+
this.minimumJumpDistance = this.lazyJumpDistance;
|
|
326
|
+
if (this.lastObject instanceof osuBase.Slider) {
|
|
327
|
+
const lastTravelTime = Math.max(this.lastObject.lazyTravelTime / clockRate, this.minDeltaTime);
|
|
328
|
+
this.minimumJumpTime = Math.max(this.strainTime - lastTravelTime, this.minDeltaTime);
|
|
329
|
+
// 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.
|
|
330
|
+
//
|
|
331
|
+
// 1. The anti-flow pattern, where players cut the slider short in order to move to the next hitobject.
|
|
332
|
+
//
|
|
333
|
+
// <======o==> ← slider
|
|
334
|
+
// | ← most natural jump path
|
|
335
|
+
// o ← a follow-up hitcircle
|
|
336
|
+
//
|
|
337
|
+
// In this case the most natural jump path is approximated by LazyJumpDistance.
|
|
338
|
+
//
|
|
339
|
+
// 2. The flow pattern, where players follow through the slider to its visual extent into the next hitobject.
|
|
340
|
+
//
|
|
341
|
+
// <======o==>---o
|
|
342
|
+
// ↑
|
|
343
|
+
// most natural jump path
|
|
344
|
+
//
|
|
345
|
+
// 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.
|
|
346
|
+
//
|
|
347
|
+
// Thus, the player is assumed to jump the minimum of these two distances in all cases.
|
|
348
|
+
const tailJumpDistance = this.lastObject.tail
|
|
413
349
|
.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);
|
|
350
|
+
.subtract(this.object.getStackedPosition(this.mode))
|
|
351
|
+
.length * scalingFactor;
|
|
352
|
+
this.minimumJumpDistance = Math.max(0, Math.min(this.lazyJumpDistance -
|
|
353
|
+
(this.maximumSliderRadius - this.assumedSliderRadius), tailJumpDistance - this.maximumSliderRadius));
|
|
354
|
+
}
|
|
355
|
+
if (this.lastLastObject && !(this.lastLastObject instanceof osuBase.Spinner)) {
|
|
356
|
+
const lastLastCursorPosition = this.getEndCursorPosition(this.lastLastObject);
|
|
357
|
+
const v1 = lastLastCursorPosition.subtract(this.lastObject.getStackedPosition(this.mode));
|
|
358
|
+
const v2 = this.object
|
|
359
|
+
.getStackedPosition(this.mode)
|
|
360
|
+
.subtract(lastCursorPosition);
|
|
361
|
+
const dot = v1.dot(v2);
|
|
362
|
+
const det = v1.x * v2.y - v1.y * v2.x;
|
|
363
|
+
this.angle = Math.abs(Math.atan2(det, dot));
|
|
458
364
|
}
|
|
459
|
-
return difficultyObjects;
|
|
460
365
|
}
|
|
461
|
-
/**
|
|
462
|
-
* Calculates a slider's cursor position.
|
|
463
|
-
*/
|
|
464
366
|
calculateSliderCursorPosition(slider) {
|
|
465
367
|
if (slider.lazyEndPosition) {
|
|
466
368
|
return;
|
|
467
369
|
}
|
|
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
|
-
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
|
-
// Temporary lazy end position until a real result can be derived.
|
|
475
|
-
slider.lazyEndPosition = slider.getStackedPosition(this.mode);
|
|
476
|
-
// Stop here if the slider has too short duration due to float number limitation.
|
|
477
|
-
// Incredibly close start and end time fluctuates travel distance and lazy
|
|
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.
|
|
482
|
-
if (osuBase.Precision.almostEqualsNumber(slider.startTime, slider.endTime)) {
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
370
|
// Not using slider.endTime due to legacy last tick offset.
|
|
487
371
|
slider.lazyTravelTime =
|
|
488
372
|
slider.nestedHitObjects.at(-1).startTime - slider.startTime;
|
|
@@ -498,7 +382,7 @@ class DifficultyHitObjectCreator {
|
|
|
498
382
|
.getStackedPosition(this.mode)
|
|
499
383
|
.add(slider.path.positionAt(endTimeMin));
|
|
500
384
|
let currentCursorPosition = slider.getStackedPosition(this.mode);
|
|
501
|
-
const scalingFactor = this.normalizedRadius / slider.
|
|
385
|
+
const scalingFactor = this.normalizedRadius / slider.radius;
|
|
502
386
|
for (let i = 1; i < slider.nestedHitObjects.length; ++i) {
|
|
503
387
|
const currentMovementObject = slider.nestedHitObjects[i];
|
|
504
388
|
let currentMovement = currentMovementObject
|
|
@@ -537,203 +421,21 @@ class DifficultyHitObjectCreator {
|
|
|
537
421
|
}
|
|
538
422
|
}
|
|
539
423
|
}
|
|
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
424
|
getEndCursorPosition(object) {
|
|
425
|
+
var _a;
|
|
572
426
|
let pos = object.getStackedPosition(this.mode);
|
|
573
427
|
if (object instanceof osuBase.Slider) {
|
|
574
428
|
this.calculateSliderCursorPosition(object);
|
|
575
|
-
pos = object.lazyEndPosition
|
|
429
|
+
pos = (_a = object.lazyEndPosition) !== null && _a !== void 0 ? _a : pos;
|
|
576
430
|
}
|
|
577
431
|
return pos;
|
|
578
432
|
}
|
|
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
433
|
}
|
|
723
434
|
|
|
724
435
|
/**
|
|
725
436
|
* An evaluator for calculating osu!droid Aim skill.
|
|
726
437
|
*/
|
|
727
438
|
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
439
|
/**
|
|
738
440
|
* Evaluates the difficulty of aiming the current object, based on:
|
|
739
441
|
*
|
|
@@ -751,15 +453,16 @@ class DroidAimEvaluator extends AimEvaluator {
|
|
|
751
453
|
current.isOverlapping(true)) {
|
|
752
454
|
return 0;
|
|
753
455
|
}
|
|
754
|
-
return (this.
|
|
755
|
-
this.
|
|
456
|
+
return (this.snapAimStrainOf(current, withSliders) +
|
|
457
|
+
this.flowAimStrainOf(current));
|
|
756
458
|
}
|
|
757
459
|
/**
|
|
758
|
-
* Calculates the aim strain of a hitobject.
|
|
460
|
+
* Calculates the snap aim strain of a hitobject.
|
|
759
461
|
*/
|
|
760
|
-
static
|
|
462
|
+
static snapAimStrainOf(current, withSliders) {
|
|
463
|
+
var _a;
|
|
761
464
|
if (current.index <= 1 ||
|
|
762
|
-
current.previous(0)
|
|
465
|
+
((_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.object) instanceof osuBase.Spinner) {
|
|
763
466
|
return 0;
|
|
764
467
|
}
|
|
765
468
|
const last = current.previous(0);
|
|
@@ -861,28 +564,33 @@ class DroidAimEvaluator extends AimEvaluator {
|
|
|
861
564
|
velocityChangeBonus * this.velocityChangeMultiplier);
|
|
862
565
|
// Add in additional slider velocity bonus.
|
|
863
566
|
if (withSliders) {
|
|
864
|
-
strain +=
|
|
567
|
+
strain +=
|
|
568
|
+
Math.pow(1 + sliderBonus * this.sliderMultiplier, 1.25) - 1;
|
|
865
569
|
}
|
|
866
570
|
return strain;
|
|
867
571
|
}
|
|
868
572
|
/**
|
|
869
|
-
* Calculates the
|
|
573
|
+
* Calculates the flow aim strain of a hitobject.
|
|
870
574
|
*/
|
|
871
|
-
static
|
|
575
|
+
static flowAimStrainOf(current) {
|
|
576
|
+
var _a, _b;
|
|
872
577
|
let speedBonus = 1;
|
|
873
578
|
if (current.strainTime < this.minSpeedBonus) {
|
|
874
579
|
speedBonus +=
|
|
875
580
|
0.75 *
|
|
876
|
-
Math.pow((this.minSpeedBonus - current.strainTime) /
|
|
581
|
+
Math.pow((this.minSpeedBonus - current.strainTime) / 40, 2);
|
|
877
582
|
}
|
|
878
|
-
const travelDistance = current.previous(0)
|
|
879
|
-
const
|
|
880
|
-
return (
|
|
881
|
-
speedBonus *
|
|
882
|
-
Math.pow(distance / this.SINGLE_SPACING_THRESHOLD, 5)) /
|
|
883
|
-
current.strainTime);
|
|
583
|
+
const travelDistance = (_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.travelDistance) !== null && _b !== void 0 ? _b : 0;
|
|
584
|
+
const shortDistancePenalty = Math.pow(Math.min(this.singleSpacingThreshold, travelDistance + current.minimumJumpDistance) / this.singleSpacingThreshold, 3.5);
|
|
585
|
+
return (200 * speedBonus * shortDistancePenalty) / current.strainTime;
|
|
884
586
|
}
|
|
885
587
|
}
|
|
588
|
+
DroidAimEvaluator.wideAngleMultiplier = 1.65;
|
|
589
|
+
DroidAimEvaluator.sliderMultiplier = 1.5;
|
|
590
|
+
DroidAimEvaluator.velocityChangeMultiplier = 0.85;
|
|
591
|
+
DroidAimEvaluator.singleSpacingThreshold = 100;
|
|
592
|
+
// 200 1/4 BPM delta time
|
|
593
|
+
DroidAimEvaluator.minSpeedBonus = 75;
|
|
886
594
|
|
|
887
595
|
/**
|
|
888
596
|
* A bare minimal abstract skill for fully custom skill implementations.
|
|
@@ -890,10 +598,6 @@ class DroidAimEvaluator extends AimEvaluator {
|
|
|
890
598
|
* This class should be considered a "processing" class and not persisted.
|
|
891
599
|
*/
|
|
892
600
|
class Skill {
|
|
893
|
-
/**
|
|
894
|
-
* The mods that this skill processes.
|
|
895
|
-
*/
|
|
896
|
-
mods;
|
|
897
601
|
constructor(mods) {
|
|
898
602
|
this.mods = mods;
|
|
899
603
|
}
|
|
@@ -904,33 +608,21 @@ class Skill {
|
|
|
904
608
|
* and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
|
|
905
609
|
*/
|
|
906
610
|
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
|
-
*/
|
|
611
|
+
constructor() {
|
|
612
|
+
super(...arguments);
|
|
613
|
+
/**
|
|
614
|
+
* Strain peaks are stored here.
|
|
615
|
+
*/
|
|
616
|
+
this.strainPeaks = [];
|
|
617
|
+
this.sectionLength = 400;
|
|
618
|
+
this.currentStrain = 0;
|
|
619
|
+
this.currentSectionPeak = 0;
|
|
620
|
+
this.currentSectionEnd = 0;
|
|
621
|
+
}
|
|
927
622
|
process(current) {
|
|
928
623
|
// 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;
|
|
624
|
+
if (current.index === 0) {
|
|
625
|
+
this.currentSectionEnd = this.calculateCurrentSectionStart(current);
|
|
934
626
|
}
|
|
935
627
|
while (current.startTime > this.currentSectionEnd) {
|
|
936
628
|
this.saveCurrentPeak();
|
|
@@ -941,6 +633,10 @@ class StrainSkill extends Skill {
|
|
|
941
633
|
this.currentStrain = this.strainValueAt(current);
|
|
942
634
|
this.saveToHitObject(current);
|
|
943
635
|
this.currentSectionPeak = Math.max(this.currentStrain, this.currentSectionPeak);
|
|
636
|
+
if (!current.next(0)) {
|
|
637
|
+
// Don't forget to save the last strain peak, which would otherwise be ignored.
|
|
638
|
+
this.saveCurrentPeak();
|
|
639
|
+
}
|
|
944
640
|
}
|
|
945
641
|
/**
|
|
946
642
|
* Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty.
|
|
@@ -956,18 +652,26 @@ class StrainSkill extends Skill {
|
|
|
956
652
|
strainDecay(ms) {
|
|
957
653
|
return Math.pow(this.strainDecayBase, ms / 1000);
|
|
958
654
|
}
|
|
655
|
+
/**
|
|
656
|
+
* Calculates the starting time of a strain section at an object.
|
|
657
|
+
*
|
|
658
|
+
* @param current The object at which the strain section starts.
|
|
659
|
+
* @returns The start time of the strain section.
|
|
660
|
+
*/
|
|
661
|
+
calculateCurrentSectionStart(current) {
|
|
662
|
+
return (Math.ceil(current.startTime / this.sectionLength) *
|
|
663
|
+
this.sectionLength);
|
|
664
|
+
}
|
|
959
665
|
/**
|
|
960
666
|
* Sets the initial strain level for a new section.
|
|
961
667
|
*
|
|
962
|
-
* @param
|
|
668
|
+
* @param time The beginning of the new section in milliseconds.
|
|
963
669
|
* @param current The current hitobject.
|
|
964
670
|
*/
|
|
965
|
-
startNewSectionFrom(
|
|
966
|
-
// The maximum strain of the new section is not zero by default
|
|
671
|
+
startNewSectionFrom(time, current) {
|
|
672
|
+
// The maximum strain of the new section is not zero by default.
|
|
967
673
|
// 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);
|
|
674
|
+
this.currentSectionPeak = this.calculateInitialStrain(time, current);
|
|
971
675
|
}
|
|
972
676
|
}
|
|
973
677
|
|
|
@@ -976,6 +680,35 @@ class StrainSkill extends Skill {
|
|
|
976
680
|
* and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
|
|
977
681
|
*/
|
|
978
682
|
class DroidSkill extends StrainSkill {
|
|
683
|
+
constructor() {
|
|
684
|
+
super(...arguments);
|
|
685
|
+
this._objectStrains = [];
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* The strains of hitobjects.
|
|
689
|
+
*/
|
|
690
|
+
get objectStrains() {
|
|
691
|
+
return this._objectStrains;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Returns the number of strains weighed against the top strain.
|
|
695
|
+
*
|
|
696
|
+
* The result is scaled by clock rate as it affects the total number of strains.
|
|
697
|
+
*/
|
|
698
|
+
countDifficultStrains() {
|
|
699
|
+
if (this._objectStrains.length === 0) {
|
|
700
|
+
return 0;
|
|
701
|
+
}
|
|
702
|
+
const maxStrain = Math.max(...this._objectStrains);
|
|
703
|
+
if (maxStrain === 0) {
|
|
704
|
+
return 0;
|
|
705
|
+
}
|
|
706
|
+
return this._objectStrains.reduce((total, next) => total + Math.pow(next / maxStrain, 4), 0);
|
|
707
|
+
}
|
|
708
|
+
process(current) {
|
|
709
|
+
super.process(current);
|
|
710
|
+
this._objectStrains.push(this.getObjectStrain(current));
|
|
711
|
+
}
|
|
979
712
|
difficultyValue() {
|
|
980
713
|
const strains = this.strainPeaks.slice();
|
|
981
714
|
if (this.reducedSectionCount > 0) {
|
|
@@ -995,41 +728,49 @@ class DroidSkill extends StrainSkill {
|
|
|
995
728
|
return a + Math.pow(v, 1 / Math.log2(this.starsPerDouble));
|
|
996
729
|
}, 0), Math.log2(this.starsPerDouble));
|
|
997
730
|
}
|
|
731
|
+
calculateCurrentSectionStart(current) {
|
|
732
|
+
return current.startTime;
|
|
733
|
+
}
|
|
998
734
|
}
|
|
999
735
|
|
|
1000
736
|
/**
|
|
1001
737
|
* Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
|
1002
738
|
*/
|
|
1003
739
|
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
740
|
constructor(mods, withSliders) {
|
|
1011
741
|
super(mods);
|
|
742
|
+
this.strainDecayBase = 0.15;
|
|
743
|
+
this.reducedSectionCount = 10;
|
|
744
|
+
this.reducedSectionBaseline = 0.75;
|
|
745
|
+
this.starsPerDouble = 1.05;
|
|
746
|
+
this.skillMultiplier = 24.55;
|
|
747
|
+
this.currentAimStrain = 0;
|
|
1012
748
|
this.withSliders = withSliders;
|
|
1013
749
|
}
|
|
1014
|
-
/**
|
|
1015
|
-
* @param current The hitobject to calculate.
|
|
1016
|
-
*/
|
|
1017
750
|
strainValueAt(current) {
|
|
1018
|
-
this.
|
|
1019
|
-
this.
|
|
751
|
+
this.currentAimStrain *= this.strainDecay(current.deltaTime);
|
|
752
|
+
this.currentAimStrain +=
|
|
1020
753
|
DroidAimEvaluator.evaluateDifficultyOf(current, this.withSliders) *
|
|
1021
754
|
this.skillMultiplier;
|
|
1022
|
-
return this.
|
|
755
|
+
return this.currentAimStrain;
|
|
756
|
+
}
|
|
757
|
+
calculateInitialStrain(time, current) {
|
|
758
|
+
var _a, _b;
|
|
759
|
+
return (this.currentAimStrain *
|
|
760
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
761
|
+
}
|
|
762
|
+
getObjectStrain() {
|
|
763
|
+
return this.currentAimStrain;
|
|
1023
764
|
}
|
|
1024
765
|
/**
|
|
1025
766
|
* @param current The hitobject to save to.
|
|
1026
767
|
*/
|
|
1027
768
|
saveToHitObject(current) {
|
|
1028
769
|
if (this.withSliders) {
|
|
1029
|
-
current.aimStrainWithSliders = this.
|
|
770
|
+
current.aimStrainWithSliders = this.currentAimStrain;
|
|
1030
771
|
}
|
|
1031
772
|
else {
|
|
1032
|
-
current.aimStrainWithoutSliders = this.
|
|
773
|
+
current.aimStrainWithoutSliders = this.currentAimStrain;
|
|
1033
774
|
}
|
|
1034
775
|
}
|
|
1035
776
|
}
|
|
@@ -1040,9 +781,9 @@ class DroidAim extends DroidSkill {
|
|
|
1040
781
|
* This class should be considered an "evaluating" class and not persisted.
|
|
1041
782
|
*/
|
|
1042
783
|
class SpeedEvaluator {
|
|
1043
|
-
// ~200 1/4 BPM streams
|
|
1044
|
-
static minSpeedBonus = 75;
|
|
1045
784
|
}
|
|
785
|
+
// ~200 1/4 BPM streams
|
|
786
|
+
SpeedEvaluator.minSpeedBonus = 75;
|
|
1046
787
|
|
|
1047
788
|
/**
|
|
1048
789
|
* An evaluator for calculating osu!droid tap skill.
|
|
@@ -1053,25 +794,26 @@ class DroidTapEvaluator extends SpeedEvaluator {
|
|
|
1053
794
|
*
|
|
1054
795
|
* - time between pressing the previous and current object,
|
|
1055
796
|
* - distance between those objects,
|
|
1056
|
-
* -
|
|
797
|
+
* - how easily they can be cheesed,
|
|
798
|
+
* - and the strain time cap.
|
|
1057
799
|
*
|
|
1058
800
|
* @param current The current object.
|
|
1059
801
|
* @param greatWindow The great hit window of the current object.
|
|
1060
802
|
* @param considerCheesability Whether to consider cheesability.
|
|
803
|
+
* @param strainTimeCap The strain time to cap the object's strain time to.
|
|
1061
804
|
*/
|
|
1062
|
-
static evaluateDifficultyOf(current, greatWindow, considerCheesability) {
|
|
805
|
+
static evaluateDifficultyOf(current, greatWindow, considerCheesability, strainTimeCap) {
|
|
1063
806
|
if (current.object instanceof osuBase.Spinner ||
|
|
1064
807
|
// Exclude overlapping objects that can be tapped at once.
|
|
1065
808
|
current.isOverlapping(false)) {
|
|
1066
809
|
return 0;
|
|
1067
810
|
}
|
|
1068
|
-
let strainTime = current.strainTime;
|
|
1069
811
|
let doubletapness = 1;
|
|
1070
812
|
if (considerCheesability) {
|
|
1071
|
-
const greatWindowFull = greatWindow * 2;
|
|
1072
813
|
// Nerf doubletappable doubles.
|
|
1073
814
|
const next = current.next(0);
|
|
1074
815
|
if (next) {
|
|
816
|
+
const greatWindowFull = greatWindow * 2;
|
|
1075
817
|
const currentDeltaTime = Math.max(1, current.deltaTime);
|
|
1076
818
|
const nextDeltaTime = Math.max(1, next.deltaTime);
|
|
1077
819
|
const deltaDifference = Math.abs(nextDeltaTime - currentDeltaTime);
|
|
@@ -1080,16 +822,18 @@ class DroidTapEvaluator extends SpeedEvaluator {
|
|
|
1080
822
|
const windowRatio = Math.pow(Math.min(1, currentDeltaTime / greatWindowFull), 2);
|
|
1081
823
|
doubletapness = Math.pow(speedRatio, 1 - windowRatio);
|
|
1082
824
|
}
|
|
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
825
|
}
|
|
826
|
+
const strainTime = strainTimeCap !== undefined
|
|
827
|
+
? // We cap the strain time to 50 here as the chance of vibro is higher in any BPM higher than 300.
|
|
828
|
+
Math.max(50, strainTimeCap, current.strainTime)
|
|
829
|
+
: current.strainTime;
|
|
1087
830
|
let speedBonus = 1;
|
|
1088
831
|
if (strainTime < this.minSpeedBonus) {
|
|
1089
832
|
speedBonus +=
|
|
1090
|
-
0.75 *
|
|
833
|
+
0.75 *
|
|
834
|
+
Math.pow(osuBase.ErrorFunction.erf((this.minSpeedBonus - strainTime) / 40), 2);
|
|
1091
835
|
}
|
|
1092
|
-
return (speedBonus * doubletapness) / strainTime;
|
|
836
|
+
return (speedBonus * Math.pow(doubletapness, 1.5)) / strainTime;
|
|
1093
837
|
}
|
|
1094
838
|
}
|
|
1095
839
|
|
|
@@ -1097,38 +841,89 @@ class DroidTapEvaluator extends SpeedEvaluator {
|
|
|
1097
841
|
* 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
842
|
*/
|
|
1099
843
|
class DroidTap extends DroidSkill {
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
greatWindow;
|
|
1108
|
-
constructor(mods, overallDifficulty) {
|
|
844
|
+
/**
|
|
845
|
+
* The delta time of hitobjects.
|
|
846
|
+
*/
|
|
847
|
+
get objectDeltaTimes() {
|
|
848
|
+
return this._objectDeltaTimes;
|
|
849
|
+
}
|
|
850
|
+
constructor(mods, overallDifficulty, considerCheesability, strainTimeCap) {
|
|
1109
851
|
super(mods);
|
|
852
|
+
this.reducedSectionCount = 10;
|
|
853
|
+
this.reducedSectionBaseline = 0.75;
|
|
854
|
+
this.strainDecayBase = 0.3;
|
|
855
|
+
this.starsPerDouble = 1.1;
|
|
856
|
+
this.currentTapStrain = 0;
|
|
857
|
+
this.currentRhythmMultiplier = 0;
|
|
858
|
+
this.skillMultiplier = 1375;
|
|
859
|
+
this._objectDeltaTimes = [];
|
|
1110
860
|
this.greatWindow = new osuBase.OsuHitWindow(overallDifficulty).hitWindowFor300();
|
|
861
|
+
this.considerCheesability = considerCheesability;
|
|
862
|
+
this.strainTimeCap = strainTimeCap;
|
|
1111
863
|
}
|
|
1112
864
|
/**
|
|
1113
|
-
*
|
|
865
|
+
* The amount of notes that are relevant to the difficulty.
|
|
866
|
+
*/
|
|
867
|
+
relevantNoteCount() {
|
|
868
|
+
if (this._objectStrains.length === 0) {
|
|
869
|
+
return 0;
|
|
870
|
+
}
|
|
871
|
+
const maxStrain = Math.max(...this._objectStrains);
|
|
872
|
+
if (maxStrain === 0) {
|
|
873
|
+
return 0;
|
|
874
|
+
}
|
|
875
|
+
return this._objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* The delta time relevant to the difficulty.
|
|
1114
879
|
*/
|
|
880
|
+
relevantDeltaTime() {
|
|
881
|
+
if (this._objectStrains.length === 0) {
|
|
882
|
+
return 0;
|
|
883
|
+
}
|
|
884
|
+
const maxStrain = Math.max(...this._objectStrains);
|
|
885
|
+
if (maxStrain === 0) {
|
|
886
|
+
return 0;
|
|
887
|
+
}
|
|
888
|
+
return (this._objectDeltaTimes.reduce((total, next, index) => total +
|
|
889
|
+
(next * 1) /
|
|
890
|
+
(1 +
|
|
891
|
+
Math.exp(-((this._objectStrains[index] / maxStrain) *
|
|
892
|
+
25 -
|
|
893
|
+
20))), 0) /
|
|
894
|
+
this._objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 25 - 20))), 0));
|
|
895
|
+
}
|
|
1115
896
|
strainValueAt(current) {
|
|
1116
|
-
|
|
1117
|
-
this.currentTapStrain *= decay;
|
|
897
|
+
this.currentTapStrain *= this.strainDecay(current.strainTime);
|
|
1118
898
|
this.currentTapStrain +=
|
|
1119
|
-
DroidTapEvaluator.evaluateDifficultyOf(current, this.greatWindow,
|
|
1120
|
-
this.
|
|
1121
|
-
this.
|
|
1122
|
-
DroidTapEvaluator.evaluateDifficultyOf(current, this.greatWindow, false) * this.skillMultiplier;
|
|
1123
|
-
this.currentOriginalTapStrain *= current.rhythmMultiplier;
|
|
899
|
+
DroidTapEvaluator.evaluateDifficultyOf(current, this.greatWindow, this.considerCheesability, this.strainTimeCap) * this.skillMultiplier;
|
|
900
|
+
this.currentRhythmMultiplier = current.rhythmMultiplier;
|
|
901
|
+
this._objectDeltaTimes.push(current.deltaTime);
|
|
1124
902
|
return this.currentTapStrain * current.rhythmMultiplier;
|
|
1125
903
|
}
|
|
904
|
+
calculateInitialStrain(time, current) {
|
|
905
|
+
var _a, _b;
|
|
906
|
+
return (this.currentTapStrain *
|
|
907
|
+
this.currentRhythmMultiplier *
|
|
908
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
909
|
+
}
|
|
910
|
+
getObjectStrain() {
|
|
911
|
+
return this.currentTapStrain * this.currentRhythmMultiplier;
|
|
912
|
+
}
|
|
1126
913
|
/**
|
|
1127
914
|
* @param current The hitobject to save to.
|
|
1128
915
|
*/
|
|
1129
916
|
saveToHitObject(current) {
|
|
1130
|
-
|
|
1131
|
-
|
|
917
|
+
if (this.strainTimeCap !== undefined) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
const strain = this.currentTapStrain * this.currentRhythmMultiplier;
|
|
921
|
+
if (this.considerCheesability) {
|
|
922
|
+
current.tapStrain = strain;
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
current.originalTapStrain = strain;
|
|
926
|
+
}
|
|
1132
927
|
}
|
|
1133
928
|
}
|
|
1134
929
|
|
|
@@ -1138,12 +933,12 @@ class DroidTap extends DroidSkill {
|
|
|
1138
933
|
* This class should be considered an "evaluating" class and not persisted.
|
|
1139
934
|
*/
|
|
1140
935
|
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
936
|
}
|
|
937
|
+
FlashlightEvaluator.maxOpacityBonus = 0.4;
|
|
938
|
+
FlashlightEvaluator.hiddenBonus = 0.2;
|
|
939
|
+
FlashlightEvaluator.minVelocity = 0.5;
|
|
940
|
+
FlashlightEvaluator.sliderMultiplier = 1.3;
|
|
941
|
+
FlashlightEvaluator.minAngleMultiplier = 0.2;
|
|
1147
942
|
|
|
1148
943
|
/**
|
|
1149
944
|
* An evaluator for calculating osu!droid Flashlight skill.
|
|
@@ -1168,7 +963,7 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
1168
963
|
current.isOverlapping(true)) {
|
|
1169
964
|
return 0;
|
|
1170
965
|
}
|
|
1171
|
-
const scalingFactor = 52 / current.object.
|
|
966
|
+
const scalingFactor = 52 / current.object.radius;
|
|
1172
967
|
let smallDistNerf = 1;
|
|
1173
968
|
let cumulativeStrainTime = 0;
|
|
1174
969
|
let result = 0;
|
|
@@ -1193,7 +988,7 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
1193
988
|
const opacityBonus = 1 +
|
|
1194
989
|
this.maxOpacityBonus *
|
|
1195
990
|
(1 -
|
|
1196
|
-
current.opacityAt(currentObject.object.startTime, isHiddenMod
|
|
991
|
+
current.opacityAt(currentObject.object.startTime, isHiddenMod));
|
|
1197
992
|
result +=
|
|
1198
993
|
(stackNerf * opacityBonus * scalingFactor * jumpDistance) /
|
|
1199
994
|
cumulativeStrainTime;
|
|
@@ -1224,8 +1019,8 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
1224
1019
|
// Longer sliders require more memorization.
|
|
1225
1020
|
sliderBonus *= pixelTravelDistance;
|
|
1226
1021
|
// Nerf sliders with repeats, as less memorization is required.
|
|
1227
|
-
if (current.object.
|
|
1228
|
-
sliderBonus /= current.object.
|
|
1022
|
+
if (current.object.repeatCount > 0)
|
|
1023
|
+
sliderBonus /= current.object.repeatCount + 1;
|
|
1229
1024
|
}
|
|
1230
1025
|
result += sliderBonus * this.sliderMultiplier;
|
|
1231
1026
|
return result;
|
|
@@ -1236,35 +1031,43 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
1236
1031
|
* Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
|
|
1237
1032
|
*/
|
|
1238
1033
|
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
1034
|
constructor(mods, withSliders) {
|
|
1247
1035
|
super(mods);
|
|
1036
|
+
this.strainDecayBase = 0.15;
|
|
1037
|
+
this.reducedSectionCount = 0;
|
|
1038
|
+
this.reducedSectionBaseline = 1;
|
|
1039
|
+
this.starsPerDouble = 1.06;
|
|
1040
|
+
this.skillMultiplier = 0.052;
|
|
1041
|
+
this.currentFlashlightStrain = 0;
|
|
1248
1042
|
this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
|
|
1249
1043
|
this.withSliders = withSliders;
|
|
1250
1044
|
}
|
|
1251
|
-
/**
|
|
1252
|
-
* @param current The hitobject to calculate.
|
|
1253
|
-
*/
|
|
1254
1045
|
strainValueAt(current) {
|
|
1255
|
-
this.
|
|
1256
|
-
this.
|
|
1046
|
+
this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
|
|
1047
|
+
this.currentFlashlightStrain +=
|
|
1257
1048
|
DroidFlashlightEvaluator.evaluateDifficultyOf(current, this.isHidden, this.withSliders) * this.skillMultiplier;
|
|
1258
|
-
return this.
|
|
1049
|
+
return this.currentFlashlightStrain;
|
|
1050
|
+
}
|
|
1051
|
+
calculateInitialStrain(time, current) {
|
|
1052
|
+
var _a, _b;
|
|
1053
|
+
return (this.currentFlashlightStrain *
|
|
1054
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
1055
|
+
}
|
|
1056
|
+
getObjectStrain() {
|
|
1057
|
+
return this.currentFlashlightStrain;
|
|
1259
1058
|
}
|
|
1260
1059
|
saveToHitObject(current) {
|
|
1261
1060
|
if (this.withSliders) {
|
|
1262
|
-
current.flashlightStrainWithSliders = this.
|
|
1061
|
+
current.flashlightStrainWithSliders = this.currentFlashlightStrain;
|
|
1263
1062
|
}
|
|
1264
1063
|
else {
|
|
1265
|
-
current.flashlightStrainWithoutSliders =
|
|
1064
|
+
current.flashlightStrainWithoutSliders =
|
|
1065
|
+
this.currentFlashlightStrain;
|
|
1266
1066
|
}
|
|
1267
1067
|
}
|
|
1068
|
+
difficultyValue() {
|
|
1069
|
+
return Math.pow(this.strainPeaks.reduce((a, v) => a + v, 0) * this.starsPerDouble, 0.8);
|
|
1070
|
+
}
|
|
1268
1071
|
}
|
|
1269
1072
|
|
|
1270
1073
|
/**
|
|
@@ -1273,9 +1076,9 @@ class DroidFlashlight extends DroidSkill {
|
|
|
1273
1076
|
* This class should be considered an "evaluating" class and not persisted.
|
|
1274
1077
|
*/
|
|
1275
1078
|
class RhythmEvaluator {
|
|
1276
|
-
static rhythmMultiplier = 0.75;
|
|
1277
|
-
static historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
|
|
1278
1079
|
}
|
|
1080
|
+
RhythmEvaluator.rhythmMultiplier = 0.75;
|
|
1081
|
+
RhythmEvaluator.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
|
|
1279
1082
|
|
|
1280
1083
|
/**
|
|
1281
1084
|
* An evaluator for calculating osu!droid Rhythm skill.
|
|
@@ -1333,8 +1136,8 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
|
|
|
1333
1136
|
Math.min(0.5, Math.pow(Math.sin(Math.PI /
|
|
1334
1137
|
(Math.min(prevDelta, currentDelta) /
|
|
1335
1138
|
Math.max(prevDelta, currentDelta))), 2));
|
|
1336
|
-
const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) - greatWindow * 0.
|
|
1337
|
-
(greatWindow * 0.
|
|
1139
|
+
const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) - greatWindow * 0.6) /
|
|
1140
|
+
(greatWindow * 0.6));
|
|
1338
1141
|
let effectiveRatio = windowPenalty * currentRatio;
|
|
1339
1142
|
if (firstDeltaSwitch) {
|
|
1340
1143
|
if (prevDelta <= 1.25 * currentDelta &&
|
|
@@ -1411,31 +1214,39 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
|
|
|
1411
1214
|
* Represents the skill required to properly follow a beatmap's rhythm.
|
|
1412
1215
|
*/
|
|
1413
1216
|
class DroidRhythm extends DroidSkill {
|
|
1414
|
-
skillMultiplier = 1;
|
|
1415
|
-
reducedSectionCount = 5;
|
|
1416
|
-
reducedSectionBaseline = 0.75;
|
|
1417
|
-
strainDecayBase = 0.3;
|
|
1418
|
-
starsPerDouble = 1.75;
|
|
1419
|
-
currentRhythm = 1;
|
|
1420
|
-
hitWindow;
|
|
1421
1217
|
constructor(mods, overallDifficulty) {
|
|
1422
1218
|
super(mods);
|
|
1219
|
+
this.reducedSectionCount = 5;
|
|
1220
|
+
this.reducedSectionBaseline = 0.75;
|
|
1221
|
+
this.strainDecayBase = 0.3;
|
|
1222
|
+
this.starsPerDouble = 1.75;
|
|
1223
|
+
this.currentRhythmStrain = 0;
|
|
1224
|
+
this.currentRhythmMultiplier = 1;
|
|
1423
1225
|
this.hitWindow = new osuBase.OsuHitWindow(overallDifficulty);
|
|
1424
1226
|
}
|
|
1425
1227
|
strainValueAt(current) {
|
|
1426
|
-
this.
|
|
1427
|
-
|
|
1428
|
-
this.
|
|
1429
|
-
|
|
1228
|
+
this.currentRhythmMultiplier =
|
|
1229
|
+
DroidRhythmEvaluator.evaluateDifficultyOf(current, this.hitWindow.hitWindowFor300());
|
|
1230
|
+
this.currentRhythmStrain *= this.strainDecay(current.deltaTime);
|
|
1231
|
+
this.currentRhythmStrain += this.currentRhythmMultiplier - 1;
|
|
1232
|
+
return this.currentRhythmStrain;
|
|
1233
|
+
}
|
|
1234
|
+
calculateInitialStrain(time, current) {
|
|
1235
|
+
var _a, _b;
|
|
1236
|
+
return (this.currentRhythmStrain *
|
|
1237
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
1238
|
+
}
|
|
1239
|
+
getObjectStrain() {
|
|
1240
|
+
return this.currentRhythmStrain;
|
|
1430
1241
|
}
|
|
1431
1242
|
saveToHitObject(current) {
|
|
1432
|
-
current.rhythmStrain = this.
|
|
1433
|
-
current.rhythmMultiplier = this.
|
|
1243
|
+
current.rhythmStrain = this.currentRhythmStrain;
|
|
1244
|
+
current.rhythmMultiplier = this.currentRhythmMultiplier;
|
|
1434
1245
|
}
|
|
1435
1246
|
}
|
|
1436
1247
|
|
|
1437
1248
|
/**
|
|
1438
|
-
* An evaluator for calculating osu!droid
|
|
1249
|
+
* An evaluator for calculating osu!droid visual skill.
|
|
1439
1250
|
*/
|
|
1440
1251
|
class DroidVisualEvaluator {
|
|
1441
1252
|
/**
|
|
@@ -1456,7 +1267,8 @@ class DroidVisualEvaluator {
|
|
|
1456
1267
|
static evaluateDifficultyOf(current, isHiddenMod, withSliders) {
|
|
1457
1268
|
if (current.object instanceof osuBase.Spinner ||
|
|
1458
1269
|
// Exclude overlapping objects that can be tapped at once.
|
|
1459
|
-
current.isOverlapping(true)
|
|
1270
|
+
current.isOverlapping(true) ||
|
|
1271
|
+
current.index === 0) {
|
|
1460
1272
|
return 0;
|
|
1461
1273
|
}
|
|
1462
1274
|
// Start with base density and give global bonus for Hidden.
|
|
@@ -1478,28 +1290,30 @@ class DroidVisualEvaluator {
|
|
|
1478
1290
|
}
|
|
1479
1291
|
// Do not consider objects that don't fall under time preempt.
|
|
1480
1292
|
if (current.object.startTime - previous.object.endTime >
|
|
1481
|
-
current.
|
|
1293
|
+
current.object.timePreempt) {
|
|
1482
1294
|
break;
|
|
1483
1295
|
}
|
|
1484
1296
|
strain +=
|
|
1485
1297
|
(1 -
|
|
1486
|
-
current.opacityAt(previous.object.startTime, isHiddenMod
|
|
1298
|
+
current.opacityAt(previous.object.startTime, isHiddenMod)) /
|
|
1487
1299
|
4;
|
|
1488
1300
|
}
|
|
1489
|
-
// Scale the value with overlapping factor.
|
|
1490
|
-
strain /= 10 * (1 + current.overlappingFactor);
|
|
1491
1301
|
if (current.timePreempt < 400) {
|
|
1492
1302
|
// Give bonus for AR higher than 10.33.
|
|
1493
|
-
strain += Math.pow(400 - current.timePreempt, 1.
|
|
1303
|
+
strain += Math.pow(400 - current.timePreempt, 1.35) / 100;
|
|
1494
1304
|
}
|
|
1305
|
+
// Scale the value with overlapping factor.
|
|
1306
|
+
strain /= 10 * (1 + current.overlappingFactor);
|
|
1495
1307
|
if (current.object instanceof osuBase.Slider && withSliders) {
|
|
1496
|
-
const scalingFactor = 50 / current.object.
|
|
1497
|
-
//
|
|
1308
|
+
const scalingFactor = 50 / current.object.radius;
|
|
1309
|
+
// Invert the scaling factor to determine the true travel distance independent of circle size.
|
|
1310
|
+
const pixelTravelDistance = current.object.lazyTravelDistance / scalingFactor;
|
|
1311
|
+
const currentVelocity = pixelTravelDistance / current.travelTime;
|
|
1498
1312
|
strain +=
|
|
1499
|
-
//
|
|
1500
|
-
Math.min(6,
|
|
1501
|
-
//
|
|
1502
|
-
|
|
1313
|
+
// Reward sliders based on velocity, while also avoiding overbuffing extremely fast sliders.
|
|
1314
|
+
Math.min(6, currentVelocity * 1.5) *
|
|
1315
|
+
// Longer sliders require more reading.
|
|
1316
|
+
(pixelTravelDistance / 100);
|
|
1503
1317
|
let cumulativeStrainTime = 0;
|
|
1504
1318
|
// Reward for velocity changes based on last few sliders.
|
|
1505
1319
|
for (let i = 0; i < Math.min(current.index, 4); ++i) {
|
|
@@ -1510,53 +1324,19 @@ class DroidVisualEvaluator {
|
|
|
1510
1324
|
last.isOverlapping(true)) {
|
|
1511
1325
|
continue;
|
|
1512
1326
|
}
|
|
1327
|
+
// Invert the scaling factor to determine the true travel distance independent of circle size.
|
|
1328
|
+
const pixelTravelDistance = last.object.lazyTravelDistance / scalingFactor;
|
|
1329
|
+
const lastVelocity = pixelTravelDistance / last.travelTime;
|
|
1513
1330
|
strain +=
|
|
1514
|
-
//
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1331
|
+
// Reward past sliders based on velocity changes, while also
|
|
1332
|
+
// avoiding overbuffing extremely fast velocity changes.
|
|
1333
|
+
Math.min(10, 2.5 * Math.abs(currentVelocity - lastVelocity)) *
|
|
1334
|
+
// Longer sliders require more reading.
|
|
1335
|
+
(pixelTravelDistance / 125) *
|
|
1336
|
+
// Avoid overbuffing past sliders.
|
|
1519
1337
|
Math.min(1, 300 / cumulativeStrainTime);
|
|
1520
1338
|
}
|
|
1521
1339
|
}
|
|
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.
|
|
1549
|
-
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
|
-
}
|
|
1558
|
-
strain += rhythmBonus;
|
|
1559
|
-
}
|
|
1560
1340
|
return strain;
|
|
1561
1341
|
}
|
|
1562
1342
|
}
|
|
@@ -1565,31 +1345,212 @@ class DroidVisualEvaluator {
|
|
|
1565
1345
|
* Represents the skill required to read every object in the map.
|
|
1566
1346
|
*/
|
|
1567
1347
|
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
1348
|
constructor(mods, withSliders) {
|
|
1576
1349
|
super(mods);
|
|
1350
|
+
this.starsPerDouble = 1.025;
|
|
1351
|
+
this.reducedSectionCount = 10;
|
|
1352
|
+
this.reducedSectionBaseline = 0.75;
|
|
1353
|
+
this.strainDecayBase = 0.1;
|
|
1354
|
+
this.currentVisualStrain = 0;
|
|
1355
|
+
this.currentRhythmMultiplier = 1;
|
|
1356
|
+
this.skillMultiplier = 10;
|
|
1577
1357
|
this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
|
|
1578
1358
|
this.withSliders = withSliders;
|
|
1579
1359
|
}
|
|
1580
1360
|
strainValueAt(current) {
|
|
1581
|
-
this.
|
|
1582
|
-
this.
|
|
1361
|
+
this.currentVisualStrain *= this.strainDecay(current.deltaTime);
|
|
1362
|
+
this.currentVisualStrain +=
|
|
1583
1363
|
DroidVisualEvaluator.evaluateDifficultyOf(current, this.isHidden, this.withSliders) * this.skillMultiplier;
|
|
1584
|
-
|
|
1364
|
+
this.currentRhythmMultiplier = current.rhythmMultiplier;
|
|
1365
|
+
return this.currentVisualStrain * this.currentRhythmMultiplier;
|
|
1366
|
+
}
|
|
1367
|
+
calculateInitialStrain(time, current) {
|
|
1368
|
+
var _a, _b;
|
|
1369
|
+
return (this.currentVisualStrain *
|
|
1370
|
+
this.currentRhythmMultiplier *
|
|
1371
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
1372
|
+
}
|
|
1373
|
+
getObjectStrain() {
|
|
1374
|
+
return this.currentVisualStrain * this.currentRhythmMultiplier;
|
|
1585
1375
|
}
|
|
1586
1376
|
saveToHitObject(current) {
|
|
1377
|
+
const strain = this.currentVisualStrain * this.currentRhythmMultiplier;
|
|
1587
1378
|
if (this.withSliders) {
|
|
1588
|
-
current.visualStrainWithSliders =
|
|
1379
|
+
current.visualStrainWithSliders = strain;
|
|
1589
1380
|
}
|
|
1590
1381
|
else {
|
|
1591
|
-
current.visualStrainWithoutSliders =
|
|
1382
|
+
current.visualStrainWithoutSliders = strain;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* Represents an osu!droid hit object with difficulty calculation values.
|
|
1389
|
+
*/
|
|
1390
|
+
class DroidDifficultyHitObject extends DifficultyHitObject {
|
|
1391
|
+
get scalingFactor() {
|
|
1392
|
+
const radius = this.object.radius;
|
|
1393
|
+
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
|
1394
|
+
let scalingFactor = this.normalizedRadius / radius;
|
|
1395
|
+
// High circle size (small CS) bonus
|
|
1396
|
+
if (radius < this.radiusBuffThreshold) {
|
|
1397
|
+
scalingFactor *=
|
|
1398
|
+
1 + Math.pow((this.radiusBuffThreshold - radius) / 50, 2);
|
|
1399
|
+
}
|
|
1400
|
+
return scalingFactor;
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Note: You **must** call `computeProperties` at some point due to how TypeScript handles
|
|
1404
|
+
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue).
|
|
1405
|
+
*
|
|
1406
|
+
* @param object The underlying hitobject.
|
|
1407
|
+
* @param lastObject The hitobject before this hitobject.
|
|
1408
|
+
* @param lastLastObject The hitobject before the last hitobject.
|
|
1409
|
+
* @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
|
|
1410
|
+
* @param clockRate The clock rate of the beatmap.
|
|
1411
|
+
* @param isForceAR Whether force AR is enabled.
|
|
1412
|
+
*/
|
|
1413
|
+
constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, isForceAR) {
|
|
1414
|
+
super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate);
|
|
1415
|
+
/**
|
|
1416
|
+
* The tap strain generated by the hitobject.
|
|
1417
|
+
*/
|
|
1418
|
+
this.tapStrain = 0;
|
|
1419
|
+
/**
|
|
1420
|
+
* The tap strain generated by the hitobject if `strainTime` isn't modified by
|
|
1421
|
+
* OD. This is used in three-finger detection.
|
|
1422
|
+
*/
|
|
1423
|
+
this.originalTapStrain = 0;
|
|
1424
|
+
/**
|
|
1425
|
+
* The rhythm strain generated by the hitobject.
|
|
1426
|
+
*/
|
|
1427
|
+
this.rhythmStrain = 0;
|
|
1428
|
+
/**
|
|
1429
|
+
* The flashlight strain generated by the hitobject if sliders are considered.
|
|
1430
|
+
*/
|
|
1431
|
+
this.flashlightStrainWithSliders = 0;
|
|
1432
|
+
/**
|
|
1433
|
+
* The flashlight strain generated by the hitobject if sliders are not considered.
|
|
1434
|
+
*/
|
|
1435
|
+
this.flashlightStrainWithoutSliders = 0;
|
|
1436
|
+
/**
|
|
1437
|
+
* The visual strain generated by the hitobject if sliders are considered.
|
|
1438
|
+
*/
|
|
1439
|
+
this.visualStrainWithSliders = 0;
|
|
1440
|
+
/**
|
|
1441
|
+
* The visual strain generated by the hitobject if sliders are not considered.
|
|
1442
|
+
*/
|
|
1443
|
+
this.visualStrainWithoutSliders = 0;
|
|
1444
|
+
/**
|
|
1445
|
+
* The note density of the hitobject.
|
|
1446
|
+
*/
|
|
1447
|
+
this.noteDensity = 1;
|
|
1448
|
+
/**
|
|
1449
|
+
* The overlapping factor of the hitobject.
|
|
1450
|
+
*
|
|
1451
|
+
* This is used to scale visual skill.
|
|
1452
|
+
*/
|
|
1453
|
+
this.overlappingFactor = 0;
|
|
1454
|
+
this.radiusBuffThreshold = 70;
|
|
1455
|
+
this.mode = osuBase.Modes.droid;
|
|
1456
|
+
this.maximumSliderRadius = this.normalizedRadius * 2;
|
|
1457
|
+
this.timePreempt = object.timePreempt;
|
|
1458
|
+
if (!isForceAR) {
|
|
1459
|
+
this.timePreempt /= clockRate;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
computeProperties(clockRate, hitObjects) {
|
|
1463
|
+
super.computeProperties(clockRate, hitObjects);
|
|
1464
|
+
this.setVisuals(clockRate, hitObjects);
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Determines whether this hitobject is considered overlapping with the hitobject before it.
|
|
1468
|
+
*
|
|
1469
|
+
* Keep in mind that "overlapping" in this case is overlapping to the point where both hitobjects
|
|
1470
|
+
* can be hit with just a single tap in osu!droid.
|
|
1471
|
+
*
|
|
1472
|
+
* @param considerDistance Whether to consider the distance between both hitobjects.
|
|
1473
|
+
* @returns Whether the hitobject is considered overlapping.
|
|
1474
|
+
*/
|
|
1475
|
+
isOverlapping(considerDistance) {
|
|
1476
|
+
if (this.object instanceof osuBase.Spinner) {
|
|
1477
|
+
return false;
|
|
1478
|
+
}
|
|
1479
|
+
const previous = this.previous(0);
|
|
1480
|
+
if (!previous || previous.object instanceof osuBase.Spinner) {
|
|
1481
|
+
return false;
|
|
1482
|
+
}
|
|
1483
|
+
if (this.deltaTime >= 5) {
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
if (considerDistance) {
|
|
1487
|
+
const endPosition = this.object.getStackedPosition(osuBase.Modes.droid);
|
|
1488
|
+
let distance = previous.object
|
|
1489
|
+
.getStackedEndPosition(osuBase.Modes.droid)
|
|
1490
|
+
.getDistance(endPosition);
|
|
1491
|
+
if (previous.object instanceof osuBase.Slider &&
|
|
1492
|
+
previous.object.lazyEndPosition) {
|
|
1493
|
+
distance = Math.min(distance, previous.object.lazyEndPosition.getDistance(endPosition));
|
|
1494
|
+
}
|
|
1495
|
+
return distance <= 2 * this.object.radius;
|
|
1592
1496
|
}
|
|
1497
|
+
return true;
|
|
1498
|
+
}
|
|
1499
|
+
setVisuals(clockRate, hitObjects) {
|
|
1500
|
+
// We'll have two visible object arrays. The first array contains objects before the current object starts in a reversed order,
|
|
1501
|
+
// while the second array contains objects after the current object ends.
|
|
1502
|
+
// For overlapping factor, we also need to consider previous visible objects.
|
|
1503
|
+
const prevVisibleObjects = [];
|
|
1504
|
+
const nextVisibleObjects = [];
|
|
1505
|
+
for (let j = this.index + 2; j < hitObjects.length; ++j) {
|
|
1506
|
+
const o = hitObjects[j];
|
|
1507
|
+
if (o instanceof osuBase.Spinner) {
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
if (o.startTime / clockRate > this.endTime + this.timePreempt) {
|
|
1511
|
+
break;
|
|
1512
|
+
}
|
|
1513
|
+
nextVisibleObjects.push(o);
|
|
1514
|
+
}
|
|
1515
|
+
for (let j = 0; j < this.index; ++j) {
|
|
1516
|
+
const prev = this.previous(j);
|
|
1517
|
+
if (prev.object instanceof osuBase.Spinner) {
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
if (prev.startTime >= this.startTime) {
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
if (prev.startTime < this.startTime - this.timePreempt) {
|
|
1524
|
+
break;
|
|
1525
|
+
}
|
|
1526
|
+
prevVisibleObjects.push(prev.object);
|
|
1527
|
+
}
|
|
1528
|
+
for (const hitObject of prevVisibleObjects) {
|
|
1529
|
+
const distance = this.object
|
|
1530
|
+
.getStackedPosition(this.mode)
|
|
1531
|
+
.getDistance(hitObject.getStackedEndPosition(this.mode));
|
|
1532
|
+
const deltaTime = this.startTime - hitObject.endTime / clockRate;
|
|
1533
|
+
this.applyToOverlappingFactor(distance, deltaTime);
|
|
1534
|
+
}
|
|
1535
|
+
for (const hitObject of nextVisibleObjects) {
|
|
1536
|
+
const distance = hitObject
|
|
1537
|
+
.getStackedPosition(this.mode)
|
|
1538
|
+
.getDistance(this.object.getStackedEndPosition(this.mode));
|
|
1539
|
+
const deltaTime = hitObject.startTime / clockRate - this.endTime;
|
|
1540
|
+
if (deltaTime >= 0) {
|
|
1541
|
+
this.noteDensity += 1 - deltaTime / this.timePreempt;
|
|
1542
|
+
}
|
|
1543
|
+
this.applyToOverlappingFactor(distance, deltaTime);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
applyToOverlappingFactor(distance, deltaTime) {
|
|
1547
|
+
// Penalize objects that are too close to the object in both distance
|
|
1548
|
+
// and delta time to prevent stream maps from being overweighted.
|
|
1549
|
+
this.overlappingFactor +=
|
|
1550
|
+
Math.max(0, 1 - distance / (2.5 * this.object.radius)) *
|
|
1551
|
+
(7.5 /
|
|
1552
|
+
(1 +
|
|
1553
|
+
Math.exp(0.15 * (Math.max(deltaTime, this.minDeltaTime) - 75))));
|
|
1593
1554
|
}
|
|
1594
1555
|
}
|
|
1595
1556
|
|
|
@@ -1597,56 +1558,97 @@ class DroidVisual extends DroidSkill {
|
|
|
1597
1558
|
* A difficulty calculator for osu!droid gamemode.
|
|
1598
1559
|
*/
|
|
1599
1560
|
class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
1561
|
+
constructor() {
|
|
1562
|
+
super(...arguments);
|
|
1563
|
+
this.attributes = {
|
|
1564
|
+
mode: "live",
|
|
1565
|
+
tapDifficulty: 0,
|
|
1566
|
+
rhythmDifficulty: 0,
|
|
1567
|
+
visualDifficulty: 0,
|
|
1568
|
+
aimNoteCount: 0,
|
|
1569
|
+
mods: [],
|
|
1570
|
+
starRating: 0,
|
|
1571
|
+
maxCombo: 0,
|
|
1572
|
+
aimDifficulty: 0,
|
|
1573
|
+
flashlightDifficulty: 0,
|
|
1574
|
+
speedNoteCount: 0,
|
|
1575
|
+
sliderFactor: 0,
|
|
1576
|
+
clockRate: 1,
|
|
1577
|
+
approachRate: 0,
|
|
1578
|
+
overallDifficulty: 0,
|
|
1579
|
+
hitCircleCount: 0,
|
|
1580
|
+
sliderCount: 0,
|
|
1581
|
+
spinnerCount: 0,
|
|
1582
|
+
aimDifficultStrainCount: 0,
|
|
1583
|
+
tapDifficultStrainCount: 0,
|
|
1584
|
+
flashlightDifficultStrainCount: 0,
|
|
1585
|
+
visualDifficultStrainCount: 0,
|
|
1586
|
+
flashlightSliderFactor: 0,
|
|
1587
|
+
visualSliderFactor: 0,
|
|
1588
|
+
possibleThreeFingeredSections: [],
|
|
1589
|
+
difficultSliders: [],
|
|
1590
|
+
averageSpeedDeltaTime: 0,
|
|
1591
|
+
vibroFactor: 1,
|
|
1592
|
+
};
|
|
1593
|
+
this.difficultyMultiplier = 0.18;
|
|
1594
|
+
this.mode = osuBase.Modes.droid;
|
|
1595
|
+
}
|
|
1600
1596
|
/**
|
|
1601
1597
|
* The aim star rating of the beatmap.
|
|
1602
1598
|
*/
|
|
1603
|
-
aim
|
|
1599
|
+
get aim() {
|
|
1600
|
+
return this.attributes.aimDifficulty;
|
|
1601
|
+
}
|
|
1604
1602
|
/**
|
|
1605
1603
|
* The tap star rating of the beatmap.
|
|
1606
1604
|
*/
|
|
1607
|
-
tap
|
|
1605
|
+
get tap() {
|
|
1606
|
+
return this.attributes.tapDifficulty;
|
|
1607
|
+
}
|
|
1608
1608
|
/**
|
|
1609
1609
|
* The rhythm star rating of the beatmap.
|
|
1610
1610
|
*/
|
|
1611
|
-
rhythm
|
|
1611
|
+
get rhythm() {
|
|
1612
|
+
return this.attributes.rhythmDifficulty;
|
|
1613
|
+
}
|
|
1612
1614
|
/**
|
|
1613
1615
|
* The flashlight star rating of the beatmap.
|
|
1614
1616
|
*/
|
|
1615
|
-
flashlight
|
|
1617
|
+
get flashlight() {
|
|
1618
|
+
return this.attributes.flashlightDifficulty;
|
|
1619
|
+
}
|
|
1616
1620
|
/**
|
|
1617
1621
|
* The visual star rating of the beatmap.
|
|
1618
1622
|
*/
|
|
1619
|
-
visual
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
}
|
|
1648
|
-
difficultyMultiplier = 0.18;
|
|
1649
|
-
mode = osuBase.Modes.droid;
|
|
1623
|
+
get visual() {
|
|
1624
|
+
return this.attributes.visualDifficulty;
|
|
1625
|
+
}
|
|
1626
|
+
get cacheableAttributes() {
|
|
1627
|
+
return {
|
|
1628
|
+
tapDifficulty: this.tap,
|
|
1629
|
+
rhythmDifficulty: this.rhythm,
|
|
1630
|
+
visualDifficulty: this.visual,
|
|
1631
|
+
mods: osuBase.ModUtil.modsToOsuString(this.attributes.mods),
|
|
1632
|
+
starRating: this.total,
|
|
1633
|
+
maxCombo: this.attributes.maxCombo,
|
|
1634
|
+
aimDifficulty: this.aim,
|
|
1635
|
+
flashlightDifficulty: this.flashlight,
|
|
1636
|
+
speedNoteCount: this.attributes.speedNoteCount,
|
|
1637
|
+
sliderFactor: this.attributes.sliderFactor,
|
|
1638
|
+
clockRate: this.attributes.clockRate,
|
|
1639
|
+
approachRate: this.attributes.approachRate,
|
|
1640
|
+
overallDifficulty: this.attributes.overallDifficulty,
|
|
1641
|
+
hitCircleCount: this.attributes.hitCircleCount,
|
|
1642
|
+
sliderCount: this.attributes.sliderCount,
|
|
1643
|
+
spinnerCount: this.attributes.spinnerCount,
|
|
1644
|
+
aimDifficultStrainCount: this.attributes.aimDifficultStrainCount,
|
|
1645
|
+
tapDifficultStrainCount: this.attributes.tapDifficultStrainCount,
|
|
1646
|
+
flashlightDifficultStrainCount: this.attributes.flashlightDifficultStrainCount,
|
|
1647
|
+
visualDifficultStrainCount: this.attributes.visualDifficultStrainCount,
|
|
1648
|
+
averageSpeedDeltaTime: this.attributes.averageSpeedDeltaTime,
|
|
1649
|
+
vibroFactor: this.attributes.vibroFactor,
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1650
1652
|
/**
|
|
1651
1653
|
* Calculates the aim star rating of the beatmap and stores it in this instance.
|
|
1652
1654
|
*/
|
|
@@ -1660,26 +1662,19 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1660
1662
|
* Calculates the tap star rating of the beatmap and stores it in this instance.
|
|
1661
1663
|
*/
|
|
1662
1664
|
calculateTap() {
|
|
1663
|
-
const
|
|
1664
|
-
this.
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
this.postCalculateTap(tapSkill);
|
|
1671
|
-
}
|
|
1672
|
-
this.calculateSpeedAttributes();
|
|
1665
|
+
const od = this.difficultyStatistics.overallDifficulty;
|
|
1666
|
+
const tapSkillCheese = new DroidTap(this.mods, od, true);
|
|
1667
|
+
const tapSkillNoCheese = new DroidTap(this.mods, od, false);
|
|
1668
|
+
this.calculateSkills(tapSkillCheese, tapSkillNoCheese);
|
|
1669
|
+
const tapSkillVibro = new DroidTap(this.mods, od, true, tapSkillCheese.relevantDeltaTime());
|
|
1670
|
+
this.calculateSkills(tapSkillVibro);
|
|
1671
|
+
this.postCalculateTap(tapSkillCheese, tapSkillVibro);
|
|
1673
1672
|
}
|
|
1674
1673
|
/**
|
|
1675
1674
|
* Calculates the rhythm star rating of the beatmap and stores it in this instance.
|
|
1676
|
-
*/
|
|
1677
|
-
calculateRhythm() {
|
|
1678
|
-
|
|
1679
|
-
this.rhythm = this.attributes.rhythmDifficulty = 0;
|
|
1680
|
-
return;
|
|
1681
|
-
}
|
|
1682
|
-
const rhythmSkill = new DroidRhythm(this.mods, this.stats.od);
|
|
1675
|
+
*/
|
|
1676
|
+
calculateRhythm() {
|
|
1677
|
+
const rhythmSkill = new DroidRhythm(this.mods, this.difficultyStatistics.overallDifficulty);
|
|
1683
1678
|
this.calculateSkills(rhythmSkill);
|
|
1684
1679
|
this.postCalculateRhythm(rhythmSkill);
|
|
1685
1680
|
}
|
|
@@ -1697,7 +1692,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1697
1692
|
*/
|
|
1698
1693
|
calculateVisual() {
|
|
1699
1694
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
1700
|
-
this.
|
|
1695
|
+
this.attributes.visualDifficulty = 0;
|
|
1701
1696
|
return;
|
|
1702
1697
|
}
|
|
1703
1698
|
const visualSkill = new DroidVisual(this.mods, true);
|
|
@@ -1719,55 +1714,35 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1719
1714
|
if (basePerformanceValue > 1e-5) {
|
|
1720
1715
|
// Document for formula derivation:
|
|
1721
1716
|
// https://docs.google.com/document/d/10DZGYYSsT_yjz2Mtp6yIJld0Rqx4E-vVHupCqiM4TNI/edit
|
|
1722
|
-
this.
|
|
1717
|
+
this.attributes.starRating =
|
|
1723
1718
|
0.027 *
|
|
1724
1719
|
(Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformanceValue) +
|
|
1725
1720
|
4);
|
|
1726
1721
|
}
|
|
1727
1722
|
else {
|
|
1728
|
-
this.
|
|
1723
|
+
this.attributes.starRating = 0;
|
|
1729
1724
|
}
|
|
1730
1725
|
}
|
|
1731
1726
|
calculateAll() {
|
|
1732
1727
|
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
1728
|
this.calculateSkills(...skills);
|
|
1740
1729
|
const aimSkill = skills[0];
|
|
1741
1730
|
const aimSkillWithoutSliders = skills[1];
|
|
1742
1731
|
const rhythmSkill = skills[2];
|
|
1743
|
-
const
|
|
1744
|
-
const flashlightSkill = skills[
|
|
1745
|
-
const flashlightSkillWithoutSliders = skills[
|
|
1746
|
-
const visualSkill = skills[
|
|
1747
|
-
const visualSkillWithoutSliders = skills[
|
|
1732
|
+
const tapSkillCheese = skills[3];
|
|
1733
|
+
const flashlightSkill = skills[5];
|
|
1734
|
+
const flashlightSkillWithoutSliders = skills[6];
|
|
1735
|
+
const visualSkill = skills[7];
|
|
1736
|
+
const visualSkillWithoutSliders = skills[8];
|
|
1737
|
+
const tapSkillVibro = new DroidTap(this.mods, this.difficultyStatistics.overallDifficulty, true, tapSkillCheese.relevantDeltaTime());
|
|
1738
|
+
this.calculateSkills(tapSkillVibro);
|
|
1748
1739
|
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
|
-
}
|
|
1740
|
+
this.postCalculateTap(tapSkillCheese, tapSkillVibro);
|
|
1741
|
+
this.postCalculateRhythm(rhythmSkill);
|
|
1759
1742
|
this.postCalculateFlashlight(flashlightSkill, flashlightSkillWithoutSliders);
|
|
1760
|
-
|
|
1761
|
-
this.postCalculateVisual(visualSkill, visualSkillWithoutSliders);
|
|
1762
|
-
}
|
|
1763
|
-
else {
|
|
1764
|
-
this.visual = this.attributes.visualDifficulty = 0;
|
|
1765
|
-
}
|
|
1743
|
+
this.postCalculateVisual(visualSkill, visualSkillWithoutSliders);
|
|
1766
1744
|
this.calculateTotal();
|
|
1767
1745
|
}
|
|
1768
|
-
/**
|
|
1769
|
-
* Returns a string representative of the class.
|
|
1770
|
-
*/
|
|
1771
1746
|
toString() {
|
|
1772
1747
|
return (this.total.toFixed(2) +
|
|
1773
1748
|
" stars (" +
|
|
@@ -1782,16 +1757,42 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1782
1757
|
this.visual.toFixed(2) +
|
|
1783
1758
|
" visual)");
|
|
1784
1759
|
}
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1760
|
+
generateDifficultyHitObjects(beatmap) {
|
|
1761
|
+
var _a, _b;
|
|
1762
|
+
const difficultyObjects = [];
|
|
1763
|
+
const { objects } = beatmap.hitObjects;
|
|
1764
|
+
const difficultyAdjustMod = this.mods.find((m) => m instanceof osuBase.ModDifficultyAdjust);
|
|
1765
|
+
for (let i = 0; i < objects.length; ++i) {
|
|
1766
|
+
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, this.difficultyStatistics.overallSpeedMultiplier, (difficultyAdjustMod === null || difficultyAdjustMod === void 0 ? void 0 : difficultyAdjustMod.ar) !== undefined);
|
|
1767
|
+
difficultyObject.computeProperties(this.difficultyStatistics.overallSpeedMultiplier, objects);
|
|
1768
|
+
difficultyObjects.push(difficultyObject);
|
|
1769
|
+
}
|
|
1770
|
+
return difficultyObjects;
|
|
1771
|
+
}
|
|
1772
|
+
computeDifficultyStatistics(options) {
|
|
1773
|
+
var _a;
|
|
1774
|
+
const { difficulty } = this.beatmap;
|
|
1775
|
+
return osuBase.calculateDroidDifficultyStatistics({
|
|
1776
|
+
circleSize: difficulty.cs,
|
|
1777
|
+
approachRate: (_a = difficulty.ar) !== null && _a !== void 0 ? _a : difficulty.od,
|
|
1778
|
+
overallDifficulty: difficulty.od,
|
|
1779
|
+
healthDrain: difficulty.hp,
|
|
1780
|
+
mods: this.mods,
|
|
1781
|
+
customSpeedMultiplier: options === null || options === void 0 ? void 0 : options.customSpeedMultiplier,
|
|
1782
|
+
oldStatistics: options === null || options === void 0 ? void 0 : options.oldStatistics,
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1788
1785
|
createSkills() {
|
|
1786
|
+
const od = this.difficultyStatistics.overallDifficulty;
|
|
1789
1787
|
return [
|
|
1790
1788
|
new DroidAim(this.mods, true),
|
|
1791
1789
|
new DroidAim(this.mods, false),
|
|
1792
1790
|
// Tap skill depends on rhythm skill, so we put it first
|
|
1793
|
-
new DroidRhythm(this.mods,
|
|
1794
|
-
|
|
1791
|
+
new DroidRhythm(this.mods, od),
|
|
1792
|
+
// Cheesability tap
|
|
1793
|
+
new DroidTap(this.mods, od, true),
|
|
1794
|
+
// Non-cheesability tap
|
|
1795
|
+
new DroidTap(this.mods, od, false),
|
|
1795
1796
|
new DroidFlashlight(this.mods, true),
|
|
1796
1797
|
new DroidFlashlight(this.mods, false),
|
|
1797
1798
|
new DroidVisual(this.mods, true),
|
|
@@ -1807,152 +1808,119 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1807
1808
|
postCalculateAim(aimSkill, aimSkillWithoutSliders) {
|
|
1808
1809
|
this.strainPeaks.aimWithSliders = aimSkill.strainPeaks;
|
|
1809
1810
|
this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
|
|
1810
|
-
this.
|
|
1811
|
+
this.attributes.aimDifficulty = this.starValue(aimSkill.difficultyValue());
|
|
1811
1812
|
if (this.aim) {
|
|
1812
1813
|
this.attributes.sliderFactor =
|
|
1813
1814
|
this.starValue(aimSkillWithoutSliders.difficultyValue()) /
|
|
1814
1815
|
this.aim;
|
|
1815
1816
|
}
|
|
1816
1817
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
1817
|
-
this.
|
|
1818
|
+
this.attributes.aimDifficulty *= 0.9;
|
|
1818
1819
|
}
|
|
1819
|
-
this.attributes.
|
|
1820
|
+
this.attributes.aimDifficultStrainCount =
|
|
1821
|
+
aimSkill.countDifficultStrains();
|
|
1820
1822
|
this.calculateAimAttributes();
|
|
1821
1823
|
}
|
|
1822
1824
|
/**
|
|
1823
1825
|
* Calculates aim-related attributes.
|
|
1824
1826
|
*/
|
|
1825
1827
|
calculateAimAttributes() {
|
|
1826
|
-
const objectStrains = [];
|
|
1827
|
-
let maxStrain = 0;
|
|
1828
|
-
// Take the top 15% most difficult sliders based on velocity.
|
|
1829
1828
|
const topDifficultSliders = [];
|
|
1830
1829
|
for (let i = 0; i < this.objects.length; ++i) {
|
|
1831
1830
|
const object = this.objects[i];
|
|
1832
|
-
objectStrains.push(object.aimStrainWithSliders);
|
|
1833
|
-
maxStrain = Math.max(maxStrain, object.aimStrainWithSliders);
|
|
1834
1831
|
const velocity = object.travelDistance / object.travelTime;
|
|
1835
1832
|
if (velocity > 0) {
|
|
1836
1833
|
topDifficultSliders.push({
|
|
1837
1834
|
index: i,
|
|
1838
1835
|
velocity: velocity,
|
|
1839
1836
|
});
|
|
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
1837
|
}
|
|
1846
1838
|
}
|
|
1847
|
-
if (maxStrain) {
|
|
1848
|
-
this.attributes.aimNoteCount = objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
|
|
1849
|
-
}
|
|
1850
1839
|
const velocitySum = topDifficultSliders.reduce((a, v) => a + v.velocity, 0);
|
|
1851
1840
|
for (const slider of topDifficultSliders) {
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1841
|
+
const difficultyRating = slider.velocity / velocitySum;
|
|
1842
|
+
// Only consider sliders that are fast enough.
|
|
1843
|
+
if (difficultyRating > 0.02) {
|
|
1844
|
+
this.attributes.difficultSliders.push({
|
|
1845
|
+
index: slider.index,
|
|
1846
|
+
difficultyRating: slider.velocity / velocitySum,
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
this.attributes.difficultSliders.sort((a, b) => b.difficultyRating - a.difficultyRating);
|
|
1851
|
+
// Take the top 15% most difficult sliders.
|
|
1852
|
+
while (this.attributes.difficultSliders.length >
|
|
1853
|
+
Math.ceil(0.15 * this.beatmap.hitObjects.sliders)) {
|
|
1854
|
+
this.attributes.difficultSliders.pop();
|
|
1856
1855
|
}
|
|
1857
1856
|
}
|
|
1858
1857
|
/**
|
|
1859
1858
|
* Called after tap skill calculation.
|
|
1860
1859
|
*
|
|
1861
|
-
* @param
|
|
1860
|
+
* @param tapSkillCheese The tap skill that considers cheesing.
|
|
1861
|
+
* @param tapSkillVibro The tap skill that considers vibro.
|
|
1862
1862
|
*/
|
|
1863
|
-
postCalculateTap(
|
|
1864
|
-
this.strainPeaks.speed =
|
|
1865
|
-
this.
|
|
1863
|
+
postCalculateTap(tapSkillCheese, tapSkillVibro) {
|
|
1864
|
+
this.strainPeaks.speed = tapSkillCheese.strainPeaks;
|
|
1865
|
+
this.attributes.tapDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
|
|
1866
|
+
? 0
|
|
1867
|
+
: this.starValue(tapSkillCheese.difficultyValue());
|
|
1868
|
+
if (this.tap) {
|
|
1869
|
+
this.attributes.vibroFactor =
|
|
1870
|
+
this.starValue(tapSkillVibro.difficultyValue()) / this.tap;
|
|
1871
|
+
}
|
|
1872
|
+
this.attributes.speedNoteCount = tapSkillCheese.relevantNoteCount();
|
|
1873
|
+
this.attributes.averageSpeedDeltaTime =
|
|
1874
|
+
tapSkillCheese.relevantDeltaTime();
|
|
1875
|
+
this.attributes.tapDifficultStrainCount =
|
|
1876
|
+
tapSkillCheese.countDifficultStrains();
|
|
1877
|
+
this.calculateTapAttributes();
|
|
1866
1878
|
}
|
|
1867
1879
|
/**
|
|
1868
|
-
* Calculates
|
|
1880
|
+
* Calculates tap-related attributes.
|
|
1869
1881
|
*/
|
|
1870
|
-
|
|
1882
|
+
calculateTapAttributes() {
|
|
1871
1883
|
this.attributes.possibleThreeFingeredSections = [];
|
|
1872
|
-
const
|
|
1873
|
-
const objectStrains = [];
|
|
1874
|
-
let maxStrain = 0;
|
|
1875
|
-
const maxSectionDeltaTime = 2000;
|
|
1884
|
+
const { threeFingerStrainThreshold } = DroidDifficultyCalculator;
|
|
1876
1885
|
const minSectionObjectCount = 5;
|
|
1877
|
-
let
|
|
1878
|
-
|
|
1886
|
+
let inSpeedSection = false;
|
|
1887
|
+
let firstSpeedObjectIndex = 0;
|
|
1888
|
+
for (let i = 2; i < this.objects.length; ++i) {
|
|
1879
1889
|
const current = this.objects[i];
|
|
1880
|
-
const
|
|
1881
|
-
if (
|
|
1882
|
-
|
|
1890
|
+
const prev = this.objects[i - 1];
|
|
1891
|
+
if (!inSpeedSection &&
|
|
1892
|
+
current.originalTapStrain >= threeFingerStrainThreshold) {
|
|
1893
|
+
inSpeedSection = true;
|
|
1894
|
+
firstSpeedObjectIndex = i;
|
|
1895
|
+
continue;
|
|
1883
1896
|
}
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
const
|
|
1887
|
-
|
|
1897
|
+
const currentDelta = current.deltaTime;
|
|
1898
|
+
const prevDelta = prev.deltaTime;
|
|
1899
|
+
const deltaRatio = Math.min(prevDelta, currentDelta) /
|
|
1900
|
+
Math.max(prevDelta, currentDelta);
|
|
1901
|
+
if (inSpeedSection &&
|
|
1902
|
+
(current.originalTapStrain < threeFingerStrainThreshold ||
|
|
1903
|
+
// Stop speed section on slowing down 1/2 rhythm change or anything slower.
|
|
1904
|
+
(prevDelta < currentDelta && deltaRatio <= 0.5) ||
|
|
1905
|
+
// Don't forget to manually add the last section, which would otherwise be ignored.
|
|
1906
|
+
i === this.objects.length - 1)) {
|
|
1907
|
+
const lastSpeedObjectIndex = i - (i === this.objects.length - 1 ? 0 : 1);
|
|
1908
|
+
inSpeedSection = false;
|
|
1888
1909
|
// 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;
|
|
1910
|
+
if (i - firstSpeedObjectIndex < minSectionObjectCount) {
|
|
1918
1911
|
continue;
|
|
1919
1912
|
}
|
|
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
1913
|
this.attributes.possibleThreeFingeredSections.push({
|
|
1933
|
-
firstObjectIndex:
|
|
1934
|
-
lastObjectIndex:
|
|
1935
|
-
sumStrain:
|
|
1914
|
+
firstObjectIndex: firstSpeedObjectIndex,
|
|
1915
|
+
lastObjectIndex: lastSpeedObjectIndex,
|
|
1916
|
+
sumStrain: Math.pow(this.objects
|
|
1917
|
+
.slice(firstSpeedObjectIndex, lastSpeedObjectIndex + 1)
|
|
1918
|
+
.reduce((a, v) => a +
|
|
1919
|
+
v.originalTapStrain /
|
|
1920
|
+
threeFingerStrainThreshold, 0), 0.75),
|
|
1936
1921
|
});
|
|
1937
1922
|
}
|
|
1938
1923
|
}
|
|
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
1924
|
}
|
|
1957
1925
|
/**
|
|
1958
1926
|
* Called after rhythm skill calculation.
|
|
@@ -1960,7 +1928,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1960
1928
|
* @param rhythmSkill The rhythm skill.
|
|
1961
1929
|
*/
|
|
1962
1930
|
postCalculateRhythm(rhythmSkill) {
|
|
1963
|
-
this.
|
|
1931
|
+
this.attributes.rhythmDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
|
|
1964
1932
|
? 0
|
|
1965
1933
|
: this.starValue(rhythmSkill.difficultyValue());
|
|
1966
1934
|
}
|
|
@@ -1972,15 +1940,17 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1972
1940
|
*/
|
|
1973
1941
|
postCalculateFlashlight(flashlightSkill, flashlightSkillWithoutSliders) {
|
|
1974
1942
|
this.strainPeaks.flashlight = flashlightSkill.strainPeaks;
|
|
1975
|
-
this.
|
|
1943
|
+
this.attributes.flashlightDifficulty = this.starValue(flashlightSkill.difficultyValue());
|
|
1976
1944
|
if (this.flashlight) {
|
|
1977
1945
|
this.attributes.flashlightSliderFactor =
|
|
1978
1946
|
this.starValue(flashlightSkillWithoutSliders.difficultyValue()) / this.flashlight;
|
|
1979
1947
|
}
|
|
1980
1948
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
1981
|
-
this.
|
|
1949
|
+
this.attributes.flashlightDifficulty *= 0.7;
|
|
1982
1950
|
}
|
|
1983
1951
|
this.attributes.flashlightDifficulty = this.flashlight;
|
|
1952
|
+
this.attributes.flashlightDifficultStrainCount =
|
|
1953
|
+
flashlightSkill.countDifficultStrains();
|
|
1984
1954
|
}
|
|
1985
1955
|
/**
|
|
1986
1956
|
* Called after visual skill calculation.
|
|
@@ -1989,7 +1959,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1989
1959
|
* @param visualSkillWithoutSliders The visual skill that doesn't consider sliders.
|
|
1990
1960
|
*/
|
|
1991
1961
|
postCalculateVisual(visualSkillWithSliders, visualSkillWithoutSliders) {
|
|
1992
|
-
this.
|
|
1962
|
+
this.attributes.visualDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
|
|
1993
1963
|
? 0
|
|
1994
1964
|
: this.starValue(visualSkillWithSliders.difficultyValue());
|
|
1995
1965
|
if (this.visual) {
|
|
@@ -1997,33 +1967,52 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1997
1967
|
this.starValue(visualSkillWithoutSliders.difficultyValue()) /
|
|
1998
1968
|
this.visual;
|
|
1999
1969
|
}
|
|
1970
|
+
this.attributes.visualDifficultStrainCount =
|
|
1971
|
+
visualSkillWithSliders.countDifficultStrains();
|
|
2000
1972
|
}
|
|
2001
1973
|
}
|
|
1974
|
+
/**
|
|
1975
|
+
* The strain threshold to start detecting for possible three-fingered section.
|
|
1976
|
+
*
|
|
1977
|
+
* Increasing this number will result in less sections being flagged.
|
|
1978
|
+
*/
|
|
1979
|
+
DroidDifficultyCalculator.threeFingerStrainThreshold = 175;
|
|
2002
1980
|
|
|
2003
1981
|
/**
|
|
2004
1982
|
* The base class of performance calculators.
|
|
2005
1983
|
*/
|
|
2006
1984
|
class PerformanceCalculator {
|
|
2007
1985
|
/**
|
|
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.
|
|
1986
|
+
* @param difficultyAttributes The difficulty attributes to calculate.
|
|
2025
1987
|
*/
|
|
2026
|
-
|
|
1988
|
+
constructor(difficultyAttributes) {
|
|
1989
|
+
/**
|
|
1990
|
+
* The overall performance value.
|
|
1991
|
+
*/
|
|
1992
|
+
this.total = 0;
|
|
1993
|
+
/**
|
|
1994
|
+
* The calculated accuracy.
|
|
1995
|
+
*/
|
|
1996
|
+
this.computedAccuracy = new osuBase.Accuracy({});
|
|
1997
|
+
/**
|
|
1998
|
+
* Penalty for combo breaks.
|
|
1999
|
+
*/
|
|
2000
|
+
this.comboPenalty = 0;
|
|
2001
|
+
/**
|
|
2002
|
+
* The amount of misses that are filtered out from sliderbreaks.
|
|
2003
|
+
*/
|
|
2004
|
+
this.effectiveMissCount = 0;
|
|
2005
|
+
/**
|
|
2006
|
+
* Nerf factor used for nerfing beatmaps with very likely dropped sliderends.
|
|
2007
|
+
*/
|
|
2008
|
+
this.sliderNerfFactor = 1;
|
|
2009
|
+
if (this.isCacheableAttribute(difficultyAttributes)) {
|
|
2010
|
+
this.difficultyAttributes = Object.assign(Object.assign({}, difficultyAttributes), { mods: osuBase.ModUtil.pcStringToMods(difficultyAttributes.mods) });
|
|
2011
|
+
}
|
|
2012
|
+
else {
|
|
2013
|
+
this.difficultyAttributes = osuBase.Utils.deepCopy(difficultyAttributes);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2027
2016
|
/**
|
|
2028
2017
|
* Calculates the performance points of the beatmap.
|
|
2029
2018
|
*
|
|
@@ -2033,7 +2022,7 @@ class PerformanceCalculator {
|
|
|
2033
2022
|
calculate(options) {
|
|
2034
2023
|
this.handleOptions(options);
|
|
2035
2024
|
this.calculateValues();
|
|
2036
|
-
this.calculateTotalValue();
|
|
2025
|
+
this.total = this.calculateTotalValue();
|
|
2037
2026
|
return this;
|
|
2038
2027
|
}
|
|
2039
2028
|
/**
|
|
@@ -2064,11 +2053,12 @@ class PerformanceCalculator {
|
|
|
2064
2053
|
* @param options Options for performance calculation.
|
|
2065
2054
|
*/
|
|
2066
2055
|
handleOptions(options) {
|
|
2056
|
+
var _a;
|
|
2067
2057
|
const maxCombo = this.difficultyAttributes.maxCombo;
|
|
2068
2058
|
const miss = this.computedAccuracy.nmiss;
|
|
2069
|
-
const combo = options
|
|
2059
|
+
const combo = (_a = options === null || options === void 0 ? void 0 : options.combo) !== null && _a !== void 0 ? _a : maxCombo - miss;
|
|
2070
2060
|
this.comboPenalty = Math.min(Math.pow(combo / maxCombo, 0.8), 1);
|
|
2071
|
-
if (options
|
|
2061
|
+
if ((options === null || options === void 0 ? void 0 : options.accPercent) instanceof osuBase.Accuracy) {
|
|
2072
2062
|
// Copy into new instance to not modify the original
|
|
2073
2063
|
this.computedAccuracy = new osuBase.Accuracy(options.accPercent);
|
|
2074
2064
|
if (this.computedAccuracy.n300 <= 0) {
|
|
@@ -2083,9 +2073,9 @@ class PerformanceCalculator {
|
|
|
2083
2073
|
}
|
|
2084
2074
|
else {
|
|
2085
2075
|
this.computedAccuracy = new osuBase.Accuracy({
|
|
2086
|
-
percent: options
|
|
2076
|
+
percent: options === null || options === void 0 ? void 0 : options.accPercent,
|
|
2087
2077
|
nobjects: this.totalHits,
|
|
2088
|
-
nmiss: options
|
|
2078
|
+
nmiss: (options === null || options === void 0 ? void 0 : options.miss) || 0,
|
|
2089
2079
|
});
|
|
2090
2080
|
}
|
|
2091
2081
|
this.effectiveMissCount = this.calculateEffectiveMissCount(combo, maxCombo);
|
|
@@ -2119,7 +2109,7 @@ class PerformanceCalculator {
|
|
|
2119
2109
|
if (this.difficultyAttributes.sliderCount > 0) {
|
|
2120
2110
|
// We assume 15% of sliders in a beatmap are difficult since there's no way to tell from the performance calculator.
|
|
2121
2111
|
const estimateDifficultSliders = this.difficultyAttributes.sliderCount * 0.15;
|
|
2122
|
-
const estimateSliderEndsDropped = osuBase.MathUtils.clamp(Math.min(this.computedAccuracy.
|
|
2112
|
+
const estimateSliderEndsDropped = osuBase.MathUtils.clamp(Math.min(this.computedAccuracy.n100 +
|
|
2123
2113
|
this.computedAccuracy.n50 +
|
|
2124
2114
|
this.computedAccuracy.nmiss, maxCombo - combo), 0, estimateDifficultSliders);
|
|
2125
2115
|
this.sliderNerfFactor =
|
|
@@ -2147,32 +2137,52 @@ class PerformanceCalculator {
|
|
|
2147
2137
|
}
|
|
2148
2138
|
return Math.max(this.computedAccuracy.nmiss, comboBasedMissCount);
|
|
2149
2139
|
}
|
|
2140
|
+
/**
|
|
2141
|
+
* Determines whether an attribute is a cacheable attribute.
|
|
2142
|
+
*
|
|
2143
|
+
* @param attributes The attributes to check.
|
|
2144
|
+
* @returns Whether the attributes are cacheable.
|
|
2145
|
+
*/
|
|
2146
|
+
isCacheableAttribute(attributes) {
|
|
2147
|
+
return typeof attributes.mods === "string";
|
|
2148
|
+
}
|
|
2150
2149
|
}
|
|
2151
2150
|
|
|
2152
2151
|
/**
|
|
2153
2152
|
* A performance points calculator that calculates performance points for osu!droid gamemode.
|
|
2154
2153
|
*/
|
|
2155
2154
|
class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2155
|
+
constructor() {
|
|
2156
|
+
super(...arguments);
|
|
2157
|
+
/**
|
|
2158
|
+
* The aim performance value.
|
|
2159
|
+
*/
|
|
2160
|
+
this.aim = 0;
|
|
2161
|
+
/**
|
|
2162
|
+
* The tap performance value.
|
|
2163
|
+
*/
|
|
2164
|
+
this.tap = 0;
|
|
2165
|
+
/**
|
|
2166
|
+
* The accuracy performance value.
|
|
2167
|
+
*/
|
|
2168
|
+
this.accuracy = 0;
|
|
2169
|
+
/**
|
|
2170
|
+
* The flashlight performance value.
|
|
2171
|
+
*/
|
|
2172
|
+
this.flashlight = 0;
|
|
2173
|
+
/**
|
|
2174
|
+
* The visual performance value.
|
|
2175
|
+
*/
|
|
2176
|
+
this.visual = 0;
|
|
2177
|
+
this.finalMultiplier = 1.24;
|
|
2178
|
+
this.mode = osuBase.Modes.droid;
|
|
2179
|
+
this._aimSliderCheesePenalty = 1;
|
|
2180
|
+
this._flashlightSliderCheesePenalty = 1;
|
|
2181
|
+
this._visualSliderCheesePenalty = 1;
|
|
2182
|
+
this._tapPenalty = 1;
|
|
2183
|
+
this._deviation = 0;
|
|
2184
|
+
this._tapDeviation = 0;
|
|
2185
|
+
}
|
|
2176
2186
|
/**
|
|
2177
2187
|
* The penalty used to penalize the tap performance value.
|
|
2178
2188
|
*
|
|
@@ -2217,22 +2227,6 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2217
2227
|
get visualSliderCheesePenalty() {
|
|
2218
2228
|
return this._visualSliderCheesePenalty;
|
|
2219
2229
|
}
|
|
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
2230
|
/**
|
|
2237
2231
|
* Applies a tap penalty value to this calculator.
|
|
2238
2232
|
*
|
|
@@ -2247,9 +2241,9 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2247
2241
|
if (value === this._tapPenalty) {
|
|
2248
2242
|
return;
|
|
2249
2243
|
}
|
|
2250
|
-
this.tap *= this._tapPenalty / value;
|
|
2251
2244
|
this._tapPenalty = value;
|
|
2252
|
-
this.
|
|
2245
|
+
this.tap = this.calculateTapValue();
|
|
2246
|
+
this.total = this.calculateTotalValue();
|
|
2253
2247
|
}
|
|
2254
2248
|
/**
|
|
2255
2249
|
* Applies an aim slider cheese penalty value to this calculator.
|
|
@@ -2269,8 +2263,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2269
2263
|
return;
|
|
2270
2264
|
}
|
|
2271
2265
|
this._aimSliderCheesePenalty = value;
|
|
2272
|
-
this.calculateAimValue();
|
|
2273
|
-
this.calculateTotalValue();
|
|
2266
|
+
this.aim = this.calculateAimValue();
|
|
2267
|
+
this.total = this.calculateTotalValue();
|
|
2274
2268
|
}
|
|
2275
2269
|
/**
|
|
2276
2270
|
* Applies a flashlight slider cheese penalty value to this calculator.
|
|
@@ -2290,8 +2284,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2290
2284
|
return;
|
|
2291
2285
|
}
|
|
2292
2286
|
this._flashlightSliderCheesePenalty = value;
|
|
2293
|
-
this.calculateFlashlightValue();
|
|
2294
|
-
this.calculateTotalValue();
|
|
2287
|
+
this.flashlight = this.calculateFlashlightValue();
|
|
2288
|
+
this.total = this.calculateTotalValue();
|
|
2295
2289
|
}
|
|
2296
2290
|
/**
|
|
2297
2291
|
* Applies a visual slider cheese penalty value to this calculator.
|
|
@@ -2311,80 +2305,95 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2311
2305
|
return;
|
|
2312
2306
|
}
|
|
2313
2307
|
this._visualSliderCheesePenalty = value;
|
|
2314
|
-
this.calculateVisualValue();
|
|
2315
|
-
this.calculateTotalValue();
|
|
2308
|
+
this.visual = this.calculateVisualValue();
|
|
2309
|
+
this.total = this.calculateTotalValue();
|
|
2316
2310
|
}
|
|
2317
2311
|
calculateValues() {
|
|
2318
2312
|
this._deviation = this.calculateDeviation();
|
|
2319
2313
|
this._tapDeviation = this.calculateTapDeviation();
|
|
2320
|
-
this.calculateAimValue();
|
|
2321
|
-
this.calculateTapValue();
|
|
2322
|
-
this.calculateAccuracyValue();
|
|
2323
|
-
this.calculateFlashlightValue();
|
|
2324
|
-
this.calculateVisualValue();
|
|
2314
|
+
this.aim = this.calculateAimValue();
|
|
2315
|
+
this.tap = this.calculateTapValue();
|
|
2316
|
+
this.accuracy = this.calculateAccuracyValue();
|
|
2317
|
+
this.flashlight = this.calculateFlashlightValue();
|
|
2318
|
+
this.visual = this.calculateVisualValue();
|
|
2325
2319
|
}
|
|
2326
2320
|
calculateTotalValue() {
|
|
2327
|
-
this.
|
|
2328
|
-
Math.pow(
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
Math.pow(this.visual, 1.1), 1 / 1.1) * this.finalMultiplier;
|
|
2321
|
+
return (Math.pow(Math.pow(this.aim, 1.1) +
|
|
2322
|
+
Math.pow(this.tap, 1.1) +
|
|
2323
|
+
Math.pow(this.accuracy, 1.1) +
|
|
2324
|
+
Math.pow(this.flashlight, 1.1) +
|
|
2325
|
+
Math.pow(this.visual, 1.1), 1 / 1.1) * this.finalMultiplier);
|
|
2333
2326
|
}
|
|
2334
2327
|
handleOptions(options) {
|
|
2335
|
-
|
|
2336
|
-
this.
|
|
2328
|
+
var _a, _b, _c, _d;
|
|
2329
|
+
this._tapPenalty = (_a = options === null || options === void 0 ? void 0 : options.tapPenalty) !== null && _a !== void 0 ? _a : 1;
|
|
2330
|
+
this._aimSliderCheesePenalty = (_b = options === null || options === void 0 ? void 0 : options.aimSliderCheesePenalty) !== null && _b !== void 0 ? _b : 1;
|
|
2337
2331
|
this._flashlightSliderCheesePenalty =
|
|
2338
|
-
options
|
|
2332
|
+
(_c = options === null || options === void 0 ? void 0 : options.flashlightSliderCheesePenalty) !== null && _c !== void 0 ? _c : 1;
|
|
2339
2333
|
this._visualSliderCheesePenalty =
|
|
2340
|
-
options
|
|
2334
|
+
(_d = options === null || options === void 0 ? void 0 : options.visualSliderCheesePenalty) !== null && _d !== void 0 ? _d : 1;
|
|
2341
2335
|
super.handleOptions(options);
|
|
2342
2336
|
}
|
|
2343
2337
|
/**
|
|
2344
2338
|
* Calculates the aim performance value of the beatmap.
|
|
2345
2339
|
*/
|
|
2346
2340
|
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;
|
|
2341
|
+
let aimValue = this.baseValue(Math.pow(this.difficultyAttributes.aimDifficulty, 0.8));
|
|
2342
|
+
aimValue *= Math.min(this.calculateStrainBasedMissPenalty(this.difficultyAttributes.aimDifficultStrainCount), this.proportionalMissPenalty);
|
|
2343
|
+
// Scale the aim value with estimated full combo deviation.
|
|
2344
|
+
aimValue *= this.calculateDeviationBasedLengthScaling();
|
|
2358
2345
|
// Scale the aim value with slider factor to nerf very likely dropped sliderends.
|
|
2359
|
-
|
|
2346
|
+
aimValue *= this.sliderNerfFactor;
|
|
2360
2347
|
// Scale the aim value with slider cheese penalty.
|
|
2361
|
-
|
|
2348
|
+
aimValue *= this._aimSliderCheesePenalty;
|
|
2362
2349
|
// Scale the aim value with deviation.
|
|
2363
|
-
|
|
2350
|
+
aimValue *=
|
|
2364
2351
|
1.05 *
|
|
2365
|
-
Math.
|
|
2352
|
+
Math.sqrt(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * this._deviation)));
|
|
2353
|
+
// OD 7 SS stays the same.
|
|
2354
|
+
aimValue *= 0.98 + Math.pow(7, 2) / 2500;
|
|
2355
|
+
return aimValue;
|
|
2366
2356
|
}
|
|
2367
2357
|
/**
|
|
2368
2358
|
* Calculates the tap performance value of the beatmap.
|
|
2369
2359
|
*/
|
|
2370
2360
|
calculateTapValue() {
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
//
|
|
2381
|
-
|
|
2361
|
+
let tapValue = this.baseValue(this.difficultyAttributes.tapDifficulty);
|
|
2362
|
+
tapValue *= this.calculateStrainBasedMissPenalty(this.difficultyAttributes.tapDifficultStrainCount);
|
|
2363
|
+
// Scale the tap value with estimated full combo deviation.
|
|
2364
|
+
// Require more objects to be present as object count can rack up easily in tap-oriented beatmaps.
|
|
2365
|
+
tapValue *= this.calculateDeviationBasedLengthScaling(this.totalHits / 1.45);
|
|
2366
|
+
// Normalize the deviation to 300 BPM.
|
|
2367
|
+
const normalizedDeviation = this.tapDeviation *
|
|
2368
|
+
Math.max(1, 50 / this.difficultyAttributes.averageSpeedDeltaTime);
|
|
2369
|
+
// We expect the player to get 7500/x deviation when doubletapping x BPM.
|
|
2370
|
+
// Using this expectation, we penalize scores with deviation above 25.
|
|
2371
|
+
const averageBPM = 60000 / 4 / this.difficultyAttributes.averageSpeedDeltaTime;
|
|
2372
|
+
const adjustedDeviation = normalizedDeviation *
|
|
2373
|
+
(1 +
|
|
2374
|
+
1 /
|
|
2375
|
+
(1 +
|
|
2376
|
+
Math.exp(-(normalizedDeviation - 7500 / averageBPM) /
|
|
2377
|
+
((2 * 300) / averageBPM))));
|
|
2382
2378
|
// Scale the tap value with tap deviation.
|
|
2383
|
-
|
|
2379
|
+
tapValue *=
|
|
2384
2380
|
1.1 *
|
|
2385
|
-
Math.pow(osuBase.ErrorFunction.erf(
|
|
2381
|
+
Math.pow(osuBase.ErrorFunction.erf(20 / (Math.SQRT2 * adjustedDeviation)), 0.625);
|
|
2382
|
+
// Additional scaling for tap value based on average BPM and how "vibroable" the beatmap is.
|
|
2383
|
+
// Higher BPMs require more precise tapping. When the deviation is too high,
|
|
2384
|
+
// it can be assumed that the player taps invariant to rhythm.
|
|
2385
|
+
// We harshen the punishment for such scenario.
|
|
2386
|
+
tapValue *=
|
|
2387
|
+
(1 - Math.pow(this.difficultyAttributes.vibroFactor, 6)) /
|
|
2388
|
+
(1 +
|
|
2389
|
+
Math.exp((this._tapDeviation - 7500 / averageBPM) /
|
|
2390
|
+
((2 * 300) / averageBPM))) +
|
|
2391
|
+
Math.pow(this.difficultyAttributes.vibroFactor, 6);
|
|
2386
2392
|
// Scale the tap value with three-fingered penalty.
|
|
2387
|
-
|
|
2393
|
+
tapValue /= this._tapPenalty;
|
|
2394
|
+
// OD 8 SS stays the same.
|
|
2395
|
+
tapValue *= 0.95 + Math.pow(8, 2) / 750;
|
|
2396
|
+
return tapValue;
|
|
2388
2397
|
}
|
|
2389
2398
|
/**
|
|
2390
2399
|
* Calculates the accuracy performance value of the beatmap.
|
|
@@ -2392,84 +2401,127 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2392
2401
|
calculateAccuracyValue() {
|
|
2393
2402
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax) ||
|
|
2394
2403
|
this.totalSuccessfulHits === 0) {
|
|
2395
|
-
|
|
2396
|
-
return;
|
|
2404
|
+
return 0;
|
|
2397
2405
|
}
|
|
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.
|
|
2406
|
+
let accuracyValue = 800 * Math.exp(-0.1 * this._deviation);
|
|
2404
2407
|
const ncircles = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModScoreV2)
|
|
2405
2408
|
? this.totalHits - this.difficultyAttributes.spinnerCount
|
|
2406
2409
|
: this.difficultyAttributes.hitCircleCount;
|
|
2407
|
-
|
|
2410
|
+
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
|
|
2411
|
+
accuracyValue *= Math.min(1.15, Math.sqrt(Math.log(1 + ((Math.E - 1) * ncircles) / 1000)));
|
|
2408
2412
|
// Scale the accuracy value with rhythm complexity.
|
|
2409
|
-
|
|
2413
|
+
accuracyValue *=
|
|
2410
2414
|
1.5 /
|
|
2411
2415
|
(1 +
|
|
2412
2416
|
Math.exp(-(this.difficultyAttributes.rhythmDifficulty - 1) / 2));
|
|
2417
|
+
// Penalize accuracy pp after the first miss.
|
|
2418
|
+
accuracyValue *= Math.pow(0.97, Math.max(0, this.effectiveMissCount - 1));
|
|
2413
2419
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
|
|
2414
|
-
|
|
2420
|
+
accuracyValue *= 1.02;
|
|
2415
2421
|
}
|
|
2422
|
+
return accuracyValue;
|
|
2416
2423
|
}
|
|
2417
2424
|
/**
|
|
2418
2425
|
* Calculates the flashlight performance value of the beatmap.
|
|
2419
2426
|
*/
|
|
2420
2427
|
calculateFlashlightValue() {
|
|
2421
2428
|
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));
|
|
2429
|
+
return 0;
|
|
2435
2430
|
}
|
|
2431
|
+
let flashlightValue = Math.pow(this.difficultyAttributes.flashlightDifficulty, 1.6) * 25;
|
|
2432
|
+
flashlightValue *= Math.min(this.calculateStrainBasedMissPenalty(this.difficultyAttributes.flashlightDifficultStrainCount), this.proportionalMissPenalty);
|
|
2436
2433
|
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
2437
|
-
|
|
2434
|
+
flashlightValue *=
|
|
2438
2435
|
0.7 +
|
|
2439
2436
|
0.1 * Math.min(1, this.totalHits / 200) +
|
|
2440
2437
|
(this.totalHits > 200
|
|
2441
2438
|
? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
|
|
2442
2439
|
: 0);
|
|
2443
2440
|
// Scale the flashlight value with slider cheese penalty.
|
|
2444
|
-
|
|
2441
|
+
flashlightValue *= this._flashlightSliderCheesePenalty;
|
|
2445
2442
|
// Scale the flashlight value with deviation.
|
|
2446
|
-
|
|
2443
|
+
flashlightValue *= osuBase.ErrorFunction.erf(50 / (Math.SQRT2 * this._deviation));
|
|
2444
|
+
return flashlightValue;
|
|
2447
2445
|
}
|
|
2448
2446
|
/**
|
|
2449
2447
|
* Calculates the visual performance value of the beatmap.
|
|
2450
2448
|
*/
|
|
2451
2449
|
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)));
|
|
2450
|
+
let visualValue = Math.pow(this.difficultyAttributes.visualDifficulty, 1.6) * 22.5;
|
|
2451
|
+
visualValue *= Math.min(this.calculateStrainBasedMissPenalty(this.difficultyAttributes.visualDifficultStrainCount), this.proportionalMissPenalty);
|
|
2452
|
+
// Scale the visual value with estimated full combo deviation.
|
|
2453
|
+
// As visual is easily "bypassable" with memorization, punish for memorization.
|
|
2454
|
+
visualValue *= this.calculateDeviationBasedLengthScaling(undefined, true);
|
|
2467
2455
|
// Scale the visual value with slider cheese penalty.
|
|
2468
|
-
|
|
2456
|
+
visualValue *= this._visualSliderCheesePenalty;
|
|
2469
2457
|
// Scale the visual value with deviation.
|
|
2470
|
-
|
|
2458
|
+
visualValue *=
|
|
2471
2459
|
1.065 *
|
|
2472
|
-
Math.pow(osuBase.ErrorFunction.erf(
|
|
2460
|
+
Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * this._deviation)), 0.8);
|
|
2461
|
+
// OD 5 SS stays the same.
|
|
2462
|
+
visualValue *= 0.98 + Math.pow(5, 2) / 2500;
|
|
2463
|
+
return visualValue;
|
|
2464
|
+
}
|
|
2465
|
+
/**
|
|
2466
|
+
* Calculates a strain-based miss penalty.
|
|
2467
|
+
*
|
|
2468
|
+
* Strain-based miss penalty assumes that a player will miss on the hardest parts of a map,
|
|
2469
|
+
* so we use the amount of relatively difficult sections to adjust miss penalty
|
|
2470
|
+
* to make it more punishing on maps with lower amount of hard sections.
|
|
2471
|
+
*/
|
|
2472
|
+
calculateStrainBasedMissPenalty(difficultStrainCount) {
|
|
2473
|
+
if (this.effectiveMissCount === 0) {
|
|
2474
|
+
return 1;
|
|
2475
|
+
}
|
|
2476
|
+
return (0.94 /
|
|
2477
|
+
(this.effectiveMissCount / (2 * Math.sqrt(difficultStrainCount)) +
|
|
2478
|
+
1));
|
|
2479
|
+
}
|
|
2480
|
+
/**
|
|
2481
|
+
* The object-based proportional miss penalty.
|
|
2482
|
+
*/
|
|
2483
|
+
get proportionalMissPenalty() {
|
|
2484
|
+
if (this.effectiveMissCount === 0) {
|
|
2485
|
+
return 1;
|
|
2486
|
+
}
|
|
2487
|
+
const missProportion = (this.totalHits - this.effectiveMissCount) / (this.totalHits + 1);
|
|
2488
|
+
const noMissProportion = this.totalHits / (this.totalHits + 1);
|
|
2489
|
+
return (
|
|
2490
|
+
// Aim deviation-based scale.
|
|
2491
|
+
(osuBase.ErrorFunction.erfInv(missProportion) /
|
|
2492
|
+
osuBase.ErrorFunction.erfInv(noMissProportion)) *
|
|
2493
|
+
// Cheesing-based scale (i.e. 50% misses is deliberately only hitting each other
|
|
2494
|
+
// note, 90% misses is deliberately only hitting 1 note every 10 notes).
|
|
2495
|
+
Math.pow(missProportion, 8));
|
|
2496
|
+
}
|
|
2497
|
+
/**
|
|
2498
|
+
* Calculates the object-based length scaling based on the deviation of a player for a full
|
|
2499
|
+
* combo in this beatmap, taking retries into account.
|
|
2500
|
+
*
|
|
2501
|
+
* @param objectCount The amount of objects to be considered. Defaults to the amount of
|
|
2502
|
+
* objects in this beatmap.
|
|
2503
|
+
* @param punishForMemorization Whether to punish the deviation for memorization. Defaults to `false`.
|
|
2504
|
+
*/
|
|
2505
|
+
calculateDeviationBasedLengthScaling(objectCount = this.totalHits, punishForMemorization = false) {
|
|
2506
|
+
// Assume a sample proportion of hits for a full combo to be `(n - 0.5) / n` due to
|
|
2507
|
+
// continuity correction, where `n` is the object count.
|
|
2508
|
+
const calculateProportion = (notes) => (notes - 0.5) / notes;
|
|
2509
|
+
// Keeping `x` notes as the benchmark, assume that a player will retry a beatmap
|
|
2510
|
+
// `max(1, x/n)` times relative to an `x`-note beatmap.
|
|
2511
|
+
const benchmarkNotes = 700;
|
|
2512
|
+
// Calculate the proportion equivalent to the bottom half of retry count percentile of
|
|
2513
|
+
// scores and take it as the player's "real" proportion.
|
|
2514
|
+
const retryProportion = (proportion, notes, tries) => proportion +
|
|
2515
|
+
Math.sqrt((2 * proportion * (1 - proportion)) / notes) *
|
|
2516
|
+
osuBase.ErrorFunction.erfInv(1 / tries - 1);
|
|
2517
|
+
// Using the proportion, we calculate the deviation based off that proportion and again
|
|
2518
|
+
// compared to the hit deviation for proportion `(n - 0.5) / n`.
|
|
2519
|
+
let multiplier = Math.max(0, osuBase.ErrorFunction.erfInv(retryProportion(calculateProportion(objectCount), objectCount, Math.max(1, benchmarkNotes / objectCount))) / osuBase.ErrorFunction.erfInv(calculateProportion(benchmarkNotes)) || 0);
|
|
2520
|
+
// Punish for memorization if needed.
|
|
2521
|
+
if (punishForMemorization) {
|
|
2522
|
+
multiplier *= Math.min(1, Math.sqrt(objectCount / benchmarkNotes));
|
|
2523
|
+
}
|
|
2524
|
+
return multiplier;
|
|
2473
2525
|
}
|
|
2474
2526
|
/**
|
|
2475
2527
|
* Estimates the player's tap deviation based on the OD, number of circles and sliders,
|
|
@@ -2490,40 +2542,65 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2490
2542
|
return Number.POSITIVE_INFINITY;
|
|
2491
2543
|
}
|
|
2492
2544
|
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
|
-
|
|
2545
|
+
// Obtain the 50 and 100 hit window for droid.
|
|
2546
|
+
const isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
2547
|
+
const droidHitWindow = new osuBase.DroidHitWindow(osuBase.DroidHitWindow.hitWindow300ToOD(hitWindow300 * this.difficultyAttributes.clockRate, isPrecise));
|
|
2548
|
+
const hitWindow50 = droidHitWindow.hitWindowFor50(isPrecise) /
|
|
2549
|
+
this.difficultyAttributes.clockRate;
|
|
2550
|
+
const hitWindow100 = droidHitWindow.hitWindowFor100(isPrecise) /
|
|
2551
|
+
this.difficultyAttributes.clockRate;
|
|
2552
|
+
const { n100, n50, nmiss } = this.computedAccuracy;
|
|
2553
|
+
const circleCount = this.difficultyAttributes.hitCircleCount;
|
|
2554
|
+
const missCountCircles = Math.min(nmiss, circleCount);
|
|
2555
|
+
const mehCountCircles = Math.min(n50, circleCount - missCountCircles);
|
|
2556
|
+
const okCountCircles = Math.min(n100, circleCount - missCountCircles - mehCountCircles);
|
|
2557
|
+
const greatCountCircles = Math.max(0, circleCount - missCountCircles - mehCountCircles - okCountCircles);
|
|
2558
|
+
// Assume 100s, 50s, and misses happen on circles. If there are less non-300s on circles than 300s,
|
|
2559
|
+
// compute the deviation on circles.
|
|
2560
|
+
if (greatCountCircles > 0) {
|
|
2561
|
+
// The probability that a player hits a circle is unknown, but we can estimate it to be
|
|
2562
|
+
// the number of greats on circles divided by the number of circles, and then add one
|
|
2563
|
+
// to the number of circles as a bias correction.
|
|
2564
|
+
const greatProbabilityCircle = greatCountCircles /
|
|
2565
|
+
(circleCount - missCountCircles - mehCountCircles + 1);
|
|
2566
|
+
// Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed.
|
|
2567
|
+
// Begin with the normal distribution first.
|
|
2568
|
+
let deviationOnCircles = hitWindow300 /
|
|
2569
|
+
(Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbabilityCircle));
|
|
2570
|
+
deviationOnCircles *= Math.sqrt(1 -
|
|
2571
|
+
(Math.sqrt(2 / Math.PI) *
|
|
2572
|
+
hitWindow100 *
|
|
2573
|
+
Math.exp(-0.5 *
|
|
2574
|
+
Math.pow(hitWindow100 / deviationOnCircles, 2))) /
|
|
2575
|
+
(deviationOnCircles *
|
|
2576
|
+
osuBase.ErrorFunction.erf(hitWindow100 /
|
|
2577
|
+
(Math.SQRT2 * deviationOnCircles))));
|
|
2578
|
+
// Then compute the variance for 50s.
|
|
2579
|
+
const mehVariance = (hitWindow50 * hitWindow50 +
|
|
2580
|
+
hitWindow100 * hitWindow50 +
|
|
2581
|
+
hitWindow100 * hitWindow100) /
|
|
2582
|
+
3;
|
|
2583
|
+
// Find the total deviation.
|
|
2584
|
+
deviationOnCircles = Math.sqrt(((greatCountCircles + okCountCircles) *
|
|
2585
|
+
Math.pow(deviationOnCircles, 2) +
|
|
2586
|
+
mehCountCircles * mehVariance) /
|
|
2587
|
+
(greatCountCircles + okCountCircles + mehCountCircles));
|
|
2588
|
+
return deviationOnCircles;
|
|
2589
|
+
}
|
|
2590
|
+
// If there are more non-300s than there are circles, compute the deviation on sliders instead.
|
|
2591
|
+
// Here, all that matters is whether or not the slider was missed, since it is impossible
|
|
2592
|
+
// to get a 100 or 50 on a slider by mis-tapping it.
|
|
2593
|
+
const sliderCount = this.difficultyAttributes.sliderCount;
|
|
2594
|
+
const missCountSliders = Math.min(sliderCount, nmiss - missCountCircles);
|
|
2595
|
+
const greatCountSliders = sliderCount - missCountSliders;
|
|
2596
|
+
// We only get here if nothing was hit. In this case, there is no estimate for deviation.
|
|
2597
|
+
// Note that this is never negative, so checking if this is only equal to 0 makes sense.
|
|
2598
|
+
if (greatCountSliders === 0) {
|
|
2520
2599
|
return Number.POSITIVE_INFINITY;
|
|
2521
2600
|
}
|
|
2522
|
-
const
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
(Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbabilitySlider));
|
|
2526
|
-
return Math.min(deviationOnCircles, deviationOnSliders);
|
|
2601
|
+
const greatProbabilitySlider = greatCountSliders / (sliderCount + 1);
|
|
2602
|
+
return (hitWindow50 /
|
|
2603
|
+
(Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbabilitySlider)));
|
|
2527
2604
|
}
|
|
2528
2605
|
/**
|
|
2529
2606
|
* Does the same as {@link calculateDeviation}, but only for notes and inaccuracies that are relevant to tap difficulty.
|
|
@@ -2535,14 +2612,59 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2535
2612
|
if (this.totalSuccessfulHits === 0) {
|
|
2536
2613
|
return Number.POSITIVE_INFINITY;
|
|
2537
2614
|
}
|
|
2538
|
-
const
|
|
2539
|
-
const
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
const
|
|
2545
|
-
|
|
2615
|
+
const { speedNoteCount, clockRate, overallDifficulty } = this.difficultyAttributes;
|
|
2616
|
+
const hitWindow300 = new osuBase.OsuHitWindow(overallDifficulty).hitWindowFor300();
|
|
2617
|
+
// Obtain the 50 and 100 hit window for droid.
|
|
2618
|
+
const isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
2619
|
+
const droidHitWindow = new osuBase.DroidHitWindow(osuBase.DroidHitWindow.hitWindow300ToOD(hitWindow300 * clockRate, isPrecise));
|
|
2620
|
+
const hitWindow50 = droidHitWindow.hitWindowFor50(isPrecise) / clockRate;
|
|
2621
|
+
const hitWindow100 = droidHitWindow.hitWindowFor100(isPrecise) / clockRate;
|
|
2622
|
+
const { n100, n50, nmiss } = this.computedAccuracy;
|
|
2623
|
+
// Assume a fixed ratio of non-300s hit in speed notes based on speed note count ratio and OD.
|
|
2624
|
+
// Graph: https://www.desmos.com/calculator/iskvgjkxr4
|
|
2625
|
+
const speedNoteRatio = speedNoteCount / this.totalHits;
|
|
2626
|
+
const nonGreatCount = n100 + n50 + nmiss;
|
|
2627
|
+
const nonGreatRatio = 1 -
|
|
2628
|
+
(Math.pow(Math.exp(Math.sqrt(hitWindow300)) + 1, 1 - speedNoteRatio) -
|
|
2629
|
+
1) /
|
|
2630
|
+
Math.exp(Math.sqrt(hitWindow300));
|
|
2631
|
+
const relevantCountGreat = Math.max(0, speedNoteCount - nonGreatCount * nonGreatRatio);
|
|
2632
|
+
const relevantCountOk = n100 * nonGreatRatio;
|
|
2633
|
+
const relevantCountMeh = n50 * nonGreatRatio;
|
|
2634
|
+
const relevantCountMiss = nmiss * nonGreatRatio;
|
|
2635
|
+
// Assume 100s, 50s, and misses happen on circles. If there are less non-300s on circles than 300s,
|
|
2636
|
+
// compute the deviation on circles.
|
|
2637
|
+
if (relevantCountGreat > 0) {
|
|
2638
|
+
// The probability that a player hits a circle is unknown, but we can estimate it to be
|
|
2639
|
+
// the number of greats on circles divided by the number of circles, and then add one
|
|
2640
|
+
// to the number of circles as a bias correction.
|
|
2641
|
+
const greatProbabilityCircle = relevantCountGreat /
|
|
2642
|
+
(speedNoteCount - relevantCountMiss - relevantCountMeh + 1);
|
|
2643
|
+
// Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed.
|
|
2644
|
+
// Begin with the normal distribution first.
|
|
2645
|
+
let deviationOnCircles = hitWindow300 /
|
|
2646
|
+
(Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbabilityCircle));
|
|
2647
|
+
deviationOnCircles *= Math.sqrt(1 -
|
|
2648
|
+
(Math.sqrt(2 / Math.PI) *
|
|
2649
|
+
hitWindow100 *
|
|
2650
|
+
Math.exp(-0.5 *
|
|
2651
|
+
Math.pow(hitWindow100 / deviationOnCircles, 2))) /
|
|
2652
|
+
(deviationOnCircles *
|
|
2653
|
+
osuBase.ErrorFunction.erf(hitWindow100 /
|
|
2654
|
+
(Math.SQRT2 * deviationOnCircles))));
|
|
2655
|
+
// Then compute the variance for 50s.
|
|
2656
|
+
const mehVariance = (hitWindow50 * hitWindow50 +
|
|
2657
|
+
hitWindow100 * hitWindow50 +
|
|
2658
|
+
hitWindow100 * hitWindow100) /
|
|
2659
|
+
3;
|
|
2660
|
+
// Find the total deviation.
|
|
2661
|
+
deviationOnCircles = Math.sqrt(((relevantCountGreat + relevantCountOk) *
|
|
2662
|
+
Math.pow(deviationOnCircles, 2) +
|
|
2663
|
+
relevantCountMeh * mehVariance) /
|
|
2664
|
+
(relevantCountGreat + relevantCountOk + relevantCountMeh));
|
|
2665
|
+
return deviationOnCircles;
|
|
2666
|
+
}
|
|
2667
|
+
return Number.POSITIVE_INFINITY;
|
|
2546
2668
|
}
|
|
2547
2669
|
toString() {
|
|
2548
2670
|
return (this.total.toFixed(2) +
|
|
@@ -2565,16 +2687,13 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2565
2687
|
* and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
|
|
2566
2688
|
*/
|
|
2567
2689
|
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;
|
|
2690
|
+
constructor() {
|
|
2691
|
+
super(...arguments);
|
|
2692
|
+
/**
|
|
2693
|
+
* The final multiplier to be applied to the final difficulty value after all other calculations.
|
|
2694
|
+
*/
|
|
2695
|
+
this.difficultyMultiplier = OsuSkill.defaultDifficultyMultiplier;
|
|
2696
|
+
}
|
|
2578
2697
|
difficultyValue() {
|
|
2579
2698
|
const strains = this.strainPeaks
|
|
2580
2699
|
.slice()
|
|
@@ -2602,6 +2721,12 @@ class OsuSkill extends StrainSkill {
|
|
|
2602
2721
|
return difficulty * this.difficultyMultiplier;
|
|
2603
2722
|
}
|
|
2604
2723
|
}
|
|
2724
|
+
/**
|
|
2725
|
+
* The default multiplier applied to the final difficulty value after all other calculations.
|
|
2726
|
+
*
|
|
2727
|
+
* May be overridden via {@link difficultyMultiplier}.
|
|
2728
|
+
*/
|
|
2729
|
+
OsuSkill.defaultDifficultyMultiplier = 1.06;
|
|
2605
2730
|
|
|
2606
2731
|
/**
|
|
2607
2732
|
* An evaluator for calculating osu!standard Aim skill.
|
|
@@ -2622,7 +2747,7 @@ class OsuAimEvaluator extends AimEvaluator {
|
|
|
2622
2747
|
const last = current.previous(0);
|
|
2623
2748
|
if (current.object instanceof osuBase.Spinner ||
|
|
2624
2749
|
current.index <= 1 ||
|
|
2625
|
-
last
|
|
2750
|
+
(last === null || last === void 0 ? void 0 : last.object) instanceof osuBase.Spinner) {
|
|
2626
2751
|
return 0;
|
|
2627
2752
|
}
|
|
2628
2753
|
const lastLast = current.previous(1);
|
|
@@ -2729,36 +2854,37 @@ class OsuAimEvaluator extends AimEvaluator {
|
|
|
2729
2854
|
* Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
|
2730
2855
|
*/
|
|
2731
2856
|
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
2857
|
constructor(mods, withSliders) {
|
|
2740
2858
|
super(mods);
|
|
2859
|
+
this.strainDecayBase = 0.15;
|
|
2860
|
+
this.reducedSectionCount = 10;
|
|
2861
|
+
this.reducedSectionBaseline = 0.75;
|
|
2862
|
+
this.decayWeight = 0.9;
|
|
2863
|
+
this.currentAimStrain = 0;
|
|
2864
|
+
this.skillMultiplier = 23.55;
|
|
2741
2865
|
this.withSliders = withSliders;
|
|
2742
2866
|
}
|
|
2743
|
-
/**
|
|
2744
|
-
* @param current The hitobject to calculate.
|
|
2745
|
-
*/
|
|
2746
2867
|
strainValueAt(current) {
|
|
2747
|
-
this.
|
|
2748
|
-
this.
|
|
2868
|
+
this.currentAimStrain *= this.strainDecay(current.deltaTime);
|
|
2869
|
+
this.currentAimStrain +=
|
|
2749
2870
|
OsuAimEvaluator.evaluateDifficultyOf(current, this.withSliders) *
|
|
2750
2871
|
this.skillMultiplier;
|
|
2751
|
-
return this.
|
|
2872
|
+
return this.currentAimStrain;
|
|
2873
|
+
}
|
|
2874
|
+
calculateInitialStrain(time, current) {
|
|
2875
|
+
var _a, _b;
|
|
2876
|
+
return (this.currentAimStrain *
|
|
2877
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
2752
2878
|
}
|
|
2753
2879
|
/**
|
|
2754
2880
|
* @param current The hitobject to save to.
|
|
2755
2881
|
*/
|
|
2756
2882
|
saveToHitObject(current) {
|
|
2757
2883
|
if (this.withSliders) {
|
|
2758
|
-
current.aimStrainWithSliders = this.
|
|
2884
|
+
current.aimStrainWithSliders = this.currentAimStrain;
|
|
2759
2885
|
}
|
|
2760
2886
|
else {
|
|
2761
|
-
current.aimStrainWithoutSliders = this.
|
|
2887
|
+
current.aimStrainWithoutSliders = this.currentAimStrain;
|
|
2762
2888
|
}
|
|
2763
2889
|
}
|
|
2764
2890
|
}
|
|
@@ -2767,10 +2893,6 @@ class OsuAim extends OsuSkill {
|
|
|
2767
2893
|
* An evaluator for calculating osu!standard speed skill.
|
|
2768
2894
|
*/
|
|
2769
2895
|
class OsuSpeedEvaluator extends SpeedEvaluator {
|
|
2770
|
-
/**
|
|
2771
|
-
* Spacing threshold for a single hitobject spacing.
|
|
2772
|
-
*/
|
|
2773
|
-
static SINGLE_SPACING_THRESHOLD = 125;
|
|
2774
2896
|
/**
|
|
2775
2897
|
* Evaluates the difficulty of tapping the current object, based on:
|
|
2776
2898
|
*
|
|
@@ -2782,6 +2904,7 @@ class OsuSpeedEvaluator extends SpeedEvaluator {
|
|
|
2782
2904
|
* @param greatWindow The great hit window of the current object.
|
|
2783
2905
|
*/
|
|
2784
2906
|
static evaluateDifficultyOf(current, greatWindow) {
|
|
2907
|
+
var _a;
|
|
2785
2908
|
if (current.object instanceof osuBase.Spinner) {
|
|
2786
2909
|
return 0;
|
|
2787
2910
|
}
|
|
@@ -2807,7 +2930,7 @@ class OsuSpeedEvaluator extends SpeedEvaluator {
|
|
|
2807
2930
|
speedBonus +=
|
|
2808
2931
|
0.75 * Math.pow((this.minSpeedBonus - strainTime) / 40, 2);
|
|
2809
2932
|
}
|
|
2810
|
-
const travelDistance = prev
|
|
2933
|
+
const travelDistance = (_a = prev === null || prev === void 0 ? void 0 : prev.travelDistance) !== null && _a !== void 0 ? _a : 0;
|
|
2811
2934
|
const distance = Math.min(this.SINGLE_SPACING_THRESHOLD, travelDistance + current.minimumJumpDistance);
|
|
2812
2935
|
return (((speedBonus +
|
|
2813
2936
|
speedBonus *
|
|
@@ -2816,6 +2939,10 @@ class OsuSpeedEvaluator extends SpeedEvaluator {
|
|
|
2816
2939
|
strainTime);
|
|
2817
2940
|
}
|
|
2818
2941
|
}
|
|
2942
|
+
/**
|
|
2943
|
+
* Spacing threshold for a single hitobject spacing.
|
|
2944
|
+
*/
|
|
2945
|
+
OsuSpeedEvaluator.SINGLE_SPACING_THRESHOLD = 125;
|
|
2819
2946
|
|
|
2820
2947
|
/**
|
|
2821
2948
|
* An evaluator for calculating osu!standard Rhythm skill.
|
|
@@ -2930,18 +3057,17 @@ class OsuRhythmEvaluator extends RhythmEvaluator {
|
|
|
2930
3057
|
* 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
3058
|
*/
|
|
2932
3059
|
class OsuSpeed extends OsuSkill {
|
|
2933
|
-
|
|
2934
|
-
strainDecayBase = 0.3;
|
|
2935
|
-
reducedSectionCount = 5;
|
|
2936
|
-
reducedSectionBaseline = 0.75;
|
|
2937
|
-
difficultyMultiplier = 1.04;
|
|
2938
|
-
decayWeight = 0.9;
|
|
2939
|
-
currentSpeedStrain = 0;
|
|
2940
|
-
currentRhythm = 0;
|
|
2941
|
-
greatWindow;
|
|
2942
|
-
constructor(mods, greatWindow) {
|
|
3060
|
+
constructor(mods, overallDifficulty) {
|
|
2943
3061
|
super(mods);
|
|
2944
|
-
this.
|
|
3062
|
+
this.strainDecayBase = 0.3;
|
|
3063
|
+
this.reducedSectionCount = 5;
|
|
3064
|
+
this.reducedSectionBaseline = 0.75;
|
|
3065
|
+
this.difficultyMultiplier = 1.04;
|
|
3066
|
+
this.decayWeight = 0.9;
|
|
3067
|
+
this.currentSpeedStrain = 0;
|
|
3068
|
+
this.currentRhythm = 0;
|
|
3069
|
+
this.skillMultiplier = 1375;
|
|
3070
|
+
this.greatWindow = new osuBase.OsuHitWindow(overallDifficulty).hitWindowFor300();
|
|
2945
3071
|
}
|
|
2946
3072
|
/**
|
|
2947
3073
|
* @param current The hitobject to calculate.
|
|
@@ -2954,11 +3080,17 @@ class OsuSpeed extends OsuSkill {
|
|
|
2954
3080
|
this.currentRhythm = OsuRhythmEvaluator.evaluateDifficultyOf(current, this.greatWindow);
|
|
2955
3081
|
return this.currentSpeedStrain * this.currentRhythm;
|
|
2956
3082
|
}
|
|
3083
|
+
calculateInitialStrain(time, current) {
|
|
3084
|
+
var _a, _b;
|
|
3085
|
+
return (this.currentSpeedStrain *
|
|
3086
|
+
this.currentRhythm *
|
|
3087
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
3088
|
+
}
|
|
2957
3089
|
/**
|
|
2958
3090
|
* @param current The hitobject to save to.
|
|
2959
3091
|
*/
|
|
2960
3092
|
saveToHitObject(current) {
|
|
2961
|
-
current.
|
|
3093
|
+
current.speedStrain = this.currentSpeedStrain * this.currentRhythm;
|
|
2962
3094
|
current.rhythmMultiplier = this.currentRhythm;
|
|
2963
3095
|
}
|
|
2964
3096
|
}
|
|
@@ -2983,7 +3115,7 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
2983
3115
|
if (current.object instanceof osuBase.Spinner) {
|
|
2984
3116
|
return 0;
|
|
2985
3117
|
}
|
|
2986
|
-
const scalingFactor = 52 / current.object.
|
|
3118
|
+
const scalingFactor = 52 / current.object.radius;
|
|
2987
3119
|
let smallDistNerf = 1;
|
|
2988
3120
|
let cumulativeStrainTime = 0;
|
|
2989
3121
|
let result = 0;
|
|
@@ -3006,7 +3138,7 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
3006
3138
|
const opacityBonus = 1 +
|
|
3007
3139
|
this.maxOpacityBonus *
|
|
3008
3140
|
(1 -
|
|
3009
|
-
current.opacityAt(currentObject.object.startTime, isHiddenMod
|
|
3141
|
+
current.opacityAt(currentObject.object.startTime, isHiddenMod));
|
|
3010
3142
|
result +=
|
|
3011
3143
|
(stackNerf * opacityBonus * scalingFactor * jumpDistance) /
|
|
3012
3144
|
cumulativeStrainTime;
|
|
@@ -3037,8 +3169,8 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
3037
3169
|
// Longer sliders require more memorization.
|
|
3038
3170
|
sliderBonus *= pixelTravelDistance;
|
|
3039
3171
|
// Nerf sliders with repeats, as less memorization is required.
|
|
3040
|
-
if (current.object.
|
|
3041
|
-
sliderBonus /= current.object.
|
|
3172
|
+
if (current.object.repeatCount > 0)
|
|
3173
|
+
sliderBonus /= current.object.repeatCount + 1;
|
|
3042
3174
|
}
|
|
3043
3175
|
result += sliderBonus * this.sliderMultiplier;
|
|
3044
3176
|
return result;
|
|
@@ -3049,27 +3181,69 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
3049
3181
|
* Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
|
|
3050
3182
|
*/
|
|
3051
3183
|
class OsuFlashlight extends OsuSkill {
|
|
3052
|
-
skillMultiplier = 0.052;
|
|
3053
|
-
strainDecayBase = 0.15;
|
|
3054
|
-
reducedSectionCount = 0;
|
|
3055
|
-
reducedSectionBaseline = 1;
|
|
3056
|
-
decayWeight = 1;
|
|
3057
|
-
isHidden;
|
|
3058
3184
|
constructor(mods) {
|
|
3059
3185
|
super(mods);
|
|
3186
|
+
this.strainDecayBase = 0.15;
|
|
3187
|
+
this.reducedSectionCount = 0;
|
|
3188
|
+
this.reducedSectionBaseline = 1;
|
|
3189
|
+
this.decayWeight = 1;
|
|
3190
|
+
this.currentFlashlightStrain = 0;
|
|
3191
|
+
this.skillMultiplier = 0.052;
|
|
3060
3192
|
this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
|
|
3061
3193
|
}
|
|
3062
|
-
/**
|
|
3063
|
-
* @param current The hitobject to calculate.
|
|
3064
|
-
*/
|
|
3065
3194
|
strainValueAt(current) {
|
|
3066
|
-
this.
|
|
3067
|
-
this.
|
|
3195
|
+
this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
|
|
3196
|
+
this.currentFlashlightStrain +=
|
|
3068
3197
|
OsuFlashlightEvaluator.evaluateDifficultyOf(current, this.isHidden) * this.skillMultiplier;
|
|
3069
|
-
return this.
|
|
3198
|
+
return this.currentFlashlightStrain;
|
|
3199
|
+
}
|
|
3200
|
+
calculateInitialStrain(time, current) {
|
|
3201
|
+
var _a, _b;
|
|
3202
|
+
return (this.currentFlashlightStrain *
|
|
3203
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
3070
3204
|
}
|
|
3071
3205
|
saveToHitObject(current) {
|
|
3072
|
-
current.
|
|
3206
|
+
current.flashlightStrain = this.currentFlashlightStrain;
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
/**
|
|
3211
|
+
* Represents an osu!standard hit object with difficulty calculation values.
|
|
3212
|
+
*/
|
|
3213
|
+
class OsuDifficultyHitObject extends DifficultyHitObject {
|
|
3214
|
+
get scalingFactor() {
|
|
3215
|
+
const radius = this.object.radius;
|
|
3216
|
+
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
|
3217
|
+
let scalingFactor = this.normalizedRadius / radius;
|
|
3218
|
+
// High circle size (small CS) bonus
|
|
3219
|
+
if (radius < this.radiusBuffThreshold) {
|
|
3220
|
+
scalingFactor *=
|
|
3221
|
+
1 + Math.min(this.radiusBuffThreshold - radius, 5) / 50;
|
|
3222
|
+
}
|
|
3223
|
+
return scalingFactor;
|
|
3224
|
+
}
|
|
3225
|
+
/**
|
|
3226
|
+
* Note: You **must** call `computeProperties` at some point due to how TypeScript handles
|
|
3227
|
+
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue).
|
|
3228
|
+
*
|
|
3229
|
+
* @param object The underlying hitobject.
|
|
3230
|
+
* @param lastObject The hitobject before this hitobject.
|
|
3231
|
+
* @param lastLastObject The hitobject before the last hitobject.
|
|
3232
|
+
* @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
|
|
3233
|
+
* @param clockRate The clock rate of the beatmap.
|
|
3234
|
+
*/
|
|
3235
|
+
constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate) {
|
|
3236
|
+
super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate);
|
|
3237
|
+
/**
|
|
3238
|
+
* The speed strain generated by the hitobject.
|
|
3239
|
+
*/
|
|
3240
|
+
this.speedStrain = 0;
|
|
3241
|
+
/**
|
|
3242
|
+
* The flashlight strain generated by this hitobject.
|
|
3243
|
+
*/
|
|
3244
|
+
this.flashlightStrain = 0;
|
|
3245
|
+
this.radiusBuffThreshold = 30;
|
|
3246
|
+
this.mode = osuBase.Modes.osu;
|
|
3073
3247
|
}
|
|
3074
3248
|
}
|
|
3075
3249
|
|
|
@@ -3077,35 +3251,48 @@ class OsuFlashlight extends OsuSkill {
|
|
|
3077
3251
|
* A difficulty calculator for osu!standard gamemode.
|
|
3078
3252
|
*/
|
|
3079
3253
|
class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
3254
|
+
constructor() {
|
|
3255
|
+
super(...arguments);
|
|
3256
|
+
this.attributes = {
|
|
3257
|
+
speedDifficulty: 0,
|
|
3258
|
+
mods: [],
|
|
3259
|
+
starRating: 0,
|
|
3260
|
+
maxCombo: 0,
|
|
3261
|
+
aimDifficulty: 0,
|
|
3262
|
+
flashlightDifficulty: 0,
|
|
3263
|
+
speedNoteCount: 0,
|
|
3264
|
+
sliderFactor: 0,
|
|
3265
|
+
clockRate: 1,
|
|
3266
|
+
approachRate: 0,
|
|
3267
|
+
overallDifficulty: 0,
|
|
3268
|
+
hitCircleCount: 0,
|
|
3269
|
+
sliderCount: 0,
|
|
3270
|
+
spinnerCount: 0,
|
|
3271
|
+
};
|
|
3272
|
+
this.difficultyMultiplier = 0.0675;
|
|
3273
|
+
this.mode = osuBase.Modes.osu;
|
|
3274
|
+
}
|
|
3080
3275
|
/**
|
|
3081
3276
|
* The aim star rating of the beatmap.
|
|
3082
3277
|
*/
|
|
3083
|
-
aim
|
|
3278
|
+
get aim() {
|
|
3279
|
+
return this.attributes.aimDifficulty;
|
|
3280
|
+
}
|
|
3084
3281
|
/**
|
|
3085
3282
|
* The speed star rating of the beatmap.
|
|
3086
3283
|
*/
|
|
3087
|
-
speed
|
|
3284
|
+
get speed() {
|
|
3285
|
+
return this.attributes.speedDifficulty;
|
|
3286
|
+
}
|
|
3088
3287
|
/**
|
|
3089
3288
|
* The flashlight star rating of the beatmap.
|
|
3090
3289
|
*/
|
|
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;
|
|
3290
|
+
get flashlight() {
|
|
3291
|
+
return this.attributes.flashlightDifficulty;
|
|
3292
|
+
}
|
|
3293
|
+
get cacheableAttributes() {
|
|
3294
|
+
return Object.assign(Object.assign({}, this.attributes), { mods: osuBase.ModUtil.modsToOsuString(this.attributes.mods) });
|
|
3295
|
+
}
|
|
3109
3296
|
/**
|
|
3110
3297
|
* Calculates the aim star rating of the beatmap and stores it in this instance.
|
|
3111
3298
|
*/
|
|
@@ -3120,10 +3307,10 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3120
3307
|
*/
|
|
3121
3308
|
calculateSpeed() {
|
|
3122
3309
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3123
|
-
this.
|
|
3310
|
+
this.attributes.speedDifficulty = 0;
|
|
3124
3311
|
return;
|
|
3125
3312
|
}
|
|
3126
|
-
const speedSkill = new OsuSpeed(this.mods,
|
|
3313
|
+
const speedSkill = new OsuSpeed(this.mods, this.difficultyStatistics.overallDifficulty);
|
|
3127
3314
|
this.calculateSkills(speedSkill);
|
|
3128
3315
|
this.postCalculateSpeed(speedSkill);
|
|
3129
3316
|
}
|
|
@@ -3148,14 +3335,14 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3148
3335
|
if (basePerformanceValue > 1e-5) {
|
|
3149
3336
|
// Document for formula derivation:
|
|
3150
3337
|
// https://docs.google.com/document/d/10DZGYYSsT_yjz2Mtp6yIJld0Rqx4E-vVHupCqiM4TNI/edit
|
|
3151
|
-
this.
|
|
3338
|
+
this.attributes.starRating =
|
|
3152
3339
|
Math.cbrt(1.14) *
|
|
3153
3340
|
0.027 *
|
|
3154
3341
|
(Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformanceValue) +
|
|
3155
3342
|
4);
|
|
3156
3343
|
}
|
|
3157
3344
|
else {
|
|
3158
|
-
this.
|
|
3345
|
+
this.attributes.starRating = 0;
|
|
3159
3346
|
}
|
|
3160
3347
|
}
|
|
3161
3348
|
calculateAll() {
|
|
@@ -3168,7 +3355,6 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3168
3355
|
const flashlightSkill = skills[3];
|
|
3169
3356
|
this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
|
|
3170
3357
|
if (isRelax) {
|
|
3171
|
-
this.speed = 0;
|
|
3172
3358
|
this.attributes.speedDifficulty = 0;
|
|
3173
3359
|
}
|
|
3174
3360
|
else {
|
|
@@ -3178,9 +3364,6 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3178
3364
|
this.postCalculateFlashlight(flashlightSkill);
|
|
3179
3365
|
this.calculateTotal();
|
|
3180
3366
|
}
|
|
3181
|
-
/**
|
|
3182
|
-
* Returns a string representative of the class.
|
|
3183
|
-
*/
|
|
3184
3367
|
toString() {
|
|
3185
3368
|
return (this.total.toFixed(2) +
|
|
3186
3369
|
" stars (" +
|
|
@@ -3191,14 +3374,34 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3191
3374
|
this.flashlight.toFixed(2) +
|
|
3192
3375
|
" flashlight)");
|
|
3193
3376
|
}
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3377
|
+
generateDifficultyHitObjects() {
|
|
3378
|
+
var _a, _b;
|
|
3379
|
+
const difficultyObjects = [];
|
|
3380
|
+
const { objects } = this.beatmap.hitObjects;
|
|
3381
|
+
for (let i = 0; i < objects.length; ++i) {
|
|
3382
|
+
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, this.difficultyStatistics.overallSpeedMultiplier);
|
|
3383
|
+
difficultyObject.computeProperties(this.difficultyStatistics.overallSpeedMultiplier, objects);
|
|
3384
|
+
difficultyObjects.push(difficultyObject);
|
|
3385
|
+
}
|
|
3386
|
+
return difficultyObjects;
|
|
3387
|
+
}
|
|
3388
|
+
computeDifficultyStatistics(options) {
|
|
3389
|
+
var _a;
|
|
3390
|
+
const { difficulty } = this.beatmap;
|
|
3391
|
+
return osuBase.calculateOsuDifficultyStatistics({
|
|
3392
|
+
circleSize: difficulty.cs,
|
|
3393
|
+
approachRate: (_a = difficulty.ar) !== null && _a !== void 0 ? _a : difficulty.od,
|
|
3394
|
+
overallDifficulty: difficulty.od,
|
|
3395
|
+
healthDrain: difficulty.hp,
|
|
3396
|
+
mods: options === null || options === void 0 ? void 0 : options.mods,
|
|
3397
|
+
customSpeedMultiplier: options === null || options === void 0 ? void 0 : options.customSpeedMultiplier,
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3197
3400
|
createSkills() {
|
|
3198
3401
|
return [
|
|
3199
3402
|
new OsuAim(this.mods, true),
|
|
3200
3403
|
new OsuAim(this.mods, false),
|
|
3201
|
-
new OsuSpeed(this.mods,
|
|
3404
|
+
new OsuSpeed(this.mods, this.difficultyStatistics.overallDifficulty),
|
|
3202
3405
|
new OsuFlashlight(this.mods),
|
|
3203
3406
|
];
|
|
3204
3407
|
}
|
|
@@ -3211,19 +3414,18 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3211
3414
|
postCalculateAim(aimSkill, aimSkillWithoutSliders) {
|
|
3212
3415
|
this.strainPeaks.aimWithSliders = aimSkill.strainPeaks;
|
|
3213
3416
|
this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
|
|
3214
|
-
this.
|
|
3417
|
+
this.attributes.aimDifficulty = this.starValue(aimSkill.difficultyValue());
|
|
3215
3418
|
if (this.aim) {
|
|
3216
3419
|
this.attributes.sliderFactor =
|
|
3217
3420
|
this.starValue(aimSkillWithoutSliders.difficultyValue()) /
|
|
3218
3421
|
this.aim;
|
|
3219
3422
|
}
|
|
3220
3423
|
if (this.mods.some((m) => m instanceof osuBase.ModTouchDevice)) {
|
|
3221
|
-
this.
|
|
3424
|
+
this.attributes.aimDifficulty = Math.pow(this.aim, 0.8);
|
|
3222
3425
|
}
|
|
3223
3426
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3224
|
-
this.
|
|
3427
|
+
this.attributes.aimDifficulty *= 0.9;
|
|
3225
3428
|
}
|
|
3226
|
-
this.attributes.aimDifficulty = this.aim;
|
|
3227
3429
|
}
|
|
3228
3430
|
/**
|
|
3229
3431
|
* Called after speed skill calculation.
|
|
@@ -3232,13 +3434,13 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3232
3434
|
*/
|
|
3233
3435
|
postCalculateSpeed(speedSkill) {
|
|
3234
3436
|
this.strainPeaks.speed = speedSkill.strainPeaks;
|
|
3235
|
-
this.
|
|
3437
|
+
this.attributes.speedDifficulty = this.starValue(speedSkill.difficultyValue());
|
|
3236
3438
|
}
|
|
3237
3439
|
/**
|
|
3238
3440
|
* Calculates speed-related attributes.
|
|
3239
3441
|
*/
|
|
3240
3442
|
calculateSpeedAttributes() {
|
|
3241
|
-
const objectStrains = this.objects.map((v) => v.
|
|
3443
|
+
const objectStrains = this.objects.map((v) => v.speedStrain);
|
|
3242
3444
|
const maxStrain = Math.max(...objectStrains);
|
|
3243
3445
|
if (maxStrain) {
|
|
3244
3446
|
this.attributes.speedNoteCount = objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
|
|
@@ -3251,55 +3453,13 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3251
3453
|
*/
|
|
3252
3454
|
postCalculateFlashlight(flashlightSkill) {
|
|
3253
3455
|
this.strainPeaks.flashlight = flashlightSkill.strainPeaks;
|
|
3254
|
-
this.
|
|
3456
|
+
this.attributes.flashlightDifficulty = this.starValue(flashlightSkill.difficultyValue());
|
|
3255
3457
|
if (this.mods.some((m) => m instanceof osuBase.ModTouchDevice)) {
|
|
3256
|
-
this.
|
|
3458
|
+
this.attributes.flashlightDifficulty = Math.pow(this.flashlight, 0.8);
|
|
3257
3459
|
}
|
|
3258
3460
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3259
|
-
this.
|
|
3461
|
+
this.attributes.flashlightDifficulty *= 0.7;
|
|
3260
3462
|
}
|
|
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
3463
|
}
|
|
3304
3464
|
}
|
|
3305
3465
|
|
|
@@ -3307,65 +3467,60 @@ class MapStars {
|
|
|
3307
3467
|
* A performance points calculator that calculates performance points for osu!standard gamemode.
|
|
3308
3468
|
*/
|
|
3309
3469
|
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);
|
|
3470
|
+
constructor() {
|
|
3471
|
+
super(...arguments);
|
|
3472
|
+
/**
|
|
3473
|
+
* The aim performance value.
|
|
3474
|
+
*/
|
|
3475
|
+
this.aim = 0;
|
|
3476
|
+
/**
|
|
3477
|
+
* The speed performance value.
|
|
3478
|
+
*/
|
|
3479
|
+
this.speed = 0;
|
|
3480
|
+
/**
|
|
3481
|
+
* The accuracy performance value.
|
|
3482
|
+
*/
|
|
3483
|
+
this.accuracy = 0;
|
|
3484
|
+
/**
|
|
3485
|
+
* The flashlight performance value.
|
|
3486
|
+
*/
|
|
3487
|
+
this.flashlight = 0;
|
|
3488
|
+
this.finalMultiplier = 1.14;
|
|
3489
|
+
this.mode = osuBase.Modes.osu;
|
|
3335
3490
|
}
|
|
3336
3491
|
calculateValues() {
|
|
3337
|
-
this.calculateAimValue();
|
|
3338
|
-
this.calculateSpeedValue();
|
|
3339
|
-
this.calculateAccuracyValue();
|
|
3340
|
-
this.calculateFlashlightValue();
|
|
3492
|
+
this.aim = this.calculateAimValue();
|
|
3493
|
+
this.speed = this.calculateSpeedValue();
|
|
3494
|
+
this.accuracy = this.calculateAccuracyValue();
|
|
3495
|
+
this.flashlight = this.calculateFlashlightValue();
|
|
3341
3496
|
}
|
|
3342
3497
|
calculateTotalValue() {
|
|
3343
|
-
this.
|
|
3344
|
-
Math.pow(
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
Math.pow(this.flashlight, 1.1), 1 / 1.1) * this.finalMultiplier;
|
|
3498
|
+
return (Math.pow(Math.pow(this.aim, 1.1) +
|
|
3499
|
+
Math.pow(this.speed, 1.1) +
|
|
3500
|
+
Math.pow(this.accuracy, 1.1) +
|
|
3501
|
+
Math.pow(this.flashlight, 1.1), 1 / 1.1) * this.finalMultiplier);
|
|
3348
3502
|
}
|
|
3349
3503
|
/**
|
|
3350
3504
|
* Calculates the aim performance value of the beatmap.
|
|
3351
3505
|
*/
|
|
3352
3506
|
calculateAimValue() {
|
|
3353
|
-
|
|
3507
|
+
let aimValue = this.baseValue(this.difficultyAttributes.aimDifficulty);
|
|
3354
3508
|
// Longer maps are worth more
|
|
3355
3509
|
let lengthBonus = 0.95 + 0.4 * Math.min(1, this.totalHits / 2000);
|
|
3356
3510
|
if (this.totalHits > 2000) {
|
|
3357
3511
|
lengthBonus += Math.log10(this.totalHits / 2000) * 0.5;
|
|
3358
3512
|
}
|
|
3359
|
-
|
|
3513
|
+
aimValue *= lengthBonus;
|
|
3360
3514
|
if (this.effectiveMissCount > 0) {
|
|
3361
|
-
// Penalize misses by assessing # of misses relative to the total # of objects.
|
|
3362
|
-
|
|
3515
|
+
// Penalize misses by assessing # of misses relative to the total # of objects.
|
|
3516
|
+
// Default a 3% reduction for any # of misses.
|
|
3517
|
+
aimValue *=
|
|
3363
3518
|
0.97 *
|
|
3364
3519
|
Math.pow(1 -
|
|
3365
3520
|
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
|
|
3366
3521
|
}
|
|
3367
3522
|
// Combo scaling
|
|
3368
|
-
|
|
3523
|
+
aimValue *= this.comboPenalty;
|
|
3369
3524
|
const calculatedAR = this.difficultyAttributes.approachRate;
|
|
3370
3525
|
if (!this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3371
3526
|
// AR scaling
|
|
@@ -3377,145 +3532,142 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3377
3532
|
arFactor += 0.05 * (8 - calculatedAR);
|
|
3378
3533
|
}
|
|
3379
3534
|
// Buff for longer maps with high AR.
|
|
3380
|
-
|
|
3535
|
+
aimValue *= 1 + arFactor * lengthBonus;
|
|
3381
3536
|
}
|
|
3382
3537
|
// 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
3538
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHidden)) {
|
|
3384
|
-
|
|
3539
|
+
aimValue *= 1 + 0.04 * (12 - calculatedAR);
|
|
3385
3540
|
}
|
|
3386
3541
|
// Scale the aim value with slider factor to nerf very likely dropped sliderends.
|
|
3387
|
-
|
|
3542
|
+
aimValue *= this.sliderNerfFactor;
|
|
3388
3543
|
// Scale the aim value with accuracy.
|
|
3389
|
-
|
|
3544
|
+
aimValue *= this.computedAccuracy.value();
|
|
3390
3545
|
// It is also important to consider accuracy difficulty when doing that.
|
|
3391
3546
|
const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
|
|
3392
|
-
|
|
3547
|
+
aimValue *= 0.98 + odScaling;
|
|
3548
|
+
return aimValue;
|
|
3393
3549
|
}
|
|
3394
3550
|
/**
|
|
3395
3551
|
* Calculates the speed performance value of the beatmap.
|
|
3396
3552
|
*/
|
|
3397
3553
|
calculateSpeedValue() {
|
|
3398
3554
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3399
|
-
|
|
3400
|
-
return;
|
|
3555
|
+
return 0;
|
|
3401
3556
|
}
|
|
3402
|
-
|
|
3403
|
-
this.speed = this.baseValue(this.difficultyAttributes.speedDifficulty);
|
|
3557
|
+
let speedValue = this.baseValue(this.difficultyAttributes.speedDifficulty);
|
|
3404
3558
|
// Longer maps are worth more
|
|
3405
3559
|
let lengthBonus = 0.95 + 0.4 * Math.min(1, this.totalHits / 2000);
|
|
3406
3560
|
if (this.totalHits > 2000) {
|
|
3407
3561
|
lengthBonus += Math.log10(this.totalHits / 2000) * 0.5;
|
|
3408
3562
|
}
|
|
3409
|
-
|
|
3563
|
+
speedValue *= lengthBonus;
|
|
3410
3564
|
if (this.effectiveMissCount > 0) {
|
|
3411
|
-
// Penalize misses by assessing # of misses relative to the total # of objects.
|
|
3412
|
-
|
|
3565
|
+
// Penalize misses by assessing # of misses relative to the total # of objects.
|
|
3566
|
+
// Default a 3% reduction for any # of misses.
|
|
3567
|
+
speedValue *=
|
|
3413
3568
|
0.97 *
|
|
3414
3569
|
Math.pow(1 -
|
|
3415
3570
|
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
|
|
3416
3571
|
}
|
|
3417
3572
|
// Combo scaling
|
|
3418
|
-
|
|
3573
|
+
speedValue *= this.comboPenalty;
|
|
3419
3574
|
// AR scaling
|
|
3420
3575
|
const calculatedAR = this.difficultyAttributes.approachRate;
|
|
3421
3576
|
if (calculatedAR > 10.33) {
|
|
3422
3577
|
// Buff for longer maps with high AR.
|
|
3423
|
-
|
|
3578
|
+
speedValue *= 1 + 0.3 * (calculatedAR - 10.33) * lengthBonus;
|
|
3424
3579
|
}
|
|
3425
3580
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHidden)) {
|
|
3426
|
-
|
|
3581
|
+
speedValue *= 1 + 0.04 * (12 - calculatedAR);
|
|
3427
3582
|
}
|
|
3428
3583
|
// Calculate accuracy assuming the worst case scenario.
|
|
3429
3584
|
const countGreat = this.computedAccuracy.n300;
|
|
3430
3585
|
const countOk = this.computedAccuracy.n100;
|
|
3431
3586
|
const countMeh = this.computedAccuracy.n50;
|
|
3432
3587
|
const relevantTotalDiff = this.totalHits - this.difficultyAttributes.speedNoteCount;
|
|
3433
|
-
const relevantAccuracy = new osuBase.Accuracy(
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3588
|
+
const relevantAccuracy = new osuBase.Accuracy(this.difficultyAttributes.speedNoteCount > 0
|
|
3589
|
+
? {
|
|
3590
|
+
n300: Math.max(0, countGreat - relevantTotalDiff),
|
|
3591
|
+
n100: Math.max(0, countOk - Math.max(0, relevantTotalDiff - countGreat)),
|
|
3592
|
+
n50: Math.max(0, countMeh -
|
|
3593
|
+
Math.max(0, relevantTotalDiff - countGreat - countOk)),
|
|
3594
|
+
}
|
|
3595
|
+
: // Set accuracy to 0.
|
|
3596
|
+
{ n300: 0, nobjects: 1 });
|
|
3439
3597
|
// Scale the speed value with accuracy and OD.
|
|
3440
|
-
|
|
3598
|
+
speedValue *=
|
|
3441
3599
|
(0.95 +
|
|
3442
3600
|
Math.pow(this.difficultyAttributes.overallDifficulty, 2) /
|
|
3443
3601
|
750) *
|
|
3444
|
-
Math.pow((this.computedAccuracy.value(
|
|
3445
|
-
relevantAccuracy.value()) /
|
|
3602
|
+
Math.pow((this.computedAccuracy.value() +
|
|
3603
|
+
relevantAccuracy.value(this.difficultyAttributes.speedNoteCount)) /
|
|
3446
3604
|
2, (14.5 -
|
|
3447
3605
|
Math.max(this.difficultyAttributes.overallDifficulty, 8)) /
|
|
3448
3606
|
2);
|
|
3449
3607
|
// Scale the speed value with # of 50s to punish doubletapping.
|
|
3450
|
-
|
|
3608
|
+
speedValue *= Math.pow(0.99, Math.max(0, this.computedAccuracy.n50 - this.totalHits / 500));
|
|
3609
|
+
return speedValue;
|
|
3451
3610
|
}
|
|
3452
3611
|
/**
|
|
3453
3612
|
* Calculates the accuracy performance value of the beatmap.
|
|
3454
3613
|
*/
|
|
3455
3614
|
calculateAccuracyValue() {
|
|
3456
3615
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3457
|
-
|
|
3458
|
-
return;
|
|
3616
|
+
return 0;
|
|
3459
3617
|
}
|
|
3460
3618
|
const ncircles = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModScoreV2)
|
|
3461
3619
|
? this.totalHits - this.difficultyAttributes.spinnerCount
|
|
3462
3620
|
: this.difficultyAttributes.hitCircleCount;
|
|
3463
3621
|
if (ncircles === 0) {
|
|
3464
|
-
|
|
3465
|
-
return;
|
|
3622
|
+
return 0;
|
|
3466
3623
|
}
|
|
3467
|
-
const realAccuracy = new osuBase.Accuracy({
|
|
3468
|
-
...this.computedAccuracy,
|
|
3469
|
-
n300: this.computedAccuracy.n300 - (this.totalHits - ncircles),
|
|
3470
|
-
});
|
|
3624
|
+
const realAccuracy = new osuBase.Accuracy(Object.assign(Object.assign({}, this.computedAccuracy), { n300: this.computedAccuracy.n300 - (this.totalHits - ncircles) }));
|
|
3471
3625
|
// Lots of arbitrary values from testing.
|
|
3472
3626
|
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
|
|
3473
|
-
this.
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3627
|
+
let accuracyValue = Math.pow(1.52163, this.difficultyAttributes.overallDifficulty) *
|
|
3628
|
+
// It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points.
|
|
3629
|
+
Math.pow(realAccuracy.n300 < 0 ? 0 : realAccuracy.value(), 24) *
|
|
3630
|
+
2.83;
|
|
3477
3631
|
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
|
|
3478
|
-
|
|
3632
|
+
accuracyValue *= Math.min(1.15, Math.pow(ncircles / 1000, 0.3));
|
|
3479
3633
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHidden)) {
|
|
3480
|
-
|
|
3634
|
+
accuracyValue *= 1.08;
|
|
3481
3635
|
}
|
|
3482
3636
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
|
|
3483
|
-
|
|
3637
|
+
accuracyValue *= 1.02;
|
|
3484
3638
|
}
|
|
3639
|
+
return accuracyValue;
|
|
3485
3640
|
}
|
|
3486
3641
|
/**
|
|
3487
3642
|
* Calculates the flashlight performance value of the beatmap.
|
|
3488
3643
|
*/
|
|
3489
3644
|
calculateFlashlightValue() {
|
|
3490
3645
|
if (!this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
|
|
3491
|
-
|
|
3492
|
-
return;
|
|
3646
|
+
return 0;
|
|
3493
3647
|
}
|
|
3494
|
-
|
|
3495
|
-
this.flashlight =
|
|
3496
|
-
Math.pow(this.difficultyAttributes.flashlightDifficulty, 2) * 25;
|
|
3648
|
+
let flashlightValue = Math.pow(this.difficultyAttributes.flashlightDifficulty, 2) * 25;
|
|
3497
3649
|
// Combo scaling
|
|
3498
|
-
|
|
3650
|
+
flashlightValue *= this.comboPenalty;
|
|
3499
3651
|
if (this.effectiveMissCount > 0) {
|
|
3500
3652
|
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
3501
|
-
|
|
3653
|
+
flashlightValue *=
|
|
3502
3654
|
0.97 *
|
|
3503
3655
|
Math.pow(1 -
|
|
3504
3656
|
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
|
|
3505
3657
|
}
|
|
3506
3658
|
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
3507
|
-
|
|
3659
|
+
flashlightValue *=
|
|
3508
3660
|
0.7 +
|
|
3509
3661
|
0.1 * Math.min(1, this.totalHits / 200) +
|
|
3510
3662
|
(this.totalHits > 200
|
|
3511
3663
|
? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
|
|
3512
3664
|
: 0);
|
|
3513
3665
|
// Scale the flashlight value with accuracy slightly.
|
|
3514
|
-
this.
|
|
3515
|
-
0.5 + this.computedAccuracy.value(this.totalHits) / 2;
|
|
3666
|
+
flashlightValue *= 0.5 + this.computedAccuracy.value() / 2;
|
|
3516
3667
|
// It is also important to consider accuracy difficulty when doing that.
|
|
3517
3668
|
const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
|
|
3518
|
-
|
|
3669
|
+
flashlightValue *= 0.98 + odScaling;
|
|
3670
|
+
return flashlightValue;
|
|
3519
3671
|
}
|
|
3520
3672
|
toString() {
|
|
3521
3673
|
return (this.total.toFixed(2) +
|
|
@@ -3534,10 +3686,10 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3534
3686
|
exports.AimEvaluator = AimEvaluator;
|
|
3535
3687
|
exports.DifficultyCalculator = DifficultyCalculator;
|
|
3536
3688
|
exports.DifficultyHitObject = DifficultyHitObject;
|
|
3537
|
-
exports.DifficultyHitObjectCreator = DifficultyHitObjectCreator;
|
|
3538
3689
|
exports.DroidAim = DroidAim;
|
|
3539
3690
|
exports.DroidAimEvaluator = DroidAimEvaluator;
|
|
3540
3691
|
exports.DroidDifficultyCalculator = DroidDifficultyCalculator;
|
|
3692
|
+
exports.DroidDifficultyHitObject = DroidDifficultyHitObject;
|
|
3541
3693
|
exports.DroidFlashlight = DroidFlashlight;
|
|
3542
3694
|
exports.DroidFlashlightEvaluator = DroidFlashlightEvaluator;
|
|
3543
3695
|
exports.DroidPerformanceCalculator = DroidPerformanceCalculator;
|
|
@@ -3548,10 +3700,10 @@ exports.DroidTapEvaluator = DroidTapEvaluator;
|
|
|
3548
3700
|
exports.DroidVisual = DroidVisual;
|
|
3549
3701
|
exports.DroidVisualEvaluator = DroidVisualEvaluator;
|
|
3550
3702
|
exports.FlashlightEvaluator = FlashlightEvaluator;
|
|
3551
|
-
exports.MapStars = MapStars;
|
|
3552
3703
|
exports.OsuAim = OsuAim;
|
|
3553
3704
|
exports.OsuAimEvaluator = OsuAimEvaluator;
|
|
3554
3705
|
exports.OsuDifficultyCalculator = OsuDifficultyCalculator;
|
|
3706
|
+
exports.OsuDifficultyHitObject = OsuDifficultyHitObject;
|
|
3555
3707
|
exports.OsuFlashlight = OsuFlashlight;
|
|
3556
3708
|
exports.OsuFlashlightEvaluator = OsuFlashlightEvaluator;
|
|
3557
3709
|
exports.OsuPerformanceCalculator = OsuPerformanceCalculator;
|