@rian8337/osu-difficulty-calculator 1.0.4 → 1.2.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/dist/DroidPerformanceCalculator.js +1 -1
- package/dist/DroidStarRating.js +84 -52
- package/dist/base/PerformanceCalculator.js +8 -5
- package/dist/index.js +1 -0
- package/dist/preprocessing/DifficultyHitObject.js +7 -1
- package/dist/skills/DroidFlashlight.js +16 -15
- package/dist/skills/DroidRhythm.js +130 -0
- package/dist/skills/DroidTap.js +5 -109
- package/package.json +3 -3
- package/typings/index.d.ts +97 -2
|
@@ -153,7 +153,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator_1.PerformanceCalc
|
|
|
153
153
|
(0.95 + (od > 0 ? odScaling : -odScaling)) *
|
|
154
154
|
Math.pow((this.computedAccuracy.value(objectCount) +
|
|
155
155
|
relevantAccuracy.value(this.stars.attributes.speedNoteCount)) /
|
|
156
|
-
2, (
|
|
156
|
+
2, (14 - Math.max(od, 2.5)) / 2);
|
|
157
157
|
// Scale the speed value with # of 50s to punish doubletapping.
|
|
158
158
|
this.tap *= Math.pow(0.98, Math.max(0, this.computedAccuracy.n50 - objectCount / 500));
|
|
159
159
|
}
|
package/dist/DroidStarRating.js
CHANGED
|
@@ -6,6 +6,7 @@ const DroidTap_1 = require("./skills/DroidTap");
|
|
|
6
6
|
const StarRating_1 = require("./base/StarRating");
|
|
7
7
|
const DroidFlashlight_1 = require("./skills/DroidFlashlight");
|
|
8
8
|
const osu_base_1 = require("@rian8337/osu-base");
|
|
9
|
+
const DroidRhythm_1 = require("./skills/DroidRhythm");
|
|
9
10
|
/**
|
|
10
11
|
* Difficulty calculator for osu!droid gamemode.
|
|
11
12
|
*/
|
|
@@ -20,6 +21,10 @@ class DroidStarRating extends StarRating_1.StarRating {
|
|
|
20
21
|
* The tap star rating of the beatmap.
|
|
21
22
|
*/
|
|
22
23
|
this.tap = 0;
|
|
24
|
+
/**
|
|
25
|
+
* The rhythm star rating of the beatmap.
|
|
26
|
+
*/
|
|
27
|
+
this.rhythm = 0;
|
|
23
28
|
/**
|
|
24
29
|
* The flashlight star rating of the beatmap.
|
|
25
30
|
*/
|
|
@@ -54,31 +59,23 @@ class DroidStarRating extends StarRating_1.StarRating {
|
|
|
54
59
|
const aimSkill = new DroidAim_1.DroidAim(this.mods, true);
|
|
55
60
|
const aimSkillWithoutSliders = new DroidAim_1.DroidAim(this.mods, false);
|
|
56
61
|
this.calculateSkills(aimSkill, aimSkillWithoutSliders);
|
|
57
|
-
this.
|
|
58
|
-
this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
|
|
59
|
-
this.aim = this.starValue(aimSkill.difficultyValue());
|
|
60
|
-
if (this.aim) {
|
|
61
|
-
this.attributes.sliderFactor =
|
|
62
|
-
this.starValue(aimSkillWithoutSliders.difficultyValue()) /
|
|
63
|
-
this.aim;
|
|
64
|
-
}
|
|
62
|
+
this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
|
|
65
63
|
}
|
|
66
64
|
/**
|
|
67
65
|
* Calculates the speed star rating of the beatmap and stores it in this instance.
|
|
68
66
|
*/
|
|
69
67
|
calculateTap() {
|
|
70
|
-
if (this.mods.some((m) => m instanceof osu_base_1.ModRelax)) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
68
|
const tapSkill = new DroidTap_1.DroidTap(this.mods, this.stats.od);
|
|
74
69
|
this.calculateSkills(tapSkill);
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
70
|
+
this.postCalculateTap(tapSkill);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Calculates the rhythm star rating of the beatmap and stores it in this instance.
|
|
74
|
+
*/
|
|
75
|
+
calculateRhythm() {
|
|
76
|
+
const rhythmSkill = new DroidRhythm_1.DroidRhythm(this.mods, this.stats.od);
|
|
77
|
+
this.calculateSkills(rhythmSkill);
|
|
78
|
+
this.postCalculateRhythm(rhythmSkill);
|
|
82
79
|
}
|
|
83
80
|
/**
|
|
84
81
|
* Calculates the flashlight star rating of the beatmap and stores it in this instance.
|
|
@@ -86,17 +83,16 @@ class DroidStarRating extends StarRating_1.StarRating {
|
|
|
86
83
|
calculateFlashlight() {
|
|
87
84
|
const flashlightSkill = new DroidFlashlight_1.DroidFlashlight(this.mods);
|
|
88
85
|
this.calculateSkills(flashlightSkill);
|
|
89
|
-
this.
|
|
90
|
-
this.flashlight = this.starValue(flashlightSkill.difficultyValue());
|
|
86
|
+
this.postCalculateFlashlight(flashlightSkill);
|
|
91
87
|
}
|
|
92
88
|
calculateTotal() {
|
|
93
89
|
const aimPerformanceValue = this.basePerformanceValue(this.aim);
|
|
94
|
-
const
|
|
90
|
+
const tapPerformanceValue = this.basePerformanceValue(this.tap);
|
|
95
91
|
const flashlightPerformanceValue = this.mods.some((m) => m instanceof osu_base_1.ModFlashlight)
|
|
96
92
|
? Math.pow(this.flashlight, 2) * 25
|
|
97
93
|
: 0;
|
|
98
94
|
const basePerformanceValue = Math.pow(Math.pow(aimPerformanceValue, 1.1) +
|
|
99
|
-
Math.pow(
|
|
95
|
+
Math.pow(tapPerformanceValue, 1.1) +
|
|
100
96
|
Math.pow(flashlightPerformanceValue, 1.1), 1 / 1.1);
|
|
101
97
|
if (basePerformanceValue > 1e-5) {
|
|
102
98
|
this.total =
|
|
@@ -109,42 +105,21 @@ class DroidStarRating extends StarRating_1.StarRating {
|
|
|
109
105
|
calculateAll() {
|
|
110
106
|
const skills = this.createSkills();
|
|
111
107
|
const isRelax = this.mods.some((m) => m instanceof osu_base_1.ModRelax);
|
|
112
|
-
if (isRelax) {
|
|
113
|
-
// Remove speed skill to prevent overhead
|
|
114
|
-
skills.splice(2, 1);
|
|
115
|
-
}
|
|
116
108
|
this.calculateSkills(...skills);
|
|
117
109
|
const aimSkill = skills[0];
|
|
118
110
|
const aimSkillWithoutSliders = skills[1];
|
|
119
|
-
|
|
120
|
-
|
|
111
|
+
const rhythmSkill = skills[2];
|
|
112
|
+
const tapSkill = skills[3];
|
|
113
|
+
const flashlightSkill = skills[4];
|
|
114
|
+
this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
|
|
121
115
|
if (!isRelax) {
|
|
122
|
-
tapSkill
|
|
123
|
-
flashlightSkill = skills[3];
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
flashlightSkill = skills[2];
|
|
127
|
-
}
|
|
128
|
-
this.strainPeaks.aimWithSliders = aimSkill.strainPeaks;
|
|
129
|
-
this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
|
|
130
|
-
this.aim = this.starValue(aimSkill.difficultyValue());
|
|
131
|
-
if (this.aim) {
|
|
132
|
-
this.attributes.sliderFactor =
|
|
133
|
-
this.starValue(aimSkillWithoutSliders.difficultyValue()) /
|
|
134
|
-
this.aim;
|
|
116
|
+
this.postCalculateTap(tapSkill);
|
|
135
117
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
this.
|
|
139
|
-
const objectStrains = this.objects.map((v) => v.tapStrain);
|
|
140
|
-
const maxStrain = Math.max(...objectStrains);
|
|
141
|
-
if (maxStrain) {
|
|
142
|
-
this.attributes.speedNoteCount = objectStrains.reduce((total, next) => total +
|
|
143
|
-
1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
|
|
144
|
-
}
|
|
118
|
+
this.calculateSpeedNoteCount();
|
|
119
|
+
if (!isRelax) {
|
|
120
|
+
this.postCalculateRhythm(rhythmSkill);
|
|
145
121
|
}
|
|
146
|
-
this.
|
|
147
|
-
this.flashlight = this.starValue(flashlightSkill.difficultyValue());
|
|
122
|
+
this.postCalculateFlashlight(flashlightSkill);
|
|
148
123
|
this.calculateTotal();
|
|
149
124
|
}
|
|
150
125
|
/**
|
|
@@ -157,6 +132,8 @@ class DroidStarRating extends StarRating_1.StarRating {
|
|
|
157
132
|
" aim, " +
|
|
158
133
|
this.tap.toFixed(2) +
|
|
159
134
|
" tap, " +
|
|
135
|
+
this.rhythm.toFixed(2) +
|
|
136
|
+
" rhythm, " +
|
|
160
137
|
this.flashlight.toFixed(2) +
|
|
161
138
|
" flashlight)");
|
|
162
139
|
}
|
|
@@ -167,9 +144,64 @@ class DroidStarRating extends StarRating_1.StarRating {
|
|
|
167
144
|
return [
|
|
168
145
|
new DroidAim_1.DroidAim(this.mods, true),
|
|
169
146
|
new DroidAim_1.DroidAim(this.mods, false),
|
|
147
|
+
// Tap skill depends on rhythm skill, so we put it first
|
|
148
|
+
new DroidRhythm_1.DroidRhythm(this.mods, this.stats.od),
|
|
170
149
|
new DroidTap_1.DroidTap(this.mods, this.stats.od),
|
|
171
150
|
new DroidFlashlight_1.DroidFlashlight(this.mods),
|
|
172
151
|
];
|
|
173
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Called after aim skill calculation.
|
|
155
|
+
*
|
|
156
|
+
* @param aimSkill The aim skill that considers sliders.
|
|
157
|
+
* @param aimSkillWithoutSliders The aim skill that doesn't consider sliders.
|
|
158
|
+
*/
|
|
159
|
+
postCalculateAim(aimSkill, aimSkillWithoutSliders) {
|
|
160
|
+
this.strainPeaks.aimWithSliders = aimSkill.strainPeaks;
|
|
161
|
+
this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
|
|
162
|
+
this.aim = this.starValue(aimSkill.difficultyValue());
|
|
163
|
+
if (this.aim) {
|
|
164
|
+
this.attributes.sliderFactor =
|
|
165
|
+
this.starValue(aimSkillWithoutSliders.difficultyValue()) /
|
|
166
|
+
this.aim;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Called after tap skill calculation.
|
|
171
|
+
*
|
|
172
|
+
* @param tapSkill The tap skill.
|
|
173
|
+
*/
|
|
174
|
+
postCalculateTap(tapSkill) {
|
|
175
|
+
this.strainPeaks.speed = tapSkill.strainPeaks;
|
|
176
|
+
this.tap = this.starValue(tapSkill.difficultyValue());
|
|
177
|
+
this.calculateSpeedNoteCount();
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Calculates the speed note count attribute.
|
|
181
|
+
*/
|
|
182
|
+
calculateSpeedNoteCount() {
|
|
183
|
+
const objectStrains = this.objects.map((v) => v.tapStrain);
|
|
184
|
+
const maxStrain = Math.max(...objectStrains);
|
|
185
|
+
if (maxStrain) {
|
|
186
|
+
this.attributes.speedNoteCount = objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Called after rhythm skill calculation.
|
|
191
|
+
*
|
|
192
|
+
* @param rhythmSkill The rhythm skill.
|
|
193
|
+
*/
|
|
194
|
+
postCalculateRhythm(rhythmSkill) {
|
|
195
|
+
this.rhythm = this.starValue(rhythmSkill.difficultyValue());
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Called after flashlight skill calculation.
|
|
199
|
+
*
|
|
200
|
+
* @param flashlightSkill The flashlight skill.
|
|
201
|
+
*/
|
|
202
|
+
postCalculateFlashlight(flashlightSkill) {
|
|
203
|
+
this.strainPeaks.flashlight = flashlightSkill.strainPeaks;
|
|
204
|
+
this.flashlight = this.starValue(flashlightSkill.difficultyValue());
|
|
205
|
+
}
|
|
174
206
|
}
|
|
175
207
|
exports.DroidStarRating = DroidStarRating;
|
|
@@ -62,8 +62,9 @@ class PerformanceCalculator {
|
|
|
62
62
|
nmiss: params.miss || 0,
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
+
this.effectiveMissCount = this.calculateEffectiveMissCount(combo, maxCombo);
|
|
65
66
|
if (this.stars.mods.some((m) => m instanceof osu_base_1.ModNoFail)) {
|
|
66
|
-
this.finalMultiplier *= Math.max(0.9, 1 - 0.02 * this.
|
|
67
|
+
this.finalMultiplier *= Math.max(0.9, 1 - 0.02 * this.effectiveMissCount);
|
|
67
68
|
}
|
|
68
69
|
if (this.stars.mods.some((m) => m instanceof osu_base_1.ModSpunOut)) {
|
|
69
70
|
this.finalMultiplier *=
|
|
@@ -71,11 +72,13 @@ class PerformanceCalculator {
|
|
|
71
72
|
Math.pow(this.stars.map.spinners / this.stars.objects.length, 0.85);
|
|
72
73
|
}
|
|
73
74
|
if (this.stars.mods.some((m) => m instanceof osu_base_1.ModRelax)) {
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
// As we're adding 100s and 50s to an approximated number of combo breaks, the result can be higher
|
|
76
|
+
// than total hits in specific scenarios (which breaks some calculations), so we need to clamp it.
|
|
77
|
+
this.effectiveMissCount = Math.min(this.effectiveMissCount +
|
|
78
|
+
this.computedAccuracy.n100 +
|
|
79
|
+
this.computedAccuracy.n50, this.stars.objects.length);
|
|
76
80
|
this.finalMultiplier *= 0.6;
|
|
77
81
|
}
|
|
78
|
-
this.effectiveMissCount = this.calculateEffectiveMissCount(combo, maxCombo);
|
|
79
82
|
this.mapStatistics = new osu_base_1.MapStats({
|
|
80
83
|
ar: baseAR,
|
|
81
84
|
od: baseOD,
|
|
@@ -119,7 +122,7 @@ class PerformanceCalculator {
|
|
|
119
122
|
comboBasedMissCount = Math.min(fullComboThreshold / Math.max(1, combo), this.stars.objects.length);
|
|
120
123
|
}
|
|
121
124
|
}
|
|
122
|
-
return Math.max(this.computedAccuracy.nmiss,
|
|
125
|
+
return Math.max(this.computedAccuracy.nmiss, comboBasedMissCount);
|
|
123
126
|
}
|
|
124
127
|
}
|
|
125
128
|
exports.PerformanceCalculator = PerformanceCalculator;
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ __exportStar(require("./preprocessing/DifficultyHitObjectCreator"), exports);
|
|
|
15
15
|
__exportStar(require("./skills/DroidAim"), exports);
|
|
16
16
|
__exportStar(require("./skills/DroidFlashlight"), exports);
|
|
17
17
|
__exportStar(require("./DroidPerformanceCalculator"), exports);
|
|
18
|
+
__exportStar(require("./skills/DroidRhythm"), exports);
|
|
18
19
|
__exportStar(require("./DroidStarRating"), exports);
|
|
19
20
|
__exportStar(require("./skills/DroidTap"), exports);
|
|
20
21
|
__exportStar(require("./MapStars"), exports);
|
|
@@ -19,6 +19,8 @@ class DifficultyHitObject {
|
|
|
19
19
|
this.aimStrainWithoutSliders = 0;
|
|
20
20
|
/**
|
|
21
21
|
* The tap strain generated by the hitobject.
|
|
22
|
+
*
|
|
23
|
+
* This is also used for osu!standard as opposed to "speed strain".
|
|
22
24
|
*/
|
|
23
25
|
this.tapStrain = 0;
|
|
24
26
|
/**
|
|
@@ -27,9 +29,13 @@ class DifficultyHitObject {
|
|
|
27
29
|
*/
|
|
28
30
|
this.originalTapStrain = 0;
|
|
29
31
|
/**
|
|
30
|
-
* The rhythm multiplier generated by the hitobject.
|
|
32
|
+
* The rhythm multiplier generated by the hitobject. This is used to alter tap strain.
|
|
31
33
|
*/
|
|
32
34
|
this.rhythmMultiplier = 0;
|
|
35
|
+
/**
|
|
36
|
+
* The rhythm strain generated by the hitobject.
|
|
37
|
+
*/
|
|
38
|
+
this.rhythmStrain = 0;
|
|
33
39
|
/**
|
|
34
40
|
* The flashlight strain generated by the hitobject.
|
|
35
41
|
*/
|
|
@@ -10,7 +10,7 @@ class DroidFlashlight extends DroidSkill_1.DroidSkill {
|
|
|
10
10
|
constructor() {
|
|
11
11
|
super(...arguments);
|
|
12
12
|
this.historyLength = 10;
|
|
13
|
-
this.skillMultiplier = 0.
|
|
13
|
+
this.skillMultiplier = 0.07;
|
|
14
14
|
this.strainDecayBase = 0.15;
|
|
15
15
|
this.reducedSectionCount = 10;
|
|
16
16
|
this.reducedSectionBaseline = 0.75;
|
|
@@ -24,22 +24,23 @@ class DroidFlashlight extends DroidSkill_1.DroidSkill {
|
|
|
24
24
|
let smallDistNerf = 1;
|
|
25
25
|
let cumulativeStrainTime = 0;
|
|
26
26
|
let result = 0;
|
|
27
|
+
let last = current;
|
|
27
28
|
for (let i = 0; i < this.previous.length; ++i) {
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
30
|
-
|
|
29
|
+
const currentObject = this.previous[i];
|
|
30
|
+
if (!(currentObject.object instanceof osu_base_1.Spinner)) {
|
|
31
|
+
const jumpDistance = current.object.stackedPosition.subtract(currentObject.object.endPosition).length;
|
|
32
|
+
cumulativeStrainTime += last.strainTime;
|
|
33
|
+
// We want to nerf objects that can be easily seen within the Flashlight circle radius.
|
|
34
|
+
if (i === 0) {
|
|
35
|
+
smallDistNerf = Math.min(1, jumpDistance / 75);
|
|
36
|
+
}
|
|
37
|
+
// We also want to nerf stacks so that only the first object of the stack is accounted for.
|
|
38
|
+
const stackNerf = Math.min(1, currentObject.lazyJumpDistance / scalingFactor / 25);
|
|
39
|
+
result +=
|
|
40
|
+
(stackNerf * scalingFactor * jumpDistance) /
|
|
41
|
+
cumulativeStrainTime;
|
|
31
42
|
}
|
|
32
|
-
|
|
33
|
-
cumulativeStrainTime += previous.strainTime;
|
|
34
|
-
// We want to nerf objects that can be easily seen within the Flashlight circle radius.
|
|
35
|
-
if (i === 0) {
|
|
36
|
-
smallDistNerf = Math.min(1, jumpDistance / 75);
|
|
37
|
-
}
|
|
38
|
-
// We also want to nerf stacks so that only the first object of the stack is accounted for.
|
|
39
|
-
const stackNerf = Math.min(1, previous.lazyJumpDistance / scalingFactor / 25);
|
|
40
|
-
result +=
|
|
41
|
-
(Math.pow(0.8, i) * stackNerf * scalingFactor * jumpDistance) /
|
|
42
|
-
cumulativeStrainTime;
|
|
43
|
+
last = currentObject;
|
|
43
44
|
}
|
|
44
45
|
return Math.pow(smallDistNerf * result, 2);
|
|
45
46
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DroidRhythm = void 0;
|
|
4
|
+
const osu_base_1 = require("@rian8337/osu-base");
|
|
5
|
+
const DroidSkill_1 = require("./DroidSkill");
|
|
6
|
+
/**
|
|
7
|
+
* Represents the skill required to properly follow a beatmap's rhythm.
|
|
8
|
+
*/
|
|
9
|
+
class DroidRhythm extends DroidSkill_1.DroidSkill {
|
|
10
|
+
constructor(mods, overallDifficulty) {
|
|
11
|
+
super(mods);
|
|
12
|
+
this.historyLength = 32;
|
|
13
|
+
this.skillMultiplier = 1;
|
|
14
|
+
this.reducedSectionCount = 5;
|
|
15
|
+
this.reducedSectionBaseline = 0.75;
|
|
16
|
+
this.strainDecayBase = 0.3;
|
|
17
|
+
this.starsPerDouble = 2;
|
|
18
|
+
this.rhythmMultiplier = 0.75;
|
|
19
|
+
this.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
|
|
20
|
+
this.currentRhythm = 1;
|
|
21
|
+
this.hitWindow = new osu_base_1.OsuHitWindow(overallDifficulty);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current object.
|
|
25
|
+
*/
|
|
26
|
+
calculateRhythmBonus(current) {
|
|
27
|
+
if (current.object instanceof osu_base_1.Spinner) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
let previousIslandSize = 0;
|
|
31
|
+
let rhythmComplexitySum = 0;
|
|
32
|
+
let islandSize = 1;
|
|
33
|
+
// Store the ratio of the current start of an island to buff for tighter rhythms.
|
|
34
|
+
let startRatio = 0;
|
|
35
|
+
let firstDeltaSwitch = false;
|
|
36
|
+
let rhythmStart = 0;
|
|
37
|
+
while (rhythmStart < this.previous.length - 2 &&
|
|
38
|
+
current.startTime - this.previous[rhythmStart].startTime <
|
|
39
|
+
this.historyTimeMax) {
|
|
40
|
+
++rhythmStart;
|
|
41
|
+
}
|
|
42
|
+
for (let i = rhythmStart; i > 0; --i) {
|
|
43
|
+
// Scale note 0 to 1 from history to now.
|
|
44
|
+
let currentHistoricalDecay = (this.historyTimeMax -
|
|
45
|
+
(current.startTime - this.previous[i - 1].startTime)) /
|
|
46
|
+
this.historyTimeMax;
|
|
47
|
+
// Either we're limited by time or limited by object count.
|
|
48
|
+
currentHistoricalDecay = Math.min(currentHistoricalDecay, (this.previous.length - i) / this.previous.length);
|
|
49
|
+
const currentDelta = this.previous[i - 1].strainTime;
|
|
50
|
+
const prevDelta = this.previous[i].strainTime;
|
|
51
|
+
const lastDelta = this.previous[i + 1].strainTime;
|
|
52
|
+
const currentRatio = 1 +
|
|
53
|
+
6 *
|
|
54
|
+
Math.min(0.5, Math.pow(Math.sin(Math.PI /
|
|
55
|
+
(Math.min(prevDelta, currentDelta) /
|
|
56
|
+
Math.max(prevDelta, currentDelta))), 2));
|
|
57
|
+
const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) -
|
|
58
|
+
this.hitWindow.hitWindowFor300() * 0.6) /
|
|
59
|
+
(this.hitWindow.hitWindowFor300() * 0.6));
|
|
60
|
+
let effectiveRatio = windowPenalty * currentRatio;
|
|
61
|
+
if (firstDeltaSwitch) {
|
|
62
|
+
if (prevDelta <= 1.25 * currentDelta &&
|
|
63
|
+
prevDelta * 1.25 >= currentDelta) {
|
|
64
|
+
// Island is still progressing, count size.
|
|
65
|
+
if (islandSize < 7) {
|
|
66
|
+
++islandSize;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
if (this.previous[i - 1].object instanceof osu_base_1.Slider) {
|
|
71
|
+
// BPM change is into slider, this is easy acc window.
|
|
72
|
+
effectiveRatio /= 8;
|
|
73
|
+
}
|
|
74
|
+
if (this.previous[i].object instanceof osu_base_1.Slider) {
|
|
75
|
+
// BPM change was from a slider, this is typically easier than circle -> circle.
|
|
76
|
+
effectiveRatio /= 4;
|
|
77
|
+
}
|
|
78
|
+
if (previousIslandSize === islandSize) {
|
|
79
|
+
// Repeated island size (ex: triplet -> triplet).
|
|
80
|
+
effectiveRatio /= 4;
|
|
81
|
+
}
|
|
82
|
+
if (previousIslandSize % 2 === islandSize % 2) {
|
|
83
|
+
// Repeated island polarity (2 -> 4, 3 -> 5).
|
|
84
|
+
effectiveRatio /= 2;
|
|
85
|
+
}
|
|
86
|
+
if (lastDelta > prevDelta + 10 &&
|
|
87
|
+
prevDelta > currentDelta + 10) {
|
|
88
|
+
// Previous increase happened a note ago.
|
|
89
|
+
// Albeit this is a 1/1 -> 1/2-1/4 type of transition, we don't want to buff this.
|
|
90
|
+
effectiveRatio /= 8;
|
|
91
|
+
}
|
|
92
|
+
rhythmComplexitySum +=
|
|
93
|
+
(((Math.sqrt(effectiveRatio * startRatio) *
|
|
94
|
+
currentHistoricalDecay *
|
|
95
|
+
Math.sqrt(4 + islandSize)) /
|
|
96
|
+
2) *
|
|
97
|
+
Math.sqrt(4 + previousIslandSize)) /
|
|
98
|
+
2;
|
|
99
|
+
startRatio = effectiveRatio;
|
|
100
|
+
previousIslandSize = islandSize;
|
|
101
|
+
if (prevDelta * 1.25 < currentDelta) {
|
|
102
|
+
// We're slowing down, stop counting.
|
|
103
|
+
// If we're speeding up, this stays as is and we keep counting island size.
|
|
104
|
+
firstDeltaSwitch = false;
|
|
105
|
+
}
|
|
106
|
+
islandSize = 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (prevDelta > 1.25 * currentDelta) {
|
|
110
|
+
// We want to be speeding up.
|
|
111
|
+
// Begin counting island until we change speed again.
|
|
112
|
+
firstDeltaSwitch = true;
|
|
113
|
+
startRatio = effectiveRatio;
|
|
114
|
+
islandSize = 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return Math.sqrt(4 + rhythmComplexitySum * this.rhythmMultiplier) / 2;
|
|
118
|
+
}
|
|
119
|
+
strainValueAt(current) {
|
|
120
|
+
this.currentRhythm = this.calculateRhythmBonus(current);
|
|
121
|
+
this.currentStrain *= this.strainDecay(current.deltaTime);
|
|
122
|
+
this.currentStrain += this.currentRhythm - 1;
|
|
123
|
+
return this.currentStrain;
|
|
124
|
+
}
|
|
125
|
+
saveToHitObject(current) {
|
|
126
|
+
current.rhythmStrain = this.currentStrain;
|
|
127
|
+
current.rhythmMultiplier = this.currentRhythm;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
exports.DroidRhythm = DroidRhythm;
|
package/dist/skills/DroidTap.js
CHANGED
|
@@ -19,11 +19,7 @@ class DroidTap extends DroidSkill_1.DroidSkill {
|
|
|
19
19
|
this.minSpeedBonus = 75;
|
|
20
20
|
this.currentTapStrain = 0;
|
|
21
21
|
this.currentOriginalTapStrain = 0;
|
|
22
|
-
this.
|
|
23
|
-
this.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
|
|
24
|
-
this.currentRhythm = 1;
|
|
25
|
-
this.overallDifficulty = overallDifficulty;
|
|
26
|
-
this.hitWindow = new osu_base_1.OsuHitWindow(this.overallDifficulty);
|
|
22
|
+
this.hitWindow = new osu_base_1.OsuHitWindow(overallDifficulty);
|
|
27
23
|
}
|
|
28
24
|
/**
|
|
29
25
|
* @param current The hitobject to calculate.
|
|
@@ -41,10 +37,8 @@ class DroidTap extends DroidSkill_1.DroidSkill {
|
|
|
41
37
|
strainTime = osu_base_1.Interpolation.lerp(this.previous[0].strainTime, strainTime, strainTime / greatWindowFull);
|
|
42
38
|
}
|
|
43
39
|
// Cap deltatime to the OD 300 hitwindow.
|
|
44
|
-
//
|
|
45
|
-
strainTime /= osu_base_1.MathUtils.clamp(strainTime /
|
|
46
|
-
new osu_base_1.OsuHitWindow(this.overallDifficulty - 21.5).hitWindowFor300() /
|
|
47
|
-
0.35, 0.9, 1);
|
|
40
|
+
// 0.58 is derived from making sure 260 BPM 1/4 OD5 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
|
41
|
+
strainTime /= osu_base_1.MathUtils.clamp(strainTime / greatWindowFull / 0.58, 0.92, 1);
|
|
48
42
|
let speedBonus = 1;
|
|
49
43
|
if (strainTime < this.minSpeedBonus) {
|
|
50
44
|
speedBonus +=
|
|
@@ -57,7 +51,6 @@ class DroidTap extends DroidSkill_1.DroidSkill {
|
|
|
57
51
|
Math.pow((this.minSpeedBonus - current.strainTime) / 40, 2);
|
|
58
52
|
}
|
|
59
53
|
const decay = this.strainDecay(current.deltaTime);
|
|
60
|
-
this.currentRhythm = this.calculateRhythmBonus(current);
|
|
61
54
|
this.currentTapStrain *= decay;
|
|
62
55
|
this.currentTapStrain +=
|
|
63
56
|
this.tapStrainOf(speedBonus, strainTime) * this.skillMultiplier;
|
|
@@ -65,103 +58,7 @@ class DroidTap extends DroidSkill_1.DroidSkill {
|
|
|
65
58
|
this.currentOriginalTapStrain +=
|
|
66
59
|
this.tapStrainOf(originalSpeedBonus, current.strainTime) *
|
|
67
60
|
this.skillMultiplier;
|
|
68
|
-
return this.currentTapStrain *
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current object.
|
|
72
|
-
*/
|
|
73
|
-
calculateRhythmBonus(current) {
|
|
74
|
-
if (current.object instanceof osu_base_1.Spinner) {
|
|
75
|
-
return 0;
|
|
76
|
-
}
|
|
77
|
-
let previousIslandSize = 0;
|
|
78
|
-
let rhythmComplexitySum = 0;
|
|
79
|
-
let islandSize = 1;
|
|
80
|
-
// Store the ratio of the current start of an island to buff for tighter rhythms.
|
|
81
|
-
let startRatio = 0;
|
|
82
|
-
let firstDeltaSwitch = false;
|
|
83
|
-
let rhythmStart = 0;
|
|
84
|
-
while (rhythmStart < this.previous.length - 2 &&
|
|
85
|
-
current.startTime - this.previous[rhythmStart].startTime <
|
|
86
|
-
this.historyTimeMax) {
|
|
87
|
-
++rhythmStart;
|
|
88
|
-
}
|
|
89
|
-
for (let i = rhythmStart; i > 0; --i) {
|
|
90
|
-
// Scale note 0 to 1 from history to now.
|
|
91
|
-
let currentHistoricalDecay = (this.historyTimeMax -
|
|
92
|
-
(current.startTime - this.previous[i - 1].startTime)) /
|
|
93
|
-
this.historyTimeMax;
|
|
94
|
-
// Either we're limited by time or limited by object count.
|
|
95
|
-
currentHistoricalDecay = Math.min(currentHistoricalDecay, (this.previous.length - i) / this.previous.length);
|
|
96
|
-
const currentDelta = this.previous[i - 1].strainTime;
|
|
97
|
-
const prevDelta = this.previous[i].strainTime;
|
|
98
|
-
const lastDelta = this.previous[i + 1].strainTime;
|
|
99
|
-
const currentRatio = 1 +
|
|
100
|
-
6 *
|
|
101
|
-
Math.min(0.5, Math.pow(Math.sin(Math.PI /
|
|
102
|
-
(Math.min(prevDelta, currentDelta) /
|
|
103
|
-
Math.max(prevDelta, currentDelta))), 2));
|
|
104
|
-
const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) -
|
|
105
|
-
this.hitWindow.hitWindowFor300() * 0.6) /
|
|
106
|
-
(this.hitWindow.hitWindowFor300() * 0.6));
|
|
107
|
-
let effectiveRatio = windowPenalty * currentRatio;
|
|
108
|
-
if (firstDeltaSwitch) {
|
|
109
|
-
if (prevDelta <= 1.25 * currentDelta &&
|
|
110
|
-
prevDelta * 1.25 >= currentDelta) {
|
|
111
|
-
// Island is still progressing, count size.
|
|
112
|
-
if (islandSize < 7) {
|
|
113
|
-
++islandSize;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
if (this.previous[i - 1].object instanceof osu_base_1.Slider) {
|
|
118
|
-
// BPM change is into slider, this is easy acc window.
|
|
119
|
-
effectiveRatio /= 8;
|
|
120
|
-
}
|
|
121
|
-
if (this.previous[i].object instanceof osu_base_1.Slider) {
|
|
122
|
-
// BPM change was from a slider, this is typically easier than circle -> circle.
|
|
123
|
-
effectiveRatio /= 4;
|
|
124
|
-
}
|
|
125
|
-
if (previousIslandSize === islandSize) {
|
|
126
|
-
// Repeated island size (ex: triplet -> triplet).
|
|
127
|
-
effectiveRatio /= 4;
|
|
128
|
-
}
|
|
129
|
-
if (previousIslandSize % 2 === islandSize % 2) {
|
|
130
|
-
// Repeated island polarity (2 -> 4, 3 -> 5).
|
|
131
|
-
effectiveRatio /= 2;
|
|
132
|
-
}
|
|
133
|
-
if (lastDelta > prevDelta + 10 &&
|
|
134
|
-
prevDelta > currentDelta + 10) {
|
|
135
|
-
// Previous increase happened a note ago.
|
|
136
|
-
// Albeit this is a 1/1 -> 1/2-1/4 type of transition, we don't want to buff this.
|
|
137
|
-
effectiveRatio /= 8;
|
|
138
|
-
}
|
|
139
|
-
rhythmComplexitySum +=
|
|
140
|
-
(((Math.sqrt(effectiveRatio * startRatio) *
|
|
141
|
-
currentHistoricalDecay *
|
|
142
|
-
Math.sqrt(4 + islandSize)) /
|
|
143
|
-
2) *
|
|
144
|
-
Math.sqrt(4 + previousIslandSize)) /
|
|
145
|
-
2;
|
|
146
|
-
startRatio = effectiveRatio;
|
|
147
|
-
previousIslandSize = islandSize;
|
|
148
|
-
if (prevDelta * 1.25 < currentDelta) {
|
|
149
|
-
// We're slowing down, stop counting.
|
|
150
|
-
// If we're speeding up, this stays as is and we keep counting island size.
|
|
151
|
-
firstDeltaSwitch = false;
|
|
152
|
-
}
|
|
153
|
-
islandSize = 1;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
else if (prevDelta > 1.25 * currentDelta) {
|
|
157
|
-
// We want to be speeding up.
|
|
158
|
-
// Begin counting island until we change speed again.
|
|
159
|
-
firstDeltaSwitch = true;
|
|
160
|
-
startRatio = effectiveRatio;
|
|
161
|
-
islandSize = 1;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return Math.sqrt(4 + rhythmComplexitySum * this.rhythmMultiplier) / 2;
|
|
61
|
+
return this.currentTapStrain * current.rhythmMultiplier;
|
|
165
62
|
}
|
|
166
63
|
/**
|
|
167
64
|
* Calculates the tap strain of a hitobject given a specific speed bonus and strain time.
|
|
@@ -180,9 +77,8 @@ class DroidTap extends DroidSkill_1.DroidSkill {
|
|
|
180
77
|
*/
|
|
181
78
|
saveToHitObject(current) {
|
|
182
79
|
current.tapStrain = this.currentStrain;
|
|
183
|
-
current.rhythmMultiplier = this.currentRhythm;
|
|
184
80
|
current.originalTapStrain =
|
|
185
|
-
this.currentOriginalTapStrain *
|
|
81
|
+
this.currentOriginalTapStrain * current.rhythmMultiplier;
|
|
186
82
|
}
|
|
187
83
|
}
|
|
188
84
|
exports.DroidTap = DroidTap;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rian8337/osu-difficulty-calculator",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A module for calculating osu!standard beatmap difficulty and performance value with respect to the current difficulty and performance algorithm.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"osu",
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"url": "https://github.com/Rian8337/osu-droid-module/issues"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@rian8337/osu-base": "^1.0.
|
|
33
|
+
"@rian8337/osu-base": "^1.0.5"
|
|
34
34
|
},
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "745e369ae1c5895e7a58188cb522eaecac618966"
|
|
39
39
|
}
|
package/typings/index.d.ts
CHANGED
|
@@ -25,13 +25,14 @@ declare module "@rian8337/osu-difficulty-calculator" {
|
|
|
25
25
|
* The aim strain generated by the hitobject if sliders are considered.
|
|
26
26
|
*/
|
|
27
27
|
aimStrainWithSliders: number;
|
|
28
|
-
|
|
29
28
|
/**
|
|
30
29
|
* The aim strain generated by the hitobject if sliders are not considered.
|
|
31
30
|
*/
|
|
32
31
|
aimStrainWithoutSliders: number;
|
|
33
32
|
/**
|
|
34
33
|
* The tap strain generated by the hitobject.
|
|
34
|
+
*
|
|
35
|
+
* This is also used for osu!standard as opposed to "speed strain".
|
|
35
36
|
*/
|
|
36
37
|
tapStrain: number;
|
|
37
38
|
/**
|
|
@@ -40,9 +41,13 @@ declare module "@rian8337/osu-difficulty-calculator" {
|
|
|
40
41
|
*/
|
|
41
42
|
originalTapStrain: number;
|
|
42
43
|
/**
|
|
43
|
-
* The rhythm multiplier generated by the hitobject.
|
|
44
|
+
* The rhythm multiplier generated by the hitobject. This is used to alter tap strain.
|
|
44
45
|
*/
|
|
45
46
|
rhythmMultiplier: number;
|
|
47
|
+
/**
|
|
48
|
+
* The rhythm strain generated by the hitobject.
|
|
49
|
+
*/
|
|
50
|
+
rhythmStrain: number;
|
|
46
51
|
/**
|
|
47
52
|
* The flashlight strain generated by the hitobject.
|
|
48
53
|
*/
|
|
@@ -277,6 +282,29 @@ declare module "@rian8337/osu-difficulty-calculator" {
|
|
|
277
282
|
override toString(): string;
|
|
278
283
|
}
|
|
279
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Represents the skill required to properly follow a beatmap's rhythm.
|
|
287
|
+
*/
|
|
288
|
+
export class DroidRhythm extends DroidSkill {
|
|
289
|
+
protected override readonly historyLength: number;
|
|
290
|
+
protected override readonly skillMultiplier: number;
|
|
291
|
+
protected override readonly reducedSectionCount: number;
|
|
292
|
+
protected override readonly reducedSectionBaseline: number;
|
|
293
|
+
protected override readonly strainDecayBase: number;
|
|
294
|
+
protected override readonly starsPerDouble: number;
|
|
295
|
+
private readonly rhythmMultiplier: number;
|
|
296
|
+
private readonly historyTimeMax: number;
|
|
297
|
+
private currentRhythm: number;
|
|
298
|
+
private readonly hitWindow: OsuHitWindow;
|
|
299
|
+
constructor(mods: Mod[], overallDifficulty: number);
|
|
300
|
+
/**
|
|
301
|
+
* Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current object.
|
|
302
|
+
*/
|
|
303
|
+
private calculateRhythmBonus(current: DifficultyHitObject): number;
|
|
304
|
+
protected override strainValueAt(current: DifficultyHitObject): number;
|
|
305
|
+
protected override saveToHitObject(current: DifficultyHitObject): void;
|
|
306
|
+
}
|
|
307
|
+
|
|
280
308
|
/**
|
|
281
309
|
* Difficulty calculator for osu!droid gamemode.
|
|
282
310
|
*/
|
|
@@ -354,6 +382,38 @@ declare module "@rian8337/osu-difficulty-calculator" {
|
|
|
354
382
|
* Creates skills to be calculated.
|
|
355
383
|
*/
|
|
356
384
|
protected override createSkills(): DroidSkill[];
|
|
385
|
+
/**
|
|
386
|
+
* Called after aim skill calculation.
|
|
387
|
+
*
|
|
388
|
+
* @param aimSkill The aim skill that considers sliders.
|
|
389
|
+
* @param aimSkillWithoutSliders The aim skill that doesn't consider sliders.
|
|
390
|
+
*/
|
|
391
|
+
private postCalculateAim(
|
|
392
|
+
aimSkill: DroidAim,
|
|
393
|
+
aimSkillWithoutSliders: DroidAim
|
|
394
|
+
): void;
|
|
395
|
+
/**
|
|
396
|
+
* Called after tap skill calculation.
|
|
397
|
+
*
|
|
398
|
+
* @param tapSkill The tap skill.
|
|
399
|
+
*/
|
|
400
|
+
private postCalculateTap(tapSkill: DroidTap): void;
|
|
401
|
+
/**
|
|
402
|
+
* Calculates the speed note count attribute.
|
|
403
|
+
*/
|
|
404
|
+
private calculateSpeedNoteCount(): void;
|
|
405
|
+
/**
|
|
406
|
+
* Called after rhythm skill calculation.
|
|
407
|
+
*
|
|
408
|
+
* @param rhythmSkill The rhythm skill.
|
|
409
|
+
*/
|
|
410
|
+
private postCalculateRhythm(rhythmSkill: DroidRhythm): void;
|
|
411
|
+
/**
|
|
412
|
+
* Called after flashlight skill calculation.
|
|
413
|
+
*
|
|
414
|
+
* @param flashlightSkill The flashlight skill.
|
|
415
|
+
*/
|
|
416
|
+
private postCalculateFlashlight(flashlightSkill: DroidFlashlight): void;
|
|
357
417
|
/**
|
|
358
418
|
* Calculates the base rating value of a difficulty.
|
|
359
419
|
*/
|
|
@@ -403,6 +463,41 @@ declare module "@rian8337/osu-difficulty-calculator" {
|
|
|
403
463
|
protected override saveToHitObject(current: DifficultyHitObject): void;
|
|
404
464
|
}
|
|
405
465
|
|
|
466
|
+
/**
|
|
467
|
+
* A star rating calculator that configures which mode to calculate difficulty for and what mods are applied.
|
|
468
|
+
*/
|
|
469
|
+
export class MapStars {
|
|
470
|
+
/**
|
|
471
|
+
* The osu!droid star rating of the beatmap.
|
|
472
|
+
*/
|
|
473
|
+
readonly droidStars: DroidStarRating;
|
|
474
|
+
/**
|
|
475
|
+
* The osu!standard star rating of the beatmap.
|
|
476
|
+
*/
|
|
477
|
+
readonly pcStars: OsuStarRating;
|
|
478
|
+
/**
|
|
479
|
+
* Calculates the star rating of a beatmap.
|
|
480
|
+
*/
|
|
481
|
+
calculate(params: {
|
|
482
|
+
/**
|
|
483
|
+
* The beatmap to calculate.
|
|
484
|
+
*/
|
|
485
|
+
map: Beatmap;
|
|
486
|
+
/**
|
|
487
|
+
* Applied modifications.
|
|
488
|
+
*/
|
|
489
|
+
mods?: Mod[];
|
|
490
|
+
/**
|
|
491
|
+
* Custom map statistics to apply speed multiplier and force AR values as well as old statistics.
|
|
492
|
+
*/
|
|
493
|
+
stats?: MapStats;
|
|
494
|
+
}): MapStars;
|
|
495
|
+
/**
|
|
496
|
+
* Returns a string representative of the class.
|
|
497
|
+
*/
|
|
498
|
+
toString(): string;
|
|
499
|
+
}
|
|
500
|
+
|
|
406
501
|
/**
|
|
407
502
|
* Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
|
408
503
|
*/
|