@rian8337/osu-difficulty-calculator 4.0.0-beta.94 → 4.0.0-beta.95
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 +1290 -802
- package/package.json +3 -3
- package/typings/index.d.ts +326 -179
package/dist/index.js
CHANGED
|
@@ -223,17 +223,8 @@ class Skill {
|
|
|
223
223
|
get objectDifficulties() {
|
|
224
224
|
return this._objectDifficulties;
|
|
225
225
|
}
|
|
226
|
-
/**
|
|
227
|
-
* The start times of {@link DifficultyHitObject}s, populated by {@link Skill.process}.
|
|
228
|
-
*
|
|
229
|
-
* Indices correspond to {@link objectDifficulties}.
|
|
230
|
-
*/
|
|
231
|
-
get objectTimes() {
|
|
232
|
-
return this._objectTimes;
|
|
233
|
-
}
|
|
234
226
|
constructor(mods) {
|
|
235
227
|
this._objectDifficulties = [];
|
|
236
|
-
this._objectTimes = [];
|
|
237
228
|
this.mods = mods;
|
|
238
229
|
}
|
|
239
230
|
/**
|
|
@@ -249,7 +240,6 @@ class Skill {
|
|
|
249
240
|
const difficultyValue = this.processInternal(current);
|
|
250
241
|
this.saveToHitObject(current, difficultyValue);
|
|
251
242
|
this._objectDifficulties.push(difficultyValue);
|
|
252
|
-
this._objectTimes.push(current.startTime);
|
|
253
243
|
}
|
|
254
244
|
/**
|
|
255
245
|
* Saves the calculated difficulty to a {@link DifficultyHitObject}.
|
|
@@ -1015,6 +1005,7 @@ class DroidFlashlightEvaluator {
|
|
|
1015
1005
|
* @param mods The mods used.
|
|
1016
1006
|
*/
|
|
1017
1007
|
static evaluateDifficultyOf(current, mods) {
|
|
1008
|
+
var _a;
|
|
1018
1009
|
if (current.object instanceof osuBase.Spinner ||
|
|
1019
1010
|
// Exclude overlapping objects that can be tapped at once.
|
|
1020
1011
|
current.isOverlapping(true)) {
|
|
@@ -1026,6 +1017,10 @@ class DroidFlashlightEvaluator {
|
|
|
1026
1017
|
let result = 0;
|
|
1027
1018
|
let last = current;
|
|
1028
1019
|
let angleRepeatCount = 0;
|
|
1020
|
+
// Hidden's fade-out only affects opacity when the "only fade approach circles" setting is off.
|
|
1021
|
+
const opacityMods = ((_a = mods.get(osuBase.ModHidden)) === null || _a === void 0 ? void 0 : _a.onlyFadeApproachCircles.value)
|
|
1022
|
+
? undefined
|
|
1023
|
+
: mods;
|
|
1029
1024
|
for (let i = 0; i < Math.min(current.index, 10); ++i) {
|
|
1030
1025
|
const currentObject = current.previous(i);
|
|
1031
1026
|
cumulativeStrainTime += last.strainTime;
|
|
@@ -1043,7 +1038,7 @@ class DroidFlashlightEvaluator {
|
|
|
1043
1038
|
const opacityBonus = 1 +
|
|
1044
1039
|
this.maxOpacityBonus *
|
|
1045
1040
|
(1 -
|
|
1046
|
-
current.opacityAt(currentObject.object.startTime,
|
|
1041
|
+
current.opacityAt(currentObject.object.startTime, opacityMods));
|
|
1047
1042
|
result +=
|
|
1048
1043
|
(stackNerf * opacityBonus * scalingFactor * jumpDistance) /
|
|
1049
1044
|
cumulativeStrainTime;
|
|
@@ -1133,7 +1128,10 @@ class StrainSkill extends Skill {
|
|
|
1133
1128
|
get peaks() {
|
|
1134
1129
|
return this.strainPeaks
|
|
1135
1130
|
.map((value, i) => ({ time: this.strainPeakTimes[i], value }))
|
|
1136
|
-
.concat({
|
|
1131
|
+
.concat({
|
|
1132
|
+
time: this.currentSectionEnd,
|
|
1133
|
+
value: this.currentSectionPeak,
|
|
1134
|
+
});
|
|
1137
1135
|
}
|
|
1138
1136
|
/**
|
|
1139
1137
|
* Converts a difficulty value to a performance value.
|
|
@@ -1180,9 +1178,7 @@ class StrainSkill extends Skill {
|
|
|
1180
1178
|
this.startNewSectionFrom(this.currentSectionEnd, current);
|
|
1181
1179
|
this.currentSectionEnd += this.sectionLength;
|
|
1182
1180
|
}
|
|
1183
|
-
// Ignore the first hitobject.
|
|
1184
1181
|
this.currentStrain = this.strainValueAt(current);
|
|
1185
|
-
this.saveToHitObject(current, this.currentStrain);
|
|
1186
1182
|
this.currentSectionPeak = Math.max(this.currentStrain, this.currentSectionPeak);
|
|
1187
1183
|
return this.currentStrain;
|
|
1188
1184
|
}
|
|
@@ -1326,6 +1322,7 @@ class HarmonicSkill extends Skill {
|
|
|
1326
1322
|
constructor() {
|
|
1327
1323
|
super(...arguments);
|
|
1328
1324
|
this._noteWeightSum = 0;
|
|
1325
|
+
this._objectTimes = [];
|
|
1329
1326
|
/**
|
|
1330
1327
|
* Scaling factor applied as `x / (i + 1)`, where `x` is the skill's {@link harmonicScale} and `i`
|
|
1331
1328
|
* is the index of the {@link DifficultyHitObject} being processed.
|
|
@@ -1348,6 +1345,14 @@ class HarmonicSkill extends Skill {
|
|
|
1348
1345
|
get noteWeightSum() {
|
|
1349
1346
|
return this._noteWeightSum;
|
|
1350
1347
|
}
|
|
1348
|
+
/**
|
|
1349
|
+
* The start times of {@link DifficultyHitObject}s, populated by {@link HarmonicSkill.process}.
|
|
1350
|
+
*
|
|
1351
|
+
* Indices correspond to {@link objectDifficulties}.
|
|
1352
|
+
*/
|
|
1353
|
+
get objectTimes() {
|
|
1354
|
+
return this._objectTimes;
|
|
1355
|
+
}
|
|
1351
1356
|
get peaks() {
|
|
1352
1357
|
return this.objectDifficulties.map((value, i) => ({
|
|
1353
1358
|
time: this.objectTimes[i],
|
|
@@ -1413,6 +1418,13 @@ class HarmonicSkill extends Skill {
|
|
|
1413
1418
|
applyDifficultyTransformation(difficulties) {
|
|
1414
1419
|
// Do nothing by default.
|
|
1415
1420
|
}
|
|
1421
|
+
process(current) {
|
|
1422
|
+
const previousLength = this.objectDifficulties.length;
|
|
1423
|
+
super.process(current);
|
|
1424
|
+
if (this.objectDifficulties.length > previousLength) {
|
|
1425
|
+
this._objectTimes.push(current.startTime);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1416
1428
|
processInternal(current) {
|
|
1417
1429
|
return this.objectDifficultyOf(current);
|
|
1418
1430
|
}
|
|
@@ -2527,6 +2539,10 @@ class DifficultyHitObject {
|
|
|
2527
2539
|
* The flashlight strain generated by the hitobject.
|
|
2528
2540
|
*/
|
|
2529
2541
|
this.flashlightStrain = 0;
|
|
2542
|
+
/**
|
|
2543
|
+
* The reading difficulty generated by the hitobject.
|
|
2544
|
+
*/
|
|
2545
|
+
this.readingDifficulty = 0;
|
|
2530
2546
|
/**
|
|
2531
2547
|
* The rhythm multiplier generated by the hitobject. This is used to alter tap strain.
|
|
2532
2548
|
*/
|
|
@@ -2685,16 +2701,9 @@ class DifficultyHitObject {
|
|
|
2685
2701
|
const nextDeltaTime = Math.max(1, nextObj.deltaTime);
|
|
2686
2702
|
const deltaDifference = Math.abs(nextDeltaTime - currentDeltaTime);
|
|
2687
2703
|
const speedRatio = currentDeltaTime / Math.max(currentDeltaTime, deltaDifference);
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
return 1 - Math.pow(speedRatio, distanceFactor * (1 - windowRatio));
|
|
2692
|
-
}
|
|
2693
|
-
else {
|
|
2694
|
-
// TODO: unported osu!standard change
|
|
2695
|
-
const windowRatio = Math.pow(Math.min(1, currentDeltaTime / this.hitWindowFor(osuBase.HitResult.Great)), 2);
|
|
2696
|
-
return 1 - Math.pow(speedRatio, 1 - windowRatio);
|
|
2697
|
-
}
|
|
2704
|
+
const windowRatio = Math.pow(Math.min(1, currentDeltaTime / this.hitWindowFor(osuBase.HitResult.Great)), 5);
|
|
2705
|
+
const distanceFactor = Math.pow(osuBase.Interpolation.reverseLerp(this.lazyJumpDistance, this.normalizedDiameter, this.normalizedRadius), 2);
|
|
2706
|
+
return 1 - Math.pow(speedRatio, distanceFactor * (1 - windowRatio));
|
|
2698
2707
|
}
|
|
2699
2708
|
/**
|
|
2700
2709
|
* Retrieves the full rate-adjusted hit window for a {@link HitResult}.
|
|
@@ -2715,17 +2724,9 @@ class DifficultyHitObject {
|
|
|
2715
2724
|
var _a;
|
|
2716
2725
|
if (this.object instanceof osuBase.Slider) {
|
|
2717
2726
|
// Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
|
2718
|
-
|
|
2719
|
-
this.
|
|
2720
|
-
this.
|
|
2721
|
-
Math.max(1, Math.pow(this.object.repeatCount, 0.3));
|
|
2722
|
-
}
|
|
2723
|
-
else {
|
|
2724
|
-
// TODO: unported osu!standard change
|
|
2725
|
-
this.travelDistance =
|
|
2726
|
-
this.lazyTravelDistance *
|
|
2727
|
-
Math.pow(1 + this.object.repeatCount / 2.5, 1 / 2.5);
|
|
2728
|
-
}
|
|
2727
|
+
this.travelDistance =
|
|
2728
|
+
this.lazyTravelDistance *
|
|
2729
|
+
Math.max(1, Math.pow(this.object.repeatCount, 0.3));
|
|
2729
2730
|
this.travelTime = Math.max(this.lazyTravelTime / clockRate, DifficultyHitObject.minDeltaTime);
|
|
2730
2731
|
}
|
|
2731
2732
|
this.minimumJumpTime = this.strainTime;
|
|
@@ -2776,29 +2777,17 @@ class DifficultyHitObject {
|
|
|
2776
2777
|
if (this.lastLastDifficultyObject &&
|
|
2777
2778
|
!(this.lastLastDifficultyObject.object instanceof osuBase.Spinner)) {
|
|
2778
2779
|
const lastLastCursorPosition = this.getEndCursorPosition(this.lastLastDifficultyObject);
|
|
2779
|
-
if (this.
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
this.lastDifficultyObject.object.stackedPosition;
|
|
2784
|
-
}
|
|
2785
|
-
const angle = this.calculateAngle(this.object.stackedPosition, lastCursorPosition, lastLastCursorPosition);
|
|
2786
|
-
const sliderAngle = this.calculateSliderAngle(this.lastDifficultyObject, lastLastCursorPosition);
|
|
2787
|
-
const v = this.object.stackedPosition.subtract(lastCursorPosition);
|
|
2788
|
-
this.normalizedVectorAngle = Math.atan2(Math.abs(v.y), Math.abs(v.x));
|
|
2789
|
-
this.angleSigned =
|
|
2790
|
-
Math.abs(angle) <= Math.abs(sliderAngle)
|
|
2791
|
-
? angle
|
|
2792
|
-
: sliderAngle;
|
|
2793
|
-
}
|
|
2794
|
-
else {
|
|
2795
|
-
// TODO: unported osu!standard change
|
|
2796
|
-
const v1 = lastLastCursorPosition.subtract(this.lastObject.stackedPosition);
|
|
2797
|
-
const v2 = this.object.stackedPosition.subtract(lastCursorPosition);
|
|
2798
|
-
const dot = v1.dot(v2);
|
|
2799
|
-
const det = v1.x * v2.y - v1.y * v2.x;
|
|
2800
|
-
this.angleSigned = Math.atan2(det, dot);
|
|
2780
|
+
if (((_a = this.lastDifficultyObject) === null || _a === void 0 ? void 0 : _a.object) instanceof osuBase.Slider &&
|
|
2781
|
+
this.lastDifficultyObject.travelDistance > 0) {
|
|
2782
|
+
lastCursorPosition =
|
|
2783
|
+
this.lastDifficultyObject.object.stackedPosition;
|
|
2801
2784
|
}
|
|
2785
|
+
const angle = this.calculateAngle(this.object.stackedPosition, lastCursorPosition, lastLastCursorPosition);
|
|
2786
|
+
const sliderAngle = this.calculateSliderAngle(this.lastDifficultyObject, lastLastCursorPosition);
|
|
2787
|
+
const v = this.object.stackedPosition.subtract(lastCursorPosition);
|
|
2788
|
+
this.normalizedVectorAngle = Math.atan2(Math.abs(v.y), Math.abs(v.x));
|
|
2789
|
+
this.angleSigned =
|
|
2790
|
+
Math.abs(angle) <= Math.abs(sliderAngle) ? angle : sliderAngle;
|
|
2802
2791
|
}
|
|
2803
2792
|
}
|
|
2804
2793
|
calculateAngle(currentPosition, lastPosition, lastLastPosition) {
|
|
@@ -2931,10 +2920,6 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
|
|
|
2931
2920
|
* The rhythm difficulty generated by the hitobject.
|
|
2932
2921
|
*/
|
|
2933
2922
|
this.rhythmDifficulty = 0;
|
|
2934
|
-
/**
|
|
2935
|
-
* The reading difficulty generated by the hitobject.
|
|
2936
|
-
*/
|
|
2937
|
-
this.readingDifficulty = 0;
|
|
2938
2923
|
this.normalizedRadius = 50;
|
|
2939
2924
|
this.mode = osuBase.Modes.Droid;
|
|
2940
2925
|
}
|
|
@@ -3022,17 +3007,24 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
|
|
|
3022
3007
|
}
|
|
3023
3008
|
}
|
|
3024
3009
|
|
|
3025
|
-
|
|
3026
|
-
|
|
3010
|
+
/**
|
|
3011
|
+
* Represents a group of consecutive objects with the same delta time.
|
|
3012
|
+
*/
|
|
3013
|
+
class Island {
|
|
3014
|
+
constructor(delta) {
|
|
3015
|
+
/**
|
|
3016
|
+
* Delta time of every object in this island.
|
|
3017
|
+
*/
|
|
3027
3018
|
this.delta = Number.MAX_SAFE_INTEGER;
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3019
|
+
/**
|
|
3020
|
+
* How long the island is.
|
|
3021
|
+
*/
|
|
3022
|
+
this.deltaCount = 1;
|
|
3023
|
+
/**
|
|
3024
|
+
* How many times the island already occured.
|
|
3025
|
+
*/
|
|
3026
|
+
this.occurrences = 1;
|
|
3027
|
+
this.delta = Math.max(Math.trunc(delta), DifficultyHitObject.minDeltaTime);
|
|
3036
3028
|
}
|
|
3037
3029
|
addDelta(delta) {
|
|
3038
3030
|
if (this.delta === Number.MAX_SAFE_INTEGER) {
|
|
@@ -3040,27 +3032,20 @@ class DroidIsland {
|
|
|
3040
3032
|
}
|
|
3041
3033
|
++this.deltaCount;
|
|
3042
3034
|
}
|
|
3043
|
-
isSimilarPolarity(other) {
|
|
3035
|
+
isSimilarPolarity(other, epsilon) {
|
|
3044
3036
|
// Single delta islands should not be compared.
|
|
3045
3037
|
if (this.deltaCount <= 1 || other.deltaCount <= 1) {
|
|
3046
3038
|
return false;
|
|
3047
3039
|
}
|
|
3048
|
-
return (Math.abs(this.delta - other.delta) <
|
|
3040
|
+
return (Math.abs(this.delta - other.delta) < epsilon &&
|
|
3049
3041
|
this.deltaCount % 2 === other.deltaCount % 2);
|
|
3050
3042
|
}
|
|
3051
|
-
|
|
3052
|
-
return (Math.abs(this.delta - other.delta) <
|
|
3043
|
+
almostEquals(other, epsilon) {
|
|
3044
|
+
return (Math.abs(this.delta - other.delta) < epsilon &&
|
|
3053
3045
|
this.deltaCount === other.deltaCount);
|
|
3054
3046
|
}
|
|
3055
3047
|
}
|
|
3056
3048
|
|
|
3057
|
-
class DroidIslandCounter {
|
|
3058
|
-
constructor(island, count) {
|
|
3059
|
-
this.island = island;
|
|
3060
|
-
this.count = count;
|
|
3061
|
-
}
|
|
3062
|
-
}
|
|
3063
|
-
|
|
3064
3049
|
/**
|
|
3065
3050
|
* An evaluator for calculating osu!droid Rhythm skill.
|
|
3066
3051
|
*/
|
|
@@ -3078,9 +3063,9 @@ class DroidRhythmEvaluator {
|
|
|
3078
3063
|
}
|
|
3079
3064
|
const deltaDifferenceEpsilon = current.hitWindowFor(osuBase.HitResult.Great) * 0.3;
|
|
3080
3065
|
let rhythmComplexitySum = 0;
|
|
3081
|
-
let island = new
|
|
3082
|
-
let previousIsland = new
|
|
3083
|
-
const
|
|
3066
|
+
let island = new Island(Number.MAX_SAFE_INTEGER);
|
|
3067
|
+
let previousIsland = new Island(Number.MAX_SAFE_INTEGER);
|
|
3068
|
+
const islands = [];
|
|
3084
3069
|
// Store the ratio of the current start of an island to buff for tighter rhythms.
|
|
3085
3070
|
let startRatio = 0;
|
|
3086
3071
|
let firstDeltaSwitch = false;
|
|
@@ -3121,7 +3106,7 @@ class DroidRhythmEvaluator {
|
|
|
3121
3106
|
const prevDelta = Math.max(prevObject.deltaTime, 1e-7);
|
|
3122
3107
|
const lastDelta = Math.max(lastObject.deltaTime, 1e-7);
|
|
3123
3108
|
if (island.delta == Number.MAX_SAFE_INTEGER) {
|
|
3124
|
-
island = new
|
|
3109
|
+
island = new Island(currentDelta);
|
|
3125
3110
|
}
|
|
3126
3111
|
// Calculate how much current delta difference deserves a rhythm bonus.
|
|
3127
3112
|
// This function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e. 100 and 200).
|
|
@@ -3158,7 +3143,7 @@ class DroidRhythmEvaluator {
|
|
|
3158
3143
|
effectiveRatio /= 2;
|
|
3159
3144
|
}
|
|
3160
3145
|
// Repeated island polarity (2 -> 4, 3 -> 5).
|
|
3161
|
-
if (island.isSimilarPolarity(previousIsland)) {
|
|
3146
|
+
if (island.isSimilarPolarity(previousIsland, deltaDifferenceEpsilon)) {
|
|
3162
3147
|
effectiveRatio /= 2;
|
|
3163
3148
|
}
|
|
3164
3149
|
// Previous increase happened a note ago.
|
|
@@ -3175,18 +3160,24 @@ class DroidRhythmEvaluator {
|
|
|
3175
3160
|
if (isSpeedingUp) {
|
|
3176
3161
|
effectiveRatio *= 0.65;
|
|
3177
3162
|
}
|
|
3178
|
-
|
|
3179
|
-
|
|
3163
|
+
let found = false;
|
|
3164
|
+
for (const existingIsland of islands) {
|
|
3165
|
+
if (!existingIsland.almostEquals(island, deltaDifferenceEpsilon)) {
|
|
3166
|
+
continue;
|
|
3167
|
+
}
|
|
3180
3168
|
// Only add island to island counts if they're going one after another.
|
|
3181
|
-
if (previousIsland.
|
|
3182
|
-
++
|
|
3169
|
+
if (previousIsland.almostEquals(island, deltaDifferenceEpsilon)) {
|
|
3170
|
+
++existingIsland.occurrences;
|
|
3183
3171
|
}
|
|
3172
|
+
const power = osuBase.MathUtils.offsetLogistic(island.delta, 58.33, 0.24, 2.75);
|
|
3184
3173
|
// Repeated island (ex: triplet -> triplet).
|
|
3185
3174
|
// Graph: https://www.desmos.com/calculator/pj7an56zwf
|
|
3186
|
-
effectiveRatio *= Math.min(3 /
|
|
3175
|
+
effectiveRatio *= Math.min(3 / existingIsland.occurrences, Math.pow(1 / existingIsland.occurrences, power));
|
|
3176
|
+
found = true;
|
|
3177
|
+
break;
|
|
3187
3178
|
}
|
|
3188
|
-
|
|
3189
|
-
|
|
3179
|
+
if (!found && island.deltaCount > 0) {
|
|
3180
|
+
islands.push(island);
|
|
3190
3181
|
}
|
|
3191
3182
|
// Scale down the difficulty if the object is doubletappable.
|
|
3192
3183
|
effectiveRatio *=
|
|
@@ -3207,7 +3198,7 @@ class DroidRhythmEvaluator {
|
|
|
3207
3198
|
// If we're speeding up, this stays as is and we keep counting island size.
|
|
3208
3199
|
firstDeltaSwitch = false;
|
|
3209
3200
|
}
|
|
3210
|
-
island = new
|
|
3201
|
+
island = new Island(currentDelta);
|
|
3211
3202
|
}
|
|
3212
3203
|
}
|
|
3213
3204
|
else if (prevDelta > currentDelta + deltaDifferenceEpsilon) {
|
|
@@ -3225,7 +3216,7 @@ class DroidRhythmEvaluator {
|
|
|
3225
3216
|
effectiveRatio *= 0.6;
|
|
3226
3217
|
}
|
|
3227
3218
|
startRatio = effectiveRatio;
|
|
3228
|
-
island = new
|
|
3219
|
+
island = new Island(currentDelta);
|
|
3229
3220
|
}
|
|
3230
3221
|
lastObject = prevObject;
|
|
3231
3222
|
prevObject = currentObject;
|
|
@@ -3289,6 +3280,7 @@ class DifficultyAttributes {
|
|
|
3289
3280
|
this.maxCombo = 0;
|
|
3290
3281
|
this.aimDifficulty = 0;
|
|
3291
3282
|
this.flashlightDifficulty = 0;
|
|
3283
|
+
this.readingDifficulty = 0;
|
|
3292
3284
|
this.speedNoteCount = 0;
|
|
3293
3285
|
this.sliderFactor = 1;
|
|
3294
3286
|
this.clockRate = 1;
|
|
@@ -3298,6 +3290,8 @@ class DifficultyAttributes {
|
|
|
3298
3290
|
this.spinnerCount = 0;
|
|
3299
3291
|
this.aimDifficultSliderCount = 0;
|
|
3300
3292
|
this.aimDifficultStrainCount = 0;
|
|
3293
|
+
this.aimTopWeightedSliderFactor = 0;
|
|
3294
|
+
this.readingDifficultNoteCount = 0;
|
|
3301
3295
|
if (!cacheableAttributes) {
|
|
3302
3296
|
return;
|
|
3303
3297
|
}
|
|
@@ -3306,6 +3300,7 @@ class DifficultyAttributes {
|
|
|
3306
3300
|
this.maxCombo = cacheableAttributes.maxCombo;
|
|
3307
3301
|
this.aimDifficulty = cacheableAttributes.aimDifficulty;
|
|
3308
3302
|
this.flashlightDifficulty = cacheableAttributes.flashlightDifficulty;
|
|
3303
|
+
this.readingDifficulty = cacheableAttributes.readingDifficulty;
|
|
3309
3304
|
this.speedNoteCount = cacheableAttributes.speedNoteCount;
|
|
3310
3305
|
this.sliderFactor = cacheableAttributes.sliderFactor;
|
|
3311
3306
|
this.clockRate = cacheableAttributes.clockRate;
|
|
@@ -3317,6 +3312,10 @@ class DifficultyAttributes {
|
|
|
3317
3312
|
cacheableAttributes.aimDifficultSliderCount;
|
|
3318
3313
|
this.aimDifficultStrainCount =
|
|
3319
3314
|
cacheableAttributes.aimDifficultStrainCount;
|
|
3315
|
+
this.aimTopWeightedSliderFactor =
|
|
3316
|
+
cacheableAttributes.aimTopWeightedSliderFactor;
|
|
3317
|
+
this.readingDifficultNoteCount =
|
|
3318
|
+
cacheableAttributes.readingDifficultNoteCount;
|
|
3320
3319
|
}
|
|
3321
3320
|
/**
|
|
3322
3321
|
* Converts this `DifficultyAttributes` instance to an attribute structure that can be cached.
|
|
@@ -3341,10 +3340,7 @@ class DroidDifficultyAttributes extends DifficultyAttributes {
|
|
|
3341
3340
|
constructor(cacheableAttributes) {
|
|
3342
3341
|
super(cacheableAttributes);
|
|
3343
3342
|
this.tapDifficulty = 0;
|
|
3344
|
-
this.readingDifficulty = 0;
|
|
3345
|
-
this.aimTopWeightedSliderFactor = 0;
|
|
3346
3343
|
this.tapTopWeightedSliderFactor = 0;
|
|
3347
|
-
this.readingDifficultNoteCount = 0;
|
|
3348
3344
|
this.rhythmDifficulty = 0;
|
|
3349
3345
|
this.tapDifficultStrainCount = 0;
|
|
3350
3346
|
this.maximumScore = 0;
|
|
@@ -3356,10 +3352,6 @@ class DroidDifficultyAttributes extends DifficultyAttributes {
|
|
|
3356
3352
|
this.readingDifficulty = cacheableAttributes.readingDifficulty;
|
|
3357
3353
|
this.tapDifficultStrainCount =
|
|
3358
3354
|
cacheableAttributes.tapDifficultStrainCount;
|
|
3359
|
-
this.readingDifficultNoteCount =
|
|
3360
|
-
cacheableAttributes.readingDifficultNoteCount;
|
|
3361
|
-
this.aimTopWeightedSliderFactor =
|
|
3362
|
-
cacheableAttributes.aimTopWeightedSliderFactor;
|
|
3363
3355
|
this.tapTopWeightedSliderFactor =
|
|
3364
3356
|
cacheableAttributes.tapTopWeightedSliderFactor;
|
|
3365
3357
|
this.maximumScore = cacheableAttributes.maximumScore;
|
|
@@ -3674,154 +3666,33 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
3674
3666
|
}
|
|
3675
3667
|
|
|
3676
3668
|
/**
|
|
3677
|
-
* An evaluator for calculating osu!standard
|
|
3669
|
+
* An evaluator for calculating osu!standard agility aim difficulty.
|
|
3678
3670
|
*/
|
|
3679
|
-
class
|
|
3671
|
+
class OsuAgilityEvaluator {
|
|
3680
3672
|
/**
|
|
3681
|
-
* Evaluates the difficulty of aiming the current object
|
|
3682
|
-
*
|
|
3683
|
-
* - cursor velocity to the current object,
|
|
3684
|
-
* - angle difficulty,
|
|
3685
|
-
* - sharp velocity increases,
|
|
3686
|
-
* - and slider difficulty.
|
|
3673
|
+
* Evaluates the difficulty of fast aiming the current object.
|
|
3687
3674
|
*
|
|
3688
3675
|
* @param current The current object.
|
|
3689
|
-
* @param withSliders Whether to take slider difficulty into account.
|
|
3690
3676
|
*/
|
|
3691
|
-
static evaluateDifficultyOf(current
|
|
3692
|
-
|
|
3693
|
-
if (current.object instanceof osuBase.Spinner
|
|
3694
|
-
current.index <= 1 ||
|
|
3695
|
-
last.object instanceof osuBase.Spinner) {
|
|
3677
|
+
static evaluateDifficultyOf(current) {
|
|
3678
|
+
var _a;
|
|
3679
|
+
if (current.object instanceof osuBase.Spinner) {
|
|
3696
3680
|
return 0;
|
|
3697
3681
|
}
|
|
3698
|
-
const
|
|
3699
|
-
const
|
|
3700
|
-
const
|
|
3701
|
-
const
|
|
3702
|
-
|
|
3703
|
-
let
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
// Calculate the slider velocity from slider head to slider end.
|
|
3707
|
-
const travelVelocity = last.travelDistance / last.travelTime;
|
|
3708
|
-
// Calculate the movement velocity from slider end to current object.
|
|
3709
|
-
const movementVelocity = current.minimumJumpDistance / current.minimumJumpTime;
|
|
3710
|
-
// Take the larger total combined velocity.
|
|
3711
|
-
currentVelocity = Math.max(currentVelocity, movementVelocity + travelVelocity);
|
|
3712
|
-
}
|
|
3713
|
-
// As above, do the same for the previous hitobject.
|
|
3714
|
-
let prevVelocity = last.lazyJumpDistance / last.strainTime;
|
|
3715
|
-
if (lastLast.object instanceof osuBase.Slider && withSliders) {
|
|
3716
|
-
const travelVelocity = lastLast.travelDistance / lastLast.travelTime;
|
|
3717
|
-
const movementVelocity = last.minimumJumpDistance / last.minimumJumpTime;
|
|
3718
|
-
prevVelocity = Math.max(prevVelocity, movementVelocity + travelVelocity);
|
|
3719
|
-
}
|
|
3720
|
-
let wideAngleBonus = 0;
|
|
3721
|
-
let acuteAngleBonus = 0;
|
|
3722
|
-
let sliderBonus = 0;
|
|
3723
|
-
let velocityChangeBonus = 0;
|
|
3724
|
-
let wiggleBonus = 0;
|
|
3725
|
-
// Start strain with regular velocity.
|
|
3726
|
-
let strain = currentVelocity;
|
|
3727
|
-
if (current.angle !== null && last.angle !== null) {
|
|
3728
|
-
const currentAngle = current.angle;
|
|
3729
|
-
const lastAngle = last.angle;
|
|
3730
|
-
// Rewarding angles, take the smaller velocity as base.
|
|
3731
|
-
const angleBonus = Math.min(currentVelocity, prevVelocity);
|
|
3732
|
-
if (
|
|
3733
|
-
// If rhythms are the same.
|
|
3734
|
-
Math.max(current.strainTime, last.strainTime) <
|
|
3735
|
-
1.25 * Math.min(current.strainTime, last.strainTime)) {
|
|
3736
|
-
acuteAngleBonus = this.calculateAcuteAngleBonus(currentAngle);
|
|
3737
|
-
// Penalize angle repetition.
|
|
3738
|
-
acuteAngleBonus *=
|
|
3739
|
-
0.08 +
|
|
3740
|
-
0.92 *
|
|
3741
|
-
(1 -
|
|
3742
|
-
Math.min(acuteAngleBonus, Math.pow(this.calculateAcuteAngleBonus(lastAngle), 3)));
|
|
3743
|
-
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
|
3744
|
-
acuteAngleBonus *=
|
|
3745
|
-
angleBonus *
|
|
3746
|
-
osuBase.MathUtils.smootherstep(osuBase.MathUtils.millisecondsToBPM(current.strainTime, 2), 300, 400) *
|
|
3747
|
-
osuBase.MathUtils.smootherstep(current.lazyJumpDistance, diameter, diameter * 2);
|
|
3748
|
-
}
|
|
3749
|
-
wideAngleBonus = this.calculateWideAngleBonus(currentAngle);
|
|
3750
|
-
// Penalize angle repetition.
|
|
3751
|
-
wideAngleBonus *=
|
|
3752
|
-
1 -
|
|
3753
|
-
Math.min(wideAngleBonus, Math.pow(this.calculateWideAngleBonus(lastAngle), 3));
|
|
3754
|
-
// Apply full wide angle bonus for distance more than one diameter
|
|
3755
|
-
wideAngleBonus *=
|
|
3756
|
-
angleBonus *
|
|
3757
|
-
osuBase.MathUtils.smootherstep(current.lazyJumpDistance, 0, diameter);
|
|
3758
|
-
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
|
3759
|
-
// https://www.desmos.com/calculator/dp0v0nvowc
|
|
3760
|
-
wiggleBonus =
|
|
3761
|
-
angleBonus *
|
|
3762
|
-
osuBase.MathUtils.smootherstep(current.lazyJumpDistance, radius, diameter) *
|
|
3763
|
-
Math.pow(osuBase.MathUtils.reverseLerp(current.lazyJumpDistance, diameter * 3, diameter), 1.8) *
|
|
3764
|
-
osuBase.MathUtils.smootherstep(currentAngle, osuBase.MathUtils.degreesToRadians(110), osuBase.MathUtils.degreesToRadians(60)) *
|
|
3765
|
-
osuBase.MathUtils.smootherstep(last.lazyJumpDistance, radius, diameter) *
|
|
3766
|
-
Math.pow(osuBase.MathUtils.reverseLerp(last.lazyJumpDistance, diameter * 3, diameter), 1.8) *
|
|
3767
|
-
osuBase.MathUtils.smootherstep(lastAngle, osuBase.MathUtils.degreesToRadians(110), osuBase.MathUtils.degreesToRadians(60));
|
|
3768
|
-
if (last2 !== null) {
|
|
3769
|
-
// If objects just go back and forth through a middle point - don't give as much wide bonus.
|
|
3770
|
-
// Use previous(2) and previous(0) because angles calculation is done prevprev-prev-curr, so any
|
|
3771
|
-
// object's angle's center point is always the previous object.
|
|
3772
|
-
const distance = last2.object.stackedPosition.getDistance(last.object.stackedPosition);
|
|
3773
|
-
if (distance < 1) {
|
|
3774
|
-
wideAngleBonus *= 1 - 0.35 * (1 - distance);
|
|
3775
|
-
}
|
|
3776
|
-
}
|
|
3777
|
-
}
|
|
3778
|
-
if (Math.max(prevVelocity, currentVelocity)) {
|
|
3779
|
-
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
|
|
3780
|
-
prevVelocity =
|
|
3781
|
-
(last.lazyJumpDistance + lastLast.travelDistance) /
|
|
3782
|
-
last.strainTime;
|
|
3783
|
-
currentVelocity =
|
|
3784
|
-
(current.lazyJumpDistance + last.travelDistance) /
|
|
3785
|
-
current.strainTime;
|
|
3786
|
-
// Scale with ratio of difference compared to half the max distance.
|
|
3787
|
-
const distanceRatio = osuBase.MathUtils.smoothstep(Math.abs(prevVelocity - currentVelocity) /
|
|
3788
|
-
Math.max(prevVelocity, currentVelocity), 0, 1);
|
|
3789
|
-
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
|
3790
|
-
const overlapVelocityBuff = Math.min((diameter * 1.25) /
|
|
3791
|
-
Math.min(current.strainTime, last.strainTime), Math.abs(prevVelocity - currentVelocity));
|
|
3792
|
-
velocityChangeBonus = overlapVelocityBuff * distanceRatio;
|
|
3793
|
-
// Penalize for rhythm changes.
|
|
3794
|
-
velocityChangeBonus *= Math.pow(Math.min(current.strainTime, last.strainTime) /
|
|
3795
|
-
Math.max(current.strainTime, last.strainTime), 2);
|
|
3796
|
-
}
|
|
3797
|
-
if (last.travelTime) {
|
|
3798
|
-
// Reward sliders based on velocity.
|
|
3799
|
-
sliderBonus = last.travelDistance / last.travelTime;
|
|
3800
|
-
}
|
|
3801
|
-
strain += wiggleBonus * this.wiggleMultiplier;
|
|
3802
|
-
strain += velocityChangeBonus * this.velocityChangeMultiplier;
|
|
3803
|
-
// Add in acute angle bonus or wide angle bonus, whichever is larger.
|
|
3804
|
-
strain += Math.max(acuteAngleBonus * this.acuteAngleMultiplier, wideAngleBonus * this.wideAngleMultiplier);
|
|
3805
|
-
// Apply high circle size bonus
|
|
3806
|
-
strain *= current.smallCircleBonus;
|
|
3807
|
-
// Add in additional slider velocity bonus.
|
|
3808
|
-
if (withSliders) {
|
|
3809
|
-
strain += sliderBonus * this.sliderMultiplier;
|
|
3810
|
-
}
|
|
3682
|
+
const prev = current.previous(0);
|
|
3683
|
+
const travelDistance = (_a = prev === null || prev === void 0 ? void 0 : prev.lazyTravelDistance) !== null && _a !== void 0 ? _a : 0;
|
|
3684
|
+
const distance = travelDistance + current.lazyJumpDistance;
|
|
3685
|
+
const distanceCap = current.normalizedDiameter * 1.2;
|
|
3686
|
+
const distanceScaled = Math.min(distance, distanceCap) / distanceCap;
|
|
3687
|
+
let strain = (distanceScaled * 1000) / current.strainTime;
|
|
3688
|
+
strain *= Math.pow(current.smallCircleBonus, 1.5);
|
|
3689
|
+
strain *= this.highBpmBonus(current.strainTime);
|
|
3811
3690
|
return strain;
|
|
3812
3691
|
}
|
|
3813
|
-
static
|
|
3814
|
-
return
|
|
3815
|
-
}
|
|
3816
|
-
static calculateAcuteAngleBonus(angle) {
|
|
3817
|
-
return osuBase.MathUtils.smoothstep(angle, osuBase.MathUtils.degreesToRadians(140), osuBase.MathUtils.degreesToRadians(40));
|
|
3692
|
+
static highBpmBonus(ms) {
|
|
3693
|
+
return 1 / (1 - Math.pow(0.2, ms / 1000));
|
|
3818
3694
|
}
|
|
3819
3695
|
}
|
|
3820
|
-
OsuAimEvaluator.wideAngleMultiplier = 1.5;
|
|
3821
|
-
OsuAimEvaluator.acuteAngleMultiplier = 2.55;
|
|
3822
|
-
OsuAimEvaluator.sliderMultiplier = 1.35;
|
|
3823
|
-
OsuAimEvaluator.velocityChangeMultiplier = 0.75;
|
|
3824
|
-
OsuAimEvaluator.wiggleMultiplier = 1.02;
|
|
3825
3696
|
|
|
3826
3697
|
/**
|
|
3827
3698
|
* An evaluator for calculating osu!standard Flashlight skill.
|
|
@@ -3840,6 +3711,7 @@ class OsuFlashlightEvaluator {
|
|
|
3840
3711
|
* @param mods The mods used.
|
|
3841
3712
|
*/
|
|
3842
3713
|
static evaluateDifficultyOf(current, mods) {
|
|
3714
|
+
var _a;
|
|
3843
3715
|
if (current.object instanceof osuBase.Spinner) {
|
|
3844
3716
|
return 0;
|
|
3845
3717
|
}
|
|
@@ -3849,6 +3721,10 @@ class OsuFlashlightEvaluator {
|
|
|
3849
3721
|
let result = 0;
|
|
3850
3722
|
let last = current;
|
|
3851
3723
|
let angleRepeatCount = 0;
|
|
3724
|
+
// Hidden's fade-out only affects opacity when the "only fade approach circles" setting is off.
|
|
3725
|
+
const opacityMods = ((_a = mods.get(osuBase.ModHidden)) === null || _a === void 0 ? void 0 : _a.onlyFadeApproachCircles.value)
|
|
3726
|
+
? undefined
|
|
3727
|
+
: mods;
|
|
3852
3728
|
for (let i = 0; i < Math.min(current.index, 10); ++i) {
|
|
3853
3729
|
const currentObject = current.previous(i);
|
|
3854
3730
|
cumulativeStrainTime += last.strainTime;
|
|
@@ -3864,7 +3740,7 @@ class OsuFlashlightEvaluator {
|
|
|
3864
3740
|
const opacityBonus = 1 +
|
|
3865
3741
|
this.maxOpacityBonus *
|
|
3866
3742
|
(1 -
|
|
3867
|
-
current.opacityAt(currentObject.object.startTime,
|
|
3743
|
+
current.opacityAt(currentObject.object.startTime, opacityMods));
|
|
3868
3744
|
result +=
|
|
3869
3745
|
(stackNerf * opacityBonus * scalingFactor * jumpDistance) /
|
|
3870
3746
|
cumulativeStrainTime;
|
|
@@ -3908,149 +3784,660 @@ OsuFlashlightEvaluator.minVelocity = 0.5;
|
|
|
3908
3784
|
OsuFlashlightEvaluator.sliderMultiplier = 1.3;
|
|
3909
3785
|
OsuFlashlightEvaluator.minAngleMultiplier = 0.2;
|
|
3910
3786
|
|
|
3911
|
-
class OsuIsland {
|
|
3912
|
-
constructor(delta, deltaDifferenceEpsilon) {
|
|
3913
|
-
this.delta = Number.MAX_SAFE_INTEGER;
|
|
3914
|
-
this.deltaCount = 0;
|
|
3915
|
-
if (deltaDifferenceEpsilon === undefined) {
|
|
3916
|
-
this.deltaDifferenceEpsilon = delta;
|
|
3917
|
-
}
|
|
3918
|
-
else {
|
|
3919
|
-
this.deltaDifferenceEpsilon = deltaDifferenceEpsilon;
|
|
3920
|
-
this.addDelta(delta);
|
|
3921
|
-
}
|
|
3922
|
-
}
|
|
3923
|
-
addDelta(delta) {
|
|
3924
|
-
if (this.delta === Number.MAX_SAFE_INTEGER) {
|
|
3925
|
-
this.delta = Math.max(Math.trunc(delta), DifficultyHitObject.minDeltaTime);
|
|
3926
|
-
}
|
|
3927
|
-
++this.deltaCount;
|
|
3928
|
-
}
|
|
3929
|
-
isSimilarPolarity(other) {
|
|
3930
|
-
// 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)
|
|
3931
|
-
// naively adding delta check here breaks _a lot_ of maps because of the flawed ratio calculation
|
|
3932
|
-
return this.deltaCount % 2 == other.deltaCount % 2;
|
|
3933
|
-
}
|
|
3934
|
-
equals(other) {
|
|
3935
|
-
return (Math.abs(this.delta - other.delta) < this.deltaDifferenceEpsilon &&
|
|
3936
|
-
this.deltaCount === other.deltaCount);
|
|
3937
|
-
}
|
|
3938
|
-
}
|
|
3939
|
-
|
|
3940
3787
|
/**
|
|
3941
|
-
* An evaluator for calculating osu!standard
|
|
3788
|
+
* An evaluator for calculating osu!standard snap aim difficulty.
|
|
3942
3789
|
*/
|
|
3943
|
-
class
|
|
3790
|
+
class OsuSnapAimEvaluator {
|
|
3944
3791
|
/**
|
|
3945
|
-
*
|
|
3946
|
-
*
|
|
3792
|
+
* Evaluates the difficulty of aiming the current object, based on:
|
|
3793
|
+
*
|
|
3794
|
+
* - cursor velocity to the current object,
|
|
3795
|
+
* - angle difficulty,
|
|
3796
|
+
* - sharp velocity increases,
|
|
3797
|
+
* - and slider difficulty.
|
|
3947
3798
|
*
|
|
3948
3799
|
* @param current The current object.
|
|
3800
|
+
* @param withSliders Whether to take slider difficulty into account.
|
|
3949
3801
|
*/
|
|
3950
|
-
static evaluateDifficultyOf(current) {
|
|
3951
|
-
|
|
3802
|
+
static evaluateDifficultyOf(current, withSliders) {
|
|
3803
|
+
const last = current.previous(0);
|
|
3804
|
+
if (current.object instanceof osuBase.Spinner ||
|
|
3805
|
+
current.index <= 1 ||
|
|
3806
|
+
last.object instanceof osuBase.Spinner) {
|
|
3952
3807
|
return 0;
|
|
3953
3808
|
}
|
|
3954
|
-
const
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
const
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
let
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
this.historyTimeMax) {
|
|
3967
|
-
++rhythmStart;
|
|
3809
|
+
const last2 = current.previous(2);
|
|
3810
|
+
const radius = current.normalizedRadius;
|
|
3811
|
+
const diameter = current.normalizedDiameter;
|
|
3812
|
+
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
|
|
3813
|
+
const currentDistance = withSliders
|
|
3814
|
+
? current.lazyJumpDistance
|
|
3815
|
+
: current.jumpDistance;
|
|
3816
|
+
let currentVelocity = currentDistance / current.strainTime;
|
|
3817
|
+
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
|
|
3818
|
+
if (last.object instanceof osuBase.Slider && withSliders) {
|
|
3819
|
+
const sliderDistance = last.lazyTravelDistance + current.lazyJumpDistance;
|
|
3820
|
+
currentVelocity = Math.max(currentVelocity, sliderDistance / current.strainTime);
|
|
3968
3821
|
}
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
const
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
3822
|
+
const prevDistance = withSliders
|
|
3823
|
+
? last.lazyJumpDistance
|
|
3824
|
+
: last.jumpDistance;
|
|
3825
|
+
const prevVelocity = prevDistance / last.strainTime;
|
|
3826
|
+
// Start strain with regular velocity.
|
|
3827
|
+
let strain = currentVelocity;
|
|
3828
|
+
strain *= this.calculateVectorAngleRepetition(current, last);
|
|
3829
|
+
if (current.angle !== null && last.angle !== null) {
|
|
3830
|
+
const currentAngle = current.angle;
|
|
3831
|
+
const lastAngle = last.angle;
|
|
3832
|
+
// Rewarding angles, take the smaller velocity as base.
|
|
3833
|
+
const velocityInfluence = Math.min(currentVelocity, prevVelocity);
|
|
3834
|
+
let acuteAngleBonus = 0;
|
|
3835
|
+
if (
|
|
3836
|
+
// If rhythms are the same.
|
|
3837
|
+
Math.max(current.strainTime, last.strainTime) <
|
|
3838
|
+
1.25 * Math.min(current.strainTime, last.strainTime)) {
|
|
3839
|
+
acuteAngleBonus =
|
|
3840
|
+
this.calculateAcuteAngleAcuteness(currentAngle);
|
|
3841
|
+
// Penalize angle repetition. It is important to do it _before_ multiplying by anything because we compare raw acuteness here.
|
|
3842
|
+
acuteAngleBonus *=
|
|
3843
|
+
0.08 +
|
|
3844
|
+
0.92 *
|
|
3845
|
+
(1 -
|
|
3846
|
+
Math.min(acuteAngleBonus, Math.pow(this.calculateAcuteAngleAcuteness(lastAngle), 3)));
|
|
3847
|
+
// Apply acute angle bonus for BPM above 300 1/2.
|
|
3848
|
+
acuteAngleBonus *=
|
|
3849
|
+
velocityInfluence *
|
|
3850
|
+
osuBase.MathUtils.smootherstep(osuBase.MathUtils.millisecondsToBPM(current.strainTime, 2), 300, 400) *
|
|
3851
|
+
osuBase.MathUtils.smootherstep(currentDistance, 0, diameter * 2);
|
|
3852
|
+
}
|
|
3853
|
+
let wideAngleBonus = this.calculateWideAngleAcuteness(currentAngle);
|
|
3854
|
+
// Penalize angle repetition. It is important to do it _before_ multiplying by velocity because we compare raw wideness here.
|
|
3855
|
+
wideAngleBonus *=
|
|
3856
|
+
0.25 +
|
|
3857
|
+
0.75 *
|
|
3858
|
+
(1 -
|
|
3859
|
+
Math.min(wideAngleBonus, Math.pow(this.calculateWideAngleAcuteness(lastAngle), 3)));
|
|
3860
|
+
// Rescale velocity for wide angle bonus.
|
|
3861
|
+
const wideAngleTimeScale = 1.45;
|
|
3862
|
+
let wideAngleCurrentVelocity = currentDistance /
|
|
3863
|
+
Math.pow(current.strainTime, wideAngleTimeScale);
|
|
3864
|
+
const wideAnglePrevVelocity = prevDistance / Math.pow(last.strainTime, wideAngleTimeScale);
|
|
3865
|
+
if (last.object instanceof osuBase.Slider && withSliders) {
|
|
3866
|
+
const sliderDistance = last.lazyTravelDistance + current.lazyJumpDistance;
|
|
3867
|
+
wideAngleCurrentVelocity = Math.max(wideAngleCurrentVelocity, sliderDistance /
|
|
3868
|
+
Math.pow(current.strainTime, wideAngleTimeScale));
|
|
3869
|
+
}
|
|
3870
|
+
wideAngleBonus *= Math.min(wideAngleCurrentVelocity, wideAnglePrevVelocity);
|
|
3871
|
+
if (last2 !== null) {
|
|
3872
|
+
// If objects just go back and forth through a middle point - don't give as much wide bonus.
|
|
3873
|
+
// Use previous(2) and previous(0) because angles calculation is done prevprev-prev-curr, so any
|
|
3874
|
+
// object's angle's center point is always the previous object.
|
|
3875
|
+
const distance = last2.object.stackedPosition.getDistance(last.object.stackedPosition);
|
|
3876
|
+
if (distance < 1) {
|
|
3877
|
+
wideAngleBonus *= 1 - 0.55 * (1 - distance);
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
// Add in acute angle bonus or wide angle bonus, whichever is larger.
|
|
3881
|
+
strain += Math.max(acuteAngleBonus * this.acuteAngleMultiplier, wideAngleBonus * this.wideAngleMultiplier);
|
|
3882
|
+
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
|
3883
|
+
// https://www.desmos.com/calculator/dp0v0nvowc
|
|
3884
|
+
strain +=
|
|
3885
|
+
velocityInfluence *
|
|
3886
|
+
osuBase.MathUtils.smootherstep(currentDistance, radius, diameter) *
|
|
3887
|
+
Math.pow(osuBase.MathUtils.reverseLerp(currentDistance, diameter * 3, diameter), 1.8) *
|
|
3888
|
+
osuBase.MathUtils.smootherstep(currentAngle, osuBase.MathUtils.degreesToRadians(110), osuBase.MathUtils.degreesToRadians(60)) *
|
|
3889
|
+
osuBase.MathUtils.smootherstep(prevDistance, radius, diameter) *
|
|
3890
|
+
Math.pow(osuBase.MathUtils.reverseLerp(prevDistance, diameter * 3, diameter), 1.8) *
|
|
3891
|
+
osuBase.MathUtils.smootherstep(lastAngle, osuBase.MathUtils.degreesToRadians(110), osuBase.MathUtils.degreesToRadians(60)) *
|
|
3892
|
+
this.wiggleMultiplier;
|
|
3893
|
+
}
|
|
3894
|
+
if (Math.max(prevVelocity, currentVelocity)) {
|
|
3895
|
+
if (withSliders) {
|
|
3896
|
+
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
|
|
3897
|
+
currentVelocity = currentDistance / current.strainTime;
|
|
3898
|
+
}
|
|
3899
|
+
// Scale with ratio of difference compared to half the max distance.
|
|
3900
|
+
const distanceRatio = osuBase.MathUtils.smoothstep(Math.abs(prevVelocity - currentVelocity) /
|
|
3901
|
+
Math.max(prevVelocity, currentVelocity), 0, 1);
|
|
3902
|
+
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
|
3903
|
+
const overlapVelocityBuff = Math.min((diameter * 1.25) /
|
|
3904
|
+
Math.min(current.strainTime, last.strainTime), Math.abs(prevVelocity - currentVelocity));
|
|
3905
|
+
let velocityChangeBonus = overlapVelocityBuff * distanceRatio;
|
|
3906
|
+
// Penalize for rhythm changes.
|
|
3907
|
+
velocityChangeBonus *= Math.pow(Math.min(current.strainTime, last.strainTime) /
|
|
3908
|
+
Math.max(current.strainTime, last.strainTime), 2);
|
|
3909
|
+
strain += velocityChangeBonus * this.velocityChangeMultiplier;
|
|
3910
|
+
}
|
|
3911
|
+
if (current.object instanceof osuBase.Slider && withSliders) {
|
|
3912
|
+
// Reward sliders based on velocity.
|
|
3913
|
+
const sliderBonus = current.travelDistance / current.travelTime;
|
|
3914
|
+
strain +=
|
|
3915
|
+
(sliderBonus < 1 ? sliderBonus : Math.pow(sliderBonus, 0.75)) *
|
|
3916
|
+
this.sliderMultiplier;
|
|
3917
|
+
}
|
|
3918
|
+
// Apply high circle size bonus
|
|
3919
|
+
strain *= current.smallCircleBonus;
|
|
3920
|
+
strain *= this.highBpmBonus(current.strainTime);
|
|
3921
|
+
return strain;
|
|
3922
|
+
}
|
|
3923
|
+
static calculateWideAngleAcuteness(angle) {
|
|
3924
|
+
return osuBase.MathUtils.smoothstep(angle, osuBase.MathUtils.degreesToRadians(40), osuBase.MathUtils.degreesToRadians(140));
|
|
3925
|
+
}
|
|
3926
|
+
static calculateAcuteAngleAcuteness(angle) {
|
|
3927
|
+
return osuBase.MathUtils.smoothstep(angle, osuBase.MathUtils.degreesToRadians(140), osuBase.MathUtils.degreesToRadians(40));
|
|
3928
|
+
}
|
|
3929
|
+
static highBpmBonus(ms) {
|
|
3930
|
+
return 1 / (1 - Math.pow(0.03, Math.pow(ms / 1000, 0.65)));
|
|
3931
|
+
}
|
|
3932
|
+
static calculateVectorAngleRepetition(current, prev) {
|
|
3933
|
+
if (current.angle === null || prev.angle === null) {
|
|
3934
|
+
return 1;
|
|
3935
|
+
}
|
|
3936
|
+
let constantAngleCount = 0;
|
|
3937
|
+
for (let i = 0; i < this.angleRepetitionNoteLimit; ++i) {
|
|
3938
|
+
const loopObj = current.previous(i);
|
|
3939
|
+
if (!loopObj) {
|
|
3940
|
+
break;
|
|
3941
|
+
}
|
|
3942
|
+
// Only consider vectors in the same jump section, as stopping to change rhythm ruins momentum.
|
|
3943
|
+
if (Math.max(current.strainTime, loopObj.strainTime) >
|
|
3944
|
+
1.1 * Math.min(current.strainTime, loopObj.strainTime)) {
|
|
3945
|
+
break;
|
|
3946
|
+
}
|
|
3947
|
+
if (loopObj.normalizedVectorAngle !== null &&
|
|
3948
|
+
current.normalizedVectorAngle !== null) {
|
|
3949
|
+
const angleDifference = Math.abs(current.normalizedVectorAngle -
|
|
3950
|
+
loopObj.normalizedVectorAngle);
|
|
3951
|
+
// Refer to this Desmos for tuning.
|
|
3952
|
+
// Constants need to be precise so that values stay within the range of 0 and 1.
|
|
3953
|
+
// https://www.desmos.com/calculator/a8jesv5sv2
|
|
3954
|
+
constantAngleCount += Math.cos(8 *
|
|
3955
|
+
Math.min(osuBase.MathUtils.degreesToRadians(11.25), angleDifference));
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
const vectorRepetition = Math.pow(Math.min(0.5 / constantAngleCount, 1), 2);
|
|
3959
|
+
const stackFactor = osuBase.MathUtils.smootherstep(current.lazyJumpDistance, 0, current.normalizedDiameter);
|
|
3960
|
+
const angleDifferenceAdjusted = Math.cos(2 *
|
|
3961
|
+
Math.min(osuBase.MathUtils.degreesToRadians(45), Math.abs(current.angle - prev.angle) * stackFactor));
|
|
3962
|
+
const baseNerf = 1 -
|
|
3963
|
+
this.maximumRepetitionNerf *
|
|
3964
|
+
this.calculateAcuteAngleAcuteness(prev.angle) *
|
|
3965
|
+
angleDifferenceAdjusted;
|
|
3966
|
+
return Math.pow(baseNerf +
|
|
3967
|
+
(1 - baseNerf) *
|
|
3968
|
+
vectorRepetition *
|
|
3969
|
+
this.maximumVectorInfluence *
|
|
3970
|
+
stackFactor, 2);
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
OsuSnapAimEvaluator.wideAngleMultiplier = 9.67;
|
|
3974
|
+
OsuSnapAimEvaluator.acuteAngleMultiplier = 2.41;
|
|
3975
|
+
OsuSnapAimEvaluator.sliderMultiplier = 1.5;
|
|
3976
|
+
OsuSnapAimEvaluator.velocityChangeMultiplier = 0.9;
|
|
3977
|
+
// Increasing this multiplier beyond 1.02 reduces difficulty as distance increases.
|
|
3978
|
+
// Refer to the desmos link above the wiggle bonus calculation.
|
|
3979
|
+
OsuSnapAimEvaluator.wiggleMultiplier = 1.02;
|
|
3980
|
+
OsuSnapAimEvaluator.angleRepetitionNoteLimit = 6;
|
|
3981
|
+
OsuSnapAimEvaluator.maximumRepetitionNerf = 0.15;
|
|
3982
|
+
OsuSnapAimEvaluator.maximumVectorInfluence = 0.5;
|
|
3983
|
+
|
|
3984
|
+
/**
|
|
3985
|
+
* An evaluator for calculating osu!standard flow aim difficulty.
|
|
3986
|
+
*/
|
|
3987
|
+
class OsuFlowAimEvaluator {
|
|
3988
|
+
static evaluateDifficultyOf(current, withSliders) {
|
|
3989
|
+
var _a;
|
|
3990
|
+
if (current.object instanceof osuBase.Spinner ||
|
|
3991
|
+
current.index <= 1 ||
|
|
3992
|
+
((_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.object) instanceof osuBase.Spinner) {
|
|
3993
|
+
return 0;
|
|
3994
|
+
}
|
|
3995
|
+
const last = current.previous(0);
|
|
3996
|
+
const lastLast = current.previous(1);
|
|
3997
|
+
const currentDistance = withSliders
|
|
3998
|
+
? current.lazyJumpDistance
|
|
3999
|
+
: current.jumpDistance;
|
|
4000
|
+
const prevDistance = withSliders
|
|
4001
|
+
? last.lazyJumpDistance
|
|
4002
|
+
: last.jumpDistance;
|
|
4003
|
+
let currentVelocity = currentDistance / current.strainTime;
|
|
4004
|
+
if (last.object instanceof osuBase.Slider && withSliders) {
|
|
4005
|
+
// If the last object is a slider, then we extend the travel velocity through the slider into the current object.
|
|
4006
|
+
const sliderDistance = last.lazyTravelDistance + current.lazyJumpDistance;
|
|
4007
|
+
currentVelocity = Math.max(currentVelocity, sliderDistance / current.strainTime);
|
|
4008
|
+
}
|
|
4009
|
+
const prevVelocity = prevDistance / last.strainTime;
|
|
4010
|
+
let flowDifficulty = currentVelocity;
|
|
4011
|
+
// Apply high circle size bonus to the base velocity.
|
|
4012
|
+
// We use reduced CS bonus here because the bonus was made for an evaluator with a different d/t scaling.
|
|
4013
|
+
flowDifficulty *= Math.sqrt(current.smallCircleBonus);
|
|
4014
|
+
// Rhythm changes are harder to flow.
|
|
4015
|
+
flowDifficulty *=
|
|
4016
|
+
1 +
|
|
4017
|
+
Math.min(0.25, Math.pow((Math.max(current.strainTime, last.strainTime) -
|
|
4018
|
+
Math.min(current.strainTime, last.strainTime)) /
|
|
4019
|
+
50, 4));
|
|
4020
|
+
if (current.angle !== null && last.angle !== null) {
|
|
4021
|
+
// Low angular velocity (consistent angles) is easier to follow than erratic flow.
|
|
4022
|
+
const angleDifference = Math.abs(current.angle - last.angle);
|
|
4023
|
+
const angleDifferenceAdjusted = Math.sin(angleDifference / 2) * 180;
|
|
4024
|
+
const angularVelocity = angleDifferenceAdjusted / (current.strainTime * 0.1);
|
|
4025
|
+
flowDifficulty *= 0.8 + Math.sqrt(angularVelocity / 270);
|
|
4026
|
+
}
|
|
4027
|
+
// If all three notes overlap, do not reward bonuses as there is no required additional movement.
|
|
4028
|
+
let overlappedNotesWeight = 1;
|
|
4029
|
+
if (current.index > 2) {
|
|
4030
|
+
const o1 = this.calculateOverlapFactor(current, last);
|
|
4031
|
+
const o2 = this.calculateOverlapFactor(current, lastLast);
|
|
4032
|
+
const o3 = this.calculateOverlapFactor(last, lastLast);
|
|
4033
|
+
overlappedNotesWeight = 1 - o1 * o2 * o3;
|
|
4034
|
+
}
|
|
4035
|
+
if (current.angle !== null) {
|
|
4036
|
+
// Acute angles are hard to flow.
|
|
4037
|
+
flowDifficulty +=
|
|
4038
|
+
currentVelocity *
|
|
4039
|
+
OsuSnapAimEvaluator.calculateAcuteAngleAcuteness(current.angle) *
|
|
4040
|
+
overlappedNotesWeight;
|
|
4041
|
+
}
|
|
4042
|
+
if (Math.max(prevVelocity, currentVelocity)) {
|
|
4043
|
+
if (withSliders) {
|
|
4044
|
+
currentVelocity = currentDistance / current.strainTime;
|
|
4045
|
+
}
|
|
4046
|
+
// Scale with ratio of difference compared to 0.5 * max distance.
|
|
4047
|
+
const distanceRatio = osuBase.MathUtils.smoothstep(Math.abs(prevVelocity - currentVelocity) /
|
|
4048
|
+
Math.max(prevVelocity, currentVelocity), 0, 1);
|
|
4049
|
+
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
|
4050
|
+
const overlapVelocityBuff = Math.min((current.normalizedDiameter * 1.25) /
|
|
4051
|
+
Math.min(current.strainTime, last.strainTime), Math.abs(prevVelocity - currentVelocity));
|
|
4052
|
+
flowDifficulty +=
|
|
4053
|
+
overlapVelocityBuff *
|
|
4054
|
+
distanceRatio *
|
|
4055
|
+
overlappedNotesWeight *
|
|
4056
|
+
this.velocityChangeMultiplier;
|
|
4057
|
+
}
|
|
4058
|
+
if (current.object instanceof osuBase.Slider && withSliders) {
|
|
4059
|
+
// Include slider velocity to make velocity more consistent with snap.
|
|
4060
|
+
flowDifficulty += current.travelDistance / current.travelTime;
|
|
4061
|
+
}
|
|
4062
|
+
// The final velocity is being raised to a power because flow difficulty scales harder with both high
|
|
4063
|
+
// distance and time, and we want to account for that.
|
|
4064
|
+
flowDifficulty = Math.pow(flowDifficulty, 1.45);
|
|
4065
|
+
// Reduce difficulty for low spacing since spacing below radius is always to be flowed.
|
|
4066
|
+
return (flowDifficulty *
|
|
4067
|
+
osuBase.MathUtils.smootherstep(currentDistance, 0, current.normalizedRadius));
|
|
4068
|
+
}
|
|
4069
|
+
static calculateOverlapFactor(o1, o2) {
|
|
4070
|
+
const distance = o1.object.stackedPosition.getDistance(o2.object.stackedPosition);
|
|
4071
|
+
const { radius } = o1.object;
|
|
4072
|
+
return osuBase.MathUtils.clamp(1 - Math.pow(Math.max(distance - radius, 0) / radius, 2), 0, 1);
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
OsuFlowAimEvaluator.velocityChangeMultiplier = 0.52;
|
|
4076
|
+
|
|
4077
|
+
/**
|
|
4078
|
+
* Evaluator for reading difficulty in osu!standard.
|
|
4079
|
+
*/
|
|
4080
|
+
class OsuReadingEvaluator {
|
|
4081
|
+
/**
|
|
4082
|
+
* Evaluates the difficulty of reading the object.
|
|
4083
|
+
*/
|
|
4084
|
+
static evaluateDifficultyOf(current, mods) {
|
|
4085
|
+
if (current.object instanceof osuBase.Spinner || current.index === 0) {
|
|
4086
|
+
return 0;
|
|
4087
|
+
}
|
|
4088
|
+
// 1.5 circles distance between centers
|
|
4089
|
+
const distanceInfluenceThreshold = current.normalizedDiameter * 1.5;
|
|
4090
|
+
const next = current.next(0);
|
|
4091
|
+
// Only allow velocity to buff
|
|
4092
|
+
const velocity = Math.max(1, current.lazyJumpDistance / current.strainTime);
|
|
4093
|
+
const currentVisibleObjectDensity = this.retrieveCurrentVisibleObjectDensity(current);
|
|
4094
|
+
const pastObjectDifficultyInfluence = this.getPastObjectDifficultyInfluence(current, distanceInfluenceThreshold);
|
|
4095
|
+
const constantAngleNerfFactor = this.getConstantAngleNerfFactor(current);
|
|
4096
|
+
const noteDensityDifficulty = this.calculateDensityDifficulty(next, distanceInfluenceThreshold, velocity, constantAngleNerfFactor, pastObjectDifficultyInfluence, currentVisibleObjectDensity);
|
|
4097
|
+
const hiddenDifficulty = this.calculateHiddenDifficulty(current, mods, pastObjectDifficultyInfluence, currentVisibleObjectDensity, velocity, constantAngleNerfFactor);
|
|
4098
|
+
const preemptDifficulty = this.calculatePreemptDifficulty(velocity, constantAngleNerfFactor, current.timePreempt);
|
|
4099
|
+
let difficulty = osuBase.MathUtils.norm(1.5, preemptDifficulty, hiddenDifficulty, noteDensityDifficulty);
|
|
4100
|
+
// Having less time to process information is harder.
|
|
4101
|
+
difficulty *= this.highBpmBonus(current.strainTime);
|
|
4102
|
+
return difficulty;
|
|
4103
|
+
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Calculates the density difficulty of the current object and how hard it is to aim it because of it based on:
|
|
4106
|
+
*
|
|
4107
|
+
* - cursor velocity to the current object,
|
|
4108
|
+
* - how many times the current object's angle was repeated,
|
|
4109
|
+
* - density of objects visible when the current object appears, and
|
|
4110
|
+
* - density of objects visible when the current object needs to be clicked.
|
|
4111
|
+
*/
|
|
4112
|
+
static calculateDensityDifficulty(next, distanceInfluenceThreshold, velocity, constantAngleNerfFactor, pastObjectDifficultyInfluence, currentVisibleObjectDensity) {
|
|
4113
|
+
// Consider future densities too because it can make the path the cursor takes less clear.
|
|
4114
|
+
let futureObjectDifficultyInfluence = Math.sqrt(currentVisibleObjectDensity);
|
|
4115
|
+
if (next !== null) {
|
|
4116
|
+
// Reduce difficulty if movement to next object is small.
|
|
4117
|
+
futureObjectDifficultyInfluence *= osuBase.MathUtils.smootherstep(next.lazyJumpDistance, 15, distanceInfluenceThreshold);
|
|
4118
|
+
}
|
|
4119
|
+
// Value higher note densities exponentially.
|
|
4120
|
+
let noteDensityDifficulty = Math.pow(pastObjectDifficultyInfluence + futureObjectDifficultyInfluence, 1.7) *
|
|
4121
|
+
0.4 *
|
|
4122
|
+
constantAngleNerfFactor *
|
|
4123
|
+
velocity;
|
|
4124
|
+
// Award only denser than average maps.
|
|
4125
|
+
noteDensityDifficulty = Math.max(0, noteDensityDifficulty - this.densityDifficultyBase);
|
|
4126
|
+
// Apply a soft cap to general density reading to account for partial memorization
|
|
4127
|
+
noteDensityDifficulty =
|
|
4128
|
+
Math.pow(noteDensityDifficulty, 0.45) * this.densityMultiplier;
|
|
4129
|
+
return noteDensityDifficulty;
|
|
4130
|
+
}
|
|
4131
|
+
/**
|
|
4132
|
+
* Calculates the difficulty of aiming the current object when the approach rate is very high based on:
|
|
4133
|
+
*
|
|
4134
|
+
* - cursor velocity to the current object,
|
|
4135
|
+
* - how many times the current object's angle was repeated, and
|
|
4136
|
+
* - how many milliseconds elapse between the approach circle appearing and touching the inner circle.
|
|
4137
|
+
*/
|
|
4138
|
+
static calculatePreemptDifficulty(velocity, constantAngleNerfFactor, preempt) {
|
|
4139
|
+
// Arbitrary curve for the base value preempt difficulty should have as approach rate increases.
|
|
4140
|
+
// https://www.desmos.com/calculator/c175335a71
|
|
4141
|
+
let preemptDifficulty = Math.pow((this.preemptStartingPoint -
|
|
4142
|
+
preempt +
|
|
4143
|
+
Math.abs(preempt - this.preemptStartingPoint)) /
|
|
4144
|
+
2, 2.5) / this.preemptBalancingFactor;
|
|
4145
|
+
preemptDifficulty *= constantAngleNerfFactor * velocity;
|
|
4146
|
+
return preemptDifficulty;
|
|
4147
|
+
}
|
|
4148
|
+
/**
|
|
4149
|
+
* Calculates the difficulty of aiming the current object when the Hidden mod is active based on:
|
|
4150
|
+
*
|
|
4151
|
+
* - cursor velocity to the current object,
|
|
4152
|
+
* - time the current object spends invisible,
|
|
4153
|
+
* - density of objects visible when the current object appears,
|
|
4154
|
+
* - density of objects visible when the current object needs to be clicked,
|
|
4155
|
+
* - how many times the current object's angle was repeated, and
|
|
4156
|
+
* - if the current object is perfectly stacked to the previous one.
|
|
4157
|
+
*/
|
|
4158
|
+
static calculateHiddenDifficulty(current, mods, pastObjectDifficultyInfluence, currentVisibleObjectDensity, velocity, constantAngleNerfFactor) {
|
|
4159
|
+
var _a;
|
|
4160
|
+
if (!mods.has(osuBase.ModHidden) ||
|
|
4161
|
+
((_a = mods.get(osuBase.ModHidden)) === null || _a === void 0 ? void 0 : _a.onlyFadeApproachCircles.value)) {
|
|
4162
|
+
return 0;
|
|
4163
|
+
}
|
|
4164
|
+
// Higher preempt means that time spent invisible is higher too, we want to reward that.
|
|
4165
|
+
const preemptFactor = Math.pow(current.timePreempt, 2.2) * 0.01;
|
|
4166
|
+
// Account for both past and current densities.
|
|
4167
|
+
const densityFactor = Math.pow(currentVisibleObjectDensity + pastObjectDifficultyInfluence, 3.3) * 3;
|
|
4168
|
+
let hiddenDifficulty = (preemptFactor + densityFactor) *
|
|
4169
|
+
constantAngleNerfFactor *
|
|
4170
|
+
velocity *
|
|
4171
|
+
0.01;
|
|
4172
|
+
// Apply a soft cap to general HD reading to account for partial memorization.
|
|
4173
|
+
hiddenDifficulty =
|
|
4174
|
+
Math.pow(hiddenDifficulty, 0.4) * this.hiddenMultiplier;
|
|
4175
|
+
const prev = current.previous(0);
|
|
4176
|
+
// Buff perfect stacks only if current note is completely invisible at the time you click the previous note.
|
|
4177
|
+
if (current.lazyJumpDistance === 0 &&
|
|
4178
|
+
current.opacityAt(prev.object.startTime, mods) === 0 &&
|
|
4179
|
+
// At the same time, we only want to buff them if the current note is already
|
|
4180
|
+
// animating at the time the previous note was clicked.
|
|
4181
|
+
prev.startTime > current.startTime - current.timePreempt) {
|
|
4182
|
+
// Perfect stacks are harder the less time between notes.
|
|
4183
|
+
hiddenDifficulty +=
|
|
4184
|
+
(this.hiddenMultiplier * 2500) /
|
|
4185
|
+
Math.pow(current.strainTime, 1.5);
|
|
4186
|
+
}
|
|
4187
|
+
return hiddenDifficulty;
|
|
4188
|
+
}
|
|
4189
|
+
static getPastObjectDifficultyInfluence(current, distanceInfluenceThreshold) {
|
|
4190
|
+
let pastObjectDifficultyInfluence = 0;
|
|
4191
|
+
for (const loopObj of this.retrievePastVisibleObjects(current)) {
|
|
4192
|
+
let loopDifficulty = current.opacityAt(loopObj.object.startTime);
|
|
4193
|
+
// When aiming an object small distances mean previous objects may be cheesed,
|
|
4194
|
+
// so it doesn't matter whether they were arranged confusingly.
|
|
4195
|
+
loopDifficulty *= osuBase.MathUtils.smootherstep(loopObj.lazyJumpDistance, 15, distanceInfluenceThreshold);
|
|
4196
|
+
// Account less for objects close to the max reading window.
|
|
4197
|
+
const timeBetweenCurrAndLoopObj = current.startTime - loopObj.startTime;
|
|
4198
|
+
const timeNerfFactor = this.getTimeNerfFactor(timeBetweenCurrAndLoopObj);
|
|
4199
|
+
loopDifficulty *= timeNerfFactor;
|
|
4200
|
+
pastObjectDifficultyInfluence += loopDifficulty;
|
|
4201
|
+
}
|
|
4202
|
+
return pastObjectDifficultyInfluence;
|
|
4203
|
+
}
|
|
4204
|
+
/**
|
|
4205
|
+
* Returns a list of objects that are visible on screen at the point in time the current object becomes visible.
|
|
4206
|
+
*/
|
|
4207
|
+
static *retrievePastVisibleObjects(current) {
|
|
4208
|
+
for (let i = 0; i < current.index; ++i) {
|
|
4209
|
+
const hitObject = current.previous(i);
|
|
4210
|
+
if (!hitObject ||
|
|
4211
|
+
current.startTime - hitObject.startTime >
|
|
4212
|
+
this.readingWindowSize ||
|
|
4213
|
+
// Current object not visible at the time object needs to be clicked
|
|
4214
|
+
hitObject.startTime < current.startTime - current.timePreempt) {
|
|
4215
|
+
break;
|
|
4216
|
+
}
|
|
4217
|
+
yield hitObject;
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
/**
|
|
4221
|
+
* Returns the density of objects visible at the point in time the current object needs to be clicked capped by the reading window.
|
|
4222
|
+
*/
|
|
4223
|
+
static retrieveCurrentVisibleObjectDensity(current) {
|
|
4224
|
+
let visibleObjectCount = 0;
|
|
4225
|
+
let hitObject = current.next(0);
|
|
4226
|
+
while (hitObject !== null) {
|
|
4227
|
+
if (hitObject.startTime - current.startTime >
|
|
4228
|
+
this.readingWindowSize ||
|
|
4229
|
+
// Object not visible at the time current object needs to be clicked.
|
|
4230
|
+
current.startTime + hitObject.timePreempt < hitObject.startTime) {
|
|
4231
|
+
break;
|
|
4232
|
+
}
|
|
4233
|
+
const timeBetweenCurrAndLoopObj = hitObject.startTime - current.startTime;
|
|
4234
|
+
const timeNerfFactor = this.getTimeNerfFactor(timeBetweenCurrAndLoopObj);
|
|
4235
|
+
visibleObjectCount +=
|
|
4236
|
+
hitObject.opacityAt(current.object.startTime) * timeNerfFactor;
|
|
4237
|
+
hitObject = hitObject.next(0);
|
|
4238
|
+
}
|
|
4239
|
+
return visibleObjectCount;
|
|
4240
|
+
}
|
|
4241
|
+
/**
|
|
4242
|
+
* Returns a factor of how often the current object's angle has been repeated in a certain time frame.
|
|
4243
|
+
* It does this by checking the difference in angle between current and past objects and sums them based on a range of similarity.
|
|
4244
|
+
* https://www.desmos.com/calculator/eb057a4822
|
|
4245
|
+
*/
|
|
4246
|
+
static getConstantAngleNerfFactor(current) {
|
|
4247
|
+
let constantAngleCount = 0;
|
|
4248
|
+
let index = 0;
|
|
4249
|
+
let currentTimeGap = 0;
|
|
4250
|
+
let loopObjPrev0 = current;
|
|
4251
|
+
let loopObjPrev1 = null;
|
|
4252
|
+
let loopObjPrev2 = null;
|
|
4253
|
+
while (currentTimeGap < this.minimumAngleRelevancyTime) {
|
|
4254
|
+
const loopObj = current.previous(index);
|
|
4255
|
+
if (!loopObj) {
|
|
4256
|
+
break;
|
|
4257
|
+
}
|
|
4258
|
+
// Account less for objects that are close to the time limit.
|
|
4259
|
+
const longIntervalFactor = 1 -
|
|
4260
|
+
osuBase.Interpolation.reverseLerp(loopObj.strainTime, this.maximumAngleRelevancyTime, this.minimumAngleRelevancyTime);
|
|
4261
|
+
if (loopObj.angle !== null && current.angle !== null) {
|
|
4262
|
+
const angleDifference = Math.abs(current.angle - loopObj.angle);
|
|
4263
|
+
let angleDifferenceAlternating = Math.PI;
|
|
4264
|
+
if (loopObjPrev0.angle !== null &&
|
|
4265
|
+
typeof (loopObjPrev1 === null || loopObjPrev1 === void 0 ? void 0 : loopObjPrev1.angle) === "number" &&
|
|
4266
|
+
typeof (loopObjPrev2 === null || loopObjPrev2 === void 0 ? void 0 : loopObjPrev2.angle) === "number") {
|
|
4267
|
+
angleDifferenceAlternating = Math.abs(loopObjPrev1.angle - loopObj.angle);
|
|
4268
|
+
angleDifferenceAlternating += Math.abs(loopObjPrev2.angle - loopObjPrev0.angle);
|
|
4269
|
+
let weight = 1;
|
|
4270
|
+
// Be sure that one of the angles is very sharp, when other is wide.
|
|
4271
|
+
weight *= osuBase.Interpolation.reverseLerp(osuBase.MathUtils.radiansToDegrees(Math.min(loopObj.angle, loopObjPrev0.angle)), 20, 5);
|
|
4272
|
+
weight *= osuBase.Interpolation.reverseLerp(osuBase.MathUtils.radiansToDegrees(Math.max(loopObj.angle, loopObjPrev0.angle)), 60, 120);
|
|
4273
|
+
// Interpolate between max angle difference and rescaled alternating difference, with
|
|
4274
|
+
// harsher scaling compared to normal difference.
|
|
4275
|
+
angleDifferenceAlternating = osuBase.Interpolation.lerp(Math.PI, 0.1 * angleDifferenceAlternating, weight);
|
|
4276
|
+
}
|
|
4277
|
+
const stackFactor = osuBase.MathUtils.smootherstep(loopObj.lazyJumpDistance, 0, current.normalizedRadius);
|
|
4278
|
+
constantAngleCount +=
|
|
4279
|
+
Math.cos(3 *
|
|
4280
|
+
Math.min(osuBase.MathUtils.degreesToRadians(30), Math.min(angleDifference, angleDifferenceAlternating) * stackFactor)) * longIntervalFactor;
|
|
4281
|
+
}
|
|
4282
|
+
currentTimeGap = current.startTime - loopObj.startTime;
|
|
4283
|
+
index++;
|
|
4284
|
+
loopObjPrev2 = loopObjPrev1;
|
|
4285
|
+
loopObjPrev1 = loopObjPrev0;
|
|
4286
|
+
loopObjPrev0 = loopObj;
|
|
4287
|
+
}
|
|
4288
|
+
return osuBase.MathUtils.clamp(2 / constantAngleCount, 0.2, 1);
|
|
4289
|
+
}
|
|
4290
|
+
/**
|
|
4291
|
+
* Returns a nerfing factor for when objects are very distant in time, affecting reading less.
|
|
4292
|
+
*/
|
|
4293
|
+
static getTimeNerfFactor(deltaTime) {
|
|
4294
|
+
return osuBase.MathUtils.clamp(2 - deltaTime / (this.readingWindowSize / 2), 0, 1);
|
|
4295
|
+
}
|
|
4296
|
+
static highBpmBonus(ms) {
|
|
4297
|
+
return 1 / (1 - Math.pow(0.8, ms / 1000));
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
OsuReadingEvaluator.readingWindowSize = 3000; // 3 seconds
|
|
4301
|
+
OsuReadingEvaluator.hiddenMultiplier = 0.28;
|
|
4302
|
+
OsuReadingEvaluator.densityMultiplier = 2.4;
|
|
4303
|
+
OsuReadingEvaluator.densityDifficultyBase = 2.5;
|
|
4304
|
+
OsuReadingEvaluator.preemptBalancingFactor = 140000;
|
|
4305
|
+
OsuReadingEvaluator.preemptStartingPoint = 500; // AR 9.66 in milliseconds
|
|
4306
|
+
OsuReadingEvaluator.minimumAngleRelevancyTime = 2000; // 2 seconds
|
|
4307
|
+
OsuReadingEvaluator.maximumAngleRelevancyTime = 200;
|
|
4308
|
+
|
|
4309
|
+
/**
|
|
4310
|
+
* An evaluator for calculating osu!standard Rhythm skill.
|
|
4311
|
+
*/
|
|
4312
|
+
class OsuRhythmEvaluator {
|
|
4313
|
+
/**
|
|
4314
|
+
* Calculates a rhythm multiplier for the difficulty of the tap associated
|
|
4315
|
+
* with historic data of the current object.
|
|
4316
|
+
*
|
|
4317
|
+
* @param current The current object.
|
|
4318
|
+
*/
|
|
4319
|
+
static evaluateDifficultyOf(current) {
|
|
4320
|
+
if (current.object instanceof osuBase.Spinner) {
|
|
4321
|
+
return 0;
|
|
4322
|
+
}
|
|
4323
|
+
const deltaDifferenceEpsilon = current.hitWindowFor(osuBase.HitResult.Great) * 0.3;
|
|
4324
|
+
let rhythmComplexitySum = 0;
|
|
4325
|
+
let island = new Island(Number.MAX_SAFE_INTEGER);
|
|
4326
|
+
let previousIsland = new Island(Number.MAX_SAFE_INTEGER);
|
|
4327
|
+
const islands = [];
|
|
4328
|
+
// Store the ratio of the current start of an island to buff for tighter rhythms.
|
|
4329
|
+
let startRatio = 0;
|
|
4330
|
+
let firstDeltaSwitch = false;
|
|
4331
|
+
let rhythmStart = 0;
|
|
4332
|
+
const historicalNoteCount = Math.min(current.index, this.historyObjectsMax);
|
|
4333
|
+
while (rhythmStart < historicalNoteCount - 2 &&
|
|
4334
|
+
current.startTime - current.previous(rhythmStart).startTime <
|
|
4335
|
+
this.historyTimeMax) {
|
|
4336
|
+
++rhythmStart;
|
|
4337
|
+
}
|
|
4338
|
+
let prevObject = current.previous(rhythmStart);
|
|
4339
|
+
let lastObject = current.previous(rhythmStart + 1);
|
|
4340
|
+
for (let i = rhythmStart; i > 0; --i) {
|
|
4341
|
+
const currentObject = current.previous(i - 1);
|
|
4342
|
+
if (currentObject.object instanceof osuBase.Spinner) {
|
|
4343
|
+
continue;
|
|
4344
|
+
}
|
|
4345
|
+
// Scale note 0 to 1 from history to now.
|
|
4346
|
+
const timeDecay = (this.historyTimeMax -
|
|
4347
|
+
(current.startTime - currentObject.startTime)) /
|
|
4348
|
+
this.historyTimeMax;
|
|
4349
|
+
const noteDecay = (historicalNoteCount - i) / historicalNoteCount;
|
|
4350
|
+
// Either we're limited by time or limited by object count.
|
|
4351
|
+
const currentHistoricalDecay = Math.min(timeDecay, noteDecay);
|
|
4352
|
+
// Use custom cap value to ensure that at this point delta time is actually zero.
|
|
4353
|
+
const currentDelta = Math.max(currentObject.deltaTime, 1e-7);
|
|
4354
|
+
const prevDelta = Math.max(prevObject.deltaTime, 1e-7);
|
|
4355
|
+
const lastDelta = Math.max(lastObject.deltaTime, 1e-7);
|
|
4356
|
+
if (island.delta == Number.MAX_SAFE_INTEGER) {
|
|
4357
|
+
island = new Island(currentDelta);
|
|
4358
|
+
}
|
|
4359
|
+
// Calculate how much current delta difference deserves a rhythm bonus
|
|
4360
|
+
// This function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e. 100 and 200)
|
|
4361
|
+
const deltaDifference = Math.max(prevDelta, currentDelta) /
|
|
4362
|
+
Math.min(prevDelta, currentDelta);
|
|
4363
|
+
// Reduce ratio bonus if delta difference is too big
|
|
4364
|
+
const differenceMultiplier = osuBase.MathUtils.clamp(2 - deltaDifference / 8, 0, 1);
|
|
4365
|
+
const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon);
|
|
4366
|
+
let effectiveRatio = windowPenalty *
|
|
4367
|
+
this.getEffectiveRatio(deltaDifference) *
|
|
4368
|
+
differenceMultiplier;
|
|
4369
|
+
// If the previous object is a slider, it might be easier to tap since you do not have to do a whole tapping motion.
|
|
4370
|
+
// While a full deltatime might end up some weird ratio, the "unpress->tap" motion might be simple.
|
|
4371
|
+
// For example, a slider-circle-circle pattern should be evaluated as a regular triple and not as a single->double.
|
|
4372
|
+
if (prevObject.object instanceof osuBase.Slider) {
|
|
4373
|
+
const sliderLazyEndDelta = currentObject.minimumJumpTime;
|
|
4374
|
+
const sliderLazyEndDeltaDifference = Math.max(sliderLazyEndDelta, currentDelta) /
|
|
4375
|
+
Math.min(sliderLazyEndDelta, currentDelta);
|
|
4376
|
+
const sliderRealEndDelta = currentObject.lastObjectEndDeltaTime;
|
|
4377
|
+
const sliderRealEndDeltaDifference = Math.max(sliderRealEndDelta, currentDelta) /
|
|
4378
|
+
Math.min(sliderRealEndDelta, currentDelta);
|
|
4379
|
+
const sliderEffectiveRatio = Math.min(this.getEffectiveRatio(sliderLazyEndDeltaDifference), this.getEffectiveRatio(sliderRealEndDeltaDifference));
|
|
4380
|
+
effectiveRatio = Math.min(sliderEffectiveRatio, effectiveRatio);
|
|
4381
|
+
}
|
|
4382
|
+
const isSpeedingUp = prevDelta > currentDelta + deltaDifferenceEpsilon;
|
|
4383
|
+
if (Math.abs(prevDelta - currentDelta) < deltaDifferenceEpsilon) {
|
|
4384
|
+
island.addDelta(currentDelta);
|
|
4385
|
+
}
|
|
4386
|
+
if (firstDeltaSwitch) {
|
|
4387
|
+
if (Math.abs(prevDelta - currentDelta) > deltaDifferenceEpsilon) {
|
|
4388
|
+
// BPM change is into slider, this is easy acc window.
|
|
4389
|
+
if (currentObject.object instanceof osuBase.Slider) {
|
|
4390
|
+
effectiveRatio /= 2;
|
|
4391
|
+
}
|
|
4392
|
+
// Repeated island polarity (2 -> 4, 3 -> 5).
|
|
4393
|
+
if (island.isSimilarPolarity(previousIsland, deltaDifferenceEpsilon)) {
|
|
4394
|
+
effectiveRatio /= 2;
|
|
4395
|
+
}
|
|
4396
|
+
// Previous increase happened a note ago.
|
|
4397
|
+
// Albeit this is a 1/1 -> 1/2-1/4 type of transition, we don't want to buff this.
|
|
4398
|
+
if (lastDelta > prevDelta + deltaDifferenceEpsilon &&
|
|
4399
|
+
prevDelta > currentDelta + deltaDifferenceEpsilon) {
|
|
4400
|
+
effectiveRatio /= 8;
|
|
4401
|
+
}
|
|
4402
|
+
// Repeated island size (ex: triplet -> triplet).
|
|
4403
|
+
// TODO: remove this nerf since its staying here only for balancing purposes because of the flawed ratio calculation
|
|
4404
|
+
if (previousIsland.deltaCount == island.deltaCount) {
|
|
4026
4405
|
effectiveRatio /= 2;
|
|
4027
4406
|
}
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4407
|
+
if (isSpeedingUp) {
|
|
4408
|
+
effectiveRatio *= 0.65;
|
|
4409
|
+
}
|
|
4410
|
+
let found = false;
|
|
4411
|
+
for (const existingIsland of islands) {
|
|
4412
|
+
if (!existingIsland.almostEquals(island, deltaDifferenceEpsilon)) {
|
|
4031
4413
|
continue;
|
|
4032
4414
|
}
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
// Only add island to island counts if they're going one after another.
|
|
4037
|
-
++islandCount;
|
|
4038
|
-
islandCounts.set(currentIsland, islandCount);
|
|
4415
|
+
// Only add island to island counts if they're going one after another.
|
|
4416
|
+
if (previousIsland.almostEquals(island, deltaDifferenceEpsilon)) {
|
|
4417
|
+
++existingIsland.occurrences;
|
|
4039
4418
|
}
|
|
4419
|
+
const power = osuBase.MathUtils.offsetLogistic(island.delta, 58.33, 0.24, 2.75);
|
|
4040
4420
|
// Repeated island (ex: triplet -> triplet).
|
|
4041
4421
|
// Graph: https://www.desmos.com/calculator/pj7an56zwf
|
|
4042
|
-
effectiveRatio *= Math.min(3 /
|
|
4422
|
+
effectiveRatio *= Math.min(3 / existingIsland.occurrences, Math.pow(1 / existingIsland.occurrences, power));
|
|
4423
|
+
found = true;
|
|
4043
4424
|
break;
|
|
4044
4425
|
}
|
|
4045
|
-
if (!
|
|
4046
|
-
|
|
4426
|
+
if (!found && island.deltaCount > 0) {
|
|
4427
|
+
islands.push(island);
|
|
4047
4428
|
}
|
|
4048
4429
|
// Scale down the difficulty if the object is doubletappable.
|
|
4049
4430
|
effectiveRatio *=
|
|
4050
4431
|
1 - prevObject.getDoubletapness(currentObject) * 0.75;
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4432
|
+
if (island.deltaCount > 1) {
|
|
4433
|
+
rhythmComplexitySum +=
|
|
4434
|
+
Math.sqrt(effectiveRatio * startRatio) *
|
|
4435
|
+
currentHistoricalDecay;
|
|
4436
|
+
}
|
|
4437
|
+
else {
|
|
4438
|
+
// Constant difficulty for single-note islands.
|
|
4439
|
+
rhythmComplexitySum += 0.7 * currentHistoricalDecay;
|
|
4440
|
+
}
|
|
4054
4441
|
startRatio = effectiveRatio;
|
|
4055
4442
|
previousIsland = island;
|
|
4056
4443
|
if (prevDelta + deltaDifferenceEpsilon < currentDelta) {
|
|
@@ -4058,7 +4445,7 @@ class OsuRhythmEvaluator {
|
|
|
4058
4445
|
// If we're speeding up, this stays as is and we keep counting island size.
|
|
4059
4446
|
firstDeltaSwitch = false;
|
|
4060
4447
|
}
|
|
4061
|
-
island = new
|
|
4448
|
+
island = new Island(currentDelta);
|
|
4062
4449
|
}
|
|
4063
4450
|
}
|
|
4064
4451
|
else if (prevDelta > currentDelta + deltaDifferenceEpsilon) {
|
|
@@ -4076,20 +4463,28 @@ class OsuRhythmEvaluator {
|
|
|
4076
4463
|
effectiveRatio *= 0.6;
|
|
4077
4464
|
}
|
|
4078
4465
|
startRatio = effectiveRatio;
|
|
4079
|
-
island = new
|
|
4466
|
+
island = new Island(currentDelta);
|
|
4080
4467
|
}
|
|
4081
4468
|
lastObject = prevObject;
|
|
4082
4469
|
prevObject = currentObject;
|
|
4083
4470
|
}
|
|
4084
|
-
|
|
4085
|
-
|
|
4471
|
+
// If the current island is long we don't want the sum to have as big of an effect.
|
|
4472
|
+
rhythmComplexitySum *= osuBase.Interpolation.reverseLerp(island.deltaCount, 22, 3);
|
|
4473
|
+
return (Math.sqrt(4 + rhythmComplexitySum * this.rhythmOverallMultiplier) /
|
|
4086
4474
|
2);
|
|
4087
4475
|
}
|
|
4476
|
+
static getEffectiveRatio(deltaDifference) {
|
|
4477
|
+
// Take only the fractional part of the value since we are only interested in punishing multiples.
|
|
4478
|
+
const deltaDifferenceFraction = deltaDifference - Math.trunc(deltaDifference);
|
|
4479
|
+
return (1 +
|
|
4480
|
+
this.rhythmRatioMultiplier *
|
|
4481
|
+
Math.min(0.5, osuBase.MathUtils.smoothstepBellCurve(deltaDifferenceFraction)));
|
|
4482
|
+
}
|
|
4088
4483
|
}
|
|
4089
4484
|
OsuRhythmEvaluator.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
|
|
4090
4485
|
OsuRhythmEvaluator.historyObjectsMax = 32;
|
|
4091
|
-
OsuRhythmEvaluator.rhythmOverallMultiplier =
|
|
4092
|
-
OsuRhythmEvaluator.rhythmRatioMultiplier =
|
|
4486
|
+
OsuRhythmEvaluator.rhythmOverallMultiplier = 0.95;
|
|
4487
|
+
OsuRhythmEvaluator.rhythmRatioMultiplier = 26;
|
|
4093
4488
|
|
|
4094
4489
|
/**
|
|
4095
4490
|
* An evaluator for calculating osu!standard speed skill.
|
|
@@ -4099,18 +4494,14 @@ class OsuSpeedEvaluator {
|
|
|
4099
4494
|
* Evaluates the difficulty of tapping the current object, based on:
|
|
4100
4495
|
*
|
|
4101
4496
|
* - time between pressing the previous and current object,
|
|
4102
|
-
* - distance between those objects,
|
|
4103
4497
|
* - and how easily they can be cheesed.
|
|
4104
4498
|
*
|
|
4105
4499
|
* @param current The current object.
|
|
4106
|
-
* @param mods The mods applied.
|
|
4107
4500
|
*/
|
|
4108
|
-
static evaluateDifficultyOf(current
|
|
4109
|
-
var _a;
|
|
4501
|
+
static evaluateDifficultyOf(current) {
|
|
4110
4502
|
if (current.object instanceof osuBase.Spinner) {
|
|
4111
4503
|
return 0;
|
|
4112
4504
|
}
|
|
4113
|
-
const prev = current.previous(0);
|
|
4114
4505
|
let strainTime = current.strainTime;
|
|
4115
4506
|
// Nerf doubletappable doubles.
|
|
4116
4507
|
const doubletapness = 1 - current.getDoubletapness(current.next(0));
|
|
@@ -4120,389 +4511,389 @@ class OsuSpeedEvaluator {
|
|
|
4120
4511
|
// speedBonus will be 0.0 for BPM < 200
|
|
4121
4512
|
let speedBonus = 0;
|
|
4122
4513
|
// Add additional scaling bonus for streams/bursts higher than 200bpm
|
|
4123
|
-
if (strainTime
|
|
4514
|
+
if (osuBase.MathUtils.millisecondsToBPM(strainTime) > this.minSpeedBonus) {
|
|
4124
4515
|
speedBonus =
|
|
4125
|
-
0.75 *
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
// Cap distance at spacing threshold
|
|
4130
|
-
const distance = Math.min(singleSpacingThreshold, travelDistance + current.minimumJumpDistance);
|
|
4131
|
-
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
|
|
4132
|
-
let distanceBonus = Math.pow(distance / singleSpacingThreshold, 3.95) *
|
|
4133
|
-
this.DISTANCE_MULTIPLIER;
|
|
4134
|
-
// Apply reduced small circle bonus because flow aim difficulty on small circles does not scale as hard as jumps.
|
|
4135
|
-
distanceBonus *= Math.sqrt(current.smallCircleBonus);
|
|
4136
|
-
if (mods.has(osuBase.ModAutopilot)) {
|
|
4137
|
-
distanceBonus = 0;
|
|
4516
|
+
0.75 *
|
|
4517
|
+
Math.pow((osuBase.MathUtils.bpmToMilliseconds(this.minSpeedBonus) -
|
|
4518
|
+
strainTime) /
|
|
4519
|
+
40, 2);
|
|
4138
4520
|
}
|
|
4139
4521
|
// Base difficulty with all bonuses
|
|
4140
|
-
|
|
4522
|
+
let difficulty = ((1 + speedBonus) * 1000) / strainTime;
|
|
4523
|
+
difficulty *= this.highBpmBonus(current.strainTime);
|
|
4141
4524
|
// Apply penalty if there's doubletappable doubles
|
|
4142
4525
|
return difficulty * doubletapness;
|
|
4143
4526
|
}
|
|
4527
|
+
static highBpmBonus(ms) {
|
|
4528
|
+
return 1 / (1 - Math.pow(0.3, ms / 1000));
|
|
4529
|
+
}
|
|
4144
4530
|
}
|
|
4145
4531
|
// ~200 1/4 BPM streams
|
|
4146
|
-
OsuSpeedEvaluator.minSpeedBonus =
|
|
4147
|
-
OsuSpeedEvaluator.DISTANCE_MULTIPLIER = 0.8;
|
|
4532
|
+
OsuSpeedEvaluator.minSpeedBonus = 200;
|
|
4148
4533
|
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
this.
|
|
4156
|
-
this.
|
|
4534
|
+
/**
|
|
4535
|
+
* Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
|
4536
|
+
*/
|
|
4537
|
+
class OsuAim extends VariableLengthStrainSkill {
|
|
4538
|
+
constructor(mods, withSliders) {
|
|
4539
|
+
super(mods);
|
|
4540
|
+
this.currentStrain = 0;
|
|
4541
|
+
this.skillMultiplierSnap = 70.9;
|
|
4542
|
+
this.skillMultiplierAgility = 2.35;
|
|
4543
|
+
this.skillMultiplierFlow = 242;
|
|
4544
|
+
this.skillMultiplierTotal = 1.12;
|
|
4545
|
+
this.combinedSnapNormExponent = 1.2;
|
|
4546
|
+
/**
|
|
4547
|
+
* The number of sections with the highest strains, which the peak strain reductions will apply to.
|
|
4548
|
+
* This is done in order to decrease their impact on the overall difficulty of the beatmap.
|
|
4549
|
+
*/
|
|
4550
|
+
this.reducedSectionTime = 4000;
|
|
4551
|
+
/**
|
|
4552
|
+
* The baseline multiplier applied to the section with the biggest strain.
|
|
4553
|
+
*/
|
|
4554
|
+
this.reducedStrainBaseline = 0.727;
|
|
4555
|
+
this.sliderStrains = [];
|
|
4556
|
+
this.maxSliderStrain = 0;
|
|
4557
|
+
this.withSliders = withSliders;
|
|
4157
4558
|
}
|
|
4158
|
-
|
|
4159
|
-
|
|
4559
|
+
/**
|
|
4560
|
+
* Obtains the amount of sliders that are considered difficult in terms of relative strain.
|
|
4561
|
+
*/
|
|
4562
|
+
countDifficultSliders() {
|
|
4563
|
+
if (this.sliderStrains.length === 0 || this.maxSliderStrain === 0) {
|
|
4160
4564
|
return 0;
|
|
4161
4565
|
}
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
let ratingMultiplier = 1;
|
|
4174
|
-
const approachRateLengthBonus = 0.95 +
|
|
4175
|
-
0.4 * Math.min(1, this.totalHits / 2000) +
|
|
4176
|
-
(this.totalHits > 2000
|
|
4177
|
-
? Math.log10(this.totalHits / 2000) * 0.5
|
|
4178
|
-
: 0);
|
|
4179
|
-
let approachRateFactor = 0;
|
|
4180
|
-
if (this.approachRate > 10.33) {
|
|
4181
|
-
approachRateFactor = 0.3 * (this.approachRate - 10.33);
|
|
4566
|
+
return this.sliderStrains.reduce((total, strain) => total +
|
|
4567
|
+
1 / (1 + Math.exp(-((strain / this.maxSliderStrain) * 12 - 6))), 0);
|
|
4568
|
+
}
|
|
4569
|
+
/**
|
|
4570
|
+
* Obtains the amount of sliders that are considered difficult in terms of relative strain, weighted by consistency.
|
|
4571
|
+
*
|
|
4572
|
+
* @param difficultyValue The final difficulty value.
|
|
4573
|
+
*/
|
|
4574
|
+
countTopWeightedSliders(difficultyValue) {
|
|
4575
|
+
if (this.sliderStrains.length === 0) {
|
|
4576
|
+
return 0;
|
|
4182
4577
|
}
|
|
4183
|
-
|
|
4184
|
-
|
|
4578
|
+
const consistentTopStrain = difficultyValue * (1 - this.decayWeight);
|
|
4579
|
+
if (consistentTopStrain === 0) {
|
|
4580
|
+
return 0;
|
|
4185
4581
|
}
|
|
4186
|
-
|
|
4187
|
-
|
|
4582
|
+
// Use a weighted sum of all strains. Constants are arbitrary and give nice values
|
|
4583
|
+
return this.sliderStrains.reduce((total, next) => total +
|
|
4584
|
+
osuBase.MathUtils.offsetLogistic(next / consistentTopStrain, 0.88, 10, 1.1), 0);
|
|
4585
|
+
}
|
|
4586
|
+
strainValueAt(current) {
|
|
4587
|
+
if (this.mods.has(osuBase.ModAutopilot)) {
|
|
4588
|
+
return 0;
|
|
4188
4589
|
}
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4590
|
+
const decay = this.strainDecay(current.strainTime);
|
|
4591
|
+
this.currentStrain *= decay;
|
|
4592
|
+
this.currentStrain +=
|
|
4593
|
+
this.calculateAdjustedDifficulty(current) * (1 - decay);
|
|
4594
|
+
if (current.object instanceof osuBase.Slider) {
|
|
4595
|
+
this.sliderStrains.push(this.currentStrain);
|
|
4596
|
+
this.maxSliderStrain = Math.max(this.maxSliderStrain, this.currentStrain);
|
|
4194
4597
|
}
|
|
4195
|
-
|
|
4196
|
-
ratingMultiplier *=
|
|
4197
|
-
0.98 + Math.pow(Math.max(0, this.overallDifficulty), 2) / 2500;
|
|
4198
|
-
return aimRating * Math.cbrt(ratingMultiplier);
|
|
4598
|
+
return this.currentStrain;
|
|
4199
4599
|
}
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4600
|
+
calculateInitialStrain(time, current) {
|
|
4601
|
+
var _a, _b;
|
|
4602
|
+
return (this.currentStrain *
|
|
4603
|
+
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
4604
|
+
}
|
|
4605
|
+
saveToHitObject(current) {
|
|
4606
|
+
if (this.withSliders) {
|
|
4607
|
+
current.aimStrainWithSliders = this.currentStrain;
|
|
4203
4608
|
}
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
speedRating *= 0.5;
|
|
4609
|
+
else {
|
|
4610
|
+
current.aimStrainWithoutSliders = this.currentStrain;
|
|
4207
4611
|
}
|
|
4612
|
+
}
|
|
4613
|
+
calculateAdjustedDifficulty(current) {
|
|
4614
|
+
const snapDifficulty = OsuSnapAimEvaluator.evaluateDifficultyOf(current, this.withSliders) * this.skillMultiplierSnap;
|
|
4615
|
+
const agilityDifficulty = OsuAgilityEvaluator.evaluateDifficultyOf(current) *
|
|
4616
|
+
this.skillMultiplierAgility;
|
|
4617
|
+
const flowDifficulty = OsuFlowAimEvaluator.evaluateDifficultyOf(current, this.withSliders) * this.skillMultiplierFlow;
|
|
4618
|
+
let totalDifficulty = this.calculateTotalValue(snapDifficulty, agilityDifficulty, flowDifficulty);
|
|
4208
4619
|
if (this.mods.has(osuBase.ModMagnetised)) {
|
|
4209
|
-
// Reduce speed rating because of the distance scaling, with maximum reduction being 0.7.
|
|
4210
4620
|
const magnetisedStrength = this.mods.get(osuBase.ModMagnetised).attractionStrength.value;
|
|
4211
|
-
|
|
4212
|
-
}
|
|
4213
|
-
let ratingMultiplier = 1;
|
|
4214
|
-
const approachRateLengthBonus = 0.95 +
|
|
4215
|
-
0.4 * Math.min(1, this.totalHits / 2000) +
|
|
4216
|
-
(this.totalHits > 2000
|
|
4217
|
-
? Math.log10(this.totalHits / 2000) * 0.5
|
|
4218
|
-
: 0);
|
|
4219
|
-
let approachRateFactor = 0;
|
|
4220
|
-
if (this.approachRate > 10.33) {
|
|
4221
|
-
approachRateFactor = 0.3 * (this.approachRate - 10.33);
|
|
4222
|
-
}
|
|
4223
|
-
if (this.mods.has(osuBase.ModAutopilot)) {
|
|
4224
|
-
approachRateFactor = 0;
|
|
4621
|
+
totalDifficulty *= 1 - magnetisedStrength;
|
|
4225
4622
|
}
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
const visibilityFactor = this.calculateSpeedVisibilityFactor();
|
|
4230
|
-
ratingMultiplier += OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.approachRate, visibilityFactor, this.sliderFactor);
|
|
4231
|
-
}
|
|
4232
|
-
ratingMultiplier *=
|
|
4233
|
-
0.95 + Math.pow(Math.max(0, this.overallDifficulty), 2) / 750;
|
|
4234
|
-
return speedRating * Math.cbrt(ratingMultiplier);
|
|
4623
|
+
totalDifficulty *=
|
|
4624
|
+
0.985 + Math.pow(Math.max(0, current.overallDifficulty), 2) / 4000;
|
|
4625
|
+
return totalDifficulty;
|
|
4235
4626
|
}
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4627
|
+
calculateTotalValue(snapDifficulty, agilityDifficulty, flowDifficulty) {
|
|
4628
|
+
// We compare flow to combined snap and agility because snap by itself does not have enough difficulty
|
|
4629
|
+
// to be above flow on streams. Agility, on the other hand, is supposed to measure the rate of cursor
|
|
4630
|
+
// velocity changes while snapping. This means snapping every circle on a stream requires an enormous
|
|
4631
|
+
// amount of agility at which point it is easier to flow.
|
|
4632
|
+
let combinedSnapDifficulty = osuBase.MathUtils.norm(this.combinedSnapNormExponent, snapDifficulty, agilityDifficulty);
|
|
4633
|
+
const pSnap = this.calculateSnapFlowProbability(flowDifficulty / combinedSnapDifficulty);
|
|
4634
|
+
const pFlow = 1 - pSnap;
|
|
4241
4635
|
if (this.mods.has(osuBase.ModTouchDevice)) {
|
|
4242
|
-
|
|
4636
|
+
// We do not adjust agility here since agility represents TD difficulty in a decent enough way.
|
|
4637
|
+
snapDifficulty = Math.pow(snapDifficulty, 0.89);
|
|
4638
|
+
combinedSnapDifficulty = osuBase.MathUtils.norm(this.combinedSnapNormExponent, snapDifficulty, agilityDifficulty);
|
|
4243
4639
|
}
|
|
4244
4640
|
if (this.mods.has(osuBase.ModRelax)) {
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
else if (this.mods.has(osuBase.ModAutopilot)) {
|
|
4248
|
-
flashlightRating *= 0.4;
|
|
4249
|
-
}
|
|
4250
|
-
if (this.mods.has(osuBase.ModMagnetised)) {
|
|
4251
|
-
const magnetisedStrength = this.mods.get(osuBase.ModMagnetised).attractionStrength.value;
|
|
4252
|
-
flashlightRating *= 1 - magnetisedStrength;
|
|
4253
|
-
}
|
|
4254
|
-
if (this.mods.has(osuBase.ModDeflate)) {
|
|
4255
|
-
const deflateInitialScale = this.mods.get(osuBase.ModDeflate).startScale.value;
|
|
4256
|
-
flashlightRating *= osuBase.MathUtils.clamp(osuBase.Interpolation.reverseLerp(deflateInitialScale, 11, 1), 0.1, 1);
|
|
4641
|
+
combinedSnapDifficulty *= 0.75;
|
|
4642
|
+
flowDifficulty *= 0.6;
|
|
4257
4643
|
}
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
ratingMultiplier *=
|
|
4261
|
-
0.7 +
|
|
4262
|
-
0.1 * Math.min(1, this.totalHits / 200) +
|
|
4263
|
-
(this.totalHits > 200
|
|
4264
|
-
? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
|
|
4265
|
-
: 0);
|
|
4266
|
-
// It is important to consider accuracy difficulty when scaling with accuracy.
|
|
4267
|
-
ratingMultiplier *=
|
|
4268
|
-
0.98 + Math.pow(Math.max(0, this.overallDifficulty), 2) / 2500;
|
|
4269
|
-
return flashlightRating * Math.sqrt(ratingMultiplier);
|
|
4270
|
-
}
|
|
4271
|
-
calculateAimVisibilityFactor() {
|
|
4272
|
-
const approachRateFactorEndpoint = 11.5;
|
|
4273
|
-
const mechanicalDifficultyFactor = osuBase.Interpolation.reverseLerp(this.mechanicalDifficultyRating, 5, 10);
|
|
4274
|
-
const approachRateFactorStartingPoint = osuBase.Interpolation.lerp(9, 10.33, mechanicalDifficultyFactor);
|
|
4275
|
-
return osuBase.Interpolation.reverseLerp(this.approachRate, approachRateFactorEndpoint, approachRateFactorStartingPoint);
|
|
4276
|
-
}
|
|
4277
|
-
calculateSpeedVisibilityFactor() {
|
|
4278
|
-
const approachRateFactorEndpoint = 11.5;
|
|
4279
|
-
const mechanicalDifficultyFactor = osuBase.Interpolation.reverseLerp(this.mechanicalDifficultyRating, 5, 10);
|
|
4280
|
-
const approachRateFactorStartingPoint = osuBase.Interpolation.lerp(10, 10.33, mechanicalDifficultyFactor);
|
|
4281
|
-
return osuBase.Interpolation.reverseLerp(this.approachRate, approachRateFactorEndpoint, approachRateFactorStartingPoint);
|
|
4644
|
+
const totalDifficulty = combinedSnapDifficulty * pSnap + flowDifficulty * pFlow;
|
|
4645
|
+
return totalDifficulty * this.skillMultiplierTotal;
|
|
4282
4646
|
}
|
|
4283
4647
|
/**
|
|
4284
|
-
*
|
|
4648
|
+
* Converts the ratio of snap to flow into the probability of snapping or flowing.
|
|
4649
|
+
*
|
|
4650
|
+
* Constraints:
|
|
4651
|
+
* - `P(snap) + P(flow) = 1` (the object is always either snapped or flowed)
|
|
4652
|
+
* - `P(snap) = f(snap / flow)` and `P(flow) = f(flow/snap)` (i.e., snap and flow are symmetric and
|
|
4653
|
+
* reversible). This means `f(x) + f(1/x) = 1`
|
|
4654
|
+
* - `0 <= f(x) <= 1` (cannot have negative or greater than 100% probability of snapping or flowing)
|
|
4655
|
+
*
|
|
4656
|
+
* This logistic function is a solution, which fits nicely with the general idea of interpolation and
|
|
4657
|
+
* provides a tuneable constant.
|
|
4285
4658
|
*
|
|
4286
|
-
* @param
|
|
4287
|
-
* @
|
|
4288
|
-
* @param visibilityFactor The visibility factor to apply.
|
|
4289
|
-
* @param sliderFactor The slider factor to apply.
|
|
4290
|
-
* @returns The visibility bonus multiplier.
|
|
4659
|
+
* @param ratio The ratio.
|
|
4660
|
+
* @returns The probability.
|
|
4291
4661
|
*/
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
// Start from normal curve, rewarding lower AR up to AR 7.
|
|
4296
|
-
// Traceable forcefully requires a lower reading bonus for now as it is post-applied in pp, which make
|
|
4297
|
-
// it multiplicative with the regular AR bonuses.
|
|
4298
|
-
// This means it has an advantage over Hidden, so we decrease the multiplier to compensate.
|
|
4299
|
-
// This should be removed once we are able to apply Traceable bonuses in star rating (requires real-time
|
|
4300
|
-
// difficulty calculations being possible).
|
|
4301
|
-
let readingBonus = (isAlwaysPartiallyVisible ? 0.025 : 0.04) *
|
|
4302
|
-
(12 - Math.max(approachRate, 7));
|
|
4303
|
-
readingBonus *= visibilityFactor;
|
|
4304
|
-
// We want to reward slideraim on low AR less.
|
|
4305
|
-
const sliderVisibilityFactor = Math.pow(sliderFactor, 3);
|
|
4306
|
-
// For AR up to 0, reduce reward for very low ARs when object is visible.
|
|
4307
|
-
if (approachRate < 7) {
|
|
4308
|
-
readingBonus +=
|
|
4309
|
-
(isAlwaysPartiallyVisible ? 0.02 : 0.045) *
|
|
4310
|
-
(7 - Math.max(approachRate, 0)) *
|
|
4311
|
-
sliderVisibilityFactor;
|
|
4662
|
+
calculateSnapFlowProbability(ratio) {
|
|
4663
|
+
if (ratio === 0) {
|
|
4664
|
+
return 0;
|
|
4312
4665
|
}
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
readingBonus +=
|
|
4316
|
-
(isAlwaysPartiallyVisible ? 0.01 : 0.1) *
|
|
4317
|
-
(1 - Math.pow(1.5, approachRate)) *
|
|
4318
|
-
sliderVisibilityFactor;
|
|
4666
|
+
if (Number.isNaN(ratio)) {
|
|
4667
|
+
return 1;
|
|
4319
4668
|
}
|
|
4320
|
-
return
|
|
4321
|
-
}
|
|
4322
|
-
static calculateDifficultyRating(difficultyValue) {
|
|
4323
|
-
return Math.sqrt(difficultyValue) * this.difficultyMultiplier;
|
|
4324
|
-
}
|
|
4325
|
-
}
|
|
4326
|
-
OsuRatingCalculator.difficultyMultiplier = 0.0675;
|
|
4327
|
-
|
|
4328
|
-
/**
|
|
4329
|
-
* Used to processes strain values of difficulty hitobjects, keep track of strain levels caused by the processed objects
|
|
4330
|
-
* and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
|
|
4331
|
-
*/
|
|
4332
|
-
class OsuSkill extends StrainSkill {
|
|
4333
|
-
constructor() {
|
|
4334
|
-
super(...arguments);
|
|
4335
|
-
this.difficulty = 0;
|
|
4669
|
+
return osuBase.MathUtils.logistic(-7.27 * Math.log(ratio));
|
|
4336
4670
|
}
|
|
4337
4671
|
difficultyValue() {
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4672
|
+
let time = 0;
|
|
4673
|
+
let difficulty = 0;
|
|
4674
|
+
for (const strain of this.getReducedStrainPeaks()) {
|
|
4675
|
+
/* Weighting function can be thought of as:
|
|
4676
|
+
b
|
|
4677
|
+
∫ decayWeight^x dx
|
|
4678
|
+
a
|
|
4679
|
+
where a = startTime and b = endTime
|
|
4680
|
+
|
|
4681
|
+
Technically, the function below has been slightly modified from the equation above.
|
|
4682
|
+
The real function would be
|
|
4683
|
+
double weight = Math.pow(this.decayWeight, startTime) - Math.pow(this.decayWeight, endTime))
|
|
4684
|
+
...
|
|
4685
|
+
return difficulty / Math.log(1 / this.decayWeight)
|
|
4686
|
+
E.g. for a decayWeight of 0.9, we're multiplying by 10 instead of 9.49122...
|
|
4687
|
+
|
|
4688
|
+
This change makes it so that a beatmap composed solely of maxSectionLength chunks will have the exact same value
|
|
4689
|
+
when summed in this class and StrainSkill.
|
|
4690
|
+
Doing this ensures the relationship between strain values and difficulty values remains the same between the two
|
|
4691
|
+
classes.
|
|
4692
|
+
*/
|
|
4693
|
+
const startTime = time;
|
|
4694
|
+
const endTime = time + strain.sectionLength / this.maxSectionLength;
|
|
4695
|
+
const weight = Math.pow(this.decayWeight, startTime) -
|
|
4696
|
+
Math.pow(this.decayWeight, endTime);
|
|
4697
|
+
difficulty += strain.value * weight;
|
|
4698
|
+
time = endTime;
|
|
4346
4699
|
}
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4700
|
+
return difficulty / (1 - this.decayWeight);
|
|
4701
|
+
}
|
|
4702
|
+
getReducedStrainPeaks() {
|
|
4703
|
+
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
|
4704
|
+
// These sections will not contribute to the difficulty.
|
|
4705
|
+
const strains = this.currentStrainPeaks
|
|
4706
|
+
.filter((s) => s.value > 0)
|
|
4707
|
+
.sort((a, b) => b.value - a.value);
|
|
4708
|
+
let time = 0;
|
|
4709
|
+
// All strains are removed at the end for optimization.
|
|
4710
|
+
let strainsToRemove = 0;
|
|
4711
|
+
// We are reducing the highest strains first to account for extreme difficulty spikes.
|
|
4712
|
+
// Strains are split into 20ms chunks to try to mitigate inconsistencies caused by reducing strains.
|
|
4713
|
+
const chunkSize = 20;
|
|
4714
|
+
while (strains.length > strainsToRemove &&
|
|
4715
|
+
time < this.reducedSectionTime) {
|
|
4716
|
+
const strain = strains[strainsToRemove];
|
|
4717
|
+
for (let addedTime = 0; addedTime < strain.sectionLength; addedTime += chunkSize) {
|
|
4718
|
+
const scale = Math.log10(osuBase.Interpolation.lerp(1, 10, osuBase.MathUtils.clamp((time + addedTime) / this.reducedSectionTime, 0, 1)));
|
|
4719
|
+
strains.push(new StrainPeak(strain.value *
|
|
4720
|
+
osuBase.Interpolation.lerp(this.reducedStrainBaseline, 1, scale), Math.min(chunkSize, strain.sectionLength - addedTime)));
|
|
4355
4721
|
}
|
|
4356
|
-
|
|
4357
|
-
|
|
4722
|
+
time += strain.sectionLength;
|
|
4723
|
+
++strainsToRemove;
|
|
4358
4724
|
}
|
|
4359
|
-
|
|
4725
|
+
strains.splice(0, strainsToRemove);
|
|
4726
|
+
return strains.sort((a, b) => b.value - a.value);
|
|
4360
4727
|
}
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
class StrainUtils {
|
|
4364
|
-
static countTopWeightedSliders(sliderStrains, difficultyValue) {
|
|
4365
|
-
if (sliderStrains.length === 0) {
|
|
4366
|
-
return 0;
|
|
4367
|
-
}
|
|
4368
|
-
const consistentTopStrain = difficultyValue / 10;
|
|
4369
|
-
if (consistentTopStrain === 0) {
|
|
4370
|
-
return 0;
|
|
4371
|
-
}
|
|
4372
|
-
// Use a weighted sum of all strains. Constants are arbitrary and give nice values
|
|
4373
|
-
return sliderStrains.reduce((total, next) => total +
|
|
4374
|
-
osuBase.MathUtils.offsetLogistic(next / consistentTopStrain, 0.88, 10, 1.1));
|
|
4728
|
+
strainDecay(ms) {
|
|
4729
|
+
return Math.pow(0.2, ms / 1000);
|
|
4375
4730
|
}
|
|
4376
4731
|
}
|
|
4377
4732
|
|
|
4378
4733
|
/**
|
|
4379
|
-
* Represents the skill required to
|
|
4734
|
+
* Represents the skill required to read every object in the beatmap.
|
|
4380
4735
|
*/
|
|
4381
|
-
class
|
|
4382
|
-
constructor(mods,
|
|
4736
|
+
class OsuReading extends HarmonicSkill {
|
|
4737
|
+
constructor(mods, clockRate, hitObjects) {
|
|
4383
4738
|
super(mods);
|
|
4384
|
-
this.
|
|
4385
|
-
this.
|
|
4386
|
-
this.
|
|
4387
|
-
this.
|
|
4388
|
-
this.
|
|
4389
|
-
this.skillMultiplier = 26;
|
|
4390
|
-
this.sliderStrains = [];
|
|
4391
|
-
this.withSliders = withSliders;
|
|
4739
|
+
this.clockRate = clockRate;
|
|
4740
|
+
this.hitObjects = hitObjects;
|
|
4741
|
+
this.currentDifficulty = 0;
|
|
4742
|
+
this.skillMultiplier = 2.5;
|
|
4743
|
+
this.difficultyDecayBase = 0.8;
|
|
4392
4744
|
}
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
*/
|
|
4396
|
-
countDifficultSliders() {
|
|
4397
|
-
if (this.sliderStrains.length === 0) {
|
|
4745
|
+
countTopWeightedObjectDifficulties(difficultyValue) {
|
|
4746
|
+
if (difficultyValue === 0) {
|
|
4398
4747
|
return 0;
|
|
4399
4748
|
}
|
|
4400
|
-
|
|
4401
|
-
if (maxSliderStrain === 0) {
|
|
4749
|
+
if (this.noteWeightSum === 0) {
|
|
4402
4750
|
return 0;
|
|
4403
4751
|
}
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
return StrainUtils.countTopWeightedSliders(this.sliderStrains, this.difficulty);
|
|
4752
|
+
// This is what the top object difficulty is if all object difficulties were identical.
|
|
4753
|
+
const consistentTopNote = difficultyValue / this.noteWeightSum;
|
|
4754
|
+
if (consistentTopNote === 0) {
|
|
4755
|
+
return 0;
|
|
4756
|
+
}
|
|
4757
|
+
return this.objectDifficulties.reduce((total, next) => total +
|
|
4758
|
+
osuBase.MathUtils.offsetLogistic(next / consistentTopNote, 1.15, 5, 1.1), 0);
|
|
4412
4759
|
}
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
this.
|
|
4416
|
-
|
|
4760
|
+
objectDifficultyOf(current) {
|
|
4761
|
+
const decay = this.difficultyDecay(current.deltaTime);
|
|
4762
|
+
this.currentDifficulty *= decay;
|
|
4763
|
+
this.currentDifficulty +=
|
|
4764
|
+
this.calculateAdjustedDifficulty(current) *
|
|
4765
|
+
(1 - decay) *
|
|
4417
4766
|
this.skillMultiplier;
|
|
4418
|
-
this.
|
|
4419
|
-
if (current.object instanceof osuBase.Slider) {
|
|
4420
|
-
this.sliderStrains.push(this.currentAimStrain);
|
|
4421
|
-
}
|
|
4422
|
-
return this.currentAimStrain;
|
|
4767
|
+
return this.currentDifficulty;
|
|
4423
4768
|
}
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4769
|
+
applyDifficultyTransformation(difficulties) {
|
|
4770
|
+
// Assume the first few seconds are completely memorized.
|
|
4771
|
+
const reducedNoteCount = this.calculateReducedNoteCount();
|
|
4772
|
+
for (let i = 0; i < Math.min(difficulties.length, reducedNoteCount); ++i) {
|
|
4773
|
+
difficulties[i] *= Math.log10(osuBase.Interpolation.lerp(1, 10, osuBase.MathUtils.clamp(i / reducedNoteCount, 0, 1)));
|
|
4774
|
+
}
|
|
4428
4775
|
}
|
|
4429
|
-
/**
|
|
4430
|
-
* @param current The hitobject to save to.
|
|
4431
|
-
*/
|
|
4432
4776
|
saveToHitObject(current, difficulty) {
|
|
4433
|
-
|
|
4434
|
-
|
|
4777
|
+
current.readingDifficulty = difficulty;
|
|
4778
|
+
}
|
|
4779
|
+
calculateAdjustedDifficulty(current) {
|
|
4780
|
+
let difficulty = OsuReadingEvaluator.evaluateDifficultyOf(current, this.mods);
|
|
4781
|
+
if (this.mods.has(osuBase.ModTouchDevice)) {
|
|
4782
|
+
difficulty = Math.pow(difficulty, 0.89);
|
|
4435
4783
|
}
|
|
4436
|
-
|
|
4437
|
-
|
|
4784
|
+
if (this.mods.has(osuBase.ModMagnetised)) {
|
|
4785
|
+
const magnetisedStrength = this.mods.get(osuBase.ModMagnetised).attractionStrength.value;
|
|
4786
|
+
difficulty *= 1 - magnetisedStrength;
|
|
4787
|
+
}
|
|
4788
|
+
if (this.mods.has(osuBase.ModRelax)) {
|
|
4789
|
+
difficulty *= 0.4;
|
|
4790
|
+
}
|
|
4791
|
+
else if (this.mods.has(osuBase.ModAutopilot)) {
|
|
4792
|
+
difficulty *= 0.1;
|
|
4793
|
+
}
|
|
4794
|
+
difficulty *=
|
|
4795
|
+
0.825 +
|
|
4796
|
+
Math.pow(Math.max(0, current.overallDifficulty), 2.2) / 1125;
|
|
4797
|
+
return difficulty;
|
|
4798
|
+
}
|
|
4799
|
+
calculateReducedNoteCount() {
|
|
4800
|
+
if (this.hitObjects.length < 2) {
|
|
4801
|
+
return 0;
|
|
4802
|
+
}
|
|
4803
|
+
const reducedDifficultyDuration = 60 * 1000;
|
|
4804
|
+
// We take the 2nd note to match `createDifficultyHitObjects`
|
|
4805
|
+
const firstDifficultyObject = this.hitObjects[1];
|
|
4806
|
+
const reducedDuration = firstDifficultyObject.startTime / this.clockRate +
|
|
4807
|
+
reducedDifficultyDuration;
|
|
4808
|
+
let reducedNoteCount = 0;
|
|
4809
|
+
for (let i = 1; i < this.hitObjects.length; ++i) {
|
|
4810
|
+
const object = this.hitObjects[i];
|
|
4811
|
+
if (object.startTime / this.clockRate > reducedDuration) {
|
|
4812
|
+
break;
|
|
4813
|
+
}
|
|
4814
|
+
++reducedNoteCount;
|
|
4438
4815
|
}
|
|
4816
|
+
return reducedNoteCount;
|
|
4439
4817
|
}
|
|
4440
|
-
|
|
4441
|
-
return Math.pow(this.
|
|
4818
|
+
difficultyDecay(ms) {
|
|
4819
|
+
return Math.pow(this.difficultyDecayBase, ms / 1000);
|
|
4442
4820
|
}
|
|
4443
4821
|
}
|
|
4444
4822
|
|
|
4445
4823
|
/**
|
|
4446
4824
|
* Represents the skill required to press keys or tap with regards to keeping up with the speed at which objects need to be hit.
|
|
4447
4825
|
*/
|
|
4448
|
-
class OsuSpeed extends
|
|
4826
|
+
class OsuSpeed extends HarmonicSkill {
|
|
4449
4827
|
constructor() {
|
|
4450
4828
|
super(...arguments);
|
|
4451
|
-
this.
|
|
4452
|
-
this.
|
|
4453
|
-
this.reducedSectionBaseline = 0.75;
|
|
4454
|
-
this.decayWeight = 0.9;
|
|
4455
|
-
this.currentSpeedStrain = 0;
|
|
4829
|
+
this.harmonicScale = 20;
|
|
4830
|
+
this.currentDifficulty = 0;
|
|
4456
4831
|
this.currentRhythm = 0;
|
|
4457
|
-
this.skillMultiplier = 1.
|
|
4458
|
-
this.
|
|
4459
|
-
this.
|
|
4832
|
+
this.skillMultiplier = 1.16;
|
|
4833
|
+
this.strainDecayBase = 0.3;
|
|
4834
|
+
this.sliderDifficulties = [];
|
|
4835
|
+
this.maxDifficulty = 0;
|
|
4460
4836
|
}
|
|
4461
4837
|
/**
|
|
4462
4838
|
* The amount of notes that are relevant to the difficulty.
|
|
4463
4839
|
*/
|
|
4464
4840
|
relevantNoteCount() {
|
|
4465
|
-
if (this.
|
|
4841
|
+
if (this.objectDifficulties.length === 0 || this.maxDifficulty === 0) {
|
|
4466
4842
|
return 0;
|
|
4467
4843
|
}
|
|
4468
|
-
return this.
|
|
4844
|
+
return this.objectDifficulties.reduce((total, next) => total +
|
|
4845
|
+
1 / (1 + Math.exp(-((next / this.maxDifficulty) * 12 - 6))), 0);
|
|
4469
4846
|
}
|
|
4470
4847
|
/**
|
|
4471
|
-
* Obtains the amount of sliders that are considered difficult in terms of relative
|
|
4848
|
+
* Obtains the amount of sliders that are considered difficult in terms of relative difficulty, weighted by consistency.
|
|
4849
|
+
*
|
|
4850
|
+
* @param difficultyValue The final difficulty value.
|
|
4472
4851
|
*/
|
|
4473
|
-
countTopWeightedSliders() {
|
|
4474
|
-
|
|
4852
|
+
countTopWeightedSliders(difficultyValue) {
|
|
4853
|
+
if (this.sliderDifficulties.length === 0) {
|
|
4854
|
+
return 0;
|
|
4855
|
+
}
|
|
4856
|
+
if (this.noteWeightSum === 0) {
|
|
4857
|
+
return 0;
|
|
4858
|
+
}
|
|
4859
|
+
// What would the top note be if all note values were identical
|
|
4860
|
+
const consistentTopNote = difficultyValue / this.noteWeightSum;
|
|
4861
|
+
if (consistentTopNote === 0) {
|
|
4862
|
+
return 0;
|
|
4863
|
+
}
|
|
4864
|
+
// Use a weighted sum of all notes. Constants are arbitrary and give nice values
|
|
4865
|
+
return this.sliderDifficulties.reduce((total, next) => total +
|
|
4866
|
+
osuBase.MathUtils.offsetLogistic(next / consistentTopNote, 0.88, 10, 1.1), 0);
|
|
4475
4867
|
}
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
this.
|
|
4482
|
-
|
|
4868
|
+
objectDifficultyOf(current) {
|
|
4869
|
+
if (this.mods.has(osuBase.ModRelax)) {
|
|
4870
|
+
return 0;
|
|
4871
|
+
}
|
|
4872
|
+
const decay = this.strainDecay(current.strainTime);
|
|
4873
|
+
this.currentDifficulty *= decay;
|
|
4874
|
+
this.currentDifficulty +=
|
|
4875
|
+
this.calculateAdjustedDifficulty(current) *
|
|
4876
|
+
(1 - decay) *
|
|
4483
4877
|
this.skillMultiplier;
|
|
4484
4878
|
this.currentRhythm = OsuRhythmEvaluator.evaluateDifficultyOf(current);
|
|
4485
|
-
const
|
|
4486
|
-
this.
|
|
4487
|
-
this.maxStrain = Math.max(this.maxStrain, strain);
|
|
4879
|
+
const difficulty = this.currentDifficulty * this.currentRhythm;
|
|
4880
|
+
this.maxDifficulty = Math.max(this.maxDifficulty, difficulty);
|
|
4488
4881
|
if (current.object instanceof osuBase.Slider) {
|
|
4489
|
-
this.
|
|
4882
|
+
this.sliderDifficulties.push(difficulty);
|
|
4490
4883
|
}
|
|
4491
|
-
return
|
|
4492
|
-
}
|
|
4493
|
-
calculateInitialStrain(time, current) {
|
|
4494
|
-
var _a, _b;
|
|
4495
|
-
return (this.currentSpeedStrain *
|
|
4496
|
-
this.currentRhythm *
|
|
4497
|
-
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
4884
|
+
return difficulty;
|
|
4498
4885
|
}
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
*/
|
|
4502
|
-
saveToHitObject(current, difficulty) {
|
|
4503
|
-
current.speedStrain = difficulty;
|
|
4886
|
+
saveToHitObject(current) {
|
|
4887
|
+
current.speedStrain = this.currentDifficulty * this.currentRhythm;
|
|
4504
4888
|
current.rhythmMultiplier = this.currentRhythm;
|
|
4505
4889
|
}
|
|
4890
|
+
calculateAdjustedDifficulty(current) {
|
|
4891
|
+
let difficulty = OsuSpeedEvaluator.evaluateDifficultyOf(current);
|
|
4892
|
+
if (this.mods.has(osuBase.ModAutopilot)) {
|
|
4893
|
+
difficulty *= 0.5;
|
|
4894
|
+
}
|
|
4895
|
+
return difficulty;
|
|
4896
|
+
}
|
|
4506
4897
|
strainDecay(ms) {
|
|
4507
4898
|
return Math.pow(this.strainDecayBase, ms / 1000);
|
|
4508
4899
|
}
|
|
@@ -4530,6 +4921,10 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4530
4921
|
* The flashlight performance value.
|
|
4531
4922
|
*/
|
|
4532
4923
|
this.flashlight = 0;
|
|
4924
|
+
/**
|
|
4925
|
+
* The reading performance value.
|
|
4926
|
+
*/
|
|
4927
|
+
this.reading = 0;
|
|
4533
4928
|
this.greatWindow = 0;
|
|
4534
4929
|
this.okWindow = 0;
|
|
4535
4930
|
this.mehWindow = 0;
|
|
@@ -4570,20 +4965,17 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4570
4965
|
this.greatWindow = hitWindow.greatWindow / clockRate;
|
|
4571
4966
|
this.okWindow = hitWindow.okWindow / clockRate;
|
|
4572
4967
|
this.mehWindow = hitWindow.mehWindow / clockRate;
|
|
4573
|
-
this.approachRate =
|
|
4574
|
-
|
|
4575
|
-
this.overallDifficulty =
|
|
4576
|
-
OsuDifficultyCalculator.calculateRateAdjustedOverallDifficulty(od, clockRate);
|
|
4968
|
+
this.approachRate = this.calculateRateAdjustedApproachRate(ar, clockRate);
|
|
4969
|
+
this.overallDifficulty = (79.5 - this.greatWindow) / 6;
|
|
4577
4970
|
this.speedDeviation = this.calculateSpeedDeviation();
|
|
4578
4971
|
this.aim = this.calculateAimValue();
|
|
4579
4972
|
this.speed = this.calculateSpeedValue();
|
|
4580
4973
|
this.accuracy = this.calculateAccuracyValue();
|
|
4581
4974
|
this.flashlight = this.calculateFlashlightValue();
|
|
4975
|
+
this.reading = this.calculateReadingValue();
|
|
4976
|
+
const cognitionValue = OsuDifficultyCalculator.sumCognitionDifficulty(this.reading, this.flashlight);
|
|
4582
4977
|
this.total =
|
|
4583
|
-
|
|
4584
|
-
Math.pow(this.speed, 1.1) +
|
|
4585
|
-
Math.pow(this.accuracy, 1.1) +
|
|
4586
|
-
Math.pow(this.flashlight, 1.1), 1 / 1.1) * finalMultiplier;
|
|
4978
|
+
osuBase.MathUtils.norm(OsuPerformanceCalculator.normExponent, this.aim, this.speed, this.accuracy, cognitionValue) * finalMultiplier;
|
|
4587
4979
|
}
|
|
4588
4980
|
/**
|
|
4589
4981
|
* Calculates the aim performance value of the beatmap.
|
|
@@ -4616,7 +5008,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4616
5008
|
}
|
|
4617
5009
|
let aimValue = OsuAim.difficultyToPerformance(aimDifficulty);
|
|
4618
5010
|
// Longer maps are worth more
|
|
4619
|
-
let lengthBonus = 0.95 + 0.
|
|
5011
|
+
let lengthBonus = 0.95 + 0.35 * Math.min(1, this.totalHits / 2000);
|
|
4620
5012
|
if (this.totalHits > 2000) {
|
|
4621
5013
|
lengthBonus += Math.log10(this.totalHits / 2000) * 0.5;
|
|
4622
5014
|
}
|
|
@@ -4641,7 +5033,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4641
5033
|
else if (this.mods.has(osuBase.ModTraceable)) {
|
|
4642
5034
|
aimValue *=
|
|
4643
5035
|
1 +
|
|
4644
|
-
|
|
5036
|
+
this.calculateTraceableBonus(this.difficultyAttributes.sliderFactor);
|
|
4645
5037
|
}
|
|
4646
5038
|
// Scale the aim value with accuracy.
|
|
4647
5039
|
aimValue *= this.computedAccuracy.value();
|
|
@@ -4656,45 +5048,23 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4656
5048
|
return 0;
|
|
4657
5049
|
}
|
|
4658
5050
|
let speedValue = OsuSpeed.difficultyToPerformance(this.difficultyAttributes.speedDifficulty);
|
|
4659
|
-
// Longer maps are worth more
|
|
4660
|
-
let lengthBonus = 0.95 + 0.4 * Math.min(1, this.totalHits / 2000);
|
|
4661
|
-
if (this.totalHits > 2000) {
|
|
4662
|
-
lengthBonus += Math.log10(this.totalHits / 2000) * 0.5;
|
|
4663
|
-
}
|
|
4664
|
-
speedValue *= lengthBonus;
|
|
4665
5051
|
if (this._effectiveMissCount > 0) {
|
|
4666
5052
|
const speedEstimatedSliderBreaks = this.calculateEstimatedSliderBreaks(this.difficultyAttributes.speedTopWeightedSliderFactor);
|
|
4667
5053
|
const relevantMissCount = Math.min(this._effectiveMissCount + speedEstimatedSliderBreaks, this.totalImperfectHits + this.sliderTicksMissed);
|
|
4668
5054
|
speedValue *= this.calculateMissPenalty(relevantMissCount, this.difficultyAttributes.speedDifficultStrainCount);
|
|
4669
5055
|
}
|
|
4670
|
-
// Traceable bonuses are excluded when Blinds is present, as the increased visual difficulty is
|
|
4671
|
-
// redundant when notes cannot be seen.
|
|
4672
5056
|
if (this.mods.has(osuBase.ModBlinds)) {
|
|
4673
5057
|
// Increasing the speed value by object count for Blinds is not ideal, so the minimum buff is given.
|
|
4674
5058
|
speedValue *= 1.12;
|
|
4675
5059
|
}
|
|
4676
|
-
else if (this.mods.has(osuBase.ModTraceable)) {
|
|
4677
|
-
speedValue *=
|
|
4678
|
-
1 +
|
|
4679
|
-
OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.approachRate, undefined, this.difficultyAttributes.sliderFactor);
|
|
4680
|
-
}
|
|
4681
|
-
// Calculate accuracy assuming the worst case scenario.
|
|
4682
|
-
const countGreat = this.computedAccuracy.n300;
|
|
4683
|
-
const countOk = this.computedAccuracy.n100;
|
|
4684
|
-
const countMeh = this.computedAccuracy.n50;
|
|
4685
|
-
const relevantTotalDiff = this.totalHits - this.difficultyAttributes.speedNoteCount;
|
|
4686
|
-
const relevantAccuracy = new osuBase.Accuracy(this.difficultyAttributes.speedNoteCount > 0
|
|
4687
|
-
? {
|
|
4688
|
-
n300: Math.max(0, countGreat - relevantTotalDiff),
|
|
4689
|
-
n100: Math.max(0, countOk - Math.max(0, relevantTotalDiff - countGreat)),
|
|
4690
|
-
n50: Math.max(0, countMeh -
|
|
4691
|
-
Math.max(0, relevantTotalDiff - countGreat - countOk)),
|
|
4692
|
-
}
|
|
4693
|
-
: // Set accuracy to 0.
|
|
4694
|
-
{ n300: 0, nobjects: 1 });
|
|
4695
5060
|
speedValue *= this.calculateSpeedHighDeviationNerf();
|
|
4696
|
-
//
|
|
4697
|
-
|
|
5061
|
+
// An effective hit window is created based on the speed SR. The higher the speed difficulty, the shorter the hit window.
|
|
5062
|
+
// For example, a speed SR of 4 leads to an effective hit window of 20ms, which is OD 10.
|
|
5063
|
+
const effectiveHitWindow = 20 * Math.pow(4 / this.difficultyAttributes.speedDifficulty, 0.35);
|
|
5064
|
+
// Find the proportion of 300s on speed notes assuming the hit window was the effective hit window.
|
|
5065
|
+
const effectiveAccuracy = osuBase.ErrorFunction.erf(effectiveHitWindow / this.speedDeviation);
|
|
5066
|
+
// Scale speed value by normalized accuracy.
|
|
5067
|
+
speedValue *= Math.pow(effectiveAccuracy, 2);
|
|
4698
5068
|
return speedValue;
|
|
4699
5069
|
}
|
|
4700
5070
|
/**
|
|
@@ -4704,7 +5074,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4704
5074
|
if (this.mods.has(osuBase.ModRelax)) {
|
|
4705
5075
|
return 0;
|
|
4706
5076
|
}
|
|
4707
|
-
const ncircles = this.mods.has(osuBase.ModScoreV2)
|
|
5077
|
+
const ncircles = !this.usingClassicSliderAccuracy || this.mods.has(osuBase.ModScoreV2)
|
|
4708
5078
|
? this.totalHits - this.difficultyAttributes.spinnerCount
|
|
4709
5079
|
: this.difficultyAttributes.hitCircleCount;
|
|
4710
5080
|
if (ncircles === 0) {
|
|
@@ -4717,21 +5087,18 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4717
5087
|
// It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points.
|
|
4718
5088
|
Math.pow(realAccuracy.n300 < 0 ? 0 : realAccuracy.value(), 24) *
|
|
4719
5089
|
2.83;
|
|
4720
|
-
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
|
|
4721
|
-
accuracyValue *= Math.
|
|
5090
|
+
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
|
|
5091
|
+
accuracyValue *= Math.pow(ncircles / 1000, ncircles < 1000 ? 0.3 : 0.1);
|
|
4722
5092
|
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
|
|
4723
5093
|
if (this.mods.has(osuBase.ModBlinds)) {
|
|
4724
5094
|
accuracyValue *= 1.14;
|
|
4725
5095
|
}
|
|
4726
|
-
else if (this.mods.has(osuBase.
|
|
5096
|
+
else if (this.mods.has(osuBase.ModTraceable)) {
|
|
4727
5097
|
// Decrease bonus for AR > 10.
|
|
4728
5098
|
accuracyValue *=
|
|
4729
5099
|
1 +
|
|
4730
5100
|
0.08 * osuBase.Interpolation.reverseLerp(this.approachRate, 11.5, 10);
|
|
4731
5101
|
}
|
|
4732
|
-
if (this.mods.has(osuBase.ModFlashlight)) {
|
|
4733
|
-
accuracyValue *= 1.02;
|
|
4734
|
-
}
|
|
4735
5102
|
return accuracyValue;
|
|
4736
5103
|
}
|
|
4737
5104
|
/**
|
|
@@ -4751,17 +5118,23 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4751
5118
|
Math.pow(1 -
|
|
4752
5119
|
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
|
|
4753
5120
|
}
|
|
4754
|
-
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
4755
|
-
flashlightValue *=
|
|
4756
|
-
0.7 +
|
|
4757
|
-
0.1 * Math.min(1, this.totalHits / 200) +
|
|
4758
|
-
(this.totalHits > 200
|
|
4759
|
-
? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
|
|
4760
|
-
: 0);
|
|
4761
5121
|
// Scale the flashlight value with accuracy slightly.
|
|
4762
5122
|
flashlightValue *= 0.5 + this.computedAccuracy.value() / 2;
|
|
4763
5123
|
return flashlightValue;
|
|
4764
5124
|
}
|
|
5125
|
+
/**
|
|
5126
|
+
* Calculates the reading performance value of the beatmap.
|
|
5127
|
+
*/
|
|
5128
|
+
calculateReadingValue() {
|
|
5129
|
+
let readingValue = OsuReading.difficultyToPerformance(this.difficultyAttributes.readingDifficulty);
|
|
5130
|
+
if (this.effectiveMissCount > 0) {
|
|
5131
|
+
const aimEstimatedSliderBreaks = this.calculateEstimatedSliderBreaks(this.difficultyAttributes.aimTopWeightedSliderFactor);
|
|
5132
|
+
readingValue *= this.calculateMissPenalty(this.effectiveMissCount + aimEstimatedSliderBreaks, this.difficultyAttributes.readingDifficultNoteCount);
|
|
5133
|
+
}
|
|
5134
|
+
// Scale the reading value with accuracy _harshly_.
|
|
5135
|
+
readingValue *= Math.pow(this.computedAccuracy.value(), 3);
|
|
5136
|
+
return readingValue;
|
|
5137
|
+
}
|
|
4765
5138
|
/**
|
|
4766
5139
|
* Calculates a strain-based miss penalty.
|
|
4767
5140
|
*
|
|
@@ -4773,9 +5146,8 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4773
5146
|
if (missCount === 0) {
|
|
4774
5147
|
return 1;
|
|
4775
5148
|
}
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
1));
|
|
5149
|
+
// https://www.desmos.com/calculator/naggvbcz0a
|
|
5150
|
+
return 0.93 / (missCount / (4 * Math.log(difficultStrainCount)) + 1);
|
|
4779
5151
|
}
|
|
4780
5152
|
/**
|
|
4781
5153
|
* Estimates a player's deviation on speed notes using {@link calculateDeviation}, assuming worst-case.
|
|
@@ -4877,19 +5249,21 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4877
5249
|
return (osuBase.Interpolation.lerp(adjustedSpeedValue, speedValue, t) / speedValue);
|
|
4878
5250
|
}
|
|
4879
5251
|
calculateEstimatedSliderBreaks(topWeightedSliderFactor) {
|
|
4880
|
-
const
|
|
4881
|
-
if (!this.usingClassicSliderAccuracy ||
|
|
5252
|
+
const nonMissMistakes = this.computedAccuracy.n100 + this.computedAccuracy.n50;
|
|
5253
|
+
if (!this.usingClassicSliderAccuracy || nonMissMistakes === 0) {
|
|
4882
5254
|
return 0;
|
|
4883
5255
|
}
|
|
4884
5256
|
const missedComboPercent = 1 - this.combo / this.difficultyAttributes.maxCombo;
|
|
4885
|
-
let estimatedSliderBreaks = Math.min(
|
|
4886
|
-
// Scores with more Oks are more likely to have slider breaks.
|
|
4887
|
-
|
|
5257
|
+
let estimatedSliderBreaks = Math.min(nonMissMistakes, this._effectiveMissCount * topWeightedSliderFactor);
|
|
5258
|
+
// Scores with more Oks and Mehs are more likely to have slider breaks.
|
|
5259
|
+
// We add an arbitrary value to both sides of the division to make it more stable on extreme ends.
|
|
5260
|
+
const nonMissMistakeAdjustment = (nonMissMistakes - estimatedSliderBreaks + 4.5) /
|
|
5261
|
+
(nonMissMistakes + 4);
|
|
4888
5262
|
// There is a low probability of extra slider breaks on effective miss counts close to 1, as
|
|
4889
5263
|
// score based calculations are good at indicating if only a single break occurred.
|
|
4890
5264
|
estimatedSliderBreaks *= osuBase.MathUtils.smoothstep(this._effectiveMissCount, 1, 2);
|
|
4891
5265
|
return (estimatedSliderBreaks *
|
|
4892
|
-
|
|
5266
|
+
nonMissMistakeAdjustment *
|
|
4893
5267
|
osuBase.MathUtils.offsetLogistic(missedComboPercent, 0.33, 15));
|
|
4894
5268
|
}
|
|
4895
5269
|
/**
|
|
@@ -4898,16 +5272,27 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4898
5272
|
calculateComboBasedEstimatedMissCount() {
|
|
4899
5273
|
let missCount = this.computedAccuracy.nmiss;
|
|
4900
5274
|
const { combo } = this;
|
|
4901
|
-
const { sliderCount, maxCombo } = this.difficultyAttributes;
|
|
5275
|
+
const { aimTopWeightedSliderFactor, sliderCount, maxCombo } = this.difficultyAttributes;
|
|
4902
5276
|
if (sliderCount <= 0) {
|
|
4903
5277
|
return missCount;
|
|
4904
5278
|
}
|
|
4905
5279
|
if (this.usingClassicSliderAccuracy) {
|
|
5280
|
+
// If sliders in the beatmap are hard, it's likely for player to drop sliderends.
|
|
5281
|
+
// However, if the beatmap has easy sliders, it's more likely for player to sliderbreak.
|
|
5282
|
+
const likelyMissedSliderendPortion = 0.04 +
|
|
5283
|
+
0.06 * Math.pow(Math.min(aimTopWeightedSliderFactor, 1), 2);
|
|
4906
5284
|
// Consider that full combo is maximum combo minus dropped slider tails since
|
|
4907
5285
|
// they don't contribute to combo but also don't break it.
|
|
4908
5286
|
// In classic scores, we can't know the amount of dropped sliders so we estimate
|
|
4909
5287
|
// to 10% of all sliders in the beatmap.
|
|
4910
|
-
const fullComboThreshold = maxCombo -
|
|
5288
|
+
const fullComboThreshold = maxCombo -
|
|
5289
|
+
Math.min(
|
|
5290
|
+
// 4 is the minimum leniency baseline to ensure that dropping one (for few) sliderends will
|
|
5291
|
+
// not instantly be treated as a sliderbreak even in cases where the slider count is low.
|
|
5292
|
+
// 4 was picked because in a lot of short stream beatmaps with small amount of sliders, there
|
|
5293
|
+
// are 2-3 sliders on which sliderends are often dropped. This is a kind of optimization to
|
|
5294
|
+
// achieve the most accurate result on average.
|
|
5295
|
+
4 + likelyMissedSliderendPortion * sliderCount, sliderCount);
|
|
4911
5296
|
if (combo < fullComboThreshold) {
|
|
4912
5297
|
missCount = fullComboThreshold / Math.max(1, combo);
|
|
4913
5298
|
}
|
|
@@ -4934,6 +5319,36 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4934
5319
|
}
|
|
4935
5320
|
return missCount;
|
|
4936
5321
|
}
|
|
5322
|
+
calculateTraceableBonus(sliderFactor = 1) {
|
|
5323
|
+
// We want to reward slider aim less, more so at lower AR.
|
|
5324
|
+
const highApproachRateSliderVisibilityFactor = 0.5 + Math.pow(sliderFactor, 6) / 2;
|
|
5325
|
+
const lowApproachRateSliderVisibilityFactor = Math.pow(sliderFactor, 6);
|
|
5326
|
+
let traceableBonus = 0.0275;
|
|
5327
|
+
// Start from normal curve, rewarding lower AR up to AR7.
|
|
5328
|
+
traceableBonus +=
|
|
5329
|
+
0.025 *
|
|
5330
|
+
(12.0 - Math.max(this.approachRate, 7)) *
|
|
5331
|
+
highApproachRateSliderVisibilityFactor;
|
|
5332
|
+
// For AR up to 0 - reduce reward for very low ARs when object is visible.
|
|
5333
|
+
if (this.approachRate < 7) {
|
|
5334
|
+
traceableBonus +=
|
|
5335
|
+
0.025 *
|
|
5336
|
+
(7 - Math.max(this.approachRate, 0)) *
|
|
5337
|
+
lowApproachRateSliderVisibilityFactor;
|
|
5338
|
+
}
|
|
5339
|
+
// Starting from AR0 - cap values so they won't grow to infinity.
|
|
5340
|
+
if (this.approachRate < 0) {
|
|
5341
|
+
traceableBonus +=
|
|
5342
|
+
0.025 *
|
|
5343
|
+
(1 - Math.pow(1.5, this.approachRate)) *
|
|
5344
|
+
lowApproachRateSliderVisibilityFactor;
|
|
5345
|
+
}
|
|
5346
|
+
return traceableBonus;
|
|
5347
|
+
}
|
|
5348
|
+
calculateRateAdjustedApproachRate(approachRate, clockRate) {
|
|
5349
|
+
const preempt = osuBase.BeatmapDifficulty.difficultyRange(approachRate, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin) / clockRate;
|
|
5350
|
+
return osuBase.BeatmapDifficulty.inverseDifficultyRange(preempt, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin);
|
|
5351
|
+
}
|
|
4937
5352
|
toString() {
|
|
4938
5353
|
return (this.total.toFixed(2) +
|
|
4939
5354
|
" pp (" +
|
|
@@ -4944,10 +5359,13 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4944
5359
|
this.accuracy.toFixed(2) +
|
|
4945
5360
|
" accuracy, " +
|
|
4946
5361
|
this.flashlight.toFixed(2) +
|
|
4947
|
-
" flashlight
|
|
5362
|
+
" flashlight, " +
|
|
5363
|
+
this.reading.toFixed(2) +
|
|
5364
|
+
" reading)");
|
|
4948
5365
|
}
|
|
4949
5366
|
}
|
|
4950
|
-
OsuPerformanceCalculator.finalMultiplier = 1.
|
|
5367
|
+
OsuPerformanceCalculator.finalMultiplier = 1.12;
|
|
5368
|
+
OsuPerformanceCalculator.normExponent = 1.1;
|
|
4951
5369
|
|
|
4952
5370
|
/**
|
|
4953
5371
|
* Represents an osu!standard hit object with difficulty calculation values.
|
|
@@ -4963,34 +5381,83 @@ class OsuDifficultyHitObject extends DifficultyHitObject {
|
|
|
4963
5381
|
this.mode = osuBase.Modes.Osu;
|
|
4964
5382
|
}
|
|
4965
5383
|
get smallCircleBonus() {
|
|
4966
|
-
return Math.max(1, 1 + (30 - this.object.radius) /
|
|
5384
|
+
return Math.max(1, 1 + (30 - this.object.radius) / 70);
|
|
5385
|
+
}
|
|
5386
|
+
get overallDifficulty() {
|
|
5387
|
+
const hitWindowGreat = this.hitWindowFor(osuBase.HitResult.Great) / 2;
|
|
5388
|
+
return (79.5 - hitWindowGreat) / 6;
|
|
4967
5389
|
}
|
|
4968
5390
|
}
|
|
4969
5391
|
|
|
4970
5392
|
/**
|
|
4971
|
-
*
|
|
5393
|
+
* Used to processes strain values of difficulty hitobjects, keep track of strain levels caused by the processed objects
|
|
5394
|
+
* and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
|
|
4972
5395
|
*/
|
|
4973
|
-
class
|
|
5396
|
+
class OsuSkill extends StrainSkill {
|
|
4974
5397
|
constructor() {
|
|
4975
5398
|
super(...arguments);
|
|
4976
|
-
this.
|
|
5399
|
+
this.difficulty = 0;
|
|
5400
|
+
}
|
|
5401
|
+
difficultyValue() {
|
|
5402
|
+
const strains = this.currentStrainPeaks.slice().sort((a, b) => b - a);
|
|
5403
|
+
if (this.reducedSectionCount > 0) {
|
|
5404
|
+
// We are reducing the highest strains first to account for extreme difficulty spikes.
|
|
5405
|
+
for (let i = 0; i < Math.min(strains.length, this.reducedSectionCount); ++i) {
|
|
5406
|
+
const scale = Math.log10(osuBase.Interpolation.lerp(1, 10, osuBase.MathUtils.clamp(i / this.reducedSectionCount, 0, 1)));
|
|
5407
|
+
strains[i] *= osuBase.Interpolation.lerp(this.reducedSectionBaseline, 1, scale);
|
|
5408
|
+
}
|
|
5409
|
+
strains.sort((a, b) => b - a);
|
|
5410
|
+
}
|
|
5411
|
+
// Difficulty is the weighted sum of the highest strains from every section.
|
|
5412
|
+
// We're sorting from highest to lowest strain.
|
|
5413
|
+
this.difficulty = 0;
|
|
5414
|
+
let weight = 1;
|
|
5415
|
+
for (const strain of strains) {
|
|
5416
|
+
const addition = strain * weight;
|
|
5417
|
+
if (this.difficulty + addition === this.difficulty) {
|
|
5418
|
+
break;
|
|
5419
|
+
}
|
|
5420
|
+
this.difficulty += addition;
|
|
5421
|
+
weight *= this.decayWeight;
|
|
5422
|
+
}
|
|
5423
|
+
return this.difficulty;
|
|
5424
|
+
}
|
|
5425
|
+
}
|
|
5426
|
+
|
|
5427
|
+
/**
|
|
5428
|
+
* Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
|
|
5429
|
+
*/
|
|
5430
|
+
class OsuFlashlight extends OsuSkill {
|
|
5431
|
+
static difficultyToPerformance(difficulty) {
|
|
5432
|
+
return Math.pow(difficulty, 2) * 25;
|
|
5433
|
+
}
|
|
5434
|
+
constructor(mods, totalObjects) {
|
|
5435
|
+
super(mods);
|
|
5436
|
+
this.totalObjects = totalObjects;
|
|
4977
5437
|
this.reducedSectionCount = 0;
|
|
4978
5438
|
this.reducedSectionBaseline = 1;
|
|
4979
5439
|
this.decayWeight = 1;
|
|
4980
5440
|
this.currentFlashlightStrain = 0;
|
|
4981
|
-
this.skillMultiplier = 0.
|
|
4982
|
-
}
|
|
4983
|
-
static difficultyToPerformance(difficulty) {
|
|
4984
|
-
return Math.pow(difficulty, 2) * 25;
|
|
5441
|
+
this.skillMultiplier = 0.058;
|
|
4985
5442
|
}
|
|
4986
5443
|
difficultyValue() {
|
|
4987
|
-
|
|
5444
|
+
let sum = this.currentStrainPeaks.reduce((a, b) => a + b, 0);
|
|
5445
|
+
// Account for shorter beatmaps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
5446
|
+
sum *=
|
|
5447
|
+
0.7 +
|
|
5448
|
+
0.1 * Math.min(1, this.totalObjects / 200) +
|
|
5449
|
+
(this.totalObjects > 200
|
|
5450
|
+
? 0.2 * Math.min(1, (this.totalObjects - 200) / 200)
|
|
5451
|
+
: 0);
|
|
5452
|
+
return sum;
|
|
4988
5453
|
}
|
|
4989
5454
|
strainValueAt(current) {
|
|
5455
|
+
if (!this.mods.has(osuBase.ModFlashlight)) {
|
|
5456
|
+
return 0;
|
|
5457
|
+
}
|
|
4990
5458
|
this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
|
|
4991
5459
|
this.currentFlashlightStrain +=
|
|
4992
|
-
|
|
4993
|
-
this.skillMultiplier;
|
|
5460
|
+
this.calculateAdjustedDifficulty(current) * this.skillMultiplier;
|
|
4994
5461
|
return this.currentFlashlightStrain;
|
|
4995
5462
|
}
|
|
4996
5463
|
calculateInitialStrain(time, current) {
|
|
@@ -4998,11 +5465,34 @@ class OsuFlashlight extends OsuSkill {
|
|
|
4998
5465
|
return (this.currentFlashlightStrain *
|
|
4999
5466
|
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
5000
5467
|
}
|
|
5001
|
-
saveToHitObject(current
|
|
5002
|
-
current.flashlightStrain =
|
|
5468
|
+
saveToHitObject(current) {
|
|
5469
|
+
current.flashlightStrain = this.currentFlashlightStrain;
|
|
5470
|
+
}
|
|
5471
|
+
calculateAdjustedDifficulty(current) {
|
|
5472
|
+
let difficulty = OsuFlashlightEvaluator.evaluateDifficultyOf(current, this.mods);
|
|
5473
|
+
if (this.mods.has(osuBase.ModTouchDevice)) {
|
|
5474
|
+
difficulty = Math.pow(difficulty, 0.9);
|
|
5475
|
+
}
|
|
5476
|
+
if (this.mods.has(osuBase.ModMagnetised)) {
|
|
5477
|
+
const magnetisedStrength = this.mods.get(osuBase.ModMagnetised).attractionStrength.value;
|
|
5478
|
+
difficulty *= 1 - magnetisedStrength;
|
|
5479
|
+
}
|
|
5480
|
+
if (this.mods.has(osuBase.ModDeflate)) {
|
|
5481
|
+
const deflateInitialScale = this.mods.get(osuBase.ModDeflate).startScale.value;
|
|
5482
|
+
difficulty *= osuBase.MathUtils.clamp(osuBase.Interpolation.reverseLerp(deflateInitialScale, 11, 1), 0.1, 1);
|
|
5483
|
+
}
|
|
5484
|
+
if (this.mods.has(osuBase.ModRelax)) {
|
|
5485
|
+
difficulty *= 0.7;
|
|
5486
|
+
}
|
|
5487
|
+
else if (this.mods.has(osuBase.ModAutopilot)) {
|
|
5488
|
+
difficulty *= 0.4;
|
|
5489
|
+
}
|
|
5490
|
+
difficulty *=
|
|
5491
|
+
0.985 + Math.pow(Math.max(0, current.overallDifficulty), 2) / 4000;
|
|
5492
|
+
return difficulty;
|
|
5003
5493
|
}
|
|
5004
5494
|
strainDecay(ms) {
|
|
5005
|
-
return Math.pow(
|
|
5495
|
+
return Math.pow(0.15, ms / 1000);
|
|
5006
5496
|
}
|
|
5007
5497
|
}
|
|
5008
5498
|
|
|
@@ -5016,7 +5506,6 @@ class OsuDifficultyAttributes extends DifficultyAttributes {
|
|
|
5016
5506
|
this.drainRate = 0;
|
|
5017
5507
|
this.speedDifficulty = 0;
|
|
5018
5508
|
this.speedDifficultStrainCount = 0;
|
|
5019
|
-
this.aimTopWeightedSliderFactor = 0;
|
|
5020
5509
|
this.speedTopWeightedSliderFactor = 0;
|
|
5021
5510
|
if (!cacheableAttributes) {
|
|
5022
5511
|
return;
|
|
@@ -5026,8 +5515,6 @@ class OsuDifficultyAttributes extends DifficultyAttributes {
|
|
|
5026
5515
|
this.speedDifficulty = cacheableAttributes.speedDifficulty;
|
|
5027
5516
|
this.speedDifficultStrainCount =
|
|
5028
5517
|
cacheableAttributes.speedDifficultStrainCount;
|
|
5029
|
-
this.aimTopWeightedSliderFactor =
|
|
5030
|
-
cacheableAttributes.aimTopWeightedSliderFactor;
|
|
5031
5518
|
this.speedTopWeightedSliderFactor =
|
|
5032
5519
|
cacheableAttributes.speedTopWeightedSliderFactor;
|
|
5033
5520
|
}
|
|
@@ -5035,7 +5522,8 @@ class OsuDifficultyAttributes extends DifficultyAttributes {
|
|
|
5035
5522
|
return (super.toString() +
|
|
5036
5523
|
` (${this.aimDifficulty.toFixed(2)} aim, ` +
|
|
5037
5524
|
`${this.speedDifficulty.toFixed(2)} speed, ` +
|
|
5038
|
-
`${this.flashlightDifficulty.toFixed(2)} flashlight
|
|
5525
|
+
`${this.flashlightDifficulty.toFixed(2)} flashlight, ` +
|
|
5526
|
+
`${this.readingDifficulty.toFixed(2)} reading)`);
|
|
5039
5527
|
}
|
|
5040
5528
|
}
|
|
5041
5529
|
|
|
@@ -5045,7 +5533,6 @@ class OsuDifficultyAttributes extends DifficultyAttributes {
|
|
|
5045
5533
|
class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
5046
5534
|
constructor() {
|
|
5047
5535
|
super();
|
|
5048
|
-
this.starRatingMultiplier = 0.0265;
|
|
5049
5536
|
this.difficultyAdjustmentMods.push(osuBase.ModTouchDevice, osuBase.ModBlinds);
|
|
5050
5537
|
}
|
|
5051
5538
|
retainDifficultyAdjustmentMods(mods) {
|
|
@@ -5053,8 +5540,8 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
5053
5540
|
mod.isOsuRelevant &&
|
|
5054
5541
|
this.difficultyAdjustmentMods.some((m) => mod instanceof m));
|
|
5055
5542
|
}
|
|
5056
|
-
createDifficultyAttributes(
|
|
5057
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
5543
|
+
createDifficultyAttributes(beatmap, playableBeatmap, skills) {
|
|
5544
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
5058
5545
|
const attributes = new OsuDifficultyAttributes();
|
|
5059
5546
|
if (playableBeatmap.hitObjects.objects.length === 0) {
|
|
5060
5547
|
return attributes;
|
|
@@ -5065,27 +5552,26 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
5065
5552
|
attributes.hitCircleCount = playableBeatmap.hitObjects.circles;
|
|
5066
5553
|
attributes.sliderCount = playableBeatmap.hitObjects.sliders;
|
|
5067
5554
|
attributes.spinnerCount = playableBeatmap.hitObjects.spinners;
|
|
5555
|
+
attributes.approachRate = playableBeatmap.difficulty.ar;
|
|
5556
|
+
attributes.overallDifficulty = playableBeatmap.difficulty.od;
|
|
5068
5557
|
attributes.drainRate = playableBeatmap.difficulty.hp;
|
|
5069
|
-
attributes.approachRate =
|
|
5070
|
-
OsuDifficultyCalculator.calculateRateAdjustedApproachRate(playableBeatmap.difficulty.ar, attributes.clockRate);
|
|
5071
|
-
attributes.overallDifficulty =
|
|
5072
|
-
OsuDifficultyCalculator.calculateRateAdjustedOverallDifficulty(playableBeatmap.difficulty.od, attributes.clockRate);
|
|
5073
5558
|
const aim = skills.find((s) => s instanceof OsuAim && s.withSliders);
|
|
5074
5559
|
const aimNoSlider = skills.find((s) => s instanceof OsuAim && !s.withSliders);
|
|
5075
5560
|
const speed = skills.find((s) => s instanceof OsuSpeed);
|
|
5076
5561
|
const flashlight = skills.find((s) => s instanceof OsuFlashlight);
|
|
5562
|
+
const reading = skills.find((s) => s instanceof OsuReading);
|
|
5077
5563
|
// Aim attributes
|
|
5078
5564
|
const aimDifficultyValue = (_a = aim === null || aim === void 0 ? void 0 : aim.difficultyValue()) !== null && _a !== void 0 ? _a : 0;
|
|
5079
|
-
|
|
5565
|
+
const aimNoSliderDifficultyValue = (_b = aimNoSlider === null || aimNoSlider === void 0 ? void 0 : aimNoSlider.difficultyValue()) !== null && _b !== void 0 ? _b : 0;
|
|
5566
|
+
attributes.aimDifficultSliderCount = (_c = aim === null || aim === void 0 ? void 0 : aim.countDifficultSliders()) !== null && _c !== void 0 ? _c : 0;
|
|
5080
5567
|
attributes.aimDifficultStrainCount =
|
|
5081
|
-
(
|
|
5568
|
+
(_d = aim === null || aim === void 0 ? void 0 : aim.countTopWeightedStrains(aimDifficultyValue)) !== null && _d !== void 0 ? _d : 0;
|
|
5082
5569
|
attributes.sliderFactor =
|
|
5083
5570
|
aimDifficultyValue > 0
|
|
5084
|
-
?
|
|
5085
|
-
OsuRatingCalculator.calculateDifficultyRating(aimDifficultyValue)
|
|
5571
|
+
? this.calculateAimDifficultyRating(aimNoSliderDifficultyValue) / this.calculateAimDifficultyRating(aimDifficultyValue)
|
|
5086
5572
|
: 1;
|
|
5087
|
-
const aimNoSliderTopWeightedSliderCount = (_e = aimNoSlider === null || aimNoSlider === void 0 ? void 0 : aimNoSlider.countTopWeightedSliders()) !== null && _e !== void 0 ? _e : 0;
|
|
5088
|
-
const aimNoSliderDifficultStrainCount = (_f = aimNoSlider === null || aimNoSlider === void 0 ? void 0 : aimNoSlider.countTopWeightedStrains(
|
|
5573
|
+
const aimNoSliderTopWeightedSliderCount = (_e = aimNoSlider === null || aimNoSlider === void 0 ? void 0 : aimNoSlider.countTopWeightedSliders(aimNoSliderDifficultyValue)) !== null && _e !== void 0 ? _e : 0;
|
|
5574
|
+
const aimNoSliderDifficultStrainCount = (_f = aimNoSlider === null || aimNoSlider === void 0 ? void 0 : aimNoSlider.countTopWeightedStrains(aimNoSliderDifficultyValue)) !== null && _f !== void 0 ? _f : 0;
|
|
5089
5575
|
attributes.aimTopWeightedSliderFactor =
|
|
5090
5576
|
aimNoSliderTopWeightedSliderCount /
|
|
5091
5577
|
Math.max(1, aimNoSliderDifficultStrainCount -
|
|
@@ -5094,27 +5580,29 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
5094
5580
|
const speedDifficultyValue = (_g = speed === null || speed === void 0 ? void 0 : speed.difficultyValue()) !== null && _g !== void 0 ? _g : 0;
|
|
5095
5581
|
attributes.speedNoteCount = (_h = speed === null || speed === void 0 ? void 0 : speed.relevantNoteCount()) !== null && _h !== void 0 ? _h : 0;
|
|
5096
5582
|
attributes.speedDifficultStrainCount =
|
|
5097
|
-
(_j = speed === null || speed === void 0 ? void 0 : speed.
|
|
5098
|
-
const speedTopWeightedSliderCount = (_k = speed === null || speed === void 0 ? void 0 : speed.countTopWeightedSliders()) !== null && _k !== void 0 ? _k : 0;
|
|
5583
|
+
(_j = speed === null || speed === void 0 ? void 0 : speed.countTopWeightedObjectDifficulties(speedDifficultyValue)) !== null && _j !== void 0 ? _j : 0;
|
|
5584
|
+
const speedTopWeightedSliderCount = (_k = speed === null || speed === void 0 ? void 0 : speed.countTopWeightedSliders(speedDifficultyValue)) !== null && _k !== void 0 ? _k : 0;
|
|
5099
5585
|
attributes.speedTopWeightedSliderFactor =
|
|
5100
5586
|
speedTopWeightedSliderCount /
|
|
5101
5587
|
Math.max(1, attributes.speedDifficultStrainCount -
|
|
5102
5588
|
speedTopWeightedSliderCount);
|
|
5589
|
+
// Reading attributes
|
|
5590
|
+
const readingDifficultyValue = (_l = reading === null || reading === void 0 ? void 0 : reading.difficultyValue()) !== null && _l !== void 0 ? _l : 0;
|
|
5591
|
+
attributes.readingDifficultNoteCount =
|
|
5592
|
+
(_m = reading === null || reading === void 0 ? void 0 : reading.countTopWeightedObjectDifficulties(readingDifficultyValue)) !== null && _m !== void 0 ? _m : 0;
|
|
5103
5593
|
// Final rating
|
|
5104
|
-
const mechanicalDifficultyRating = this.calculateMechanicalDifficultyRating(aimDifficultyValue, speedDifficultyValue);
|
|
5105
|
-
const ratingCalculator = new OsuRatingCalculator(attributes.mods, playableBeatmap.hitObjects.objects.length, attributes.approachRate, attributes.overallDifficulty, mechanicalDifficultyRating, attributes.sliderFactor);
|
|
5106
5594
|
attributes.aimDifficulty =
|
|
5107
|
-
|
|
5595
|
+
this.calculateAimDifficultyRating(aimDifficultyValue);
|
|
5108
5596
|
attributes.speedDifficulty =
|
|
5109
|
-
|
|
5110
|
-
attributes.flashlightDifficulty =
|
|
5111
|
-
|
|
5597
|
+
this.calculateDifficultyRating(speedDifficultyValue);
|
|
5598
|
+
attributes.flashlightDifficulty = this.calculateDifficultyRating((_o = flashlight === null || flashlight === void 0 ? void 0 : flashlight.difficultyValue()) !== null && _o !== void 0 ? _o : 0);
|
|
5599
|
+
attributes.readingDifficulty = this.calculateDifficultyRating(readingDifficultyValue);
|
|
5112
5600
|
const baseAimPerformance = OsuAim.difficultyToPerformance(attributes.aimDifficulty);
|
|
5113
5601
|
const baseSpeedPerformance = OsuSpeed.difficultyToPerformance(attributes.speedDifficulty);
|
|
5114
5602
|
const baseFlashlightPerformance = OsuFlashlight.difficultyToPerformance(attributes.flashlightDifficulty);
|
|
5115
|
-
const
|
|
5116
|
-
|
|
5117
|
-
|
|
5603
|
+
const baseReadingPerformance = OsuReading.difficultyToPerformance(attributes.readingDifficulty);
|
|
5604
|
+
const baseCognitionPerformance = OsuDifficultyCalculator.sumCognitionDifficulty(baseReadingPerformance, baseFlashlightPerformance);
|
|
5605
|
+
const basePerformance = osuBase.MathUtils.norm(OsuPerformanceCalculator.normExponent, baseAimPerformance, baseSpeedPerformance, baseCognitionPerformance);
|
|
5118
5606
|
attributes.starRating = this.calculateStarRating(basePerformance);
|
|
5119
5607
|
return attributes;
|
|
5120
5608
|
}
|
|
@@ -5122,12 +5610,11 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
5122
5610
|
return beatmap.createOsuPlayableBeatmap(mods);
|
|
5123
5611
|
}
|
|
5124
5612
|
createDifficultyHitObjects(beatmap) {
|
|
5125
|
-
var _a;
|
|
5126
5613
|
const clockRate = beatmap.speedMultiplier;
|
|
5127
5614
|
const difficultyObjects = [];
|
|
5128
5615
|
const { objects } = beatmap.hitObjects;
|
|
5129
5616
|
for (let i = 1; i < objects.length; ++i) {
|
|
5130
|
-
const difficultyObject = new OsuDifficultyHitObject(objects[i],
|
|
5617
|
+
const difficultyObject = new OsuDifficultyHitObject(objects[i], objects[i - 1], difficultyObjects, clockRate, i - 1);
|
|
5131
5618
|
difficultyObject.computeProperties(clockRate);
|
|
5132
5619
|
difficultyObjects.push(difficultyObject);
|
|
5133
5620
|
}
|
|
@@ -5143,8 +5630,9 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
5143
5630
|
if (!mods.has(osuBase.ModRelax)) {
|
|
5144
5631
|
skills.push(new OsuSpeed(mods));
|
|
5145
5632
|
}
|
|
5633
|
+
skills.push(new OsuReading(mods, beatmap.speedMultiplier, beatmap.hitObjects.objects));
|
|
5146
5634
|
if (mods.has(osuBase.ModFlashlight)) {
|
|
5147
|
-
skills.push(new OsuFlashlight(mods));
|
|
5635
|
+
skills.push(new OsuFlashlight(mods, beatmap.hitObjects.objects.length));
|
|
5148
5636
|
}
|
|
5149
5637
|
return skills;
|
|
5150
5638
|
}
|
|
@@ -5154,30 +5642,26 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
5154
5642
|
new OsuAim(mods, true),
|
|
5155
5643
|
new OsuAim(mods, false),
|
|
5156
5644
|
new OsuSpeed(mods),
|
|
5157
|
-
new OsuFlashlight(mods),
|
|
5645
|
+
new OsuFlashlight(mods, beatmap.hitObjects.objects.length),
|
|
5158
5646
|
];
|
|
5159
5647
|
}
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
return
|
|
5648
|
+
calculateAimDifficultyRating(difficultyValue) {
|
|
5649
|
+
return Math.pow(difficultyValue, 0.63) * 0.02275;
|
|
5650
|
+
}
|
|
5651
|
+
calculateDifficultyRating(difficultyValue) {
|
|
5652
|
+
return Math.sqrt(difficultyValue) * 0.0675;
|
|
5165
5653
|
}
|
|
5166
5654
|
calculateStarRating(basePerformance) {
|
|
5167
|
-
|
|
5168
|
-
return 0;
|
|
5169
|
-
}
|
|
5170
|
-
return (Math.cbrt(OsuPerformanceCalculator.finalMultiplier) *
|
|
5171
|
-
this.starRatingMultiplier *
|
|
5172
|
-
(Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformance) + 4));
|
|
5173
|
-
}
|
|
5174
|
-
static calculateRateAdjustedApproachRate(approachRate, clockRate) {
|
|
5175
|
-
const preempt = osuBase.BeatmapDifficulty.difficultyRange(approachRate, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin) / clockRate;
|
|
5176
|
-
return osuBase.BeatmapDifficulty.inverseDifficultyRange(preempt, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin);
|
|
5655
|
+
return Math.cbrt(basePerformance * OsuPerformanceCalculator.finalMultiplier);
|
|
5177
5656
|
}
|
|
5178
|
-
static
|
|
5179
|
-
|
|
5180
|
-
|
|
5657
|
+
static sumCognitionDifficulty(reading, flashlight) {
|
|
5658
|
+
if (reading <= 0) {
|
|
5659
|
+
return flashlight;
|
|
5660
|
+
}
|
|
5661
|
+
if (flashlight <= 0) {
|
|
5662
|
+
return reading;
|
|
5663
|
+
}
|
|
5664
|
+
return osuBase.MathUtils.norm(OsuPerformanceCalculator.normExponent, reading, flashlight * osuBase.MathUtils.clamp(flashlight / reading, 0.25, 1));
|
|
5181
5665
|
}
|
|
5182
5666
|
}
|
|
5183
5667
|
|
|
@@ -5201,15 +5685,19 @@ exports.DroidSnapAimEvaluator = DroidSnapAimEvaluator;
|
|
|
5201
5685
|
exports.DroidTap = DroidTap;
|
|
5202
5686
|
exports.DroidTapEvaluator = DroidTapEvaluator;
|
|
5203
5687
|
exports.ExtendedDroidDifficultyAttributes = ExtendedDroidDifficultyAttributes;
|
|
5688
|
+
exports.OsuAgilityEvaluator = OsuAgilityEvaluator;
|
|
5204
5689
|
exports.OsuAim = OsuAim;
|
|
5205
|
-
exports.OsuAimEvaluator = OsuAimEvaluator;
|
|
5206
5690
|
exports.OsuDifficultyAttributes = OsuDifficultyAttributes;
|
|
5207
5691
|
exports.OsuDifficultyCalculator = OsuDifficultyCalculator;
|
|
5208
5692
|
exports.OsuDifficultyHitObject = OsuDifficultyHitObject;
|
|
5209
5693
|
exports.OsuFlashlight = OsuFlashlight;
|
|
5210
5694
|
exports.OsuFlashlightEvaluator = OsuFlashlightEvaluator;
|
|
5695
|
+
exports.OsuFlowAimEvaluator = OsuFlowAimEvaluator;
|
|
5211
5696
|
exports.OsuPerformanceCalculator = OsuPerformanceCalculator;
|
|
5697
|
+
exports.OsuReading = OsuReading;
|
|
5698
|
+
exports.OsuReadingEvaluator = OsuReadingEvaluator;
|
|
5212
5699
|
exports.OsuRhythmEvaluator = OsuRhythmEvaluator;
|
|
5700
|
+
exports.OsuSnapAimEvaluator = OsuSnapAimEvaluator;
|
|
5213
5701
|
exports.OsuSpeed = OsuSpeed;
|
|
5214
5702
|
exports.OsuSpeedEvaluator = OsuSpeedEvaluator;
|
|
5215
5703
|
exports.PerformanceCalculator = PerformanceCalculator;
|