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