@rian8337/osu-difficulty-calculator 4.0.0-beta.93 → 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 +1292 -808
- package/package.json +3 -3
- package/typings/index.d.ts +327 -180
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
|
}
|
|
@@ -1268,8 +1264,8 @@ class DroidFlashlight extends DroidSkill {
|
|
|
1268
1264
|
this.totalObjects = totalObjects;
|
|
1269
1265
|
this.reducedSectionCount = 0;
|
|
1270
1266
|
this.reducedSectionBaseline = 1;
|
|
1271
|
-
this.starsPerDouble = 1
|
|
1272
|
-
this.skillMultiplier = 0.
|
|
1267
|
+
this.starsPerDouble = 1;
|
|
1268
|
+
this.skillMultiplier = 0.058;
|
|
1273
1269
|
this.currentFlashlightStrain = 0;
|
|
1274
1270
|
}
|
|
1275
1271
|
strainValueAt(current) {
|
|
@@ -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;
|
|
@@ -3640,13 +3632,9 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
3640
3632
|
attributes.rhythmDifficulty = this.calculateDifficultyRating((_a = rhythm === null || rhythm === void 0 ? void 0 : rhythm.difficultyValue()) !== null && _a !== void 0 ? _a : 0);
|
|
3641
3633
|
}
|
|
3642
3634
|
populateFlashlightAttributes(attributes, skills) {
|
|
3635
|
+
var _a;
|
|
3643
3636
|
const flashlight = skills.find((s) => s instanceof DroidFlashlight);
|
|
3644
|
-
|
|
3645
|
-
attributes.flashlightDifficulty = 0;
|
|
3646
|
-
return;
|
|
3647
|
-
}
|
|
3648
|
-
attributes.flashlightDifficulty =
|
|
3649
|
-
Math.sqrt(flashlight.difficultyValue()) * 0.18;
|
|
3637
|
+
attributes.flashlightDifficulty = this.calculateDifficultyRating((_a = flashlight === null || flashlight === void 0 ? void 0 : flashlight.difficultyValue()) !== null && _a !== void 0 ? _a : 0);
|
|
3650
3638
|
}
|
|
3651
3639
|
populateReadingAttributes(attributes, skills) {
|
|
3652
3640
|
const reading = skills.find((s) => s instanceof DroidReading);
|
|
@@ -3678,154 +3666,33 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
3678
3666
|
}
|
|
3679
3667
|
|
|
3680
3668
|
/**
|
|
3681
|
-
* An evaluator for calculating osu!standard
|
|
3669
|
+
* An evaluator for calculating osu!standard agility aim difficulty.
|
|
3682
3670
|
*/
|
|
3683
|
-
class
|
|
3671
|
+
class OsuAgilityEvaluator {
|
|
3684
3672
|
/**
|
|
3685
|
-
* Evaluates the difficulty of aiming the current object
|
|
3686
|
-
*
|
|
3687
|
-
* - cursor velocity to the current object,
|
|
3688
|
-
* - angle difficulty,
|
|
3689
|
-
* - sharp velocity increases,
|
|
3690
|
-
* - and slider difficulty.
|
|
3673
|
+
* Evaluates the difficulty of fast aiming the current object.
|
|
3691
3674
|
*
|
|
3692
3675
|
* @param current The current object.
|
|
3693
|
-
* @param withSliders Whether to take slider difficulty into account.
|
|
3694
3676
|
*/
|
|
3695
|
-
static evaluateDifficultyOf(current
|
|
3696
|
-
|
|
3697
|
-
if (current.object instanceof osuBase.Spinner
|
|
3698
|
-
current.index <= 1 ||
|
|
3699
|
-
last.object instanceof osuBase.Spinner) {
|
|
3677
|
+
static evaluateDifficultyOf(current) {
|
|
3678
|
+
var _a;
|
|
3679
|
+
if (current.object instanceof osuBase.Spinner) {
|
|
3700
3680
|
return 0;
|
|
3701
3681
|
}
|
|
3702
|
-
const
|
|
3703
|
-
const
|
|
3704
|
-
const
|
|
3705
|
-
const
|
|
3706
|
-
|
|
3707
|
-
let
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
// Calculate the slider velocity from slider head to slider end.
|
|
3711
|
-
const travelVelocity = last.travelDistance / last.travelTime;
|
|
3712
|
-
// Calculate the movement velocity from slider end to current object.
|
|
3713
|
-
const movementVelocity = current.minimumJumpDistance / current.minimumJumpTime;
|
|
3714
|
-
// Take the larger total combined velocity.
|
|
3715
|
-
currentVelocity = Math.max(currentVelocity, movementVelocity + travelVelocity);
|
|
3716
|
-
}
|
|
3717
|
-
// As above, do the same for the previous hitobject.
|
|
3718
|
-
let prevVelocity = last.lazyJumpDistance / last.strainTime;
|
|
3719
|
-
if (lastLast.object instanceof osuBase.Slider && withSliders) {
|
|
3720
|
-
const travelVelocity = lastLast.travelDistance / lastLast.travelTime;
|
|
3721
|
-
const movementVelocity = last.minimumJumpDistance / last.minimumJumpTime;
|
|
3722
|
-
prevVelocity = Math.max(prevVelocity, movementVelocity + travelVelocity);
|
|
3723
|
-
}
|
|
3724
|
-
let wideAngleBonus = 0;
|
|
3725
|
-
let acuteAngleBonus = 0;
|
|
3726
|
-
let sliderBonus = 0;
|
|
3727
|
-
let velocityChangeBonus = 0;
|
|
3728
|
-
let wiggleBonus = 0;
|
|
3729
|
-
// Start strain with regular velocity.
|
|
3730
|
-
let strain = currentVelocity;
|
|
3731
|
-
if (current.angle !== null && last.angle !== null) {
|
|
3732
|
-
const currentAngle = current.angle;
|
|
3733
|
-
const lastAngle = last.angle;
|
|
3734
|
-
// Rewarding angles, take the smaller velocity as base.
|
|
3735
|
-
const angleBonus = Math.min(currentVelocity, prevVelocity);
|
|
3736
|
-
if (
|
|
3737
|
-
// If rhythms are the same.
|
|
3738
|
-
Math.max(current.strainTime, last.strainTime) <
|
|
3739
|
-
1.25 * Math.min(current.strainTime, last.strainTime)) {
|
|
3740
|
-
acuteAngleBonus = this.calculateAcuteAngleBonus(currentAngle);
|
|
3741
|
-
// Penalize angle repetition.
|
|
3742
|
-
acuteAngleBonus *=
|
|
3743
|
-
0.08 +
|
|
3744
|
-
0.92 *
|
|
3745
|
-
(1 -
|
|
3746
|
-
Math.min(acuteAngleBonus, Math.pow(this.calculateAcuteAngleBonus(lastAngle), 3)));
|
|
3747
|
-
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
|
3748
|
-
acuteAngleBonus *=
|
|
3749
|
-
angleBonus *
|
|
3750
|
-
osuBase.MathUtils.smootherstep(osuBase.MathUtils.millisecondsToBPM(current.strainTime, 2), 300, 400) *
|
|
3751
|
-
osuBase.MathUtils.smootherstep(current.lazyJumpDistance, diameter, diameter * 2);
|
|
3752
|
-
}
|
|
3753
|
-
wideAngleBonus = this.calculateWideAngleBonus(currentAngle);
|
|
3754
|
-
// Penalize angle repetition.
|
|
3755
|
-
wideAngleBonus *=
|
|
3756
|
-
1 -
|
|
3757
|
-
Math.min(wideAngleBonus, Math.pow(this.calculateWideAngleBonus(lastAngle), 3));
|
|
3758
|
-
// Apply full wide angle bonus for distance more than one diameter
|
|
3759
|
-
wideAngleBonus *=
|
|
3760
|
-
angleBonus *
|
|
3761
|
-
osuBase.MathUtils.smootherstep(current.lazyJumpDistance, 0, diameter);
|
|
3762
|
-
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
|
3763
|
-
// https://www.desmos.com/calculator/dp0v0nvowc
|
|
3764
|
-
wiggleBonus =
|
|
3765
|
-
angleBonus *
|
|
3766
|
-
osuBase.MathUtils.smootherstep(current.lazyJumpDistance, radius, diameter) *
|
|
3767
|
-
Math.pow(osuBase.MathUtils.reverseLerp(current.lazyJumpDistance, diameter * 3, diameter), 1.8) *
|
|
3768
|
-
osuBase.MathUtils.smootherstep(currentAngle, osuBase.MathUtils.degreesToRadians(110), osuBase.MathUtils.degreesToRadians(60)) *
|
|
3769
|
-
osuBase.MathUtils.smootherstep(last.lazyJumpDistance, radius, diameter) *
|
|
3770
|
-
Math.pow(osuBase.MathUtils.reverseLerp(last.lazyJumpDistance, diameter * 3, diameter), 1.8) *
|
|
3771
|
-
osuBase.MathUtils.smootherstep(lastAngle, osuBase.MathUtils.degreesToRadians(110), osuBase.MathUtils.degreesToRadians(60));
|
|
3772
|
-
if (last2 !== null) {
|
|
3773
|
-
// If objects just go back and forth through a middle point - don't give as much wide bonus.
|
|
3774
|
-
// Use previous(2) and previous(0) because angles calculation is done prevprev-prev-curr, so any
|
|
3775
|
-
// object's angle's center point is always the previous object.
|
|
3776
|
-
const distance = last2.object.stackedPosition.getDistance(last.object.stackedPosition);
|
|
3777
|
-
if (distance < 1) {
|
|
3778
|
-
wideAngleBonus *= 1 - 0.35 * (1 - distance);
|
|
3779
|
-
}
|
|
3780
|
-
}
|
|
3781
|
-
}
|
|
3782
|
-
if (Math.max(prevVelocity, currentVelocity)) {
|
|
3783
|
-
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
|
|
3784
|
-
prevVelocity =
|
|
3785
|
-
(last.lazyJumpDistance + lastLast.travelDistance) /
|
|
3786
|
-
last.strainTime;
|
|
3787
|
-
currentVelocity =
|
|
3788
|
-
(current.lazyJumpDistance + last.travelDistance) /
|
|
3789
|
-
current.strainTime;
|
|
3790
|
-
// Scale with ratio of difference compared to half the max distance.
|
|
3791
|
-
const distanceRatio = osuBase.MathUtils.smoothstep(Math.abs(prevVelocity - currentVelocity) /
|
|
3792
|
-
Math.max(prevVelocity, currentVelocity), 0, 1);
|
|
3793
|
-
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
|
3794
|
-
const overlapVelocityBuff = Math.min((diameter * 1.25) /
|
|
3795
|
-
Math.min(current.strainTime, last.strainTime), Math.abs(prevVelocity - currentVelocity));
|
|
3796
|
-
velocityChangeBonus = overlapVelocityBuff * distanceRatio;
|
|
3797
|
-
// Penalize for rhythm changes.
|
|
3798
|
-
velocityChangeBonus *= Math.pow(Math.min(current.strainTime, last.strainTime) /
|
|
3799
|
-
Math.max(current.strainTime, last.strainTime), 2);
|
|
3800
|
-
}
|
|
3801
|
-
if (last.travelTime) {
|
|
3802
|
-
// Reward sliders based on velocity.
|
|
3803
|
-
sliderBonus = last.travelDistance / last.travelTime;
|
|
3804
|
-
}
|
|
3805
|
-
strain += wiggleBonus * this.wiggleMultiplier;
|
|
3806
|
-
strain += velocityChangeBonus * this.velocityChangeMultiplier;
|
|
3807
|
-
// Add in acute angle bonus or wide angle bonus, whichever is larger.
|
|
3808
|
-
strain += Math.max(acuteAngleBonus * this.acuteAngleMultiplier, wideAngleBonus * this.wideAngleMultiplier);
|
|
3809
|
-
// Apply high circle size bonus
|
|
3810
|
-
strain *= current.smallCircleBonus;
|
|
3811
|
-
// Add in additional slider velocity bonus.
|
|
3812
|
-
if (withSliders) {
|
|
3813
|
-
strain += sliderBonus * this.sliderMultiplier;
|
|
3814
|
-
}
|
|
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);
|
|
3815
3690
|
return strain;
|
|
3816
3691
|
}
|
|
3817
|
-
static
|
|
3818
|
-
return
|
|
3819
|
-
}
|
|
3820
|
-
static calculateAcuteAngleBonus(angle) {
|
|
3821
|
-
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));
|
|
3822
3694
|
}
|
|
3823
3695
|
}
|
|
3824
|
-
OsuAimEvaluator.wideAngleMultiplier = 1.5;
|
|
3825
|
-
OsuAimEvaluator.acuteAngleMultiplier = 2.55;
|
|
3826
|
-
OsuAimEvaluator.sliderMultiplier = 1.35;
|
|
3827
|
-
OsuAimEvaluator.velocityChangeMultiplier = 0.75;
|
|
3828
|
-
OsuAimEvaluator.wiggleMultiplier = 1.02;
|
|
3829
3696
|
|
|
3830
3697
|
/**
|
|
3831
3698
|
* An evaluator for calculating osu!standard Flashlight skill.
|
|
@@ -3844,6 +3711,7 @@ class OsuFlashlightEvaluator {
|
|
|
3844
3711
|
* @param mods The mods used.
|
|
3845
3712
|
*/
|
|
3846
3713
|
static evaluateDifficultyOf(current, mods) {
|
|
3714
|
+
var _a;
|
|
3847
3715
|
if (current.object instanceof osuBase.Spinner) {
|
|
3848
3716
|
return 0;
|
|
3849
3717
|
}
|
|
@@ -3853,6 +3721,10 @@ class OsuFlashlightEvaluator {
|
|
|
3853
3721
|
let result = 0;
|
|
3854
3722
|
let last = current;
|
|
3855
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;
|
|
3856
3728
|
for (let i = 0; i < Math.min(current.index, 10); ++i) {
|
|
3857
3729
|
const currentObject = current.previous(i);
|
|
3858
3730
|
cumulativeStrainTime += last.strainTime;
|
|
@@ -3868,7 +3740,7 @@ class OsuFlashlightEvaluator {
|
|
|
3868
3740
|
const opacityBonus = 1 +
|
|
3869
3741
|
this.maxOpacityBonus *
|
|
3870
3742
|
(1 -
|
|
3871
|
-
current.opacityAt(currentObject.object.startTime,
|
|
3743
|
+
current.opacityAt(currentObject.object.startTime, opacityMods));
|
|
3872
3744
|
result +=
|
|
3873
3745
|
(stackNerf * opacityBonus * scalingFactor * jumpDistance) /
|
|
3874
3746
|
cumulativeStrainTime;
|
|
@@ -3912,149 +3784,660 @@ OsuFlashlightEvaluator.minVelocity = 0.5;
|
|
|
3912
3784
|
OsuFlashlightEvaluator.sliderMultiplier = 1.3;
|
|
3913
3785
|
OsuFlashlightEvaluator.minAngleMultiplier = 0.2;
|
|
3914
3786
|
|
|
3915
|
-
class OsuIsland {
|
|
3916
|
-
constructor(delta, deltaDifferenceEpsilon) {
|
|
3917
|
-
this.delta = Number.MAX_SAFE_INTEGER;
|
|
3918
|
-
this.deltaCount = 0;
|
|
3919
|
-
if (deltaDifferenceEpsilon === undefined) {
|
|
3920
|
-
this.deltaDifferenceEpsilon = delta;
|
|
3921
|
-
}
|
|
3922
|
-
else {
|
|
3923
|
-
this.deltaDifferenceEpsilon = deltaDifferenceEpsilon;
|
|
3924
|
-
this.addDelta(delta);
|
|
3925
|
-
}
|
|
3926
|
-
}
|
|
3927
|
-
addDelta(delta) {
|
|
3928
|
-
if (this.delta === Number.MAX_SAFE_INTEGER) {
|
|
3929
|
-
this.delta = Math.max(Math.trunc(delta), DifficultyHitObject.minDeltaTime);
|
|
3930
|
-
}
|
|
3931
|
-
++this.deltaCount;
|
|
3932
|
-
}
|
|
3933
|
-
isSimilarPolarity(other) {
|
|
3934
|
-
// 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)
|
|
3935
|
-
// naively adding delta check here breaks _a lot_ of maps because of the flawed ratio calculation
|
|
3936
|
-
return this.deltaCount % 2 == other.deltaCount % 2;
|
|
3937
|
-
}
|
|
3938
|
-
equals(other) {
|
|
3939
|
-
return (Math.abs(this.delta - other.delta) < this.deltaDifferenceEpsilon &&
|
|
3940
|
-
this.deltaCount === other.deltaCount);
|
|
3941
|
-
}
|
|
3942
|
-
}
|
|
3943
|
-
|
|
3944
3787
|
/**
|
|
3945
|
-
* An evaluator for calculating osu!standard
|
|
3788
|
+
* An evaluator for calculating osu!standard snap aim difficulty.
|
|
3946
3789
|
*/
|
|
3947
|
-
class
|
|
3790
|
+
class OsuSnapAimEvaluator {
|
|
3948
3791
|
/**
|
|
3949
|
-
*
|
|
3950
|
-
*
|
|
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.
|
|
3951
3798
|
*
|
|
3952
3799
|
* @param current The current object.
|
|
3800
|
+
* @param withSliders Whether to take slider difficulty into account.
|
|
3953
3801
|
*/
|
|
3954
|
-
static evaluateDifficultyOf(current) {
|
|
3955
|
-
|
|
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) {
|
|
3956
3807
|
return 0;
|
|
3957
3808
|
}
|
|
3958
|
-
const
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
const
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
let
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
this.historyTimeMax) {
|
|
3971
|
-
++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);
|
|
3972
3821
|
}
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
const
|
|
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
|
-
|
|
4026
|
-
|
|
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
|
+
}
|
|
4027
4402
|
// Repeated island size (ex: triplet -> triplet).
|
|
4028
4403
|
// TODO: remove this nerf since its staying here only for balancing purposes because of the flawed ratio calculation
|
|
4029
4404
|
if (previousIsland.deltaCount == island.deltaCount) {
|
|
4030
4405
|
effectiveRatio /= 2;
|
|
4031
4406
|
}
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4407
|
+
if (isSpeedingUp) {
|
|
4408
|
+
effectiveRatio *= 0.65;
|
|
4409
|
+
}
|
|
4410
|
+
let found = false;
|
|
4411
|
+
for (const existingIsland of islands) {
|
|
4412
|
+
if (!existingIsland.almostEquals(island, deltaDifferenceEpsilon)) {
|
|
4035
4413
|
continue;
|
|
4036
4414
|
}
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
// Only add island to island counts if they're going one after another.
|
|
4041
|
-
++islandCount;
|
|
4042
|
-
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;
|
|
4043
4418
|
}
|
|
4419
|
+
const power = osuBase.MathUtils.offsetLogistic(island.delta, 58.33, 0.24, 2.75);
|
|
4044
4420
|
// Repeated island (ex: triplet -> triplet).
|
|
4045
4421
|
// Graph: https://www.desmos.com/calculator/pj7an56zwf
|
|
4046
|
-
effectiveRatio *= Math.min(3 /
|
|
4422
|
+
effectiveRatio *= Math.min(3 / existingIsland.occurrences, Math.pow(1 / existingIsland.occurrences, power));
|
|
4423
|
+
found = true;
|
|
4047
4424
|
break;
|
|
4048
4425
|
}
|
|
4049
|
-
if (!
|
|
4050
|
-
|
|
4426
|
+
if (!found && island.deltaCount > 0) {
|
|
4427
|
+
islands.push(island);
|
|
4051
4428
|
}
|
|
4052
4429
|
// Scale down the difficulty if the object is doubletappable.
|
|
4053
4430
|
effectiveRatio *=
|
|
4054
4431
|
1 - prevObject.getDoubletapness(currentObject) * 0.75;
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
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
|
+
}
|
|
4058
4441
|
startRatio = effectiveRatio;
|
|
4059
4442
|
previousIsland = island;
|
|
4060
4443
|
if (prevDelta + deltaDifferenceEpsilon < currentDelta) {
|
|
@@ -4062,7 +4445,7 @@ class OsuRhythmEvaluator {
|
|
|
4062
4445
|
// If we're speeding up, this stays as is and we keep counting island size.
|
|
4063
4446
|
firstDeltaSwitch = false;
|
|
4064
4447
|
}
|
|
4065
|
-
island = new
|
|
4448
|
+
island = new Island(currentDelta);
|
|
4066
4449
|
}
|
|
4067
4450
|
}
|
|
4068
4451
|
else if (prevDelta > currentDelta + deltaDifferenceEpsilon) {
|
|
@@ -4080,20 +4463,28 @@ class OsuRhythmEvaluator {
|
|
|
4080
4463
|
effectiveRatio *= 0.6;
|
|
4081
4464
|
}
|
|
4082
4465
|
startRatio = effectiveRatio;
|
|
4083
|
-
island = new
|
|
4466
|
+
island = new Island(currentDelta);
|
|
4084
4467
|
}
|
|
4085
4468
|
lastObject = prevObject;
|
|
4086
4469
|
prevObject = currentObject;
|
|
4087
4470
|
}
|
|
4088
|
-
|
|
4089
|
-
|
|
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) /
|
|
4090
4474
|
2);
|
|
4091
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
|
+
}
|
|
4092
4483
|
}
|
|
4093
4484
|
OsuRhythmEvaluator.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
|
|
4094
4485
|
OsuRhythmEvaluator.historyObjectsMax = 32;
|
|
4095
|
-
OsuRhythmEvaluator.rhythmOverallMultiplier =
|
|
4096
|
-
OsuRhythmEvaluator.rhythmRatioMultiplier =
|
|
4486
|
+
OsuRhythmEvaluator.rhythmOverallMultiplier = 0.95;
|
|
4487
|
+
OsuRhythmEvaluator.rhythmRatioMultiplier = 26;
|
|
4097
4488
|
|
|
4098
4489
|
/**
|
|
4099
4490
|
* An evaluator for calculating osu!standard speed skill.
|
|
@@ -4103,18 +4494,14 @@ class OsuSpeedEvaluator {
|
|
|
4103
4494
|
* Evaluates the difficulty of tapping the current object, based on:
|
|
4104
4495
|
*
|
|
4105
4496
|
* - time between pressing the previous and current object,
|
|
4106
|
-
* - distance between those objects,
|
|
4107
4497
|
* - and how easily they can be cheesed.
|
|
4108
4498
|
*
|
|
4109
4499
|
* @param current The current object.
|
|
4110
|
-
* @param mods The mods applied.
|
|
4111
4500
|
*/
|
|
4112
|
-
static evaluateDifficultyOf(current
|
|
4113
|
-
var _a;
|
|
4501
|
+
static evaluateDifficultyOf(current) {
|
|
4114
4502
|
if (current.object instanceof osuBase.Spinner) {
|
|
4115
4503
|
return 0;
|
|
4116
4504
|
}
|
|
4117
|
-
const prev = current.previous(0);
|
|
4118
4505
|
let strainTime = current.strainTime;
|
|
4119
4506
|
// Nerf doubletappable doubles.
|
|
4120
4507
|
const doubletapness = 1 - current.getDoubletapness(current.next(0));
|
|
@@ -4124,389 +4511,389 @@ class OsuSpeedEvaluator {
|
|
|
4124
4511
|
// speedBonus will be 0.0 for BPM < 200
|
|
4125
4512
|
let speedBonus = 0;
|
|
4126
4513
|
// Add additional scaling bonus for streams/bursts higher than 200bpm
|
|
4127
|
-
if (strainTime
|
|
4514
|
+
if (osuBase.MathUtils.millisecondsToBPM(strainTime) > this.minSpeedBonus) {
|
|
4128
4515
|
speedBonus =
|
|
4129
|
-
0.75 *
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
// Cap distance at spacing threshold
|
|
4134
|
-
const distance = Math.min(singleSpacingThreshold, travelDistance + current.minimumJumpDistance);
|
|
4135
|
-
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
|
|
4136
|
-
let distanceBonus = Math.pow(distance / singleSpacingThreshold, 3.95) *
|
|
4137
|
-
this.DISTANCE_MULTIPLIER;
|
|
4138
|
-
// Apply reduced small circle bonus because flow aim difficulty on small circles does not scale as hard as jumps.
|
|
4139
|
-
distanceBonus *= Math.sqrt(current.smallCircleBonus);
|
|
4140
|
-
if (mods.has(osuBase.ModAutopilot)) {
|
|
4141
|
-
distanceBonus = 0;
|
|
4516
|
+
0.75 *
|
|
4517
|
+
Math.pow((osuBase.MathUtils.bpmToMilliseconds(this.minSpeedBonus) -
|
|
4518
|
+
strainTime) /
|
|
4519
|
+
40, 2);
|
|
4142
4520
|
}
|
|
4143
4521
|
// Base difficulty with all bonuses
|
|
4144
|
-
|
|
4522
|
+
let difficulty = ((1 + speedBonus) * 1000) / strainTime;
|
|
4523
|
+
difficulty *= this.highBpmBonus(current.strainTime);
|
|
4145
4524
|
// Apply penalty if there's doubletappable doubles
|
|
4146
4525
|
return difficulty * doubletapness;
|
|
4147
4526
|
}
|
|
4527
|
+
static highBpmBonus(ms) {
|
|
4528
|
+
return 1 / (1 - Math.pow(0.3, ms / 1000));
|
|
4529
|
+
}
|
|
4148
4530
|
}
|
|
4149
4531
|
// ~200 1/4 BPM streams
|
|
4150
|
-
OsuSpeedEvaluator.minSpeedBonus =
|
|
4151
|
-
OsuSpeedEvaluator.DISTANCE_MULTIPLIER = 0.8;
|
|
4532
|
+
OsuSpeedEvaluator.minSpeedBonus = 200;
|
|
4152
4533
|
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
this.
|
|
4160
|
-
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;
|
|
4161
4558
|
}
|
|
4162
|
-
|
|
4163
|
-
|
|
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) {
|
|
4164
4564
|
return 0;
|
|
4165
4565
|
}
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
let ratingMultiplier = 1;
|
|
4178
|
-
const approachRateLengthBonus = 0.95 +
|
|
4179
|
-
0.4 * Math.min(1, this.totalHits / 2000) +
|
|
4180
|
-
(this.totalHits > 2000
|
|
4181
|
-
? Math.log10(this.totalHits / 2000) * 0.5
|
|
4182
|
-
: 0);
|
|
4183
|
-
let approachRateFactor = 0;
|
|
4184
|
-
if (this.approachRate > 10.33) {
|
|
4185
|
-
approachRateFactor = 0.3 * (this.approachRate - 10.33);
|
|
4186
|
-
}
|
|
4187
|
-
else if (this.approachRate < 8) {
|
|
4188
|
-
approachRateFactor = 0.05 * (8 - this.approachRate);
|
|
4189
|
-
}
|
|
4190
|
-
if (this.mods.has(osuBase.ModRelax)) {
|
|
4191
|
-
approachRateFactor = 0;
|
|
4192
|
-
}
|
|
4193
|
-
// Buff for longer beatmaps with high AR.
|
|
4194
|
-
ratingMultiplier += approachRateFactor * approachRateLengthBonus;
|
|
4195
|
-
if (this.mods.has(osuBase.ModHidden)) {
|
|
4196
|
-
const visibilityFactor = this.calculateAimVisibilityFactor();
|
|
4197
|
-
ratingMultiplier += OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.approachRate, visibilityFactor, this.sliderFactor);
|
|
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;
|
|
4198
4577
|
}
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
0.98 + Math.pow(Math.max(0, this.overallDifficulty), 2) / 2500;
|
|
4202
|
-
return aimRating * Math.cbrt(ratingMultiplier);
|
|
4203
|
-
}
|
|
4204
|
-
computeSpeedRating(speedDifficultyValue) {
|
|
4205
|
-
if (this.mods.has(osuBase.ModRelax)) {
|
|
4578
|
+
const consistentTopStrain = difficultyValue * (1 - this.decayWeight);
|
|
4579
|
+
if (consistentTopStrain === 0) {
|
|
4206
4580
|
return 0;
|
|
4207
4581
|
}
|
|
4208
|
-
|
|
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) {
|
|
4209
4587
|
if (this.mods.has(osuBase.ModAutopilot)) {
|
|
4210
|
-
|
|
4588
|
+
return 0;
|
|
4211
4589
|
}
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
0.4 * Math.min(1, this.totalHits / 2000) +
|
|
4220
|
-
(this.totalHits > 2000
|
|
4221
|
-
? Math.log10(this.totalHits / 2000) * 0.5
|
|
4222
|
-
: 0);
|
|
4223
|
-
let approachRateFactor = 0;
|
|
4224
|
-
if (this.approachRate > 10.33) {
|
|
4225
|
-
approachRateFactor = 0.3 * (this.approachRate - 10.33);
|
|
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);
|
|
4226
4597
|
}
|
|
4227
|
-
|
|
4228
|
-
|
|
4598
|
+
return this.currentStrain;
|
|
4599
|
+
}
|
|
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;
|
|
4229
4608
|
}
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
if (this.mods.has(osuBase.ModHidden)) {
|
|
4233
|
-
const visibilityFactor = this.calculateSpeedVisibilityFactor();
|
|
4234
|
-
ratingMultiplier += OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.approachRate, visibilityFactor, this.sliderFactor);
|
|
4609
|
+
else {
|
|
4610
|
+
current.aimStrainWithoutSliders = this.currentStrain;
|
|
4235
4611
|
}
|
|
4236
|
-
ratingMultiplier *=
|
|
4237
|
-
0.95 + Math.pow(Math.max(0, this.overallDifficulty), 2) / 750;
|
|
4238
|
-
return speedRating * Math.cbrt(ratingMultiplier);
|
|
4239
4612
|
}
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
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);
|
|
4619
|
+
if (this.mods.has(osuBase.ModMagnetised)) {
|
|
4620
|
+
const magnetisedStrength = this.mods.get(osuBase.ModMagnetised).attractionStrength.value;
|
|
4621
|
+
totalDifficulty *= 1 - magnetisedStrength;
|
|
4243
4622
|
}
|
|
4244
|
-
|
|
4623
|
+
totalDifficulty *=
|
|
4624
|
+
0.985 + Math.pow(Math.max(0, current.overallDifficulty), 2) / 4000;
|
|
4625
|
+
return totalDifficulty;
|
|
4626
|
+
}
|
|
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;
|
|
4245
4635
|
if (this.mods.has(osuBase.ModTouchDevice)) {
|
|
4246
|
-
|
|
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);
|
|
4247
4639
|
}
|
|
4248
4640
|
if (this.mods.has(osuBase.ModRelax)) {
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
else if (this.mods.has(osuBase.ModAutopilot)) {
|
|
4252
|
-
flashlightRating *= 0.4;
|
|
4253
|
-
}
|
|
4254
|
-
if (this.mods.has(osuBase.ModMagnetised)) {
|
|
4255
|
-
const magnetisedStrength = this.mods.get(osuBase.ModMagnetised).attractionStrength.value;
|
|
4256
|
-
flashlightRating *= 1 - magnetisedStrength;
|
|
4257
|
-
}
|
|
4258
|
-
if (this.mods.has(osuBase.ModDeflate)) {
|
|
4259
|
-
const deflateInitialScale = this.mods.get(osuBase.ModDeflate).startScale.value;
|
|
4260
|
-
flashlightRating *= osuBase.MathUtils.clamp(osuBase.Interpolation.reverseLerp(deflateInitialScale, 11, 1), 0.1, 1);
|
|
4641
|
+
combinedSnapDifficulty *= 0.75;
|
|
4642
|
+
flowDifficulty *= 0.6;
|
|
4261
4643
|
}
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
ratingMultiplier *=
|
|
4265
|
-
0.7 +
|
|
4266
|
-
0.1 * Math.min(1, this.totalHits / 200) +
|
|
4267
|
-
(this.totalHits > 200
|
|
4268
|
-
? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
|
|
4269
|
-
: 0);
|
|
4270
|
-
// It is important to consider accuracy difficulty when scaling with accuracy.
|
|
4271
|
-
ratingMultiplier *=
|
|
4272
|
-
0.98 + Math.pow(Math.max(0, this.overallDifficulty), 2) / 2500;
|
|
4273
|
-
return flashlightRating * Math.sqrt(ratingMultiplier);
|
|
4274
|
-
}
|
|
4275
|
-
calculateAimVisibilityFactor() {
|
|
4276
|
-
const approachRateFactorEndpoint = 11.5;
|
|
4277
|
-
const mechanicalDifficultyFactor = osuBase.Interpolation.reverseLerp(this.mechanicalDifficultyRating, 5, 10);
|
|
4278
|
-
const approachRateFactorStartingPoint = osuBase.Interpolation.lerp(9, 10.33, mechanicalDifficultyFactor);
|
|
4279
|
-
return osuBase.Interpolation.reverseLerp(this.approachRate, approachRateFactorEndpoint, approachRateFactorStartingPoint);
|
|
4280
|
-
}
|
|
4281
|
-
calculateSpeedVisibilityFactor() {
|
|
4282
|
-
const approachRateFactorEndpoint = 11.5;
|
|
4283
|
-
const mechanicalDifficultyFactor = osuBase.Interpolation.reverseLerp(this.mechanicalDifficultyRating, 5, 10);
|
|
4284
|
-
const approachRateFactorStartingPoint = osuBase.Interpolation.lerp(10, 10.33, mechanicalDifficultyFactor);
|
|
4285
|
-
return osuBase.Interpolation.reverseLerp(this.approachRate, approachRateFactorEndpoint, approachRateFactorStartingPoint);
|
|
4644
|
+
const totalDifficulty = combinedSnapDifficulty * pSnap + flowDifficulty * pFlow;
|
|
4645
|
+
return totalDifficulty * this.skillMultiplierTotal;
|
|
4286
4646
|
}
|
|
4287
4647
|
/**
|
|
4288
|
-
*
|
|
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.
|
|
4289
4658
|
*
|
|
4290
|
-
* @param
|
|
4291
|
-
* @
|
|
4292
|
-
* @param visibilityFactor The visibility factor to apply.
|
|
4293
|
-
* @param sliderFactor The slider factor to apply.
|
|
4294
|
-
* @returns The visibility bonus multiplier.
|
|
4659
|
+
* @param ratio The ratio.
|
|
4660
|
+
* @returns The probability.
|
|
4295
4661
|
*/
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
// Start from normal curve, rewarding lower AR up to AR 7.
|
|
4300
|
-
// Traceable forcefully requires a lower reading bonus for now as it is post-applied in pp, which make
|
|
4301
|
-
// it multiplicative with the regular AR bonuses.
|
|
4302
|
-
// This means it has an advantage over Hidden, so we decrease the multiplier to compensate.
|
|
4303
|
-
// This should be removed once we are able to apply Traceable bonuses in star rating (requires real-time
|
|
4304
|
-
// difficulty calculations being possible).
|
|
4305
|
-
let readingBonus = (isAlwaysPartiallyVisible ? 0.025 : 0.04) *
|
|
4306
|
-
(12 - Math.max(approachRate, 7));
|
|
4307
|
-
readingBonus *= visibilityFactor;
|
|
4308
|
-
// We want to reward slideraim on low AR less.
|
|
4309
|
-
const sliderVisibilityFactor = Math.pow(sliderFactor, 3);
|
|
4310
|
-
// For AR up to 0, reduce reward for very low ARs when object is visible.
|
|
4311
|
-
if (approachRate < 7) {
|
|
4312
|
-
readingBonus +=
|
|
4313
|
-
(isAlwaysPartiallyVisible ? 0.02 : 0.045) *
|
|
4314
|
-
(7 - Math.max(approachRate, 0)) *
|
|
4315
|
-
sliderVisibilityFactor;
|
|
4662
|
+
calculateSnapFlowProbability(ratio) {
|
|
4663
|
+
if (ratio === 0) {
|
|
4664
|
+
return 0;
|
|
4316
4665
|
}
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
readingBonus +=
|
|
4320
|
-
(isAlwaysPartiallyVisible ? 0.01 : 0.1) *
|
|
4321
|
-
(1 - Math.pow(1.5, approachRate)) *
|
|
4322
|
-
sliderVisibilityFactor;
|
|
4666
|
+
if (Number.isNaN(ratio)) {
|
|
4667
|
+
return 1;
|
|
4323
4668
|
}
|
|
4324
|
-
return
|
|
4325
|
-
}
|
|
4326
|
-
static calculateDifficultyRating(difficultyValue) {
|
|
4327
|
-
return Math.sqrt(difficultyValue) * this.difficultyMultiplier;
|
|
4328
|
-
}
|
|
4329
|
-
}
|
|
4330
|
-
OsuRatingCalculator.difficultyMultiplier = 0.0675;
|
|
4331
|
-
|
|
4332
|
-
/**
|
|
4333
|
-
* Used to processes strain values of difficulty hitobjects, keep track of strain levels caused by the processed objects
|
|
4334
|
-
* and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
|
|
4335
|
-
*/
|
|
4336
|
-
class OsuSkill extends StrainSkill {
|
|
4337
|
-
constructor() {
|
|
4338
|
-
super(...arguments);
|
|
4339
|
-
this.difficulty = 0;
|
|
4669
|
+
return osuBase.MathUtils.logistic(-7.27 * Math.log(ratio));
|
|
4340
4670
|
}
|
|
4341
4671
|
difficultyValue() {
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
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;
|
|
4350
4699
|
}
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
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)));
|
|
4359
4721
|
}
|
|
4360
|
-
|
|
4361
|
-
|
|
4722
|
+
time += strain.sectionLength;
|
|
4723
|
+
++strainsToRemove;
|
|
4362
4724
|
}
|
|
4363
|
-
|
|
4725
|
+
strains.splice(0, strainsToRemove);
|
|
4726
|
+
return strains.sort((a, b) => b.value - a.value);
|
|
4364
4727
|
}
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
class StrainUtils {
|
|
4368
|
-
static countTopWeightedSliders(sliderStrains, difficultyValue) {
|
|
4369
|
-
if (sliderStrains.length === 0) {
|
|
4370
|
-
return 0;
|
|
4371
|
-
}
|
|
4372
|
-
const consistentTopStrain = difficultyValue / 10;
|
|
4373
|
-
if (consistentTopStrain === 0) {
|
|
4374
|
-
return 0;
|
|
4375
|
-
}
|
|
4376
|
-
// Use a weighted sum of all strains. Constants are arbitrary and give nice values
|
|
4377
|
-
return sliderStrains.reduce((total, next) => total +
|
|
4378
|
-
osuBase.MathUtils.offsetLogistic(next / consistentTopStrain, 0.88, 10, 1.1));
|
|
4728
|
+
strainDecay(ms) {
|
|
4729
|
+
return Math.pow(0.2, ms / 1000);
|
|
4379
4730
|
}
|
|
4380
4731
|
}
|
|
4381
4732
|
|
|
4382
4733
|
/**
|
|
4383
|
-
* Represents the skill required to
|
|
4734
|
+
* Represents the skill required to read every object in the beatmap.
|
|
4384
4735
|
*/
|
|
4385
|
-
class
|
|
4386
|
-
constructor(mods,
|
|
4736
|
+
class OsuReading extends HarmonicSkill {
|
|
4737
|
+
constructor(mods, clockRate, hitObjects) {
|
|
4387
4738
|
super(mods);
|
|
4388
|
-
this.
|
|
4389
|
-
this.
|
|
4390
|
-
this.
|
|
4391
|
-
this.
|
|
4392
|
-
this.
|
|
4393
|
-
this.skillMultiplier = 26;
|
|
4394
|
-
this.sliderStrains = [];
|
|
4395
|
-
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;
|
|
4396
4744
|
}
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
*/
|
|
4400
|
-
countDifficultSliders() {
|
|
4401
|
-
if (this.sliderStrains.length === 0) {
|
|
4745
|
+
countTopWeightedObjectDifficulties(difficultyValue) {
|
|
4746
|
+
if (difficultyValue === 0) {
|
|
4402
4747
|
return 0;
|
|
4403
4748
|
}
|
|
4404
|
-
|
|
4405
|
-
if (maxSliderStrain === 0) {
|
|
4749
|
+
if (this.noteWeightSum === 0) {
|
|
4406
4750
|
return 0;
|
|
4407
4751
|
}
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
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);
|
|
4416
4759
|
}
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
this.
|
|
4420
|
-
|
|
4760
|
+
objectDifficultyOf(current) {
|
|
4761
|
+
const decay = this.difficultyDecay(current.deltaTime);
|
|
4762
|
+
this.currentDifficulty *= decay;
|
|
4763
|
+
this.currentDifficulty +=
|
|
4764
|
+
this.calculateAdjustedDifficulty(current) *
|
|
4765
|
+
(1 - decay) *
|
|
4421
4766
|
this.skillMultiplier;
|
|
4422
|
-
this.
|
|
4423
|
-
if (current.object instanceof osuBase.Slider) {
|
|
4424
|
-
this.sliderStrains.push(this.currentAimStrain);
|
|
4425
|
-
}
|
|
4426
|
-
return this.currentAimStrain;
|
|
4767
|
+
return this.currentDifficulty;
|
|
4427
4768
|
}
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
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
|
+
}
|
|
4432
4775
|
}
|
|
4433
|
-
/**
|
|
4434
|
-
* @param current The hitobject to save to.
|
|
4435
|
-
*/
|
|
4436
4776
|
saveToHitObject(current, difficulty) {
|
|
4437
|
-
|
|
4438
|
-
|
|
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);
|
|
4439
4783
|
}
|
|
4440
|
-
|
|
4441
|
-
|
|
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;
|
|
4442
4815
|
}
|
|
4816
|
+
return reducedNoteCount;
|
|
4443
4817
|
}
|
|
4444
|
-
|
|
4445
|
-
return Math.pow(this.
|
|
4818
|
+
difficultyDecay(ms) {
|
|
4819
|
+
return Math.pow(this.difficultyDecayBase, ms / 1000);
|
|
4446
4820
|
}
|
|
4447
4821
|
}
|
|
4448
4822
|
|
|
4449
4823
|
/**
|
|
4450
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.
|
|
4451
4825
|
*/
|
|
4452
|
-
class OsuSpeed extends
|
|
4826
|
+
class OsuSpeed extends HarmonicSkill {
|
|
4453
4827
|
constructor() {
|
|
4454
4828
|
super(...arguments);
|
|
4455
|
-
this.
|
|
4456
|
-
this.
|
|
4457
|
-
this.reducedSectionBaseline = 0.75;
|
|
4458
|
-
this.decayWeight = 0.9;
|
|
4459
|
-
this.currentSpeedStrain = 0;
|
|
4829
|
+
this.harmonicScale = 20;
|
|
4830
|
+
this.currentDifficulty = 0;
|
|
4460
4831
|
this.currentRhythm = 0;
|
|
4461
|
-
this.skillMultiplier = 1.
|
|
4462
|
-
this.
|
|
4463
|
-
this.
|
|
4832
|
+
this.skillMultiplier = 1.16;
|
|
4833
|
+
this.strainDecayBase = 0.3;
|
|
4834
|
+
this.sliderDifficulties = [];
|
|
4835
|
+
this.maxDifficulty = 0;
|
|
4464
4836
|
}
|
|
4465
4837
|
/**
|
|
4466
4838
|
* The amount of notes that are relevant to the difficulty.
|
|
4467
4839
|
*/
|
|
4468
4840
|
relevantNoteCount() {
|
|
4469
|
-
if (this.
|
|
4841
|
+
if (this.objectDifficulties.length === 0 || this.maxDifficulty === 0) {
|
|
4470
4842
|
return 0;
|
|
4471
4843
|
}
|
|
4472
|
-
return this.
|
|
4844
|
+
return this.objectDifficulties.reduce((total, next) => total +
|
|
4845
|
+
1 / (1 + Math.exp(-((next / this.maxDifficulty) * 12 - 6))), 0);
|
|
4473
4846
|
}
|
|
4474
4847
|
/**
|
|
4475
|
-
* 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.
|
|
4476
4851
|
*/
|
|
4477
|
-
countTopWeightedSliders() {
|
|
4478
|
-
|
|
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);
|
|
4479
4867
|
}
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
this.
|
|
4486
|
-
|
|
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) *
|
|
4487
4877
|
this.skillMultiplier;
|
|
4488
4878
|
this.currentRhythm = OsuRhythmEvaluator.evaluateDifficultyOf(current);
|
|
4489
|
-
const
|
|
4490
|
-
this.
|
|
4491
|
-
this.maxStrain = Math.max(this.maxStrain, strain);
|
|
4879
|
+
const difficulty = this.currentDifficulty * this.currentRhythm;
|
|
4880
|
+
this.maxDifficulty = Math.max(this.maxDifficulty, difficulty);
|
|
4492
4881
|
if (current.object instanceof osuBase.Slider) {
|
|
4493
|
-
this.
|
|
4882
|
+
this.sliderDifficulties.push(difficulty);
|
|
4494
4883
|
}
|
|
4495
|
-
return
|
|
4496
|
-
}
|
|
4497
|
-
calculateInitialStrain(time, current) {
|
|
4498
|
-
var _a, _b;
|
|
4499
|
-
return (this.currentSpeedStrain *
|
|
4500
|
-
this.currentRhythm *
|
|
4501
|
-
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;
|
|
4502
4885
|
}
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
*/
|
|
4506
|
-
saveToHitObject(current, difficulty) {
|
|
4507
|
-
current.speedStrain = difficulty;
|
|
4886
|
+
saveToHitObject(current) {
|
|
4887
|
+
current.speedStrain = this.currentDifficulty * this.currentRhythm;
|
|
4508
4888
|
current.rhythmMultiplier = this.currentRhythm;
|
|
4509
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
|
+
}
|
|
4510
4897
|
strainDecay(ms) {
|
|
4511
4898
|
return Math.pow(this.strainDecayBase, ms / 1000);
|
|
4512
4899
|
}
|
|
@@ -4534,6 +4921,10 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4534
4921
|
* The flashlight performance value.
|
|
4535
4922
|
*/
|
|
4536
4923
|
this.flashlight = 0;
|
|
4924
|
+
/**
|
|
4925
|
+
* The reading performance value.
|
|
4926
|
+
*/
|
|
4927
|
+
this.reading = 0;
|
|
4537
4928
|
this.greatWindow = 0;
|
|
4538
4929
|
this.okWindow = 0;
|
|
4539
4930
|
this.mehWindow = 0;
|
|
@@ -4574,20 +4965,17 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4574
4965
|
this.greatWindow = hitWindow.greatWindow / clockRate;
|
|
4575
4966
|
this.okWindow = hitWindow.okWindow / clockRate;
|
|
4576
4967
|
this.mehWindow = hitWindow.mehWindow / clockRate;
|
|
4577
|
-
this.approachRate =
|
|
4578
|
-
|
|
4579
|
-
this.overallDifficulty =
|
|
4580
|
-
OsuDifficultyCalculator.calculateRateAdjustedOverallDifficulty(od, clockRate);
|
|
4968
|
+
this.approachRate = this.calculateRateAdjustedApproachRate(ar, clockRate);
|
|
4969
|
+
this.overallDifficulty = (79.5 - this.greatWindow) / 6;
|
|
4581
4970
|
this.speedDeviation = this.calculateSpeedDeviation();
|
|
4582
4971
|
this.aim = this.calculateAimValue();
|
|
4583
4972
|
this.speed = this.calculateSpeedValue();
|
|
4584
4973
|
this.accuracy = this.calculateAccuracyValue();
|
|
4585
4974
|
this.flashlight = this.calculateFlashlightValue();
|
|
4975
|
+
this.reading = this.calculateReadingValue();
|
|
4976
|
+
const cognitionValue = OsuDifficultyCalculator.sumCognitionDifficulty(this.reading, this.flashlight);
|
|
4586
4977
|
this.total =
|
|
4587
|
-
|
|
4588
|
-
Math.pow(this.speed, 1.1) +
|
|
4589
|
-
Math.pow(this.accuracy, 1.1) +
|
|
4590
|
-
Math.pow(this.flashlight, 1.1), 1 / 1.1) * finalMultiplier;
|
|
4978
|
+
osuBase.MathUtils.norm(OsuPerformanceCalculator.normExponent, this.aim, this.speed, this.accuracy, cognitionValue) * finalMultiplier;
|
|
4591
4979
|
}
|
|
4592
4980
|
/**
|
|
4593
4981
|
* Calculates the aim performance value of the beatmap.
|
|
@@ -4620,7 +5008,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4620
5008
|
}
|
|
4621
5009
|
let aimValue = OsuAim.difficultyToPerformance(aimDifficulty);
|
|
4622
5010
|
// Longer maps are worth more
|
|
4623
|
-
let lengthBonus = 0.95 + 0.
|
|
5011
|
+
let lengthBonus = 0.95 + 0.35 * Math.min(1, this.totalHits / 2000);
|
|
4624
5012
|
if (this.totalHits > 2000) {
|
|
4625
5013
|
lengthBonus += Math.log10(this.totalHits / 2000) * 0.5;
|
|
4626
5014
|
}
|
|
@@ -4645,7 +5033,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4645
5033
|
else if (this.mods.has(osuBase.ModTraceable)) {
|
|
4646
5034
|
aimValue *=
|
|
4647
5035
|
1 +
|
|
4648
|
-
|
|
5036
|
+
this.calculateTraceableBonus(this.difficultyAttributes.sliderFactor);
|
|
4649
5037
|
}
|
|
4650
5038
|
// Scale the aim value with accuracy.
|
|
4651
5039
|
aimValue *= this.computedAccuracy.value();
|
|
@@ -4660,45 +5048,23 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4660
5048
|
return 0;
|
|
4661
5049
|
}
|
|
4662
5050
|
let speedValue = OsuSpeed.difficultyToPerformance(this.difficultyAttributes.speedDifficulty);
|
|
4663
|
-
// Longer maps are worth more
|
|
4664
|
-
let lengthBonus = 0.95 + 0.4 * Math.min(1, this.totalHits / 2000);
|
|
4665
|
-
if (this.totalHits > 2000) {
|
|
4666
|
-
lengthBonus += Math.log10(this.totalHits / 2000) * 0.5;
|
|
4667
|
-
}
|
|
4668
|
-
speedValue *= lengthBonus;
|
|
4669
5051
|
if (this._effectiveMissCount > 0) {
|
|
4670
5052
|
const speedEstimatedSliderBreaks = this.calculateEstimatedSliderBreaks(this.difficultyAttributes.speedTopWeightedSliderFactor);
|
|
4671
5053
|
const relevantMissCount = Math.min(this._effectiveMissCount + speedEstimatedSliderBreaks, this.totalImperfectHits + this.sliderTicksMissed);
|
|
4672
5054
|
speedValue *= this.calculateMissPenalty(relevantMissCount, this.difficultyAttributes.speedDifficultStrainCount);
|
|
4673
5055
|
}
|
|
4674
|
-
// Traceable bonuses are excluded when Blinds is present, as the increased visual difficulty is
|
|
4675
|
-
// redundant when notes cannot be seen.
|
|
4676
5056
|
if (this.mods.has(osuBase.ModBlinds)) {
|
|
4677
5057
|
// Increasing the speed value by object count for Blinds is not ideal, so the minimum buff is given.
|
|
4678
5058
|
speedValue *= 1.12;
|
|
4679
5059
|
}
|
|
4680
|
-
else if (this.mods.has(osuBase.ModTraceable)) {
|
|
4681
|
-
speedValue *=
|
|
4682
|
-
1 +
|
|
4683
|
-
OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.approachRate, undefined, this.difficultyAttributes.sliderFactor);
|
|
4684
|
-
}
|
|
4685
|
-
// Calculate accuracy assuming the worst case scenario.
|
|
4686
|
-
const countGreat = this.computedAccuracy.n300;
|
|
4687
|
-
const countOk = this.computedAccuracy.n100;
|
|
4688
|
-
const countMeh = this.computedAccuracy.n50;
|
|
4689
|
-
const relevantTotalDiff = this.totalHits - this.difficultyAttributes.speedNoteCount;
|
|
4690
|
-
const relevantAccuracy = new osuBase.Accuracy(this.difficultyAttributes.speedNoteCount > 0
|
|
4691
|
-
? {
|
|
4692
|
-
n300: Math.max(0, countGreat - relevantTotalDiff),
|
|
4693
|
-
n100: Math.max(0, countOk - Math.max(0, relevantTotalDiff - countGreat)),
|
|
4694
|
-
n50: Math.max(0, countMeh -
|
|
4695
|
-
Math.max(0, relevantTotalDiff - countGreat - countOk)),
|
|
4696
|
-
}
|
|
4697
|
-
: // Set accuracy to 0.
|
|
4698
|
-
{ n300: 0, nobjects: 1 });
|
|
4699
5060
|
speedValue *= this.calculateSpeedHighDeviationNerf();
|
|
4700
|
-
//
|
|
4701
|
-
|
|
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);
|
|
4702
5068
|
return speedValue;
|
|
4703
5069
|
}
|
|
4704
5070
|
/**
|
|
@@ -4708,7 +5074,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4708
5074
|
if (this.mods.has(osuBase.ModRelax)) {
|
|
4709
5075
|
return 0;
|
|
4710
5076
|
}
|
|
4711
|
-
const ncircles = this.mods.has(osuBase.ModScoreV2)
|
|
5077
|
+
const ncircles = !this.usingClassicSliderAccuracy || this.mods.has(osuBase.ModScoreV2)
|
|
4712
5078
|
? this.totalHits - this.difficultyAttributes.spinnerCount
|
|
4713
5079
|
: this.difficultyAttributes.hitCircleCount;
|
|
4714
5080
|
if (ncircles === 0) {
|
|
@@ -4721,21 +5087,18 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4721
5087
|
// It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points.
|
|
4722
5088
|
Math.pow(realAccuracy.n300 < 0 ? 0 : realAccuracy.value(), 24) *
|
|
4723
5089
|
2.83;
|
|
4724
|
-
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
|
|
4725
|
-
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);
|
|
4726
5092
|
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
|
|
4727
5093
|
if (this.mods.has(osuBase.ModBlinds)) {
|
|
4728
5094
|
accuracyValue *= 1.14;
|
|
4729
5095
|
}
|
|
4730
|
-
else if (this.mods.has(osuBase.
|
|
5096
|
+
else if (this.mods.has(osuBase.ModTraceable)) {
|
|
4731
5097
|
// Decrease bonus for AR > 10.
|
|
4732
5098
|
accuracyValue *=
|
|
4733
5099
|
1 +
|
|
4734
5100
|
0.08 * osuBase.Interpolation.reverseLerp(this.approachRate, 11.5, 10);
|
|
4735
5101
|
}
|
|
4736
|
-
if (this.mods.has(osuBase.ModFlashlight)) {
|
|
4737
|
-
accuracyValue *= 1.02;
|
|
4738
|
-
}
|
|
4739
5102
|
return accuracyValue;
|
|
4740
5103
|
}
|
|
4741
5104
|
/**
|
|
@@ -4755,17 +5118,23 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4755
5118
|
Math.pow(1 -
|
|
4756
5119
|
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
|
|
4757
5120
|
}
|
|
4758
|
-
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
4759
|
-
flashlightValue *=
|
|
4760
|
-
0.7 +
|
|
4761
|
-
0.1 * Math.min(1, this.totalHits / 200) +
|
|
4762
|
-
(this.totalHits > 200
|
|
4763
|
-
? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
|
|
4764
|
-
: 0);
|
|
4765
5121
|
// Scale the flashlight value with accuracy slightly.
|
|
4766
5122
|
flashlightValue *= 0.5 + this.computedAccuracy.value() / 2;
|
|
4767
5123
|
return flashlightValue;
|
|
4768
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
|
+
}
|
|
4769
5138
|
/**
|
|
4770
5139
|
* Calculates a strain-based miss penalty.
|
|
4771
5140
|
*
|
|
@@ -4777,9 +5146,8 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4777
5146
|
if (missCount === 0) {
|
|
4778
5147
|
return 1;
|
|
4779
5148
|
}
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
1));
|
|
5149
|
+
// https://www.desmos.com/calculator/naggvbcz0a
|
|
5150
|
+
return 0.93 / (missCount / (4 * Math.log(difficultStrainCount)) + 1);
|
|
4783
5151
|
}
|
|
4784
5152
|
/**
|
|
4785
5153
|
* Estimates a player's deviation on speed notes using {@link calculateDeviation}, assuming worst-case.
|
|
@@ -4881,19 +5249,21 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4881
5249
|
return (osuBase.Interpolation.lerp(adjustedSpeedValue, speedValue, t) / speedValue);
|
|
4882
5250
|
}
|
|
4883
5251
|
calculateEstimatedSliderBreaks(topWeightedSliderFactor) {
|
|
4884
|
-
const
|
|
4885
|
-
if (!this.usingClassicSliderAccuracy ||
|
|
5252
|
+
const nonMissMistakes = this.computedAccuracy.n100 + this.computedAccuracy.n50;
|
|
5253
|
+
if (!this.usingClassicSliderAccuracy || nonMissMistakes === 0) {
|
|
4886
5254
|
return 0;
|
|
4887
5255
|
}
|
|
4888
5256
|
const missedComboPercent = 1 - this.combo / this.difficultyAttributes.maxCombo;
|
|
4889
|
-
let estimatedSliderBreaks = Math.min(
|
|
4890
|
-
// Scores with more Oks are more likely to have slider breaks.
|
|
4891
|
-
|
|
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);
|
|
4892
5262
|
// There is a low probability of extra slider breaks on effective miss counts close to 1, as
|
|
4893
5263
|
// score based calculations are good at indicating if only a single break occurred.
|
|
4894
5264
|
estimatedSliderBreaks *= osuBase.MathUtils.smoothstep(this._effectiveMissCount, 1, 2);
|
|
4895
5265
|
return (estimatedSliderBreaks *
|
|
4896
|
-
|
|
5266
|
+
nonMissMistakeAdjustment *
|
|
4897
5267
|
osuBase.MathUtils.offsetLogistic(missedComboPercent, 0.33, 15));
|
|
4898
5268
|
}
|
|
4899
5269
|
/**
|
|
@@ -4902,16 +5272,27 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4902
5272
|
calculateComboBasedEstimatedMissCount() {
|
|
4903
5273
|
let missCount = this.computedAccuracy.nmiss;
|
|
4904
5274
|
const { combo } = this;
|
|
4905
|
-
const { sliderCount, maxCombo } = this.difficultyAttributes;
|
|
5275
|
+
const { aimTopWeightedSliderFactor, sliderCount, maxCombo } = this.difficultyAttributes;
|
|
4906
5276
|
if (sliderCount <= 0) {
|
|
4907
5277
|
return missCount;
|
|
4908
5278
|
}
|
|
4909
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);
|
|
4910
5284
|
// Consider that full combo is maximum combo minus dropped slider tails since
|
|
4911
5285
|
// they don't contribute to combo but also don't break it.
|
|
4912
5286
|
// In classic scores, we can't know the amount of dropped sliders so we estimate
|
|
4913
5287
|
// to 10% of all sliders in the beatmap.
|
|
4914
|
-
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);
|
|
4915
5296
|
if (combo < fullComboThreshold) {
|
|
4916
5297
|
missCount = fullComboThreshold / Math.max(1, combo);
|
|
4917
5298
|
}
|
|
@@ -4938,6 +5319,36 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4938
5319
|
}
|
|
4939
5320
|
return missCount;
|
|
4940
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
|
+
}
|
|
4941
5352
|
toString() {
|
|
4942
5353
|
return (this.total.toFixed(2) +
|
|
4943
5354
|
" pp (" +
|
|
@@ -4948,10 +5359,13 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
4948
5359
|
this.accuracy.toFixed(2) +
|
|
4949
5360
|
" accuracy, " +
|
|
4950
5361
|
this.flashlight.toFixed(2) +
|
|
4951
|
-
" flashlight
|
|
5362
|
+
" flashlight, " +
|
|
5363
|
+
this.reading.toFixed(2) +
|
|
5364
|
+
" reading)");
|
|
4952
5365
|
}
|
|
4953
5366
|
}
|
|
4954
|
-
OsuPerformanceCalculator.finalMultiplier = 1.
|
|
5367
|
+
OsuPerformanceCalculator.finalMultiplier = 1.12;
|
|
5368
|
+
OsuPerformanceCalculator.normExponent = 1.1;
|
|
4955
5369
|
|
|
4956
5370
|
/**
|
|
4957
5371
|
* Represents an osu!standard hit object with difficulty calculation values.
|
|
@@ -4967,34 +5381,83 @@ class OsuDifficultyHitObject extends DifficultyHitObject {
|
|
|
4967
5381
|
this.mode = osuBase.Modes.Osu;
|
|
4968
5382
|
}
|
|
4969
5383
|
get smallCircleBonus() {
|
|
4970
|
-
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;
|
|
4971
5389
|
}
|
|
4972
5390
|
}
|
|
4973
5391
|
|
|
4974
5392
|
/**
|
|
4975
|
-
*
|
|
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.
|
|
4976
5395
|
*/
|
|
4977
|
-
class
|
|
5396
|
+
class OsuSkill extends StrainSkill {
|
|
4978
5397
|
constructor() {
|
|
4979
5398
|
super(...arguments);
|
|
4980
|
-
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;
|
|
4981
5437
|
this.reducedSectionCount = 0;
|
|
4982
5438
|
this.reducedSectionBaseline = 1;
|
|
4983
5439
|
this.decayWeight = 1;
|
|
4984
5440
|
this.currentFlashlightStrain = 0;
|
|
4985
|
-
this.skillMultiplier = 0.
|
|
4986
|
-
}
|
|
4987
|
-
static difficultyToPerformance(difficulty) {
|
|
4988
|
-
return Math.pow(difficulty, 2) * 25;
|
|
5441
|
+
this.skillMultiplier = 0.058;
|
|
4989
5442
|
}
|
|
4990
5443
|
difficultyValue() {
|
|
4991
|
-
|
|
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;
|
|
4992
5453
|
}
|
|
4993
5454
|
strainValueAt(current) {
|
|
5455
|
+
if (!this.mods.has(osuBase.ModFlashlight)) {
|
|
5456
|
+
return 0;
|
|
5457
|
+
}
|
|
4994
5458
|
this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
|
|
4995
5459
|
this.currentFlashlightStrain +=
|
|
4996
|
-
|
|
4997
|
-
this.skillMultiplier;
|
|
5460
|
+
this.calculateAdjustedDifficulty(current) * this.skillMultiplier;
|
|
4998
5461
|
return this.currentFlashlightStrain;
|
|
4999
5462
|
}
|
|
5000
5463
|
calculateInitialStrain(time, current) {
|
|
@@ -5002,11 +5465,34 @@ class OsuFlashlight extends OsuSkill {
|
|
|
5002
5465
|
return (this.currentFlashlightStrain *
|
|
5003
5466
|
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
5004
5467
|
}
|
|
5005
|
-
saveToHitObject(current
|
|
5006
|
-
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;
|
|
5007
5493
|
}
|
|
5008
5494
|
strainDecay(ms) {
|
|
5009
|
-
return Math.pow(
|
|
5495
|
+
return Math.pow(0.15, ms / 1000);
|
|
5010
5496
|
}
|
|
5011
5497
|
}
|
|
5012
5498
|
|
|
@@ -5020,7 +5506,6 @@ class OsuDifficultyAttributes extends DifficultyAttributes {
|
|
|
5020
5506
|
this.drainRate = 0;
|
|
5021
5507
|
this.speedDifficulty = 0;
|
|
5022
5508
|
this.speedDifficultStrainCount = 0;
|
|
5023
|
-
this.aimTopWeightedSliderFactor = 0;
|
|
5024
5509
|
this.speedTopWeightedSliderFactor = 0;
|
|
5025
5510
|
if (!cacheableAttributes) {
|
|
5026
5511
|
return;
|
|
@@ -5030,8 +5515,6 @@ class OsuDifficultyAttributes extends DifficultyAttributes {
|
|
|
5030
5515
|
this.speedDifficulty = cacheableAttributes.speedDifficulty;
|
|
5031
5516
|
this.speedDifficultStrainCount =
|
|
5032
5517
|
cacheableAttributes.speedDifficultStrainCount;
|
|
5033
|
-
this.aimTopWeightedSliderFactor =
|
|
5034
|
-
cacheableAttributes.aimTopWeightedSliderFactor;
|
|
5035
5518
|
this.speedTopWeightedSliderFactor =
|
|
5036
5519
|
cacheableAttributes.speedTopWeightedSliderFactor;
|
|
5037
5520
|
}
|
|
@@ -5039,7 +5522,8 @@ class OsuDifficultyAttributes extends DifficultyAttributes {
|
|
|
5039
5522
|
return (super.toString() +
|
|
5040
5523
|
` (${this.aimDifficulty.toFixed(2)} aim, ` +
|
|
5041
5524
|
`${this.speedDifficulty.toFixed(2)} speed, ` +
|
|
5042
|
-
`${this.flashlightDifficulty.toFixed(2)} flashlight
|
|
5525
|
+
`${this.flashlightDifficulty.toFixed(2)} flashlight, ` +
|
|
5526
|
+
`${this.readingDifficulty.toFixed(2)} reading)`);
|
|
5043
5527
|
}
|
|
5044
5528
|
}
|
|
5045
5529
|
|
|
@@ -5049,7 +5533,6 @@ class OsuDifficultyAttributes extends DifficultyAttributes {
|
|
|
5049
5533
|
class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
5050
5534
|
constructor() {
|
|
5051
5535
|
super();
|
|
5052
|
-
this.starRatingMultiplier = 0.0265;
|
|
5053
5536
|
this.difficultyAdjustmentMods.push(osuBase.ModTouchDevice, osuBase.ModBlinds);
|
|
5054
5537
|
}
|
|
5055
5538
|
retainDifficultyAdjustmentMods(mods) {
|
|
@@ -5057,8 +5540,8 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
5057
5540
|
mod.isOsuRelevant &&
|
|
5058
5541
|
this.difficultyAdjustmentMods.some((m) => mod instanceof m));
|
|
5059
5542
|
}
|
|
5060
|
-
createDifficultyAttributes(
|
|
5061
|
-
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;
|
|
5062
5545
|
const attributes = new OsuDifficultyAttributes();
|
|
5063
5546
|
if (playableBeatmap.hitObjects.objects.length === 0) {
|
|
5064
5547
|
return attributes;
|
|
@@ -5069,27 +5552,26 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
5069
5552
|
attributes.hitCircleCount = playableBeatmap.hitObjects.circles;
|
|
5070
5553
|
attributes.sliderCount = playableBeatmap.hitObjects.sliders;
|
|
5071
5554
|
attributes.spinnerCount = playableBeatmap.hitObjects.spinners;
|
|
5555
|
+
attributes.approachRate = playableBeatmap.difficulty.ar;
|
|
5556
|
+
attributes.overallDifficulty = playableBeatmap.difficulty.od;
|
|
5072
5557
|
attributes.drainRate = playableBeatmap.difficulty.hp;
|
|
5073
|
-
attributes.approachRate =
|
|
5074
|
-
OsuDifficultyCalculator.calculateRateAdjustedApproachRate(playableBeatmap.difficulty.ar, attributes.clockRate);
|
|
5075
|
-
attributes.overallDifficulty =
|
|
5076
|
-
OsuDifficultyCalculator.calculateRateAdjustedOverallDifficulty(playableBeatmap.difficulty.od, attributes.clockRate);
|
|
5077
5558
|
const aim = skills.find((s) => s instanceof OsuAim && s.withSliders);
|
|
5078
5559
|
const aimNoSlider = skills.find((s) => s instanceof OsuAim && !s.withSliders);
|
|
5079
5560
|
const speed = skills.find((s) => s instanceof OsuSpeed);
|
|
5080
5561
|
const flashlight = skills.find((s) => s instanceof OsuFlashlight);
|
|
5562
|
+
const reading = skills.find((s) => s instanceof OsuReading);
|
|
5081
5563
|
// Aim attributes
|
|
5082
5564
|
const aimDifficultyValue = (_a = aim === null || aim === void 0 ? void 0 : aim.difficultyValue()) !== null && _a !== void 0 ? _a : 0;
|
|
5083
|
-
|
|
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;
|
|
5084
5567
|
attributes.aimDifficultStrainCount =
|
|
5085
|
-
(
|
|
5568
|
+
(_d = aim === null || aim === void 0 ? void 0 : aim.countTopWeightedStrains(aimDifficultyValue)) !== null && _d !== void 0 ? _d : 0;
|
|
5086
5569
|
attributes.sliderFactor =
|
|
5087
5570
|
aimDifficultyValue > 0
|
|
5088
|
-
?
|
|
5089
|
-
OsuRatingCalculator.calculateDifficultyRating(aimDifficultyValue)
|
|
5571
|
+
? this.calculateAimDifficultyRating(aimNoSliderDifficultyValue) / this.calculateAimDifficultyRating(aimDifficultyValue)
|
|
5090
5572
|
: 1;
|
|
5091
|
-
const aimNoSliderTopWeightedSliderCount = (_e = aimNoSlider === null || aimNoSlider === void 0 ? void 0 : aimNoSlider.countTopWeightedSliders()) !== null && _e !== void 0 ? _e : 0;
|
|
5092
|
-
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;
|
|
5093
5575
|
attributes.aimTopWeightedSliderFactor =
|
|
5094
5576
|
aimNoSliderTopWeightedSliderCount /
|
|
5095
5577
|
Math.max(1, aimNoSliderDifficultStrainCount -
|
|
@@ -5098,27 +5580,29 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
5098
5580
|
const speedDifficultyValue = (_g = speed === null || speed === void 0 ? void 0 : speed.difficultyValue()) !== null && _g !== void 0 ? _g : 0;
|
|
5099
5581
|
attributes.speedNoteCount = (_h = speed === null || speed === void 0 ? void 0 : speed.relevantNoteCount()) !== null && _h !== void 0 ? _h : 0;
|
|
5100
5582
|
attributes.speedDifficultStrainCount =
|
|
5101
|
-
(_j = speed === null || speed === void 0 ? void 0 : speed.
|
|
5102
|
-
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;
|
|
5103
5585
|
attributes.speedTopWeightedSliderFactor =
|
|
5104
5586
|
speedTopWeightedSliderCount /
|
|
5105
5587
|
Math.max(1, attributes.speedDifficultStrainCount -
|
|
5106
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;
|
|
5107
5593
|
// Final rating
|
|
5108
|
-
const mechanicalDifficultyRating = this.calculateMechanicalDifficultyRating(aimDifficultyValue, speedDifficultyValue);
|
|
5109
|
-
const ratingCalculator = new OsuRatingCalculator(attributes.mods, playableBeatmap.hitObjects.objects.length, attributes.approachRate, attributes.overallDifficulty, mechanicalDifficultyRating, attributes.sliderFactor);
|
|
5110
5594
|
attributes.aimDifficulty =
|
|
5111
|
-
|
|
5595
|
+
this.calculateAimDifficultyRating(aimDifficultyValue);
|
|
5112
5596
|
attributes.speedDifficulty =
|
|
5113
|
-
|
|
5114
|
-
attributes.flashlightDifficulty =
|
|
5115
|
-
|
|
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);
|
|
5116
5600
|
const baseAimPerformance = OsuAim.difficultyToPerformance(attributes.aimDifficulty);
|
|
5117
5601
|
const baseSpeedPerformance = OsuSpeed.difficultyToPerformance(attributes.speedDifficulty);
|
|
5118
5602
|
const baseFlashlightPerformance = OsuFlashlight.difficultyToPerformance(attributes.flashlightDifficulty);
|
|
5119
|
-
const
|
|
5120
|
-
|
|
5121
|
-
|
|
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);
|
|
5122
5606
|
attributes.starRating = this.calculateStarRating(basePerformance);
|
|
5123
5607
|
return attributes;
|
|
5124
5608
|
}
|
|
@@ -5126,12 +5610,11 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
5126
5610
|
return beatmap.createOsuPlayableBeatmap(mods);
|
|
5127
5611
|
}
|
|
5128
5612
|
createDifficultyHitObjects(beatmap) {
|
|
5129
|
-
var _a;
|
|
5130
5613
|
const clockRate = beatmap.speedMultiplier;
|
|
5131
5614
|
const difficultyObjects = [];
|
|
5132
5615
|
const { objects } = beatmap.hitObjects;
|
|
5133
5616
|
for (let i = 1; i < objects.length; ++i) {
|
|
5134
|
-
const difficultyObject = new OsuDifficultyHitObject(objects[i],
|
|
5617
|
+
const difficultyObject = new OsuDifficultyHitObject(objects[i], objects[i - 1], difficultyObjects, clockRate, i - 1);
|
|
5135
5618
|
difficultyObject.computeProperties(clockRate);
|
|
5136
5619
|
difficultyObjects.push(difficultyObject);
|
|
5137
5620
|
}
|
|
@@ -5147,8 +5630,9 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
5147
5630
|
if (!mods.has(osuBase.ModRelax)) {
|
|
5148
5631
|
skills.push(new OsuSpeed(mods));
|
|
5149
5632
|
}
|
|
5633
|
+
skills.push(new OsuReading(mods, beatmap.speedMultiplier, beatmap.hitObjects.objects));
|
|
5150
5634
|
if (mods.has(osuBase.ModFlashlight)) {
|
|
5151
|
-
skills.push(new OsuFlashlight(mods));
|
|
5635
|
+
skills.push(new OsuFlashlight(mods, beatmap.hitObjects.objects.length));
|
|
5152
5636
|
}
|
|
5153
5637
|
return skills;
|
|
5154
5638
|
}
|
|
@@ -5158,30 +5642,26 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
5158
5642
|
new OsuAim(mods, true),
|
|
5159
5643
|
new OsuAim(mods, false),
|
|
5160
5644
|
new OsuSpeed(mods),
|
|
5161
|
-
new OsuFlashlight(mods),
|
|
5645
|
+
new OsuFlashlight(mods, beatmap.hitObjects.objects.length),
|
|
5162
5646
|
];
|
|
5163
5647
|
}
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
return
|
|
5648
|
+
calculateAimDifficultyRating(difficultyValue) {
|
|
5649
|
+
return Math.pow(difficultyValue, 0.63) * 0.02275;
|
|
5650
|
+
}
|
|
5651
|
+
calculateDifficultyRating(difficultyValue) {
|
|
5652
|
+
return Math.sqrt(difficultyValue) * 0.0675;
|
|
5169
5653
|
}
|
|
5170
5654
|
calculateStarRating(basePerformance) {
|
|
5171
|
-
|
|
5172
|
-
return 0;
|
|
5173
|
-
}
|
|
5174
|
-
return (Math.cbrt(OsuPerformanceCalculator.finalMultiplier) *
|
|
5175
|
-
this.starRatingMultiplier *
|
|
5176
|
-
(Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformance) + 4));
|
|
5177
|
-
}
|
|
5178
|
-
static calculateRateAdjustedApproachRate(approachRate, clockRate) {
|
|
5179
|
-
const preempt = osuBase.BeatmapDifficulty.difficultyRange(approachRate, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin) / clockRate;
|
|
5180
|
-
return osuBase.BeatmapDifficulty.inverseDifficultyRange(preempt, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin);
|
|
5655
|
+
return Math.cbrt(basePerformance * OsuPerformanceCalculator.finalMultiplier);
|
|
5181
5656
|
}
|
|
5182
|
-
static
|
|
5183
|
-
|
|
5184
|
-
|
|
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));
|
|
5185
5665
|
}
|
|
5186
5666
|
}
|
|
5187
5667
|
|
|
@@ -5205,15 +5685,19 @@ exports.DroidSnapAimEvaluator = DroidSnapAimEvaluator;
|
|
|
5205
5685
|
exports.DroidTap = DroidTap;
|
|
5206
5686
|
exports.DroidTapEvaluator = DroidTapEvaluator;
|
|
5207
5687
|
exports.ExtendedDroidDifficultyAttributes = ExtendedDroidDifficultyAttributes;
|
|
5688
|
+
exports.OsuAgilityEvaluator = OsuAgilityEvaluator;
|
|
5208
5689
|
exports.OsuAim = OsuAim;
|
|
5209
|
-
exports.OsuAimEvaluator = OsuAimEvaluator;
|
|
5210
5690
|
exports.OsuDifficultyAttributes = OsuDifficultyAttributes;
|
|
5211
5691
|
exports.OsuDifficultyCalculator = OsuDifficultyCalculator;
|
|
5212
5692
|
exports.OsuDifficultyHitObject = OsuDifficultyHitObject;
|
|
5213
5693
|
exports.OsuFlashlight = OsuFlashlight;
|
|
5214
5694
|
exports.OsuFlashlightEvaluator = OsuFlashlightEvaluator;
|
|
5695
|
+
exports.OsuFlowAimEvaluator = OsuFlowAimEvaluator;
|
|
5215
5696
|
exports.OsuPerformanceCalculator = OsuPerformanceCalculator;
|
|
5697
|
+
exports.OsuReading = OsuReading;
|
|
5698
|
+
exports.OsuReadingEvaluator = OsuReadingEvaluator;
|
|
5216
5699
|
exports.OsuRhythmEvaluator = OsuRhythmEvaluator;
|
|
5700
|
+
exports.OsuSnapAimEvaluator = OsuSnapAimEvaluator;
|
|
5217
5701
|
exports.OsuSpeed = OsuSpeed;
|
|
5218
5702
|
exports.OsuSpeedEvaluator = OsuSpeedEvaluator;
|
|
5219
5703
|
exports.PerformanceCalculator = PerformanceCalculator;
|