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