@rian8337/osu-difficulty-calculator 1.0.0
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/LICENSE +21 -0
- package/README.md +11 -0
- package/dist/DroidPerformanceCalculator.js +248 -0
- package/dist/DroidStarRating.js +175 -0
- package/dist/MapStars.js +50 -0
- package/dist/OsuPerformanceCalculator.js +217 -0
- package/dist/OsuStarRating.js +147 -0
- package/dist/base/DifficultyAttributes.js +2 -0
- package/dist/base/PerformanceCalculator.js +125 -0
- package/dist/base/Skill.js +29 -0
- package/dist/base/StarRating.js +130 -0
- package/dist/base/StrainSkill.js +75 -0
- package/dist/index.js +25 -0
- package/dist/preprocessing/DifficultyHitObject.js +89 -0
- package/dist/preprocessing/DifficultyHitObjectCreator.js +237 -0
- package/dist/skills/DroidAim.js +209 -0
- package/dist/skills/DroidFlashlight.js +59 -0
- package/dist/skills/DroidSkill.js +16 -0
- package/dist/skills/DroidTap.js +188 -0
- package/dist/skills/OsuAim.js +170 -0
- package/dist/skills/OsuFlashlight.js +60 -0
- package/dist/skills/OsuSkill.js +37 -0
- package/dist/skills/OsuSpeed.js +175 -0
- package/package.json +37 -0
- package/typings/index.d.ts +1052 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OsuPerformanceCalculator = void 0;
|
|
4
|
+
const OsuStarRating_1 = require("./OsuStarRating");
|
|
5
|
+
const PerformanceCalculator_1 = require("./base/PerformanceCalculator");
|
|
6
|
+
const osu_base_1 = require("@rian8337/osu-base");
|
|
7
|
+
/**
|
|
8
|
+
* A performance points calculator that calculates performance points for osu!standard gamemode.
|
|
9
|
+
*/
|
|
10
|
+
class OsuPerformanceCalculator extends PerformanceCalculator_1.PerformanceCalculator {
|
|
11
|
+
constructor() {
|
|
12
|
+
super(...arguments);
|
|
13
|
+
this.stars = new OsuStarRating_1.OsuStarRating();
|
|
14
|
+
this.finalMultiplier = 1.12;
|
|
15
|
+
/**
|
|
16
|
+
* The aim performance value.
|
|
17
|
+
*/
|
|
18
|
+
this.aim = 0;
|
|
19
|
+
/**
|
|
20
|
+
* The speed performance value.
|
|
21
|
+
*/
|
|
22
|
+
this.speed = 0;
|
|
23
|
+
/**
|
|
24
|
+
* The accuracy performance value.
|
|
25
|
+
*/
|
|
26
|
+
this.accuracy = 0;
|
|
27
|
+
/**
|
|
28
|
+
* The flashlight performance value.
|
|
29
|
+
*/
|
|
30
|
+
this.flashlight = 0;
|
|
31
|
+
}
|
|
32
|
+
calculate(params) {
|
|
33
|
+
this.handleParams(params, osu_base_1.modes.osu);
|
|
34
|
+
this.calculateAimValue();
|
|
35
|
+
this.calculateSpeedValue();
|
|
36
|
+
this.calculateAccuracyValue();
|
|
37
|
+
this.calculateFlashlightValue();
|
|
38
|
+
this.total =
|
|
39
|
+
Math.pow(Math.pow(this.aim, 1.1) +
|
|
40
|
+
Math.pow(this.speed, 1.1) +
|
|
41
|
+
Math.pow(this.accuracy, 1.1) +
|
|
42
|
+
Math.pow(this.flashlight, 1.1), 1 / 1.1) * this.finalMultiplier;
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Calculates the aim performance value of the beatmap.
|
|
47
|
+
*/
|
|
48
|
+
calculateAimValue() {
|
|
49
|
+
// Global variables
|
|
50
|
+
const objectCount = this.stars.objects.length;
|
|
51
|
+
const calculatedAR = this.mapStatistics.ar;
|
|
52
|
+
this.aim = this.baseValue(Math.pow(this.stars.aim, this.stars.mods.some((m) => m instanceof osu_base_1.ModTouchDevice)
|
|
53
|
+
? 0.8
|
|
54
|
+
: 1));
|
|
55
|
+
// Longer maps are worth more
|
|
56
|
+
let lengthBonus = 0.95 + 0.4 * Math.min(1, objectCount / 2000);
|
|
57
|
+
if (objectCount > 2000) {
|
|
58
|
+
lengthBonus += Math.log10(objectCount / 2000) * 0.5;
|
|
59
|
+
}
|
|
60
|
+
this.aim *= lengthBonus;
|
|
61
|
+
if (this.effectiveMissCount > 0) {
|
|
62
|
+
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
63
|
+
this.aim *=
|
|
64
|
+
0.97 *
|
|
65
|
+
Math.pow(1 - Math.pow(this.effectiveMissCount / objectCount, 0.775), this.effectiveMissCount);
|
|
66
|
+
}
|
|
67
|
+
// Combo scaling
|
|
68
|
+
this.aim *= this.comboPenalty;
|
|
69
|
+
// AR scaling
|
|
70
|
+
let arFactor = 0;
|
|
71
|
+
if (calculatedAR > 10.33) {
|
|
72
|
+
arFactor += 0.3 * (calculatedAR - 10.33);
|
|
73
|
+
}
|
|
74
|
+
else if (calculatedAR < 8) {
|
|
75
|
+
arFactor += 0.1 * (8 - calculatedAR);
|
|
76
|
+
}
|
|
77
|
+
// Buff for longer maps with high AR.
|
|
78
|
+
this.aim *= 1 + arFactor * lengthBonus;
|
|
79
|
+
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
|
80
|
+
let hiddenBonus = 1;
|
|
81
|
+
if (this.stars.mods.some((m) => m instanceof osu_base_1.ModHidden)) {
|
|
82
|
+
hiddenBonus += 0.04 * (12 - calculatedAR);
|
|
83
|
+
}
|
|
84
|
+
this.aim *= hiddenBonus;
|
|
85
|
+
// Scale the aim value with slider factor to nerf very likely dropped sliderends.
|
|
86
|
+
this.aim *= this.sliderNerfFactor;
|
|
87
|
+
// Scale the aim value with accuracy.
|
|
88
|
+
this.aim *= this.computedAccuracy.value(objectCount);
|
|
89
|
+
// It is also important to consider accuracy difficulty when doing that.
|
|
90
|
+
const odScaling = Math.pow(this.mapStatistics.od, 2) / 2500;
|
|
91
|
+
this.aim *= 0.98 + odScaling;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Calculates the speed performance value of the beatmap.
|
|
95
|
+
*/
|
|
96
|
+
calculateSpeedValue() {
|
|
97
|
+
// Global variables
|
|
98
|
+
const objectCount = this.stars.objects.length;
|
|
99
|
+
const calculatedAR = this.mapStatistics.ar;
|
|
100
|
+
const n50 = this.computedAccuracy.n50;
|
|
101
|
+
this.speed = this.baseValue(this.stars.speed);
|
|
102
|
+
// Longer maps are worth more
|
|
103
|
+
let lengthBonus = 0.95 + 0.4 * Math.min(1, objectCount / 2000);
|
|
104
|
+
if (objectCount > 2000) {
|
|
105
|
+
lengthBonus += Math.log10(objectCount / 2000) * 0.5;
|
|
106
|
+
}
|
|
107
|
+
this.speed *= lengthBonus;
|
|
108
|
+
if (this.effectiveMissCount > 0) {
|
|
109
|
+
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
110
|
+
this.speed *=
|
|
111
|
+
0.97 *
|
|
112
|
+
Math.pow(1 - Math.pow(this.effectiveMissCount / objectCount, 0.775), Math.pow(this.effectiveMissCount, 0.875));
|
|
113
|
+
}
|
|
114
|
+
// Combo scaling
|
|
115
|
+
this.speed *= this.comboPenalty;
|
|
116
|
+
// AR scaling
|
|
117
|
+
if (calculatedAR > 10.33) {
|
|
118
|
+
// Buff for longer maps with high AR.
|
|
119
|
+
this.speed *= 1 + 0.3 * (calculatedAR - 10.33) * lengthBonus;
|
|
120
|
+
}
|
|
121
|
+
if (this.stars.mods.some((m) => m instanceof osu_base_1.ModHidden)) {
|
|
122
|
+
this.speed *= 1 + 0.04 * (12 - calculatedAR);
|
|
123
|
+
}
|
|
124
|
+
// Scale the speed value with accuracy and OD.
|
|
125
|
+
this.speed *=
|
|
126
|
+
(0.95 + Math.pow(this.mapStatistics.od, 2) / 750) *
|
|
127
|
+
Math.pow(this.computedAccuracy.value(objectCount), (14.5 - Math.max(this.mapStatistics.od, 8)) / 2);
|
|
128
|
+
// Scale the speed value with # of 50s to punish doubletapping.
|
|
129
|
+
this.speed *= Math.pow(0.98, Math.max(0, n50 - objectCount / 500));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Calculates the accuracy performance value of the beatmap.
|
|
133
|
+
*/
|
|
134
|
+
calculateAccuracyValue() {
|
|
135
|
+
if (this.stars.mods.some((m) => m instanceof osu_base_1.ModRelax)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Global variables
|
|
139
|
+
const nobjects = this.stars.objects.length;
|
|
140
|
+
const ncircles = this.stars.mods.some((m) => m instanceof osu_base_1.ModScoreV2)
|
|
141
|
+
? nobjects - this.stars.map.spinners
|
|
142
|
+
: this.stars.map.circles;
|
|
143
|
+
if (ncircles === 0) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const realAccuracy = new osu_base_1.Accuracy({
|
|
147
|
+
...this.computedAccuracy,
|
|
148
|
+
n300: this.computedAccuracy.n300 -
|
|
149
|
+
(this.stars.objects.length - ncircles),
|
|
150
|
+
});
|
|
151
|
+
// Lots of arbitrary values from testing.
|
|
152
|
+
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
|
|
153
|
+
this.accuracy =
|
|
154
|
+
Math.pow(1.52163, this.mapStatistics.od) *
|
|
155
|
+
Math.pow(realAccuracy.value(ncircles), 24) *
|
|
156
|
+
2.83;
|
|
157
|
+
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
|
|
158
|
+
this.accuracy *= Math.min(1.15, Math.pow(ncircles / 1000, 0.3));
|
|
159
|
+
if (this.stars.mods.some((m) => m instanceof osu_base_1.ModHidden)) {
|
|
160
|
+
this.accuracy *= 1.08;
|
|
161
|
+
}
|
|
162
|
+
if (this.stars.mods.some((m) => m instanceof osu_base_1.ModFlashlight)) {
|
|
163
|
+
this.accuracy *= 1.02;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Calculates the flashlight performance value of the beatmap.
|
|
168
|
+
*/
|
|
169
|
+
calculateFlashlightValue() {
|
|
170
|
+
if (!this.stars.mods.some((m) => m instanceof osu_base_1.ModFlashlight)) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// Global variables
|
|
174
|
+
const objectCount = this.stars.objects.length;
|
|
175
|
+
this.flashlight =
|
|
176
|
+
Math.pow(Math.pow(this.stars.flashlight, this.stars.mods.some((m) => m instanceof osu_base_1.ModTouchDevice)
|
|
177
|
+
? 0.8
|
|
178
|
+
: 1), 2) * 25;
|
|
179
|
+
// Add an additional bonus for HDFL.
|
|
180
|
+
if (this.stars.mods.some((m) => m instanceof osu_base_1.ModHidden)) {
|
|
181
|
+
this.flashlight *= 1.3;
|
|
182
|
+
}
|
|
183
|
+
// Combo scaling
|
|
184
|
+
this.flashlight *= this.comboPenalty;
|
|
185
|
+
if (this.effectiveMissCount > 0) {
|
|
186
|
+
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
187
|
+
this.flashlight *=
|
|
188
|
+
0.97 *
|
|
189
|
+
Math.pow(1 - Math.pow(this.effectiveMissCount / objectCount, 0.775), Math.pow(this.effectiveMissCount, 0.875));
|
|
190
|
+
}
|
|
191
|
+
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
192
|
+
this.flashlight *=
|
|
193
|
+
0.7 +
|
|
194
|
+
0.1 * Math.min(1, objectCount / 200) +
|
|
195
|
+
(objectCount > 200
|
|
196
|
+
? 0.2 * Math.min(1, (objectCount - 200) / 200)
|
|
197
|
+
: 0);
|
|
198
|
+
// Scale the flashlight value with accuracy slightly.
|
|
199
|
+
this.flashlight *= 0.5 + this.computedAccuracy.value(objectCount) / 2;
|
|
200
|
+
// It is also important to consider accuracy difficulty when doing that.
|
|
201
|
+
const odScaling = Math.pow(this.mapStatistics.od, 2) / 2500;
|
|
202
|
+
this.flashlight *= 0.98 + odScaling;
|
|
203
|
+
}
|
|
204
|
+
toString() {
|
|
205
|
+
return (this.total.toFixed(2) +
|
|
206
|
+
" pp (" +
|
|
207
|
+
this.aim.toFixed(2) +
|
|
208
|
+
" aim, " +
|
|
209
|
+
this.speed.toFixed(2) +
|
|
210
|
+
" speed, " +
|
|
211
|
+
this.accuracy.toFixed(2) +
|
|
212
|
+
" acc, " +
|
|
213
|
+
this.flashlight.toFixed(2) +
|
|
214
|
+
" flashlight)");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
exports.OsuPerformanceCalculator = OsuPerformanceCalculator;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OsuStarRating = void 0;
|
|
4
|
+
const OsuAim_1 = require("./skills/OsuAim");
|
|
5
|
+
const OsuSpeed_1 = require("./skills/OsuSpeed");
|
|
6
|
+
const StarRating_1 = require("./base/StarRating");
|
|
7
|
+
const OsuFlashlight_1 = require("./skills/OsuFlashlight");
|
|
8
|
+
const osu_base_1 = require("@rian8337/osu-base");
|
|
9
|
+
/**
|
|
10
|
+
* Difficulty calculator for osu!standard gamemode.
|
|
11
|
+
*/
|
|
12
|
+
class OsuStarRating extends StarRating_1.StarRating {
|
|
13
|
+
constructor() {
|
|
14
|
+
super(...arguments);
|
|
15
|
+
/**
|
|
16
|
+
* The aim star rating of the beatmap.
|
|
17
|
+
*/
|
|
18
|
+
this.aim = 0;
|
|
19
|
+
/**
|
|
20
|
+
* The speed star rating of the beatmap.
|
|
21
|
+
*/
|
|
22
|
+
this.speed = 0;
|
|
23
|
+
/**
|
|
24
|
+
* The flashlight star rating of the beatmap.
|
|
25
|
+
*/
|
|
26
|
+
this.flashlight = 0;
|
|
27
|
+
this.difficultyMultiplier = 0.0675;
|
|
28
|
+
}
|
|
29
|
+
calculate(params) {
|
|
30
|
+
return super.calculate(params, osu_base_1.modes.osu);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Calculates the aim star rating of the beatmap and stores it in this instance.
|
|
34
|
+
*/
|
|
35
|
+
calculateAim() {
|
|
36
|
+
const aimSkill = new OsuAim_1.OsuAim(this.mods, true);
|
|
37
|
+
const aimSkillWithoutSliders = new OsuAim_1.OsuAim(this.mods, false);
|
|
38
|
+
this.calculateSkills(aimSkill);
|
|
39
|
+
this.strainPeaks.aimWithSliders = aimSkill.strainPeaks;
|
|
40
|
+
this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
|
|
41
|
+
this.aim = this.starValue(aimSkill.difficultyValue());
|
|
42
|
+
if (this.aim) {
|
|
43
|
+
this.attributes.sliderFactor =
|
|
44
|
+
this.starValue(aimSkillWithoutSliders.difficultyValue()) /
|
|
45
|
+
this.aim;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Calculates the speed star rating of the beatmap and stores it in this instance.
|
|
50
|
+
*/
|
|
51
|
+
calculateSpeed() {
|
|
52
|
+
if (this.mods.some((m) => m instanceof osu_base_1.ModRelax)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const speedSkill = new OsuSpeed_1.OsuSpeed(this.mods, new osu_base_1.OsuHitWindow(this.stats.od).hitWindowFor300());
|
|
56
|
+
this.calculateSkills(speedSkill);
|
|
57
|
+
this.strainPeaks.speed = speedSkill.strainPeaks;
|
|
58
|
+
this.speed = this.starValue(speedSkill.difficultyValue());
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Calculates the flashlight star rating of the beatmap and stores it in this instance.
|
|
62
|
+
*/
|
|
63
|
+
calculateFlashlight() {
|
|
64
|
+
const flashlightSkill = new OsuFlashlight_1.OsuFlashlight(this.mods);
|
|
65
|
+
this.calculateSkills(flashlightSkill);
|
|
66
|
+
this.strainPeaks.flashlight = flashlightSkill.strainPeaks;
|
|
67
|
+
this.flashlight = this.starValue(flashlightSkill.difficultyValue());
|
|
68
|
+
}
|
|
69
|
+
calculateTotal() {
|
|
70
|
+
const aimPerformanceValue = this.basePerformanceValue(this.aim);
|
|
71
|
+
const speedPerformanceValue = this.basePerformanceValue(this.speed);
|
|
72
|
+
let flashlightPerformanceValue = 0;
|
|
73
|
+
if (this.mods.some((m) => m instanceof osu_base_1.ModFlashlight)) {
|
|
74
|
+
flashlightPerformanceValue = Math.pow(this.flashlight, 2) * 25;
|
|
75
|
+
}
|
|
76
|
+
const basePerformanceValue = Math.pow(Math.pow(aimPerformanceValue, 1.1) +
|
|
77
|
+
Math.pow(speedPerformanceValue, 1.1) +
|
|
78
|
+
Math.pow(flashlightPerformanceValue, 1.1), 1 / 1.1);
|
|
79
|
+
if (basePerformanceValue > 1e-5) {
|
|
80
|
+
this.total =
|
|
81
|
+
Math.cbrt(1.12) *
|
|
82
|
+
0.027 *
|
|
83
|
+
(Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformanceValue) +
|
|
84
|
+
4);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
calculateAll() {
|
|
88
|
+
const skills = this.createSkills();
|
|
89
|
+
const isRelax = this.mods.some((m) => m instanceof osu_base_1.ModRelax);
|
|
90
|
+
if (isRelax) {
|
|
91
|
+
// Remove speed skill to prevent overhead
|
|
92
|
+
skills.splice(2, 1);
|
|
93
|
+
}
|
|
94
|
+
this.calculateSkills(...skills);
|
|
95
|
+
const aimSkill = skills[0];
|
|
96
|
+
const aimSkillWithoutSliders = skills[1];
|
|
97
|
+
let speedSkill;
|
|
98
|
+
let flashlightSkill;
|
|
99
|
+
if (isRelax) {
|
|
100
|
+
flashlightSkill = skills[2];
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
speedSkill = skills[2];
|
|
104
|
+
flashlightSkill = skills[3];
|
|
105
|
+
}
|
|
106
|
+
this.strainPeaks.aimWithSliders = aimSkill.strainPeaks;
|
|
107
|
+
this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
|
|
108
|
+
this.aim = this.starValue(aimSkill.difficultyValue());
|
|
109
|
+
if (this.aim) {
|
|
110
|
+
this.attributes.sliderFactor =
|
|
111
|
+
this.starValue(aimSkillWithoutSliders.difficultyValue()) /
|
|
112
|
+
this.aim;
|
|
113
|
+
}
|
|
114
|
+
if (speedSkill) {
|
|
115
|
+
this.strainPeaks.speed = speedSkill.strainPeaks;
|
|
116
|
+
this.speed = this.starValue(speedSkill.difficultyValue());
|
|
117
|
+
}
|
|
118
|
+
this.strainPeaks.flashlight = flashlightSkill.strainPeaks;
|
|
119
|
+
this.flashlight = this.starValue(flashlightSkill.difficultyValue());
|
|
120
|
+
this.calculateTotal();
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Returns a string representative of the class.
|
|
124
|
+
*/
|
|
125
|
+
toString() {
|
|
126
|
+
return (this.total.toFixed(2) +
|
|
127
|
+
" stars (" +
|
|
128
|
+
this.aim.toFixed(2) +
|
|
129
|
+
" aim, " +
|
|
130
|
+
this.speed.toFixed(2) +
|
|
131
|
+
" speed, " +
|
|
132
|
+
this.flashlight.toFixed(2) +
|
|
133
|
+
" flashlight)");
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Creates skills to be calculated.
|
|
137
|
+
*/
|
|
138
|
+
createSkills() {
|
|
139
|
+
return [
|
|
140
|
+
new OsuAim_1.OsuAim(this.mods, true),
|
|
141
|
+
new OsuAim_1.OsuAim(this.mods, false),
|
|
142
|
+
new OsuSpeed_1.OsuSpeed(this.mods, new osu_base_1.OsuHitWindow(this.stats.od).hitWindowFor300()),
|
|
143
|
+
new OsuFlashlight_1.OsuFlashlight(this.mods),
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
exports.OsuStarRating = OsuStarRating;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PerformanceCalculator = void 0;
|
|
4
|
+
const osu_base_1 = require("@rian8337/osu-base");
|
|
5
|
+
/**
|
|
6
|
+
* The base class of performance calculators.
|
|
7
|
+
*/
|
|
8
|
+
class PerformanceCalculator {
|
|
9
|
+
constructor() {
|
|
10
|
+
/**
|
|
11
|
+
* The overall performance value.
|
|
12
|
+
*/
|
|
13
|
+
this.total = 0;
|
|
14
|
+
/**
|
|
15
|
+
* The calculated accuracy.
|
|
16
|
+
*/
|
|
17
|
+
this.computedAccuracy = new osu_base_1.Accuracy({});
|
|
18
|
+
/**
|
|
19
|
+
* The map statistics after applying modifications.
|
|
20
|
+
*/
|
|
21
|
+
this.mapStatistics = new osu_base_1.MapStats();
|
|
22
|
+
/**
|
|
23
|
+
* Penalty for combo breaks.
|
|
24
|
+
*/
|
|
25
|
+
this.comboPenalty = 0;
|
|
26
|
+
/**
|
|
27
|
+
* The amount of misses that are filtered out from sliderbreaks.
|
|
28
|
+
*/
|
|
29
|
+
this.effectiveMissCount = 0;
|
|
30
|
+
/**
|
|
31
|
+
* Nerf factor used for nerfing beatmaps with very likely dropped sliderends.
|
|
32
|
+
*/
|
|
33
|
+
this.sliderNerfFactor = 1;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Calculates the base performance value for of a star rating.
|
|
37
|
+
*/
|
|
38
|
+
baseValue(stars) {
|
|
39
|
+
return Math.pow(5 * Math.max(1, stars / 0.0675) - 4, 3) / 100000;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Processes given parameters for usage in performance calculation.
|
|
43
|
+
*/
|
|
44
|
+
handleParams(params, mode) {
|
|
45
|
+
this.stars = params.stars;
|
|
46
|
+
const maxCombo = this.stars.map.maxCombo;
|
|
47
|
+
const miss = this.computedAccuracy.nmiss;
|
|
48
|
+
const combo = params.combo ?? maxCombo - miss;
|
|
49
|
+
const mod = this.stars.mods;
|
|
50
|
+
const baseAR = this.stars.map.ar;
|
|
51
|
+
const baseOD = this.stars.map.od;
|
|
52
|
+
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
53
|
+
this.comboPenalty = Math.min(Math.pow(combo / maxCombo, 0.8), 1);
|
|
54
|
+
if (params.accPercent instanceof osu_base_1.Accuracy) {
|
|
55
|
+
// Copy into new instance to not modify the original
|
|
56
|
+
this.computedAccuracy = new osu_base_1.Accuracy(params.accPercent);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
this.computedAccuracy = new osu_base_1.Accuracy({
|
|
60
|
+
percent: params.accPercent,
|
|
61
|
+
nobjects: this.stars.objects.length,
|
|
62
|
+
nmiss: params.miss || 0,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (this.stars.mods.some((m) => m instanceof osu_base_1.ModNoFail)) {
|
|
66
|
+
this.finalMultiplier *= Math.max(0.9, 1 - 0.02 * this.computedAccuracy.nmiss);
|
|
67
|
+
}
|
|
68
|
+
if (this.stars.mods.some((m) => m instanceof osu_base_1.ModSpunOut)) {
|
|
69
|
+
this.finalMultiplier *=
|
|
70
|
+
1 -
|
|
71
|
+
Math.pow(this.stars.map.spinners / this.stars.objects.length, 0.85);
|
|
72
|
+
}
|
|
73
|
+
if (this.stars.mods.some((m) => m instanceof osu_base_1.ModRelax)) {
|
|
74
|
+
this.computedAccuracy.nmiss +=
|
|
75
|
+
this.computedAccuracy.n100 + this.computedAccuracy.n50;
|
|
76
|
+
this.finalMultiplier *= 0.6;
|
|
77
|
+
}
|
|
78
|
+
this.effectiveMissCount = this.calculateEffectiveMissCount(combo, maxCombo);
|
|
79
|
+
this.mapStatistics = new osu_base_1.MapStats({
|
|
80
|
+
ar: baseAR,
|
|
81
|
+
od: baseOD,
|
|
82
|
+
mods: mod,
|
|
83
|
+
});
|
|
84
|
+
// We assume 15% of sliders in a beatmap are difficult since there's no way to tell from the performance calculator.
|
|
85
|
+
const estimateDifficultSliders = this.stars.map.sliders * 0.15;
|
|
86
|
+
const estimateSliderEndsDropped = osu_base_1.MathUtils.clamp(Math.min(this.computedAccuracy.n300 +
|
|
87
|
+
this.computedAccuracy.n50 +
|
|
88
|
+
this.computedAccuracy.nmiss, maxCombo - combo), 0, estimateDifficultSliders);
|
|
89
|
+
if (this.stars.map.sliders > 0) {
|
|
90
|
+
this.sliderNerfFactor =
|
|
91
|
+
(1 - this.stars.attributes.sliderFactor) *
|
|
92
|
+
Math.pow(1 -
|
|
93
|
+
estimateSliderEndsDropped /
|
|
94
|
+
estimateDifficultSliders, 3) +
|
|
95
|
+
this.stars.attributes.sliderFactor;
|
|
96
|
+
}
|
|
97
|
+
if (params.stats) {
|
|
98
|
+
this.mapStatistics.ar = params.stats.ar ?? this.mapStatistics.ar;
|
|
99
|
+
this.mapStatistics.isForceAR =
|
|
100
|
+
params.stats.isForceAR ?? this.mapStatistics.isForceAR;
|
|
101
|
+
this.mapStatistics.speedMultiplier =
|
|
102
|
+
params.stats.speedMultiplier ??
|
|
103
|
+
this.mapStatistics.speedMultiplier;
|
|
104
|
+
this.mapStatistics.oldStatistics =
|
|
105
|
+
params.stats.oldStatistics ?? this.mapStatistics.oldStatistics;
|
|
106
|
+
}
|
|
107
|
+
this.mapStatistics.calculate({ mode: mode });
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Calculates the amount of misses + sliderbreaks from combo.
|
|
111
|
+
*/
|
|
112
|
+
calculateEffectiveMissCount(combo, maxCombo) {
|
|
113
|
+
let comboBasedMissCount = 0;
|
|
114
|
+
if (this.stars.map.sliders > 0) {
|
|
115
|
+
const fullComboThreshold = maxCombo - 0.1 * this.stars.map.sliders;
|
|
116
|
+
if (combo < fullComboThreshold) {
|
|
117
|
+
// We're clamping miss count because since it's derived from combo, it can
|
|
118
|
+
// be higher than the amount of objects and that breaks some calculations.
|
|
119
|
+
comboBasedMissCount = Math.min(fullComboThreshold / Math.max(1, combo), this.stars.objects.length);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return Math.max(this.computedAccuracy.nmiss, Math.floor(comboBasedMissCount));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
exports.PerformanceCalculator = PerformanceCalculator;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Skill = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* A bare minimal abstract skill for fully custom skill implementations.
|
|
6
|
+
*/
|
|
7
|
+
class Skill {
|
|
8
|
+
constructor(mods) {
|
|
9
|
+
/**
|
|
10
|
+
* The hitobjects that were processed previously. They can affect the strain values of the following objects.
|
|
11
|
+
*
|
|
12
|
+
* The latest hitobject is at index 0.
|
|
13
|
+
*/
|
|
14
|
+
this.previous = [];
|
|
15
|
+
/**
|
|
16
|
+
* Number of previous hitobjects to keep inside the `previous` array.
|
|
17
|
+
*/
|
|
18
|
+
this.historyLength = 2;
|
|
19
|
+
this.mods = mods;
|
|
20
|
+
}
|
|
21
|
+
processInternal(current) {
|
|
22
|
+
while (this.previous.length > this.historyLength) {
|
|
23
|
+
this.previous.pop();
|
|
24
|
+
}
|
|
25
|
+
this.process(current);
|
|
26
|
+
this.previous.unshift(current);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.Skill = Skill;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StarRating = void 0;
|
|
4
|
+
const osu_base_1 = require("@rian8337/osu-base");
|
|
5
|
+
const DifficultyHitObjectCreator_1 = require("../preprocessing/DifficultyHitObjectCreator");
|
|
6
|
+
/**
|
|
7
|
+
* The base of difficulty calculation.
|
|
8
|
+
*/
|
|
9
|
+
class StarRating {
|
|
10
|
+
constructor() {
|
|
11
|
+
/**
|
|
12
|
+
* The calculated beatmap.
|
|
13
|
+
*/
|
|
14
|
+
this.map = new osu_base_1.Beatmap();
|
|
15
|
+
/**
|
|
16
|
+
* The difficulty objects of the beatmap.
|
|
17
|
+
*/
|
|
18
|
+
this.objects = [];
|
|
19
|
+
/**
|
|
20
|
+
* The modifications applied.
|
|
21
|
+
*/
|
|
22
|
+
this.mods = [];
|
|
23
|
+
/**
|
|
24
|
+
* The total star rating of the beatmap.
|
|
25
|
+
*/
|
|
26
|
+
this.total = 0;
|
|
27
|
+
/**
|
|
28
|
+
* The map statistics of the beatmap after modifications are applied.
|
|
29
|
+
*/
|
|
30
|
+
this.stats = new osu_base_1.MapStats();
|
|
31
|
+
/**
|
|
32
|
+
* The strain peaks of various calculated difficulties.
|
|
33
|
+
*/
|
|
34
|
+
this.strainPeaks = {
|
|
35
|
+
aimWithSliders: [],
|
|
36
|
+
aimWithoutSliders: [],
|
|
37
|
+
speed: [],
|
|
38
|
+
flashlight: [],
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Additional data that is used in performance calculation.
|
|
42
|
+
*/
|
|
43
|
+
this.attributes = {
|
|
44
|
+
speedNoteCount: 0,
|
|
45
|
+
sliderFactor: 1,
|
|
46
|
+
};
|
|
47
|
+
this.sectionLength = 400;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Calculates the star rating of the specified beatmap.
|
|
51
|
+
*
|
|
52
|
+
* The beatmap is analyzed in chunks of `sectionLength` duration.
|
|
53
|
+
* For each chunk the highest hitobject strains are added to
|
|
54
|
+
* a list which is then collapsed into a weighted sum, much
|
|
55
|
+
* like scores are weighted on a user's profile.
|
|
56
|
+
*
|
|
57
|
+
* For subsequent chunks, the initial max strain is calculated
|
|
58
|
+
* by decaying the previous hitobject's strain until the
|
|
59
|
+
* beginning of the new chunk.
|
|
60
|
+
*
|
|
61
|
+
* The first object doesn't generate a strain
|
|
62
|
+
* so we begin calculating from the second object.
|
|
63
|
+
*
|
|
64
|
+
* Also don't forget to manually add the peak strain for the last
|
|
65
|
+
* section which would otherwise be ignored.
|
|
66
|
+
*/
|
|
67
|
+
calculate(params, mode) {
|
|
68
|
+
const map = (this.map = osu_base_1.Utils.deepCopy(params.map));
|
|
69
|
+
const mod = (this.mods = params.mods ?? this.mods);
|
|
70
|
+
this.stats = new osu_base_1.MapStats({
|
|
71
|
+
cs: map.cs,
|
|
72
|
+
ar: map.ar,
|
|
73
|
+
od: map.od,
|
|
74
|
+
hp: map.hp,
|
|
75
|
+
mods: mod,
|
|
76
|
+
speedMultiplier: params.stats?.speedMultiplier ?? 1,
|
|
77
|
+
oldStatistics: params.stats?.oldStatistics ?? false,
|
|
78
|
+
}).calculate({ mode: mode });
|
|
79
|
+
this.generateDifficultyHitObjects(mode);
|
|
80
|
+
this.calculateAll();
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Generates difficulty hitobjects for this calculator.
|
|
85
|
+
*
|
|
86
|
+
* @param mode The gamemode to generate difficulty hitobjects for.
|
|
87
|
+
*/
|
|
88
|
+
generateDifficultyHitObjects(mode) {
|
|
89
|
+
this.objects.length = 0;
|
|
90
|
+
this.objects.push(...new DifficultyHitObjectCreator_1.DifficultyHitObjectCreator().generateDifficultyObjects({
|
|
91
|
+
objects: this.map.objects,
|
|
92
|
+
circleSize: this.stats.cs,
|
|
93
|
+
speedMultiplier: this.stats.speedMultiplier,
|
|
94
|
+
mode: mode,
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Calculates the skills provided.
|
|
99
|
+
*
|
|
100
|
+
* @param skills The skills to calculate.
|
|
101
|
+
*/
|
|
102
|
+
calculateSkills(...skills) {
|
|
103
|
+
this.objects.slice(1).forEach((h, i) => {
|
|
104
|
+
skills.forEach((skill) => {
|
|
105
|
+
skill.processInternal(h);
|
|
106
|
+
if (i === this.objects.length - 2) {
|
|
107
|
+
// Don't forget to save the last strain peak, which would otherwise be ignored.
|
|
108
|
+
skill.saveCurrentPeak();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Calculates the star rating value of a difficulty.
|
|
115
|
+
*
|
|
116
|
+
* @param difficulty The difficulty to calculate.
|
|
117
|
+
*/
|
|
118
|
+
starValue(difficulty) {
|
|
119
|
+
return Math.sqrt(difficulty) * this.difficultyMultiplier;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Calculates the base performance value of a difficulty rating.
|
|
123
|
+
*
|
|
124
|
+
* @param rating The difficulty rating.
|
|
125
|
+
*/
|
|
126
|
+
basePerformanceValue(rating) {
|
|
127
|
+
return Math.pow(5 * Math.max(1, rating / 0.0675) - 4, 3) / 100000;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
exports.StarRating = StarRating;
|