@rian8337/osu-difficulty-calculator 4.0.0-beta.24 → 4.0.0-beta.26
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 +336 -294
- package/package.json +3 -3
- package/typings/index.d.ts +60 -67
package/dist/index.js
CHANGED
|
@@ -32,6 +32,12 @@ AimEvaluator.velocityChangeMultiplier = 0.75;
|
|
|
32
32
|
* The base of a difficulty calculator.
|
|
33
33
|
*/
|
|
34
34
|
class DifficultyCalculator {
|
|
35
|
+
/**
|
|
36
|
+
* The difficulty objects of the beatmap.
|
|
37
|
+
*/
|
|
38
|
+
get objects() {
|
|
39
|
+
return this._objects;
|
|
40
|
+
}
|
|
35
41
|
/**
|
|
36
42
|
* The total star rating of the beatmap.
|
|
37
43
|
*/
|
|
@@ -41,14 +47,13 @@ class DifficultyCalculator {
|
|
|
41
47
|
/**
|
|
42
48
|
* Constructs a new instance of the calculator.
|
|
43
49
|
*
|
|
44
|
-
* @param beatmap The beatmap to calculate.
|
|
50
|
+
* @param beatmap The beatmap to calculate.
|
|
45
51
|
*/
|
|
46
52
|
constructor(beatmap) {
|
|
47
|
-
var _a;
|
|
48
53
|
/**
|
|
49
54
|
* The difficulty objects of the beatmap.
|
|
50
55
|
*/
|
|
51
|
-
this.
|
|
56
|
+
this._objects = [];
|
|
52
57
|
/**
|
|
53
58
|
* The modifications applied.
|
|
54
59
|
*/
|
|
@@ -63,13 +68,6 @@ class DifficultyCalculator {
|
|
|
63
68
|
flashlight: [],
|
|
64
69
|
};
|
|
65
70
|
this.beatmap = beatmap;
|
|
66
|
-
this.difficultyStatistics = {
|
|
67
|
-
circleSize: beatmap.difficulty.cs,
|
|
68
|
-
approachRate: (_a = beatmap.difficulty.ar) !== null && _a !== void 0 ? _a : beatmap.difficulty.od,
|
|
69
|
-
overallDifficulty: beatmap.difficulty.od,
|
|
70
|
-
healthDrain: beatmap.difficulty.hp,
|
|
71
|
-
overallSpeedMultiplier: 1,
|
|
72
|
-
};
|
|
73
71
|
}
|
|
74
72
|
/**
|
|
75
73
|
* Calculates the star rating of the specified beatmap.
|
|
@@ -87,16 +85,17 @@ class DifficultyCalculator {
|
|
|
87
85
|
* @returns The current instance.
|
|
88
86
|
*/
|
|
89
87
|
calculate(options) {
|
|
90
|
-
var _a;
|
|
88
|
+
var _a, _b;
|
|
91
89
|
this.mods = (_a = options === null || options === void 0 ? void 0 : options.mods) !== null && _a !== void 0 ? _a : [];
|
|
92
|
-
const
|
|
90
|
+
const playableBeatmap = this.beatmap.createPlayableBeatmap({
|
|
93
91
|
mode: this.mode,
|
|
94
92
|
mods: this.mods,
|
|
95
93
|
customSpeedMultiplier: options === null || options === void 0 ? void 0 : options.customSpeedMultiplier,
|
|
96
94
|
});
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this.
|
|
95
|
+
const clockRate = osuBase.ModUtil.calculateRateWithMods(this.mods) *
|
|
96
|
+
((_b = options === null || options === void 0 ? void 0 : options.customSpeedMultiplier) !== null && _b !== void 0 ? _b : 1);
|
|
97
|
+
this.populateDifficultyAttributes(playableBeatmap, clockRate);
|
|
98
|
+
this._objects = this.generateDifficultyHitObjects(playableBeatmap, clockRate);
|
|
100
99
|
this.calculateAll();
|
|
101
100
|
return this;
|
|
102
101
|
}
|
|
@@ -115,18 +114,27 @@ class DifficultyCalculator {
|
|
|
115
114
|
}
|
|
116
115
|
/**
|
|
117
116
|
* Populates the stored difficulty attributes with necessary data.
|
|
117
|
+
*
|
|
118
|
+
* @param beatmap The beatmap to populate the attributes with.
|
|
119
|
+
* @param clockRate The clock rate of the beatmap.
|
|
118
120
|
*/
|
|
119
|
-
populateDifficultyAttributes() {
|
|
120
|
-
this.attributes.approachRate = this.difficultyStatistics.approachRate;
|
|
121
|
+
populateDifficultyAttributes(beatmap, clockRate) {
|
|
121
122
|
this.attributes.hitCircleCount = this.beatmap.hitObjects.circles;
|
|
122
123
|
this.attributes.maxCombo = this.beatmap.maxCombo;
|
|
123
124
|
this.attributes.mods = this.mods.slice();
|
|
124
|
-
this.attributes.overallDifficulty =
|
|
125
|
-
this.difficultyStatistics.overallDifficulty;
|
|
126
125
|
this.attributes.sliderCount = this.beatmap.hitObjects.sliders;
|
|
127
126
|
this.attributes.spinnerCount = this.beatmap.hitObjects.spinners;
|
|
128
|
-
this.attributes.clockRate =
|
|
129
|
-
|
|
127
|
+
this.attributes.clockRate = clockRate;
|
|
128
|
+
let greatWindow;
|
|
129
|
+
switch (this.mode) {
|
|
130
|
+
case osuBase.Modes.droid:
|
|
131
|
+
greatWindow = new osuBase.DroidHitWindow(beatmap.difficulty.od).hitWindowFor300(this.mods.some((m) => m instanceof osuBase.ModPrecise));
|
|
132
|
+
break;
|
|
133
|
+
case osuBase.Modes.osu:
|
|
134
|
+
greatWindow = new osuBase.OsuHitWindow(beatmap.difficulty.od).hitWindowFor300();
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
this.attributes.overallDifficulty = osuBase.OsuHitWindow.hitWindow300ToOD(greatWindow / clockRate);
|
|
130
138
|
}
|
|
131
139
|
/**
|
|
132
140
|
* Calculates the star rating value of a difficulty.
|
|
@@ -159,8 +167,9 @@ class DifficultyHitObject {
|
|
|
159
167
|
* @param lastLastObject The hitobject before the last hitobject.
|
|
160
168
|
* @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
|
|
161
169
|
* @param clockRate The clock rate of the beatmap.
|
|
170
|
+
* @param greatWindow The great window of the hitobject.
|
|
162
171
|
*/
|
|
163
|
-
constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate) {
|
|
172
|
+
constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, greatWindow) {
|
|
164
173
|
/**
|
|
165
174
|
* The aim strain generated by the hitobject if sliders are considered.
|
|
166
175
|
*/
|
|
@@ -213,18 +222,18 @@ class DifficultyHitObject {
|
|
|
213
222
|
this.normalizedRadius = 50;
|
|
214
223
|
this.maximumSliderRadius = this.normalizedRadius * 2.4;
|
|
215
224
|
this.assumedSliderRadius = this.normalizedRadius * 1.8;
|
|
216
|
-
this.minDeltaTime = 25;
|
|
217
225
|
this.object = object;
|
|
218
226
|
this.lastObject = lastObject;
|
|
219
227
|
this.lastLastObject = lastLastObject;
|
|
220
228
|
this.hitObjects = difficultyHitObjects;
|
|
229
|
+
this.fullGreatWindow = greatWindow * 2;
|
|
221
230
|
this.index = difficultyHitObjects.length - 1;
|
|
222
231
|
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
|
223
232
|
this.startTime = object.startTime / clockRate;
|
|
224
233
|
this.endTime = object.endTime / clockRate;
|
|
225
234
|
if (lastObject) {
|
|
226
235
|
this.deltaTime = this.startTime - lastObject.startTime / clockRate;
|
|
227
|
-
this.strainTime = Math.max(this.deltaTime,
|
|
236
|
+
this.strainTime = Math.max(this.deltaTime, DifficultyHitObject.minDeltaTime);
|
|
228
237
|
}
|
|
229
238
|
else {
|
|
230
239
|
this.deltaTime = 0;
|
|
@@ -295,6 +304,24 @@ class DifficultyHitObject {
|
|
|
295
304
|
}
|
|
296
305
|
return osuBase.MathUtils.clamp((time - fadeInStartTime) / fadeInDuration, 0, 1);
|
|
297
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* How possible is it to doubletap this object together with the next one and get perfect
|
|
309
|
+
* judgement in range from 0 to 1.
|
|
310
|
+
*
|
|
311
|
+
* A value closer to 1 indicates a higher possibility.
|
|
312
|
+
*/
|
|
313
|
+
get doubletapness() {
|
|
314
|
+
const next = this.next(0);
|
|
315
|
+
if (!next) {
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
const currentDeltaTime = Math.max(1, this.deltaTime);
|
|
319
|
+
const nextDeltaTime = Math.max(1, next.deltaTime);
|
|
320
|
+
const deltaDifference = Math.abs(nextDeltaTime - currentDeltaTime);
|
|
321
|
+
const speedRatio = currentDeltaTime / Math.max(currentDeltaTime, deltaDifference);
|
|
322
|
+
const windowRatio = Math.pow(Math.min(1, currentDeltaTime / this.fullGreatWindow), 2);
|
|
323
|
+
return 1 - Math.pow(speedRatio, 1 - windowRatio);
|
|
324
|
+
}
|
|
298
325
|
setDistances(clockRate) {
|
|
299
326
|
if (this.object instanceof osuBase.Slider) {
|
|
300
327
|
this.calculateSliderCursorPosition(this.object);
|
|
@@ -306,7 +333,7 @@ class DifficultyHitObject {
|
|
|
306
333
|
else {
|
|
307
334
|
this.travelDistance *= Math.pow(1 + this.object.repeatCount / 2.5, 1 / 2.5);
|
|
308
335
|
}
|
|
309
|
-
this.travelTime = Math.max(this.object.lazyTravelTime / clockRate,
|
|
336
|
+
this.travelTime = Math.max(this.object.lazyTravelTime / clockRate, DifficultyHitObject.minDeltaTime);
|
|
310
337
|
}
|
|
311
338
|
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner.
|
|
312
339
|
if (!this.lastObject ||
|
|
@@ -324,8 +351,8 @@ class DifficultyHitObject {
|
|
|
324
351
|
this.minimumJumpTime = this.strainTime;
|
|
325
352
|
this.minimumJumpDistance = this.lazyJumpDistance;
|
|
326
353
|
if (this.lastObject instanceof osuBase.Slider) {
|
|
327
|
-
const lastTravelTime = Math.max(this.lastObject.lazyTravelTime / clockRate,
|
|
328
|
-
this.minimumJumpTime = Math.max(this.strainTime - lastTravelTime,
|
|
354
|
+
const lastTravelTime = Math.max(this.lastObject.lazyTravelTime / clockRate, DifficultyHitObject.minDeltaTime);
|
|
355
|
+
this.minimumJumpTime = Math.max(this.strainTime - lastTravelTime, DifficultyHitObject.minDeltaTime);
|
|
329
356
|
// There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects.
|
|
330
357
|
//
|
|
331
358
|
// 1. The anti-flow pattern, where players cut the slider short in order to move to the next hitobject.
|
|
@@ -440,6 +467,10 @@ class DifficultyHitObject {
|
|
|
440
467
|
return pos;
|
|
441
468
|
}
|
|
442
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* The lowest possible delta time value.
|
|
472
|
+
*/
|
|
473
|
+
DifficultyHitObject.minDeltaTime = 25;
|
|
443
474
|
|
|
444
475
|
/**
|
|
445
476
|
* An evaluator for calculating osu!droid Aim skill.
|
|
@@ -692,6 +723,7 @@ class DroidSkill extends StrainSkill {
|
|
|
692
723
|
constructor() {
|
|
693
724
|
super(...arguments);
|
|
694
725
|
this._objectStrains = [];
|
|
726
|
+
this.difficulty = 0;
|
|
695
727
|
}
|
|
696
728
|
/**
|
|
697
729
|
* The strains of hitobjects.
|
|
@@ -705,14 +737,14 @@ class DroidSkill extends StrainSkill {
|
|
|
705
737
|
* The result is scaled by clock rate as it affects the total number of strains.
|
|
706
738
|
*/
|
|
707
739
|
countDifficultStrains() {
|
|
708
|
-
if (this.
|
|
740
|
+
if (this.difficulty === 0) {
|
|
709
741
|
return 0;
|
|
710
742
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
743
|
+
// This is what the top strain is if all strain values were identical.
|
|
744
|
+
const consistentTopStrain = this.difficulty / 10;
|
|
745
|
+
// Use a weighted sum of all strains.
|
|
746
|
+
return this._objectStrains.reduce((total, next) => total +
|
|
747
|
+
1.1 / (1 + Math.exp(-10 * (next / consistentTopStrain - 0.88))), 0);
|
|
716
748
|
}
|
|
717
749
|
process(current) {
|
|
718
750
|
super.process(current);
|
|
@@ -730,12 +762,12 @@ class DroidSkill extends StrainSkill {
|
|
|
730
762
|
}
|
|
731
763
|
// Math here preserves the property that two notes of equal difficulty x, we have their summed difficulty = x * starsPerDouble.
|
|
732
764
|
// This also applies to two sets of notes with equal difficulty.
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
765
|
+
this.difficulty = 0;
|
|
766
|
+
for (const strain of strains) {
|
|
767
|
+
this.difficulty += Math.pow(strain, 1 / Math.log2(this.starsPerDouble));
|
|
768
|
+
}
|
|
769
|
+
this.difficulty = Math.pow(this.difficulty, Math.log2(this.starsPerDouble));
|
|
770
|
+
return this.difficulty;
|
|
739
771
|
}
|
|
740
772
|
calculateCurrentSectionStart(current) {
|
|
741
773
|
return current.startTime;
|
|
@@ -811,27 +843,16 @@ class DroidTapEvaluator extends SpeedEvaluator {
|
|
|
811
843
|
* @param considerCheesability Whether to consider cheesability.
|
|
812
844
|
* @param strainTimeCap The strain time to cap the object's strain time to.
|
|
813
845
|
*/
|
|
814
|
-
static evaluateDifficultyOf(current,
|
|
846
|
+
static evaluateDifficultyOf(current, considerCheesability, strainTimeCap) {
|
|
815
847
|
if (current.object instanceof osuBase.Spinner ||
|
|
816
848
|
// Exclude overlapping objects that can be tapped at once.
|
|
817
849
|
current.isOverlapping(false)) {
|
|
818
850
|
return 0;
|
|
819
851
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
if (next) {
|
|
825
|
-
const greatWindowFull = greatWindow * 2;
|
|
826
|
-
const currentDeltaTime = Math.max(1, current.deltaTime);
|
|
827
|
-
const nextDeltaTime = Math.max(1, next.deltaTime);
|
|
828
|
-
const deltaDifference = Math.abs(nextDeltaTime - currentDeltaTime);
|
|
829
|
-
const speedRatio = currentDeltaTime /
|
|
830
|
-
Math.max(currentDeltaTime, deltaDifference);
|
|
831
|
-
const windowRatio = Math.pow(Math.min(1, currentDeltaTime / greatWindowFull), 2);
|
|
832
|
-
doubletapness = Math.pow(speedRatio, 1 - windowRatio);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
852
|
+
// Nerf doubletappable doubles.
|
|
853
|
+
const doubletapness = considerCheesability
|
|
854
|
+
? 1 - current.doubletapness
|
|
855
|
+
: 1;
|
|
835
856
|
const strainTime = strainTimeCap !== undefined
|
|
836
857
|
? // We cap the strain time to 50 here as the chance of vibro is higher in any BPM higher than 300.
|
|
837
858
|
Math.max(50, strainTimeCap, current.strainTime)
|
|
@@ -856,7 +877,7 @@ class DroidTap extends DroidSkill {
|
|
|
856
877
|
get objectDeltaTimes() {
|
|
857
878
|
return this._objectDeltaTimes;
|
|
858
879
|
}
|
|
859
|
-
constructor(mods,
|
|
880
|
+
constructor(mods, considerCheesability, strainTimeCap) {
|
|
860
881
|
super(mods);
|
|
861
882
|
this.reducedSectionCount = 10;
|
|
862
883
|
this.reducedSectionBaseline = 0.75;
|
|
@@ -866,7 +887,6 @@ class DroidTap extends DroidSkill {
|
|
|
866
887
|
this.currentRhythmMultiplier = 0;
|
|
867
888
|
this.skillMultiplier = 1375;
|
|
868
889
|
this._objectDeltaTimes = [];
|
|
869
|
-
this.greatWindow = new osuBase.OsuHitWindow(overallDifficulty).hitWindowFor300();
|
|
870
890
|
this.considerCheesability = considerCheesability;
|
|
871
891
|
this.strainTimeCap = strainTimeCap;
|
|
872
892
|
}
|
|
@@ -905,7 +925,7 @@ class DroidTap extends DroidSkill {
|
|
|
905
925
|
strainValueAt(current) {
|
|
906
926
|
this.currentTapStrain *= this.strainDecay(current.strainTime);
|
|
907
927
|
this.currentTapStrain +=
|
|
908
|
-
DroidTapEvaluator.evaluateDifficultyOf(current, this.
|
|
928
|
+
DroidTapEvaluator.evaluateDifficultyOf(current, this.considerCheesability, this.strainTimeCap) * this.skillMultiplier;
|
|
909
929
|
this.currentRhythmMultiplier = current.rhythmMultiplier;
|
|
910
930
|
this._objectDeltaTimes.push(current.deltaTime);
|
|
911
931
|
return this.currentTapStrain * current.rhythmMultiplier;
|
|
@@ -1046,7 +1066,7 @@ class DroidFlashlight extends DroidSkill {
|
|
|
1046
1066
|
this.reducedSectionCount = 0;
|
|
1047
1067
|
this.reducedSectionBaseline = 1;
|
|
1048
1068
|
this.starsPerDouble = 1.06;
|
|
1049
|
-
this.skillMultiplier = 0.
|
|
1069
|
+
this.skillMultiplier = 0.02;
|
|
1050
1070
|
this.currentFlashlightStrain = 0;
|
|
1051
1071
|
this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
|
|
1052
1072
|
this.withSliders = withSliders;
|
|
@@ -1075,45 +1095,62 @@ class DroidFlashlight extends DroidSkill {
|
|
|
1075
1095
|
}
|
|
1076
1096
|
}
|
|
1077
1097
|
difficultyValue() {
|
|
1078
|
-
return
|
|
1098
|
+
return (this.strainPeaks.reduce((a, v) => a + v, 0) * this.starsPerDouble);
|
|
1079
1099
|
}
|
|
1080
1100
|
}
|
|
1081
1101
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1102
|
+
class Island {
|
|
1103
|
+
constructor(delta, deltaDifferenceEpsilon) {
|
|
1104
|
+
this.delta = Number.MAX_SAFE_INTEGER;
|
|
1105
|
+
this.deltaCount = 0;
|
|
1106
|
+
if (deltaDifferenceEpsilon === undefined) {
|
|
1107
|
+
this.deltaDifferenceEpsilon = delta;
|
|
1108
|
+
}
|
|
1109
|
+
else {
|
|
1110
|
+
this.deltaDifferenceEpsilon = deltaDifferenceEpsilon;
|
|
1111
|
+
this.addDelta(delta);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
addDelta(delta) {
|
|
1115
|
+
if (this.delta === Number.MAX_SAFE_INTEGER) {
|
|
1116
|
+
this.delta = Math.max(Math.trunc(delta), DifficultyHitObject.minDeltaTime);
|
|
1117
|
+
}
|
|
1118
|
+
++this.deltaCount;
|
|
1119
|
+
}
|
|
1120
|
+
isSimilarPolarity(other) {
|
|
1121
|
+
// TODO: consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple)
|
|
1122
|
+
// naively adding delta check here breaks _a lot_ of maps because of the flawed ratio calculation
|
|
1123
|
+
return this.deltaCount % 2 == other.deltaCount % 2;
|
|
1124
|
+
}
|
|
1125
|
+
equals(other) {
|
|
1126
|
+
return (Math.abs(this.delta - other.delta) < this.deltaDifferenceEpsilon &&
|
|
1127
|
+
this.deltaCount === other.deltaCount);
|
|
1128
|
+
}
|
|
1088
1129
|
}
|
|
1089
|
-
RhythmEvaluator.rhythmMultiplier = 0.75;
|
|
1090
|
-
RhythmEvaluator.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
|
|
1091
|
-
|
|
1092
1130
|
/**
|
|
1093
1131
|
* An evaluator for calculating osu!droid Rhythm skill.
|
|
1094
1132
|
*/
|
|
1095
|
-
class DroidRhythmEvaluator
|
|
1133
|
+
class DroidRhythmEvaluator {
|
|
1096
1134
|
/**
|
|
1097
1135
|
* Calculates a rhythm multiplier for the difficulty of the tap associated
|
|
1098
1136
|
* with historic data of the current object.
|
|
1099
1137
|
*
|
|
1100
1138
|
* @param current The current object.
|
|
1101
|
-
* @param greatWindow The great hit window of the current object.
|
|
1102
1139
|
*/
|
|
1103
|
-
static evaluateDifficultyOf(current
|
|
1104
|
-
if (current.object instanceof osuBase.Spinner
|
|
1105
|
-
// Exclude overlapping objects that can be tapped at once.
|
|
1106
|
-
current.isOverlapping(false)) {
|
|
1140
|
+
static evaluateDifficultyOf(current) {
|
|
1141
|
+
if (current.object instanceof osuBase.Spinner) {
|
|
1107
1142
|
return 1;
|
|
1108
1143
|
}
|
|
1109
|
-
|
|
1144
|
+
const deltaDifferenceEpsilon = current.fullGreatWindow * 0.3;
|
|
1110
1145
|
let rhythmComplexitySum = 0;
|
|
1111
|
-
let
|
|
1146
|
+
let island = new Island(deltaDifferenceEpsilon);
|
|
1147
|
+
let previousIsland = new Island(deltaDifferenceEpsilon);
|
|
1148
|
+
const islandCounts = new Map();
|
|
1112
1149
|
// Store the ratio of the current start of an island to buff for tighter rhythms.
|
|
1113
1150
|
let startRatio = 0;
|
|
1114
1151
|
let firstDeltaSwitch = false;
|
|
1115
1152
|
let rhythmStart = 0;
|
|
1116
|
-
const historicalNoteCount = Math.min(current.index,
|
|
1153
|
+
const historicalNoteCount = Math.min(current.index, this.historyObjectsMax);
|
|
1117
1154
|
// Exclude overlapping objects that can be tapped at once.
|
|
1118
1155
|
const validPrevious = [];
|
|
1119
1156
|
for (let i = 0; i < historicalNoteCount; ++i) {
|
|
@@ -1131,111 +1168,140 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
|
|
|
1131
1168
|
++rhythmStart;
|
|
1132
1169
|
}
|
|
1133
1170
|
for (let i = rhythmStart; i > 0; --i) {
|
|
1171
|
+
const currentObject = validPrevious[i - 1];
|
|
1172
|
+
const prevObject = validPrevious[i];
|
|
1173
|
+
const lastObject = validPrevious[i + 1];
|
|
1134
1174
|
// Scale note 0 to 1 from history to now.
|
|
1135
|
-
|
|
1136
|
-
(current.startTime -
|
|
1175
|
+
const timeDecay = (this.historyTimeMax -
|
|
1176
|
+
(current.startTime - currentObject.startTime)) /
|
|
1137
1177
|
this.historyTimeMax;
|
|
1178
|
+
const noteDecay = (validPrevious.length - i) / validPrevious.length;
|
|
1138
1179
|
// Either we're limited by time or limited by object count.
|
|
1139
|
-
currentHistoricalDecay = Math.min(
|
|
1140
|
-
const currentDelta =
|
|
1141
|
-
const prevDelta =
|
|
1142
|
-
const lastDelta =
|
|
1180
|
+
const currentHistoricalDecay = Math.min(timeDecay, noteDecay);
|
|
1181
|
+
const currentDelta = currentObject.strainTime;
|
|
1182
|
+
const prevDelta = prevObject.strainTime;
|
|
1183
|
+
const lastDelta = lastObject.strainTime;
|
|
1184
|
+
// Calculate how much current delta difference deserves a rhythm bonus
|
|
1185
|
+
// This function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e. 100 and 200)
|
|
1186
|
+
const deltaDifferenceRatio = Math.min(prevDelta, currentDelta) /
|
|
1187
|
+
Math.max(prevDelta, currentDelta);
|
|
1143
1188
|
const currentRatio = 1 +
|
|
1144
|
-
|
|
1145
|
-
Math.min(0.5, Math.pow(Math.sin(Math.PI /
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
let effectiveRatio = windowPenalty * currentRatio;
|
|
1189
|
+
this.rhythmRatioMultiplier *
|
|
1190
|
+
Math.min(0.5, Math.pow(Math.sin(Math.PI / deltaDifferenceRatio), 2));
|
|
1191
|
+
// Reduce ratio bonus if delta difference is too big
|
|
1192
|
+
const fraction = Math.max(prevDelta / currentDelta, currentDelta / prevDelta);
|
|
1193
|
+
const fractionMultiplier = osuBase.MathUtils.clamp(2 - fraction / 8, 0, 1);
|
|
1194
|
+
const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon);
|
|
1195
|
+
let effectiveRatio = windowPenalty * currentRatio * fractionMultiplier;
|
|
1151
1196
|
if (firstDeltaSwitch) {
|
|
1152
|
-
if (prevDelta
|
|
1153
|
-
prevDelta * 1.25 >= currentDelta) {
|
|
1197
|
+
if (Math.abs(prevDelta - currentDelta) < deltaDifferenceEpsilon) {
|
|
1154
1198
|
// Island is still progressing, count size.
|
|
1155
|
-
|
|
1156
|
-
++islandSize;
|
|
1157
|
-
}
|
|
1199
|
+
island.addDelta(currentDelta);
|
|
1158
1200
|
}
|
|
1159
1201
|
else {
|
|
1160
|
-
|
|
1161
|
-
|
|
1202
|
+
// BPM change is into slider, this is easy acc window.
|
|
1203
|
+
if (currentObject.object instanceof osuBase.Slider) {
|
|
1162
1204
|
effectiveRatio /= 8;
|
|
1163
1205
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
// Repeated island size (ex: triplet -> triplet).
|
|
1170
|
-
effectiveRatio /= 4;
|
|
1206
|
+
// BPM change was from a slider, this is easier typically than circle -> circle.
|
|
1207
|
+
// Unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty
|
|
1208
|
+
// than bursts without sliders.
|
|
1209
|
+
if (prevObject.object instanceof osuBase.Slider) {
|
|
1210
|
+
effectiveRatio *= 0.3;
|
|
1171
1211
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1212
|
+
// Repeated island polarity (2 -> 4, 3 -> 5).
|
|
1213
|
+
if (island.isSimilarPolarity(previousIsland)) {
|
|
1174
1214
|
effectiveRatio /= 2;
|
|
1175
1215
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1216
|
+
// Previous increase happened a note ago.
|
|
1217
|
+
// Albeit this is a 1/1 -> 1/2-1/4 type of transition, we don't want to buff this.
|
|
1218
|
+
if (lastDelta > prevDelta + deltaDifferenceEpsilon &&
|
|
1219
|
+
prevDelta > currentDelta + deltaDifferenceEpsilon) {
|
|
1180
1220
|
effectiveRatio /= 8;
|
|
1181
1221
|
}
|
|
1222
|
+
// Repeated island size (ex: triplet -> triplet).
|
|
1223
|
+
// TODO: remove this nerf since its staying here only for balancing purposes because of the flawed ratio calculation
|
|
1224
|
+
if (previousIsland.deltaCount == island.deltaCount) {
|
|
1225
|
+
effectiveRatio /= 2;
|
|
1226
|
+
}
|
|
1227
|
+
let islandFound = false;
|
|
1228
|
+
for (const [currentIsland, count] of islandCounts) {
|
|
1229
|
+
if (!island.equals(currentIsland)) {
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
islandFound = true;
|
|
1233
|
+
let islandCount = count;
|
|
1234
|
+
if (previousIsland.equals(island)) {
|
|
1235
|
+
// Only add island to island counts if they're going one after another.
|
|
1236
|
+
++islandCount;
|
|
1237
|
+
islandCounts.set(currentIsland, islandCount);
|
|
1238
|
+
}
|
|
1239
|
+
// Repeated island (ex: triplet -> triplet).
|
|
1240
|
+
// Graph: https://www.desmos.com/calculator/pj7an56zwf
|
|
1241
|
+
effectiveRatio *= Math.min(3 / islandCount, Math.pow(1 / islandCount, 2.75 / (1 + Math.exp(14 - 0.24 * island.delta))));
|
|
1242
|
+
break;
|
|
1243
|
+
}
|
|
1244
|
+
if (!islandFound) {
|
|
1245
|
+
islandCounts.set(island, 1);
|
|
1246
|
+
}
|
|
1247
|
+
// Scale down the difficulty if the object is doubletappable.
|
|
1248
|
+
effectiveRatio *= 1 - prevObject.doubletapness * 0.75;
|
|
1182
1249
|
rhythmComplexitySum +=
|
|
1183
|
-
|
|
1184
|
-
currentHistoricalDecay
|
|
1185
|
-
Math.sqrt(4 + islandSize)) /
|
|
1186
|
-
2) *
|
|
1187
|
-
Math.sqrt(4 + previousIslandSize)) /
|
|
1188
|
-
2;
|
|
1250
|
+
Math.sqrt(effectiveRatio * startRatio) *
|
|
1251
|
+
currentHistoricalDecay;
|
|
1189
1252
|
startRatio = effectiveRatio;
|
|
1190
|
-
|
|
1191
|
-
if (prevDelta
|
|
1253
|
+
previousIsland = island;
|
|
1254
|
+
if (prevDelta + deltaDifferenceEpsilon < currentDelta) {
|
|
1192
1255
|
// We're slowing down, stop counting.
|
|
1193
1256
|
// If we're speeding up, this stays as is and we keep counting island size.
|
|
1194
1257
|
firstDeltaSwitch = false;
|
|
1195
1258
|
}
|
|
1196
|
-
|
|
1259
|
+
island = new Island(currentDelta, deltaDifferenceEpsilon);
|
|
1197
1260
|
}
|
|
1198
1261
|
}
|
|
1199
|
-
else if (prevDelta >
|
|
1200
|
-
// We
|
|
1262
|
+
else if (prevDelta > currentDelta + deltaDifferenceEpsilon) {
|
|
1263
|
+
// We are speeding up.
|
|
1201
1264
|
// Begin counting island until we change speed again.
|
|
1202
1265
|
firstDeltaSwitch = true;
|
|
1266
|
+
// BPM change is into slider, this is easy acc window.
|
|
1267
|
+
if (currentObject.object instanceof osuBase.Slider) {
|
|
1268
|
+
effectiveRatio *= 0.6;
|
|
1269
|
+
}
|
|
1270
|
+
// BPM change was from a slider, this is easier typically than circle -> circle
|
|
1271
|
+
// Unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty
|
|
1272
|
+
// than bursts without sliders
|
|
1273
|
+
if (prevObject.object instanceof osuBase.Slider) {
|
|
1274
|
+
effectiveRatio *= 0.6;
|
|
1275
|
+
}
|
|
1203
1276
|
startRatio = effectiveRatio;
|
|
1204
|
-
|
|
1277
|
+
island = new Island(currentDelta, deltaDifferenceEpsilon);
|
|
1205
1278
|
}
|
|
1206
1279
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
let doubletapness = 1;
|
|
1210
|
-
if (next) {
|
|
1211
|
-
const currentDeltaTime = Math.max(1, current.deltaTime);
|
|
1212
|
-
const nextDeltaTime = Math.max(1, next.deltaTime);
|
|
1213
|
-
const deltaDifference = Math.abs(nextDeltaTime - currentDeltaTime);
|
|
1214
|
-
const speedRatio = currentDeltaTime / Math.max(currentDeltaTime, deltaDifference);
|
|
1215
|
-
const windowRatio = Math.pow(Math.min(1, currentDeltaTime / (greatWindow * 2)), 2);
|
|
1216
|
-
doubletapness = Math.pow(speedRatio, 1 - windowRatio);
|
|
1217
|
-
}
|
|
1218
|
-
return (Math.sqrt(4 + rhythmComplexitySum * this.rhythmMultiplier * doubletapness) / 2);
|
|
1280
|
+
return (Math.sqrt(4 + rhythmComplexitySum * this.rhythmOverallMultiplier) /
|
|
1281
|
+
2);
|
|
1219
1282
|
}
|
|
1220
1283
|
}
|
|
1284
|
+
DroidRhythmEvaluator.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
|
|
1285
|
+
DroidRhythmEvaluator.historyObjectsMax = 32;
|
|
1286
|
+
DroidRhythmEvaluator.rhythmOverallMultiplier = 0.95;
|
|
1287
|
+
DroidRhythmEvaluator.rhythmRatioMultiplier = 12;
|
|
1221
1288
|
|
|
1222
1289
|
/**
|
|
1223
1290
|
* Represents the skill required to properly follow a beatmap's rhythm.
|
|
1224
1291
|
*/
|
|
1225
1292
|
class DroidRhythm extends DroidSkill {
|
|
1226
|
-
constructor(
|
|
1227
|
-
super(
|
|
1293
|
+
constructor() {
|
|
1294
|
+
super(...arguments);
|
|
1228
1295
|
this.reducedSectionCount = 5;
|
|
1229
1296
|
this.reducedSectionBaseline = 0.75;
|
|
1230
1297
|
this.strainDecayBase = 0.3;
|
|
1231
1298
|
this.starsPerDouble = 1.75;
|
|
1232
1299
|
this.currentRhythmStrain = 0;
|
|
1233
1300
|
this.currentRhythmMultiplier = 1;
|
|
1234
|
-
this.hitWindow = new osuBase.OsuHitWindow(overallDifficulty);
|
|
1235
1301
|
}
|
|
1236
1302
|
strainValueAt(current) {
|
|
1237
1303
|
this.currentRhythmMultiplier =
|
|
1238
|
-
DroidRhythmEvaluator.evaluateDifficultyOf(current
|
|
1304
|
+
DroidRhythmEvaluator.evaluateDifficultyOf(current);
|
|
1239
1305
|
this.currentRhythmStrain *= this.strainDecay(current.deltaTime);
|
|
1240
1306
|
this.currentRhythmStrain += this.currentRhythmMultiplier - 1;
|
|
1241
1307
|
return this.currentRhythmStrain;
|
|
@@ -1318,11 +1384,12 @@ class DroidVisualEvaluator {
|
|
|
1318
1384
|
// Invert the scaling factor to determine the true travel distance independent of circle size.
|
|
1319
1385
|
const pixelTravelDistance = current.object.lazyTravelDistance / scalingFactor;
|
|
1320
1386
|
const currentVelocity = pixelTravelDistance / current.travelTime;
|
|
1387
|
+
const spanTravelDistance = pixelTravelDistance / current.object.spanCount;
|
|
1321
1388
|
strain +=
|
|
1322
1389
|
// Reward sliders based on velocity, while also avoiding overbuffing extremely fast sliders.
|
|
1323
1390
|
Math.min(6, currentVelocity * 1.5) *
|
|
1324
1391
|
// Longer sliders require more reading.
|
|
1325
|
-
(
|
|
1392
|
+
(spanTravelDistance / 100);
|
|
1326
1393
|
let cumulativeStrainTime = 0;
|
|
1327
1394
|
// Reward for velocity changes based on last few sliders.
|
|
1328
1395
|
for (let i = 0; i < Math.min(current.index, 4); ++i) {
|
|
@@ -1334,14 +1401,15 @@ class DroidVisualEvaluator {
|
|
|
1334
1401
|
continue;
|
|
1335
1402
|
}
|
|
1336
1403
|
// Invert the scaling factor to determine the true travel distance independent of circle size.
|
|
1337
|
-
const
|
|
1338
|
-
const lastVelocity =
|
|
1404
|
+
const lastPixelTravelDistance = last.object.lazyTravelDistance / scalingFactor;
|
|
1405
|
+
const lastVelocity = lastPixelTravelDistance / last.travelTime;
|
|
1406
|
+
const lastSpanTravelDistance = lastPixelTravelDistance / last.object.spanCount;
|
|
1339
1407
|
strain +=
|
|
1340
1408
|
// Reward past sliders based on velocity changes, while also
|
|
1341
1409
|
// avoiding overbuffing extremely fast velocity changes.
|
|
1342
1410
|
Math.min(10, 2.5 * Math.abs(currentVelocity - lastVelocity)) *
|
|
1343
1411
|
// Longer sliders require more reading.
|
|
1344
|
-
(
|
|
1412
|
+
(lastSpanTravelDistance / 125) *
|
|
1345
1413
|
// Avoid overbuffing past sliders.
|
|
1346
1414
|
Math.min(1, 300 / cumulativeStrainTime);
|
|
1347
1415
|
}
|
|
@@ -1417,10 +1485,10 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
|
|
|
1417
1485
|
* @param lastLastObject The hitobject before the last hitobject.
|
|
1418
1486
|
* @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
|
|
1419
1487
|
* @param clockRate The clock rate of the beatmap.
|
|
1420
|
-
* @param
|
|
1488
|
+
* @param greatWindow The great window of the hitobject.
|
|
1421
1489
|
*/
|
|
1422
|
-
constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate,
|
|
1423
|
-
super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate);
|
|
1490
|
+
constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, greatWindow) {
|
|
1491
|
+
super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, greatWindow);
|
|
1424
1492
|
/**
|
|
1425
1493
|
* The tap strain generated by the hitobject.
|
|
1426
1494
|
*/
|
|
@@ -1463,10 +1531,7 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
|
|
|
1463
1531
|
this.radiusBuffThreshold = 70;
|
|
1464
1532
|
this.mode = osuBase.Modes.droid;
|
|
1465
1533
|
this.maximumSliderRadius = this.normalizedRadius * 2;
|
|
1466
|
-
this.timePreempt = object.timePreempt;
|
|
1467
|
-
if (!isForceAR) {
|
|
1468
|
-
this.timePreempt /= clockRate;
|
|
1469
|
-
}
|
|
1534
|
+
this.timePreempt = object.timePreempt / clockRate;
|
|
1470
1535
|
}
|
|
1471
1536
|
computeProperties(clockRate, hitObjects) {
|
|
1472
1537
|
super.computeProperties(clockRate, hitObjects);
|
|
@@ -1478,6 +1543,9 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
|
|
|
1478
1543
|
* Keep in mind that "overlapping" in this case is overlapping to the point where both hitobjects
|
|
1479
1544
|
* can be hit with just a single tap in osu!droid.
|
|
1480
1545
|
*
|
|
1546
|
+
* In the case of sliders, it is considered overlapping if all nested hitobjects can be hit with
|
|
1547
|
+
* one aim motion.
|
|
1548
|
+
*
|
|
1481
1549
|
* @param considerDistance Whether to consider the distance between both hitobjects.
|
|
1482
1550
|
* @returns Whether the hitobject is considered overlapping.
|
|
1483
1551
|
*/
|
|
@@ -1485,23 +1553,48 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
|
|
|
1485
1553
|
if (this.object instanceof osuBase.Spinner) {
|
|
1486
1554
|
return false;
|
|
1487
1555
|
}
|
|
1488
|
-
const
|
|
1489
|
-
if (!
|
|
1556
|
+
const prev = this.previous(0);
|
|
1557
|
+
if (!prev || prev.object instanceof osuBase.Spinner) {
|
|
1490
1558
|
return false;
|
|
1491
1559
|
}
|
|
1492
|
-
if (this.
|
|
1560
|
+
if (this.object.startTime !== prev.object.startTime) {
|
|
1493
1561
|
return false;
|
|
1494
1562
|
}
|
|
1495
|
-
if (considerDistance) {
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1563
|
+
if (!considerDistance) {
|
|
1564
|
+
return true;
|
|
1565
|
+
}
|
|
1566
|
+
const distanceThreshold = 2 * this.object.radius;
|
|
1567
|
+
const startPosition = this.object.getStackedPosition(osuBase.Modes.droid);
|
|
1568
|
+
const prevStartPosition = prev.object.getStackedPosition(osuBase.Modes.droid);
|
|
1569
|
+
// We need to consider two cases:
|
|
1570
|
+
//
|
|
1571
|
+
// Case 1: Current object is a circle, or previous object is a circle.
|
|
1572
|
+
// In this case, we only need to check if their positions are close enough to be tapped together.
|
|
1573
|
+
//
|
|
1574
|
+
// Case 2: Both objects are sliders.
|
|
1575
|
+
// In this case, we need to check if all nested hitobjects can be hit together.
|
|
1576
|
+
// To start with, check if the starting positions can be tapped together.
|
|
1577
|
+
if (startPosition.getDistance(prevStartPosition) > distanceThreshold) {
|
|
1578
|
+
return false;
|
|
1579
|
+
}
|
|
1580
|
+
if (this.object instanceof osuBase.Circle || prev.object instanceof osuBase.Circle) {
|
|
1581
|
+
return true;
|
|
1582
|
+
}
|
|
1583
|
+
// Check if all nested hitobjects can be hit together.
|
|
1584
|
+
for (let i = 1; i < this.object.nestedHitObjects.length; ++i) {
|
|
1585
|
+
const position = this.object.nestedHitObjects[i].getStackedPosition(osuBase.Modes.droid);
|
|
1586
|
+
const prevPosition = prevStartPosition.add(prev.object.curvePositionAt(i / (this.object.nestedHitObjects.length - 1)));
|
|
1587
|
+
if (position.getDistance(prevPosition) > distanceThreshold) {
|
|
1588
|
+
return false;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
// Do the same for the previous slider as well.
|
|
1592
|
+
for (let i = 1; i < prev.object.nestedHitObjects.length; ++i) {
|
|
1593
|
+
const prevPosition = prev.object.nestedHitObjects[i].getStackedPosition(osuBase.Modes.droid);
|
|
1594
|
+
const position = startPosition.add(this.object.curvePositionAt(i / (prev.object.nestedHitObjects.length - 1)));
|
|
1595
|
+
if (prevPosition.getDistance(position) > distanceThreshold) {
|
|
1596
|
+
return false;
|
|
1503
1597
|
}
|
|
1504
|
-
return distance <= 2 * this.object.radius;
|
|
1505
1598
|
}
|
|
1506
1599
|
return true;
|
|
1507
1600
|
}
|
|
@@ -1559,7 +1652,9 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
|
|
|
1559
1652
|
Math.max(0, 1 - distance / (2.5 * this.object.radius)) *
|
|
1560
1653
|
(7.5 /
|
|
1561
1654
|
(1 +
|
|
1562
|
-
Math.exp(0.15 *
|
|
1655
|
+
Math.exp(0.15 *
|
|
1656
|
+
(Math.max(deltaTime, DifficultyHitObject.minDeltaTime) -
|
|
1657
|
+
75))));
|
|
1563
1658
|
}
|
|
1564
1659
|
}
|
|
1565
1660
|
|
|
@@ -1583,7 +1678,6 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1583
1678
|
speedNoteCount: 0,
|
|
1584
1679
|
sliderFactor: 0,
|
|
1585
1680
|
clockRate: 1,
|
|
1586
|
-
approachRate: 0,
|
|
1587
1681
|
overallDifficulty: 0,
|
|
1588
1682
|
hitCircleCount: 0,
|
|
1589
1683
|
sliderCount: 0,
|
|
@@ -1633,30 +1727,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1633
1727
|
return this.attributes.visualDifficulty;
|
|
1634
1728
|
}
|
|
1635
1729
|
get cacheableAttributes() {
|
|
1636
|
-
return {
|
|
1637
|
-
tapDifficulty: this.tap,
|
|
1638
|
-
rhythmDifficulty: this.rhythm,
|
|
1639
|
-
visualDifficulty: this.visual,
|
|
1640
|
-
mods: osuBase.ModUtil.modsToOsuString(this.attributes.mods),
|
|
1641
|
-
starRating: this.total,
|
|
1642
|
-
maxCombo: this.attributes.maxCombo,
|
|
1643
|
-
aimDifficulty: this.aim,
|
|
1644
|
-
flashlightDifficulty: this.flashlight,
|
|
1645
|
-
speedNoteCount: this.attributes.speedNoteCount,
|
|
1646
|
-
sliderFactor: this.attributes.sliderFactor,
|
|
1647
|
-
clockRate: this.attributes.clockRate,
|
|
1648
|
-
approachRate: this.attributes.approachRate,
|
|
1649
|
-
overallDifficulty: this.attributes.overallDifficulty,
|
|
1650
|
-
hitCircleCount: this.attributes.hitCircleCount,
|
|
1651
|
-
sliderCount: this.attributes.sliderCount,
|
|
1652
|
-
spinnerCount: this.attributes.spinnerCount,
|
|
1653
|
-
aimDifficultStrainCount: this.attributes.aimDifficultStrainCount,
|
|
1654
|
-
tapDifficultStrainCount: this.attributes.tapDifficultStrainCount,
|
|
1655
|
-
flashlightDifficultStrainCount: this.attributes.flashlightDifficultStrainCount,
|
|
1656
|
-
visualDifficultStrainCount: this.attributes.visualDifficultStrainCount,
|
|
1657
|
-
averageSpeedDeltaTime: this.attributes.averageSpeedDeltaTime,
|
|
1658
|
-
vibroFactor: this.attributes.vibroFactor,
|
|
1659
|
-
};
|
|
1730
|
+
return Object.assign(Object.assign({}, this.attributes), { mods: osuBase.ModUtil.modsToOsuString(this.attributes.mods) });
|
|
1660
1731
|
}
|
|
1661
1732
|
/**
|
|
1662
1733
|
* Calculates the aim star rating of the beatmap and stores it in this instance.
|
|
@@ -1671,11 +1742,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1671
1742
|
* Calculates the tap star rating of the beatmap and stores it in this instance.
|
|
1672
1743
|
*/
|
|
1673
1744
|
calculateTap() {
|
|
1674
|
-
const
|
|
1675
|
-
const
|
|
1676
|
-
const tapSkillNoCheese = new DroidTap(this.mods, od, false);
|
|
1745
|
+
const tapSkillCheese = new DroidTap(this.mods, true);
|
|
1746
|
+
const tapSkillNoCheese = new DroidTap(this.mods, false);
|
|
1677
1747
|
this.calculateSkills(tapSkillCheese, tapSkillNoCheese);
|
|
1678
|
-
const tapSkillVibro = new DroidTap(this.mods,
|
|
1748
|
+
const tapSkillVibro = new DroidTap(this.mods, true, tapSkillCheese.relevantDeltaTime());
|
|
1679
1749
|
this.calculateSkills(tapSkillVibro);
|
|
1680
1750
|
this.postCalculateTap(tapSkillCheese, tapSkillVibro);
|
|
1681
1751
|
}
|
|
@@ -1683,7 +1753,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1683
1753
|
* Calculates the rhythm star rating of the beatmap and stores it in this instance.
|
|
1684
1754
|
*/
|
|
1685
1755
|
calculateRhythm() {
|
|
1686
|
-
const rhythmSkill = new DroidRhythm(this.mods
|
|
1756
|
+
const rhythmSkill = new DroidRhythm(this.mods);
|
|
1687
1757
|
this.calculateSkills(rhythmSkill);
|
|
1688
1758
|
this.postCalculateRhythm(rhythmSkill);
|
|
1689
1759
|
}
|
|
@@ -1743,7 +1813,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1743
1813
|
const flashlightSkillWithoutSliders = skills[6];
|
|
1744
1814
|
const visualSkill = skills[7];
|
|
1745
1815
|
const visualSkillWithoutSliders = skills[8];
|
|
1746
|
-
const tapSkillVibro = new DroidTap(this.mods,
|
|
1816
|
+
const tapSkillVibro = new DroidTap(this.mods, true, tapSkillCheese.relevantDeltaTime());
|
|
1747
1817
|
this.calculateSkills(tapSkillVibro);
|
|
1748
1818
|
this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
|
|
1749
1819
|
this.postCalculateTap(tapSkillCheese, tapSkillVibro);
|
|
@@ -1766,42 +1836,29 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1766
1836
|
this.visual.toFixed(2) +
|
|
1767
1837
|
" visual)");
|
|
1768
1838
|
}
|
|
1769
|
-
generateDifficultyHitObjects(
|
|
1839
|
+
generateDifficultyHitObjects(beatmap, clockRate) {
|
|
1770
1840
|
var _a, _b;
|
|
1771
1841
|
const difficultyObjects = [];
|
|
1772
|
-
const { objects } =
|
|
1773
|
-
const
|
|
1842
|
+
const { objects } = beatmap.hitObjects;
|
|
1843
|
+
const isPrecise = this.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
1844
|
+
const greatWindow = new osuBase.DroidHitWindow(beatmap.difficulty.od).hitWindowFor300(isPrecise) / clockRate;
|
|
1774
1845
|
for (let i = 0; i < objects.length; ++i) {
|
|
1775
|
-
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,
|
|
1776
|
-
difficultyObject.computeProperties(
|
|
1846
|
+
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, greatWindow);
|
|
1847
|
+
difficultyObject.computeProperties(clockRate, objects);
|
|
1777
1848
|
difficultyObjects.push(difficultyObject);
|
|
1778
1849
|
}
|
|
1779
1850
|
return difficultyObjects;
|
|
1780
1851
|
}
|
|
1781
|
-
computeDifficultyStatistics(options) {
|
|
1782
|
-
var _a;
|
|
1783
|
-
const { difficulty } = this.beatmap;
|
|
1784
|
-
return osuBase.calculateDroidDifficultyStatistics({
|
|
1785
|
-
circleSize: difficulty.cs,
|
|
1786
|
-
approachRate: (_a = difficulty.ar) !== null && _a !== void 0 ? _a : difficulty.od,
|
|
1787
|
-
overallDifficulty: difficulty.od,
|
|
1788
|
-
healthDrain: difficulty.hp,
|
|
1789
|
-
mods: this.mods,
|
|
1790
|
-
customSpeedMultiplier: options === null || options === void 0 ? void 0 : options.customSpeedMultiplier,
|
|
1791
|
-
oldStatistics: options === null || options === void 0 ? void 0 : options.oldStatistics,
|
|
1792
|
-
});
|
|
1793
|
-
}
|
|
1794
1852
|
createSkills() {
|
|
1795
|
-
const od = this.difficultyStatistics.overallDifficulty;
|
|
1796
1853
|
return [
|
|
1797
1854
|
new DroidAim(this.mods, true),
|
|
1798
1855
|
new DroidAim(this.mods, false),
|
|
1799
1856
|
// Tap skill depends on rhythm skill, so we put it first
|
|
1800
|
-
new DroidRhythm(this.mods
|
|
1857
|
+
new DroidRhythm(this.mods),
|
|
1801
1858
|
// Cheesability tap
|
|
1802
|
-
new DroidTap(this.mods,
|
|
1859
|
+
new DroidTap(this.mods, true),
|
|
1803
1860
|
// Non-cheesability tap
|
|
1804
|
-
new DroidTap(this.mods,
|
|
1861
|
+
new DroidTap(this.mods, false),
|
|
1805
1862
|
new DroidFlashlight(this.mods, true),
|
|
1806
1863
|
new DroidFlashlight(this.mods, false),
|
|
1807
1864
|
new DroidVisual(this.mods, true),
|
|
@@ -1817,7 +1874,9 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1817
1874
|
postCalculateAim(aimSkill, aimSkillWithoutSliders) {
|
|
1818
1875
|
this.strainPeaks.aimWithSliders = aimSkill.strainPeaks;
|
|
1819
1876
|
this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
|
|
1820
|
-
this.attributes.aimDifficulty = this.
|
|
1877
|
+
this.attributes.aimDifficulty = this.mods.some((m) => m instanceof osuBase.ModAutopilot)
|
|
1878
|
+
? 0
|
|
1879
|
+
: this.starValue(aimSkill.difficultyValue());
|
|
1821
1880
|
if (this.aim) {
|
|
1822
1881
|
this.attributes.sliderFactor =
|
|
1823
1882
|
this.starValue(aimSkillWithoutSliders.difficultyValue()) /
|
|
@@ -1834,6 +1893,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1834
1893
|
* Calculates aim-related attributes.
|
|
1835
1894
|
*/
|
|
1836
1895
|
calculateAimAttributes() {
|
|
1896
|
+
this.attributes.difficultSliders = [];
|
|
1837
1897
|
const topDifficultSliders = [];
|
|
1838
1898
|
for (let i = 0; i < this.objects.length; ++i) {
|
|
1839
1899
|
const object = this.objects[i];
|
|
@@ -1954,10 +2014,12 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1954
2014
|
this.attributes.flashlightSliderFactor =
|
|
1955
2015
|
this.starValue(flashlightSkillWithoutSliders.difficultyValue()) / this.flashlight;
|
|
1956
2016
|
}
|
|
2017
|
+
if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
|
|
2018
|
+
this.attributes.flashlightDifficulty *= 0.3;
|
|
2019
|
+
}
|
|
1957
2020
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
1958
2021
|
this.attributes.flashlightDifficulty *= 0.7;
|
|
1959
2022
|
}
|
|
1960
|
-
this.attributes.flashlightDifficulty = this.flashlight;
|
|
1961
2023
|
this.attributes.flashlightDifficultStrainCount =
|
|
1962
2024
|
flashlightSkill.countDifficultStrains();
|
|
1963
2025
|
}
|
|
@@ -1976,6 +2038,9 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1976
2038
|
this.starValue(visualSkillWithoutSliders.difficultyValue()) /
|
|
1977
2039
|
this.visual;
|
|
1978
2040
|
}
|
|
2041
|
+
if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
|
|
2042
|
+
this.attributes.visualDifficulty *= 0.8;
|
|
2043
|
+
}
|
|
1979
2044
|
this.attributes.visualDifficultStrainCount =
|
|
1980
2045
|
visualSkillWithSliders.countDifficultStrains();
|
|
1981
2046
|
}
|
|
@@ -2357,8 +2422,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2357
2422
|
aimValue *= this._aimSliderCheesePenalty;
|
|
2358
2423
|
// Scale the aim value with deviation.
|
|
2359
2424
|
aimValue *=
|
|
2360
|
-
1.
|
|
2361
|
-
Math.
|
|
2425
|
+
1.025 *
|
|
2426
|
+
Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * this._deviation)), 0.475);
|
|
2362
2427
|
// OD 7 SS stays the same.
|
|
2363
2428
|
aimValue *= 0.98 + Math.pow(7, 2) / 2500;
|
|
2364
2429
|
return aimValue;
|
|
@@ -2370,8 +2435,9 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2370
2435
|
let tapValue = this.baseValue(this.difficultyAttributes.tapDifficulty);
|
|
2371
2436
|
tapValue *= this.calculateStrainBasedMissPenalty(this.difficultyAttributes.tapDifficultStrainCount);
|
|
2372
2437
|
// Scale the tap value with estimated full combo deviation.
|
|
2373
|
-
//
|
|
2374
|
-
|
|
2438
|
+
// Consider notes that are difficult to tap with respect to other notes, but
|
|
2439
|
+
// also cap the note count to prevent buffing filler patterns.
|
|
2440
|
+
tapValue *= this.calculateDeviationBasedLengthScaling(Math.min(this.difficultyAttributes.speedNoteCount, this.totalHits / 1.45));
|
|
2375
2441
|
// Normalize the deviation to 300 BPM.
|
|
2376
2442
|
const normalizedDeviation = this.tapDeviation *
|
|
2377
2443
|
Math.max(1, 50 / this.difficultyAttributes.averageSpeedDeltaTime);
|
|
@@ -2386,8 +2452,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2386
2452
|
((2 * 300) / averageBPM))));
|
|
2387
2453
|
// Scale the tap value with tap deviation.
|
|
2388
2454
|
tapValue *=
|
|
2389
|
-
1.
|
|
2390
|
-
Math.pow(osuBase.ErrorFunction.erf(20 / (Math.SQRT2 * adjustedDeviation)), 0.
|
|
2455
|
+
1.05 *
|
|
2456
|
+
Math.pow(osuBase.ErrorFunction.erf(20 / (Math.SQRT2 * adjustedDeviation)), 0.6);
|
|
2391
2457
|
// Additional scaling for tap value based on average BPM and how "vibroable" the beatmap is.
|
|
2392
2458
|
// Higher BPMs require more precise tapping. When the deviation is too high,
|
|
2393
2459
|
// it can be assumed that the player taps invariant to rhythm.
|
|
@@ -2412,7 +2478,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2412
2478
|
this.totalSuccessfulHits === 0) {
|
|
2413
2479
|
return 0;
|
|
2414
2480
|
}
|
|
2415
|
-
let accuracyValue =
|
|
2481
|
+
let accuracyValue = 650 * Math.exp(-0.1 * this._deviation);
|
|
2416
2482
|
const ncircles = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModScoreV2)
|
|
2417
2483
|
? this.totalHits - this.difficultyAttributes.spinnerCount
|
|
2418
2484
|
: this.difficultyAttributes.hitCircleCount;
|
|
@@ -2465,8 +2531,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2465
2531
|
visualValue *= this._visualSliderCheesePenalty;
|
|
2466
2532
|
// Scale the visual value with deviation.
|
|
2467
2533
|
visualValue *=
|
|
2468
|
-
1.
|
|
2469
|
-
Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * this._deviation)), 0.
|
|
2534
|
+
1.05 *
|
|
2535
|
+
Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * this._deviation)), 0.775);
|
|
2470
2536
|
// OD 5 SS stays the same.
|
|
2471
2537
|
visualValue *= 0.98 + Math.pow(5, 2) / 2500;
|
|
2472
2538
|
return visualValue;
|
|
@@ -2482,8 +2548,9 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2482
2548
|
if (this.effectiveMissCount === 0) {
|
|
2483
2549
|
return 1;
|
|
2484
2550
|
}
|
|
2485
|
-
return (0.
|
|
2486
|
-
(this.effectiveMissCount /
|
|
2551
|
+
return (0.96 /
|
|
2552
|
+
(this.effectiveMissCount /
|
|
2553
|
+
(4 * Math.pow(Math.log(difficultStrainCount), 0.94)) +
|
|
2487
2554
|
1));
|
|
2488
2555
|
}
|
|
2489
2556
|
/**
|
|
@@ -2910,30 +2977,19 @@ class OsuSpeedEvaluator extends SpeedEvaluator {
|
|
|
2910
2977
|
* - and how easily they can be cheesed.
|
|
2911
2978
|
*
|
|
2912
2979
|
* @param current The current object.
|
|
2913
|
-
* @param greatWindow The great hit window of the current object.
|
|
2914
2980
|
*/
|
|
2915
|
-
static evaluateDifficultyOf(current
|
|
2981
|
+
static evaluateDifficultyOf(current) {
|
|
2916
2982
|
var _a;
|
|
2917
2983
|
if (current.object instanceof osuBase.Spinner) {
|
|
2918
2984
|
return 0;
|
|
2919
2985
|
}
|
|
2920
2986
|
const prev = current.previous(0);
|
|
2921
2987
|
let strainTime = current.strainTime;
|
|
2922
|
-
const greatWindowFull = greatWindow * 2;
|
|
2923
2988
|
// Nerf doubletappable doubles.
|
|
2924
|
-
const
|
|
2925
|
-
let doubletapness = 1;
|
|
2926
|
-
if (next) {
|
|
2927
|
-
const currentDeltaTime = Math.max(1, current.deltaTime);
|
|
2928
|
-
const nextDeltaTime = Math.max(1, next.deltaTime);
|
|
2929
|
-
const deltaDifference = Math.abs(nextDeltaTime - currentDeltaTime);
|
|
2930
|
-
const speedRatio = currentDeltaTime / Math.max(currentDeltaTime, deltaDifference);
|
|
2931
|
-
const windowRatio = Math.pow(Math.min(1, currentDeltaTime / greatWindowFull), 2);
|
|
2932
|
-
doubletapness = Math.pow(speedRatio, 1 - windowRatio);
|
|
2933
|
-
}
|
|
2989
|
+
const doubletapness = 1 - current.doubletapness;
|
|
2934
2990
|
// Cap deltatime to the OD 300 hitwindow.
|
|
2935
2991
|
// 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.
|
|
2936
|
-
strainTime /= osuBase.MathUtils.clamp(strainTime /
|
|
2992
|
+
strainTime /= osuBase.MathUtils.clamp(strainTime / current.fullGreatWindow / 0.93, 0.92, 1);
|
|
2937
2993
|
let speedBonus = 1;
|
|
2938
2994
|
if (strainTime < this.minSpeedBonus) {
|
|
2939
2995
|
speedBonus +=
|
|
@@ -2956,7 +3012,7 @@ OsuSpeedEvaluator.SINGLE_SPACING_THRESHOLD = 125;
|
|
|
2956
3012
|
/**
|
|
2957
3013
|
* An evaluator for calculating osu!standard Rhythm skill.
|
|
2958
3014
|
*/
|
|
2959
|
-
class OsuRhythmEvaluator
|
|
3015
|
+
class OsuRhythmEvaluator {
|
|
2960
3016
|
/**
|
|
2961
3017
|
* Calculates a rhythm multiplier for the difficulty of the tap associated
|
|
2962
3018
|
* with historic data of the current object.
|
|
@@ -2964,7 +3020,7 @@ class OsuRhythmEvaluator extends RhythmEvaluator {
|
|
|
2964
3020
|
* @param current The current object.
|
|
2965
3021
|
* @param greatWindow The great hit window of the current object.
|
|
2966
3022
|
*/
|
|
2967
|
-
static evaluateDifficultyOf(current
|
|
3023
|
+
static evaluateDifficultyOf(current) {
|
|
2968
3024
|
if (current.object instanceof osuBase.Spinner) {
|
|
2969
3025
|
return 0;
|
|
2970
3026
|
}
|
|
@@ -2999,8 +3055,9 @@ class OsuRhythmEvaluator extends RhythmEvaluator {
|
|
|
2999
3055
|
Math.min(0.5, Math.pow(Math.sin(Math.PI /
|
|
3000
3056
|
(Math.min(prevDelta, currentDelta) /
|
|
3001
3057
|
Math.max(prevDelta, currentDelta))), 2));
|
|
3002
|
-
const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) -
|
|
3003
|
-
|
|
3058
|
+
const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) -
|
|
3059
|
+
current.fullGreatWindow * 0.3) /
|
|
3060
|
+
(current.fullGreatWindow * 0.3));
|
|
3004
3061
|
let effectiveRatio = windowPenalty * currentRatio;
|
|
3005
3062
|
if (firstDeltaSwitch) {
|
|
3006
3063
|
if (prevDelta <= 1.25 * currentDelta &&
|
|
@@ -3061,13 +3118,15 @@ class OsuRhythmEvaluator extends RhythmEvaluator {
|
|
|
3061
3118
|
return Math.sqrt(4 + rhythmComplexitySum * this.rhythmMultiplier) / 2;
|
|
3062
3119
|
}
|
|
3063
3120
|
}
|
|
3121
|
+
OsuRhythmEvaluator.rhythmMultiplier = 0.75;
|
|
3122
|
+
OsuRhythmEvaluator.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
|
|
3064
3123
|
|
|
3065
3124
|
/**
|
|
3066
3125
|
* Represents the skill required to press keys or tap with regards to keeping up with the speed at which objects need to be hit.
|
|
3067
3126
|
*/
|
|
3068
3127
|
class OsuSpeed extends OsuSkill {
|
|
3069
|
-
constructor(
|
|
3070
|
-
super(
|
|
3128
|
+
constructor() {
|
|
3129
|
+
super(...arguments);
|
|
3071
3130
|
this.strainDecayBase = 0.3;
|
|
3072
3131
|
this.reducedSectionCount = 5;
|
|
3073
3132
|
this.reducedSectionBaseline = 0.75;
|
|
@@ -3076,7 +3135,6 @@ class OsuSpeed extends OsuSkill {
|
|
|
3076
3135
|
this.currentSpeedStrain = 0;
|
|
3077
3136
|
this.currentRhythm = 0;
|
|
3078
3137
|
this.skillMultiplier = 1375;
|
|
3079
|
-
this.greatWindow = new osuBase.OsuHitWindow(overallDifficulty).hitWindowFor300();
|
|
3080
3138
|
}
|
|
3081
3139
|
/**
|
|
3082
3140
|
* @param current The hitobject to calculate.
|
|
@@ -3084,9 +3142,9 @@ class OsuSpeed extends OsuSkill {
|
|
|
3084
3142
|
strainValueAt(current) {
|
|
3085
3143
|
this.currentSpeedStrain *= this.strainDecay(current.strainTime);
|
|
3086
3144
|
this.currentSpeedStrain +=
|
|
3087
|
-
OsuSpeedEvaluator.evaluateDifficultyOf(current
|
|
3145
|
+
OsuSpeedEvaluator.evaluateDifficultyOf(current) *
|
|
3088
3146
|
this.skillMultiplier;
|
|
3089
|
-
this.currentRhythm = OsuRhythmEvaluator.evaluateDifficultyOf(current
|
|
3147
|
+
this.currentRhythm = OsuRhythmEvaluator.evaluateDifficultyOf(current);
|
|
3090
3148
|
return this.currentSpeedStrain * this.currentRhythm;
|
|
3091
3149
|
}
|
|
3092
3150
|
calculateInitialStrain(time, current) {
|
|
@@ -3220,29 +3278,8 @@ class OsuFlashlight extends OsuSkill {
|
|
|
3220
3278
|
* Represents an osu!standard hit object with difficulty calculation values.
|
|
3221
3279
|
*/
|
|
3222
3280
|
class OsuDifficultyHitObject extends DifficultyHitObject {
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
|
3226
|
-
let scalingFactor = this.normalizedRadius / radius;
|
|
3227
|
-
// High circle size (small CS) bonus
|
|
3228
|
-
if (radius < this.radiusBuffThreshold) {
|
|
3229
|
-
scalingFactor *=
|
|
3230
|
-
1 + Math.min(this.radiusBuffThreshold - radius, 5) / 50;
|
|
3231
|
-
}
|
|
3232
|
-
return scalingFactor;
|
|
3233
|
-
}
|
|
3234
|
-
/**
|
|
3235
|
-
* Note: You **must** call `computeProperties` at some point due to how TypeScript handles
|
|
3236
|
-
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue).
|
|
3237
|
-
*
|
|
3238
|
-
* @param object The underlying hitobject.
|
|
3239
|
-
* @param lastObject The hitobject before this hitobject.
|
|
3240
|
-
* @param lastLastObject The hitobject before the last hitobject.
|
|
3241
|
-
* @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
|
|
3242
|
-
* @param clockRate The clock rate of the beatmap.
|
|
3243
|
-
*/
|
|
3244
|
-
constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate) {
|
|
3245
|
-
super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate);
|
|
3281
|
+
constructor() {
|
|
3282
|
+
super(...arguments);
|
|
3246
3283
|
/**
|
|
3247
3284
|
* The speed strain generated by the hitobject.
|
|
3248
3285
|
*/
|
|
@@ -3254,6 +3291,17 @@ class OsuDifficultyHitObject extends DifficultyHitObject {
|
|
|
3254
3291
|
this.radiusBuffThreshold = 30;
|
|
3255
3292
|
this.mode = osuBase.Modes.osu;
|
|
3256
3293
|
}
|
|
3294
|
+
get scalingFactor() {
|
|
3295
|
+
const radius = this.object.radius;
|
|
3296
|
+
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
|
3297
|
+
let scalingFactor = this.normalizedRadius / radius;
|
|
3298
|
+
// High circle size (small CS) bonus
|
|
3299
|
+
if (radius < this.radiusBuffThreshold) {
|
|
3300
|
+
scalingFactor *=
|
|
3301
|
+
1 + Math.min(this.radiusBuffThreshold - radius, 5) / 50;
|
|
3302
|
+
}
|
|
3303
|
+
return scalingFactor;
|
|
3304
|
+
}
|
|
3257
3305
|
}
|
|
3258
3306
|
|
|
3259
3307
|
/**
|
|
@@ -3319,7 +3367,7 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3319
3367
|
this.attributes.speedDifficulty = 0;
|
|
3320
3368
|
return;
|
|
3321
3369
|
}
|
|
3322
|
-
const speedSkill = new OsuSpeed(this.mods
|
|
3370
|
+
const speedSkill = new OsuSpeed(this.mods);
|
|
3323
3371
|
this.calculateSkills(speedSkill);
|
|
3324
3372
|
this.postCalculateSpeed(speedSkill);
|
|
3325
3373
|
}
|
|
@@ -3383,37 +3431,32 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3383
3431
|
this.flashlight.toFixed(2) +
|
|
3384
3432
|
" flashlight)");
|
|
3385
3433
|
}
|
|
3386
|
-
generateDifficultyHitObjects(
|
|
3434
|
+
generateDifficultyHitObjects(beatmap, clockRate) {
|
|
3387
3435
|
var _a, _b;
|
|
3388
3436
|
const difficultyObjects = [];
|
|
3389
|
-
const { objects } =
|
|
3437
|
+
const { objects } = beatmap.hitObjects;
|
|
3438
|
+
const greatWindow = new osuBase.OsuHitWindow(beatmap.difficulty.od).hitWindowFor300() /
|
|
3439
|
+
clockRate;
|
|
3390
3440
|
for (let i = 0; i < objects.length; ++i) {
|
|
3391
|
-
const difficultyObject = new OsuDifficultyHitObject(objects[i], (_a = objects[i - 1]) !== null && _a !== void 0 ? _a : null, (_b = objects[i - 2]) !== null && _b !== void 0 ? _b : null, difficultyObjects,
|
|
3392
|
-
difficultyObject.computeProperties(
|
|
3441
|
+
const difficultyObject = new OsuDifficultyHitObject(objects[i], (_a = objects[i - 1]) !== null && _a !== void 0 ? _a : null, (_b = objects[i - 2]) !== null && _b !== void 0 ? _b : null, difficultyObjects, clockRate, greatWindow);
|
|
3442
|
+
difficultyObject.computeProperties(clockRate, objects);
|
|
3393
3443
|
difficultyObjects.push(difficultyObject);
|
|
3394
3444
|
}
|
|
3395
3445
|
return difficultyObjects;
|
|
3396
3446
|
}
|
|
3397
|
-
computeDifficultyStatistics(options) {
|
|
3398
|
-
var _a;
|
|
3399
|
-
const { difficulty } = this.beatmap;
|
|
3400
|
-
return osuBase.calculateOsuDifficultyStatistics({
|
|
3401
|
-
circleSize: difficulty.cs,
|
|
3402
|
-
approachRate: (_a = difficulty.ar) !== null && _a !== void 0 ? _a : difficulty.od,
|
|
3403
|
-
overallDifficulty: difficulty.od,
|
|
3404
|
-
healthDrain: difficulty.hp,
|
|
3405
|
-
mods: options === null || options === void 0 ? void 0 : options.mods,
|
|
3406
|
-
customSpeedMultiplier: options === null || options === void 0 ? void 0 : options.customSpeedMultiplier,
|
|
3407
|
-
});
|
|
3408
|
-
}
|
|
3409
3447
|
createSkills() {
|
|
3410
3448
|
return [
|
|
3411
3449
|
new OsuAim(this.mods, true),
|
|
3412
3450
|
new OsuAim(this.mods, false),
|
|
3413
|
-
new OsuSpeed(this.mods
|
|
3451
|
+
new OsuSpeed(this.mods),
|
|
3414
3452
|
new OsuFlashlight(this.mods),
|
|
3415
3453
|
];
|
|
3416
3454
|
}
|
|
3455
|
+
populateDifficultyAttributes(beatmap, clockRate) {
|
|
3456
|
+
super.populateDifficultyAttributes(beatmap, clockRate);
|
|
3457
|
+
const preempt = osuBase.BeatmapDifficulty.difficultyRange(beatmap.difficulty.ar, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin) / clockRate;
|
|
3458
|
+
this.attributes.approachRate = osuBase.BeatmapDifficulty.inverseDifficultyRange(preempt, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin);
|
|
3459
|
+
}
|
|
3417
3460
|
/**
|
|
3418
3461
|
* Called after aim skill calculation.
|
|
3419
3462
|
*
|
|
@@ -3720,6 +3763,5 @@ exports.OsuRhythmEvaluator = OsuRhythmEvaluator;
|
|
|
3720
3763
|
exports.OsuSpeed = OsuSpeed;
|
|
3721
3764
|
exports.OsuSpeedEvaluator = OsuSpeedEvaluator;
|
|
3722
3765
|
exports.PerformanceCalculator = PerformanceCalculator;
|
|
3723
|
-
exports.RhythmEvaluator = RhythmEvaluator;
|
|
3724
3766
|
exports.SpeedEvaluator = SpeedEvaluator;
|
|
3725
3767
|
//# sourceMappingURL=index.js.map
|