@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.
@@ -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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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;