@rian8337/osu-difficulty-calculator 4.0.0-beta.87 → 4.0.0-beta.89
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 +219 -218
- package/package.json +3 -3
- package/typings/index.d.ts +8 -3
package/dist/index.js
CHANGED
|
@@ -1919,15 +1919,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1919
1919
|
attributes.hitCircleCount = beatmap.hitObjects.circles;
|
|
1920
1920
|
attributes.sliderCount = beatmap.hitObjects.sliders;
|
|
1921
1921
|
attributes.spinnerCount = beatmap.hitObjects.spinners;
|
|
1922
|
-
|
|
1923
|
-
if (attributes.mods.has(osuBase.ModPrecise)) {
|
|
1924
|
-
greatWindow = new osuBase.PreciseDroidHitWindow(beatmap.difficulty.od)
|
|
1925
|
-
.greatWindow;
|
|
1926
|
-
}
|
|
1927
|
-
else {
|
|
1928
|
-
greatWindow = new osuBase.DroidHitWindow(beatmap.difficulty.od).greatWindow;
|
|
1929
|
-
}
|
|
1930
|
-
attributes.overallDifficulty = osuBase.OsuHitWindow.greatWindowToOD(greatWindow / attributes.clockRate);
|
|
1922
|
+
attributes.overallDifficulty = beatmap.difficulty.od;
|
|
1931
1923
|
this.populateAimAttributes(attributes, skills, objects);
|
|
1932
1924
|
this.populateTapAttributes(attributes, skills, objects);
|
|
1933
1925
|
this.populateRhythmAttributes(attributes, skills);
|
|
@@ -2162,8 +2154,16 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
2162
2154
|
attributes.readingDifficulty *= 0.4;
|
|
2163
2155
|
}
|
|
2164
2156
|
// Consider accuracy difficulty.
|
|
2165
|
-
|
|
2166
|
-
|
|
2157
|
+
let greatWindow;
|
|
2158
|
+
if (attributes.mods.has(osuBase.ModPrecise)) {
|
|
2159
|
+
greatWindow = new osuBase.PreciseDroidHitWindow(attributes.overallDifficulty).greatWindow;
|
|
2160
|
+
}
|
|
2161
|
+
else {
|
|
2162
|
+
greatWindow = new osuBase.DroidHitWindow(attributes.overallDifficulty)
|
|
2163
|
+
.greatWindow;
|
|
2164
|
+
}
|
|
2165
|
+
const overallDifficulty = osuBase.UnadjustedOsuHitWindow.greatWindowToOD(greatWindow / attributes.clockRate);
|
|
2166
|
+
const ratingMultiplier = 0.75 + Math.pow(Math.max(0, overallDifficulty), 2.2) / 800;
|
|
2167
2167
|
attributes.readingDifficulty *= Math.sqrt(ratingMultiplier);
|
|
2168
2168
|
}
|
|
2169
2169
|
/**
|
|
@@ -2687,7 +2687,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2687
2687
|
return Number.POSITIVE_INFINITY;
|
|
2688
2688
|
}
|
|
2689
2689
|
const { clockRate } = this.difficultyAttributes;
|
|
2690
|
-
const hitWindow = this.
|
|
2690
|
+
const hitWindow = this.getHitWindow();
|
|
2691
2691
|
const hitWindow300 = hitWindow.greatWindow / clockRate;
|
|
2692
2692
|
const hitWindow100 = hitWindow.okWindow / clockRate;
|
|
2693
2693
|
const hitWindow50 = hitWindow.mehWindow / clockRate;
|
|
@@ -2756,7 +2756,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2756
2756
|
return Number.POSITIVE_INFINITY;
|
|
2757
2757
|
}
|
|
2758
2758
|
const { clockRate, speedNoteCount } = this.difficultyAttributes;
|
|
2759
|
-
const hitWindow = this.
|
|
2759
|
+
const hitWindow = this.getHitWindow();
|
|
2760
2760
|
const hitWindow300 = hitWindow.greatWindow / clockRate;
|
|
2761
2761
|
const hitWindow100 = hitWindow.okWindow / clockRate;
|
|
2762
2762
|
const hitWindow50 = hitWindow.mehWindow / clockRate;
|
|
@@ -2842,14 +2842,11 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2842
2842
|
const t = 1 - osuBase.Interpolation.reverseLerp(this.tapDeviation, 25, 30);
|
|
2843
2843
|
return osuBase.Interpolation.lerp(adjustedTapValue, tapValue, t) / tapValue;
|
|
2844
2844
|
}
|
|
2845
|
-
|
|
2846
|
-
const
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
else {
|
|
2851
|
-
return new osuBase.DroidHitWindow(osuBase.DroidHitWindow.greatWindowToOD(hitWindow300 * this.difficultyAttributes.clockRate));
|
|
2852
|
-
}
|
|
2845
|
+
getHitWindow() {
|
|
2846
|
+
const { overallDifficulty } = this.difficultyAttributes;
|
|
2847
|
+
return this.mods.has(osuBase.ModPrecise)
|
|
2848
|
+
? new osuBase.PreciseDroidHitWindow(overallDifficulty)
|
|
2849
|
+
: new osuBase.DroidHitWindow(overallDifficulty);
|
|
2853
2850
|
}
|
|
2854
2851
|
toString() {
|
|
2855
2852
|
return (this.total.toFixed(2) +
|
|
@@ -3182,6 +3179,185 @@ class OsuDifficultyAttributes extends DifficultyAttributes {
|
|
|
3182
3179
|
}
|
|
3183
3180
|
}
|
|
3184
3181
|
|
|
3182
|
+
class OsuRatingCalculator {
|
|
3183
|
+
constructor(mods, totalHits, approachRate, overallDifficulty, mechanicalDifficultyRating, sliderFactor) {
|
|
3184
|
+
this.mods = mods;
|
|
3185
|
+
this.totalHits = totalHits;
|
|
3186
|
+
this.approachRate = approachRate;
|
|
3187
|
+
this.overallDifficulty = overallDifficulty;
|
|
3188
|
+
this.mechanicalDifficultyRating = mechanicalDifficultyRating;
|
|
3189
|
+
this.sliderFactor = sliderFactor;
|
|
3190
|
+
}
|
|
3191
|
+
computeAimRating(aimDifficultyValue) {
|
|
3192
|
+
if (this.mods.has(osuBase.ModAutopilot)) {
|
|
3193
|
+
return 0;
|
|
3194
|
+
}
|
|
3195
|
+
let aimRating = OsuRatingCalculator.calculateDifficultyRating(aimDifficultyValue);
|
|
3196
|
+
if (this.mods.has(osuBase.ModTouchDevice)) {
|
|
3197
|
+
aimRating = Math.pow(aimRating, 0.8);
|
|
3198
|
+
}
|
|
3199
|
+
if (this.mods.has(osuBase.ModRelax)) {
|
|
3200
|
+
aimRating *= 0.9;
|
|
3201
|
+
}
|
|
3202
|
+
if (this.mods.has(osuBase.ModMagnetised)) {
|
|
3203
|
+
const magnetisedStrength = this.mods.get(osuBase.ModMagnetised).attractionStrength.value;
|
|
3204
|
+
aimRating *= 1 - magnetisedStrength;
|
|
3205
|
+
}
|
|
3206
|
+
let ratingMultiplier = 1;
|
|
3207
|
+
const approachRateLengthBonus = 0.95 +
|
|
3208
|
+
0.4 * Math.min(1, this.totalHits / 2000) +
|
|
3209
|
+
(this.totalHits > 2000
|
|
3210
|
+
? Math.log10(this.totalHits / 2000) * 0.5
|
|
3211
|
+
: 0);
|
|
3212
|
+
let approachRateFactor = 0;
|
|
3213
|
+
if (this.approachRate > 10.33) {
|
|
3214
|
+
approachRateFactor = 0.3 * (this.approachRate - 10.33);
|
|
3215
|
+
}
|
|
3216
|
+
else if (this.approachRate < 8) {
|
|
3217
|
+
approachRateFactor = 0.05 * (8 - this.approachRate);
|
|
3218
|
+
}
|
|
3219
|
+
if (this.mods.has(osuBase.ModRelax)) {
|
|
3220
|
+
approachRateFactor = 0;
|
|
3221
|
+
}
|
|
3222
|
+
// Buff for longer beatmaps with high AR.
|
|
3223
|
+
ratingMultiplier += approachRateFactor * approachRateLengthBonus;
|
|
3224
|
+
if (this.mods.has(osuBase.ModHidden)) {
|
|
3225
|
+
const visibilityFactor = this.calculateAimVisibilityFactor();
|
|
3226
|
+
ratingMultiplier += OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.approachRate, visibilityFactor, this.sliderFactor);
|
|
3227
|
+
}
|
|
3228
|
+
// It is important to consider accuracy difficulty when scaling with accuracy.
|
|
3229
|
+
ratingMultiplier *=
|
|
3230
|
+
0.98 + Math.pow(Math.max(0, this.overallDifficulty), 2) / 2500;
|
|
3231
|
+
return aimRating * Math.cbrt(ratingMultiplier);
|
|
3232
|
+
}
|
|
3233
|
+
computeSpeedRating(speedDifficultyValue) {
|
|
3234
|
+
if (this.mods.has(osuBase.ModRelax)) {
|
|
3235
|
+
return 0;
|
|
3236
|
+
}
|
|
3237
|
+
let speedRating = OsuRatingCalculator.calculateDifficultyRating(speedDifficultyValue);
|
|
3238
|
+
if (this.mods.has(osuBase.ModAutopilot)) {
|
|
3239
|
+
speedRating *= 0.5;
|
|
3240
|
+
}
|
|
3241
|
+
if (this.mods.has(osuBase.ModMagnetised)) {
|
|
3242
|
+
// Reduce speed rating because of the distance scaling, with maximum reduction being 0.7.
|
|
3243
|
+
const magnetisedStrength = this.mods.get(osuBase.ModMagnetised).attractionStrength.value;
|
|
3244
|
+
speedRating *= 1 - magnetisedStrength * 0.3;
|
|
3245
|
+
}
|
|
3246
|
+
let ratingMultiplier = 1;
|
|
3247
|
+
const approachRateLengthBonus = 0.95 +
|
|
3248
|
+
0.4 * Math.min(1, this.totalHits / 2000) +
|
|
3249
|
+
(this.totalHits > 2000
|
|
3250
|
+
? Math.log10(this.totalHits / 2000) * 0.5
|
|
3251
|
+
: 0);
|
|
3252
|
+
let approachRateFactor = 0;
|
|
3253
|
+
if (this.approachRate > 10.33) {
|
|
3254
|
+
approachRateFactor = 0.3 * (this.approachRate - 10.33);
|
|
3255
|
+
}
|
|
3256
|
+
if (this.mods.has(osuBase.ModAutopilot)) {
|
|
3257
|
+
approachRateFactor = 0;
|
|
3258
|
+
}
|
|
3259
|
+
// Buff for longer beatmaps with high AR.
|
|
3260
|
+
ratingMultiplier += approachRateFactor * approachRateLengthBonus;
|
|
3261
|
+
if (this.mods.has(osuBase.ModHidden)) {
|
|
3262
|
+
const visibilityFactor = this.calculateSpeedVisibilityFactor();
|
|
3263
|
+
ratingMultiplier += OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.approachRate, visibilityFactor, this.sliderFactor);
|
|
3264
|
+
}
|
|
3265
|
+
ratingMultiplier *=
|
|
3266
|
+
0.95 + Math.pow(Math.max(0, this.overallDifficulty), 2) / 750;
|
|
3267
|
+
return speedRating * Math.cbrt(ratingMultiplier);
|
|
3268
|
+
}
|
|
3269
|
+
computeFlashlightRating(flashlightDifficultyValue) {
|
|
3270
|
+
if (!this.mods.has(osuBase.ModFlashlight)) {
|
|
3271
|
+
return 0;
|
|
3272
|
+
}
|
|
3273
|
+
let flashlightRating = OsuRatingCalculator.calculateDifficultyRating(flashlightDifficultyValue);
|
|
3274
|
+
if (this.mods.has(osuBase.ModTouchDevice)) {
|
|
3275
|
+
flashlightRating = Math.pow(flashlightRating, 0.8);
|
|
3276
|
+
}
|
|
3277
|
+
if (this.mods.has(osuBase.ModRelax)) {
|
|
3278
|
+
flashlightRating *= 0.7;
|
|
3279
|
+
}
|
|
3280
|
+
else if (this.mods.has(osuBase.ModAutopilot)) {
|
|
3281
|
+
flashlightRating *= 0.4;
|
|
3282
|
+
}
|
|
3283
|
+
if (this.mods.has(osuBase.ModMagnetised)) {
|
|
3284
|
+
const magnetisedStrength = this.mods.get(osuBase.ModMagnetised).attractionStrength.value;
|
|
3285
|
+
flashlightRating *= 1 - magnetisedStrength;
|
|
3286
|
+
}
|
|
3287
|
+
if (this.mods.has(osuBase.ModDeflate)) {
|
|
3288
|
+
const deflateInitialScale = this.mods.get(osuBase.ModDeflate).startScale.value;
|
|
3289
|
+
flashlightRating *= osuBase.MathUtils.clamp(osuBase.Interpolation.reverseLerp(deflateInitialScale, 11, 1), 0.1, 1);
|
|
3290
|
+
}
|
|
3291
|
+
let ratingMultiplier = 1;
|
|
3292
|
+
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
3293
|
+
ratingMultiplier *=
|
|
3294
|
+
0.7 +
|
|
3295
|
+
0.1 * Math.min(1, this.totalHits / 200) +
|
|
3296
|
+
(this.totalHits > 200
|
|
3297
|
+
? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
|
|
3298
|
+
: 0);
|
|
3299
|
+
// It is important to consider accuracy difficulty when scaling with accuracy.
|
|
3300
|
+
ratingMultiplier *=
|
|
3301
|
+
0.98 + Math.pow(Math.max(0, this.overallDifficulty), 2) / 2500;
|
|
3302
|
+
return flashlightRating * Math.sqrt(ratingMultiplier);
|
|
3303
|
+
}
|
|
3304
|
+
calculateAimVisibilityFactor() {
|
|
3305
|
+
const approachRateFactorEndpoint = 11.5;
|
|
3306
|
+
const mechanicalDifficultyFactor = osuBase.Interpolation.reverseLerp(this.mechanicalDifficultyRating, 5, 10);
|
|
3307
|
+
const approachRateFactorStartingPoint = osuBase.Interpolation.lerp(9, 10.33, mechanicalDifficultyFactor);
|
|
3308
|
+
return osuBase.Interpolation.reverseLerp(this.approachRate, approachRateFactorEndpoint, approachRateFactorStartingPoint);
|
|
3309
|
+
}
|
|
3310
|
+
calculateSpeedVisibilityFactor() {
|
|
3311
|
+
const approachRateFactorEndpoint = 11.5;
|
|
3312
|
+
const mechanicalDifficultyFactor = osuBase.Interpolation.reverseLerp(this.mechanicalDifficultyRating, 5, 10);
|
|
3313
|
+
const approachRateFactorStartingPoint = osuBase.Interpolation.lerp(10, 10.33, mechanicalDifficultyFactor);
|
|
3314
|
+
return osuBase.Interpolation.reverseLerp(this.approachRate, approachRateFactorEndpoint, approachRateFactorStartingPoint);
|
|
3315
|
+
}
|
|
3316
|
+
/**
|
|
3317
|
+
* Calculates a visibility bonus that is applicable to Hidden and Traceable.
|
|
3318
|
+
*
|
|
3319
|
+
* @param mods The mods applied to the calculation.
|
|
3320
|
+
* @param approachRate The approach rate of the beatmap.
|
|
3321
|
+
* @param visibilityFactor The visibility factor to apply.
|
|
3322
|
+
* @param sliderFactor The slider factor to apply.
|
|
3323
|
+
* @returns The visibility bonus multiplier.
|
|
3324
|
+
*/
|
|
3325
|
+
static calculateVisibilityBonus(mods, approachRate, visibilityFactor = 1, sliderFactor = 1) {
|
|
3326
|
+
var _a, _b;
|
|
3327
|
+
const isAlwaysPartiallyVisible = (_b = (_a = mods.get(osuBase.ModHidden)) === null || _a === void 0 ? void 0 : _a.onlyFadeApproachCircles.value) !== null && _b !== void 0 ? _b : mods.has(osuBase.ModTraceable);
|
|
3328
|
+
// Start from normal curve, rewarding lower AR up to AR 7.
|
|
3329
|
+
// Traceable forcefully requires a lower reading bonus for now as it is post-applied in pp, which make
|
|
3330
|
+
// it multiplicative with the regular AR bonuses.
|
|
3331
|
+
// This means it has an advantage over Hidden, so we decrease the multiplier to compensate.
|
|
3332
|
+
// This should be removed once we are able to apply Traceable bonuses in star rating (requires real-time
|
|
3333
|
+
// difficulty calculations being possible).
|
|
3334
|
+
let readingBonus = (isAlwaysPartiallyVisible ? 0.025 : 0.04) *
|
|
3335
|
+
(12 - Math.max(approachRate, 7));
|
|
3336
|
+
readingBonus *= visibilityFactor;
|
|
3337
|
+
// We want to reward slideraim on low AR less.
|
|
3338
|
+
const sliderVisibilityFactor = Math.pow(sliderFactor, 3);
|
|
3339
|
+
// For AR up to 0, reduce reward for very low ARs when object is visible.
|
|
3340
|
+
if (approachRate < 7) {
|
|
3341
|
+
readingBonus +=
|
|
3342
|
+
(isAlwaysPartiallyVisible ? 0.02 : 0.045) *
|
|
3343
|
+
(7 - Math.max(approachRate, 0)) *
|
|
3344
|
+
sliderVisibilityFactor;
|
|
3345
|
+
}
|
|
3346
|
+
// Starting from AR 0, cap values so they won't grow to infinity.
|
|
3347
|
+
if (approachRate < 0) {
|
|
3348
|
+
readingBonus +=
|
|
3349
|
+
(isAlwaysPartiallyVisible ? 0.01 : 0.1) *
|
|
3350
|
+
(1 - Math.pow(1.5, approachRate)) *
|
|
3351
|
+
sliderVisibilityFactor;
|
|
3352
|
+
}
|
|
3353
|
+
return readingBonus;
|
|
3354
|
+
}
|
|
3355
|
+
static calculateDifficultyRating(difficultyValue) {
|
|
3356
|
+
return Math.sqrt(difficultyValue) * this.difficultyMultiplier;
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
OsuRatingCalculator.difficultyMultiplier = 0.0675;
|
|
3360
|
+
|
|
3185
3361
|
/**
|
|
3186
3362
|
* An evaluator for calculating osu!standard Rhythm skill.
|
|
3187
3363
|
*/
|
|
@@ -3459,185 +3635,6 @@ class OsuSpeed extends OsuSkill {
|
|
|
3459
3635
|
}
|
|
3460
3636
|
}
|
|
3461
3637
|
|
|
3462
|
-
class OsuRatingCalculator {
|
|
3463
|
-
constructor(mods, totalHits, approachRate, overallDifficulty, mechanicalDifficultyRating, sliderFactor) {
|
|
3464
|
-
this.mods = mods;
|
|
3465
|
-
this.totalHits = totalHits;
|
|
3466
|
-
this.approachRate = approachRate;
|
|
3467
|
-
this.overallDifficulty = overallDifficulty;
|
|
3468
|
-
this.mechanicalDifficultyRating = mechanicalDifficultyRating;
|
|
3469
|
-
this.sliderFactor = sliderFactor;
|
|
3470
|
-
}
|
|
3471
|
-
computeAimRating(aimDifficultyValue) {
|
|
3472
|
-
if (this.mods.has(osuBase.ModAutopilot)) {
|
|
3473
|
-
return 0;
|
|
3474
|
-
}
|
|
3475
|
-
let aimRating = OsuRatingCalculator.calculateDifficultyRating(aimDifficultyValue);
|
|
3476
|
-
if (this.mods.has(osuBase.ModTouchDevice)) {
|
|
3477
|
-
aimRating = Math.pow(aimRating, 0.8);
|
|
3478
|
-
}
|
|
3479
|
-
if (this.mods.has(osuBase.ModRelax)) {
|
|
3480
|
-
aimRating *= 0.9;
|
|
3481
|
-
}
|
|
3482
|
-
if (this.mods.has(osuBase.ModMagnetised)) {
|
|
3483
|
-
const magnetisedStrength = this.mods.get(osuBase.ModMagnetised).attractionStrength.value;
|
|
3484
|
-
aimRating *= 1 - magnetisedStrength;
|
|
3485
|
-
}
|
|
3486
|
-
let ratingMultiplier = 1;
|
|
3487
|
-
const approachRateLengthBonus = 0.95 +
|
|
3488
|
-
0.4 * Math.min(1, this.totalHits / 2000) +
|
|
3489
|
-
(this.totalHits > 2000
|
|
3490
|
-
? Math.log10(this.totalHits / 2000) * 0.5
|
|
3491
|
-
: 0);
|
|
3492
|
-
let approachRateFactor = 0;
|
|
3493
|
-
if (this.approachRate > 10.33) {
|
|
3494
|
-
approachRateFactor = 0.3 * (this.approachRate - 10.33);
|
|
3495
|
-
}
|
|
3496
|
-
else if (this.approachRate < 8) {
|
|
3497
|
-
approachRateFactor = 0.05 * (8 - this.approachRate);
|
|
3498
|
-
}
|
|
3499
|
-
if (this.mods.has(osuBase.ModRelax)) {
|
|
3500
|
-
approachRateFactor = 0;
|
|
3501
|
-
}
|
|
3502
|
-
// Buff for longer beatmaps with high AR.
|
|
3503
|
-
ratingMultiplier += approachRateFactor * approachRateLengthBonus;
|
|
3504
|
-
if (this.mods.has(osuBase.ModHidden)) {
|
|
3505
|
-
const visibilityFactor = this.calculateAimVisibilityFactor();
|
|
3506
|
-
ratingMultiplier += OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.approachRate, visibilityFactor, this.sliderFactor);
|
|
3507
|
-
}
|
|
3508
|
-
// It is important to consider accuracy difficulty when scaling with accuracy.
|
|
3509
|
-
ratingMultiplier *=
|
|
3510
|
-
0.98 + Math.pow(Math.max(0, this.overallDifficulty), 2) / 2500;
|
|
3511
|
-
return aimRating * Math.cbrt(ratingMultiplier);
|
|
3512
|
-
}
|
|
3513
|
-
computeSpeedRating(speedDifficultyValue) {
|
|
3514
|
-
if (this.mods.has(osuBase.ModRelax)) {
|
|
3515
|
-
return 0;
|
|
3516
|
-
}
|
|
3517
|
-
let speedRating = OsuRatingCalculator.calculateDifficultyRating(speedDifficultyValue);
|
|
3518
|
-
if (this.mods.has(osuBase.ModAutopilot)) {
|
|
3519
|
-
speedRating *= 0.5;
|
|
3520
|
-
}
|
|
3521
|
-
if (this.mods.has(osuBase.ModMagnetised)) {
|
|
3522
|
-
// Reduce speed rating because of the distance scaling, with maximum reduction being 0.7.
|
|
3523
|
-
const magnetisedStrength = this.mods.get(osuBase.ModMagnetised).attractionStrength.value;
|
|
3524
|
-
speedRating *= 1 - magnetisedStrength * 0.3;
|
|
3525
|
-
}
|
|
3526
|
-
let ratingMultiplier = 1;
|
|
3527
|
-
const approachRateLengthBonus = 0.95 +
|
|
3528
|
-
0.4 * Math.min(1, this.totalHits / 2000) +
|
|
3529
|
-
(this.totalHits > 2000
|
|
3530
|
-
? Math.log10(this.totalHits / 2000) * 0.5
|
|
3531
|
-
: 0);
|
|
3532
|
-
let approachRateFactor = 0;
|
|
3533
|
-
if (this.approachRate > 10.33) {
|
|
3534
|
-
approachRateFactor = 0.3 * (this.approachRate - 10.33);
|
|
3535
|
-
}
|
|
3536
|
-
if (this.mods.has(osuBase.ModAutopilot)) {
|
|
3537
|
-
approachRateFactor = 0;
|
|
3538
|
-
}
|
|
3539
|
-
// Buff for longer beatmaps with high AR.
|
|
3540
|
-
ratingMultiplier += approachRateFactor * approachRateLengthBonus;
|
|
3541
|
-
if (this.mods.has(osuBase.ModHidden)) {
|
|
3542
|
-
const visibilityFactor = this.calculateSpeedVisibilityFactor();
|
|
3543
|
-
ratingMultiplier += OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.approachRate, visibilityFactor, this.sliderFactor);
|
|
3544
|
-
}
|
|
3545
|
-
ratingMultiplier *=
|
|
3546
|
-
0.95 + Math.pow(Math.max(0, this.overallDifficulty), 2) / 750;
|
|
3547
|
-
return speedRating * Math.cbrt(ratingMultiplier);
|
|
3548
|
-
}
|
|
3549
|
-
computeFlashlightRating(flashlightDifficultyValue) {
|
|
3550
|
-
if (!this.mods.has(osuBase.ModFlashlight)) {
|
|
3551
|
-
return 0;
|
|
3552
|
-
}
|
|
3553
|
-
let flashlightRating = OsuRatingCalculator.calculateDifficultyRating(flashlightDifficultyValue);
|
|
3554
|
-
if (this.mods.has(osuBase.ModTouchDevice)) {
|
|
3555
|
-
flashlightRating = Math.pow(flashlightRating, 0.8);
|
|
3556
|
-
}
|
|
3557
|
-
if (this.mods.has(osuBase.ModRelax)) {
|
|
3558
|
-
flashlightRating *= 0.7;
|
|
3559
|
-
}
|
|
3560
|
-
else if (this.mods.has(osuBase.ModAutopilot)) {
|
|
3561
|
-
flashlightRating *= 0.4;
|
|
3562
|
-
}
|
|
3563
|
-
if (this.mods.has(osuBase.ModMagnetised)) {
|
|
3564
|
-
const magnetisedStrength = this.mods.get(osuBase.ModMagnetised).attractionStrength.value;
|
|
3565
|
-
flashlightRating *= 1 - magnetisedStrength;
|
|
3566
|
-
}
|
|
3567
|
-
if (this.mods.has(osuBase.ModDeflate)) {
|
|
3568
|
-
const deflateInitialScale = this.mods.get(osuBase.ModDeflate).startScale.value;
|
|
3569
|
-
flashlightRating *= osuBase.MathUtils.clamp(osuBase.Interpolation.reverseLerp(deflateInitialScale, 11, 1), 0.1, 1);
|
|
3570
|
-
}
|
|
3571
|
-
let ratingMultiplier = 1;
|
|
3572
|
-
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
3573
|
-
ratingMultiplier *=
|
|
3574
|
-
0.7 +
|
|
3575
|
-
0.1 * Math.min(1, this.totalHits / 200) +
|
|
3576
|
-
(this.totalHits > 200
|
|
3577
|
-
? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
|
|
3578
|
-
: 0);
|
|
3579
|
-
// It is important to consider accuracy difficulty when scaling with accuracy.
|
|
3580
|
-
ratingMultiplier *=
|
|
3581
|
-
0.98 + Math.pow(Math.max(0, this.overallDifficulty), 2) / 2500;
|
|
3582
|
-
return flashlightRating * Math.sqrt(ratingMultiplier);
|
|
3583
|
-
}
|
|
3584
|
-
calculateAimVisibilityFactor() {
|
|
3585
|
-
const approachRateFactorEndpoint = 11.5;
|
|
3586
|
-
const mechanicalDifficultyFactor = osuBase.Interpolation.reverseLerp(this.mechanicalDifficultyRating, 5, 10);
|
|
3587
|
-
const approachRateFactorStartingPoint = osuBase.Interpolation.lerp(9, 10.33, mechanicalDifficultyFactor);
|
|
3588
|
-
return osuBase.Interpolation.reverseLerp(this.approachRate, approachRateFactorEndpoint, approachRateFactorStartingPoint);
|
|
3589
|
-
}
|
|
3590
|
-
calculateSpeedVisibilityFactor() {
|
|
3591
|
-
const approachRateFactorEndpoint = 11.5;
|
|
3592
|
-
const mechanicalDifficultyFactor = osuBase.Interpolation.reverseLerp(this.mechanicalDifficultyRating, 5, 10);
|
|
3593
|
-
const approachRateFactorStartingPoint = osuBase.Interpolation.lerp(10, 10.33, mechanicalDifficultyFactor);
|
|
3594
|
-
return osuBase.Interpolation.reverseLerp(this.approachRate, approachRateFactorEndpoint, approachRateFactorStartingPoint);
|
|
3595
|
-
}
|
|
3596
|
-
/**
|
|
3597
|
-
* Calculates a visibility bonus that is applicable to Hidden and Traceable.
|
|
3598
|
-
*
|
|
3599
|
-
* @param mods The mods applied to the calculation.
|
|
3600
|
-
* @param approachRate The approach rate of the beatmap.
|
|
3601
|
-
* @param visibilityFactor The visibility factor to apply.
|
|
3602
|
-
* @param sliderFactor The slider factor to apply.
|
|
3603
|
-
* @returns The visibility bonus multiplier.
|
|
3604
|
-
*/
|
|
3605
|
-
static calculateVisibilityBonus(mods, approachRate, visibilityFactor = 1, sliderFactor = 1) {
|
|
3606
|
-
var _a, _b;
|
|
3607
|
-
const isAlwaysPartiallyVisible = (_b = (_a = mods.get(osuBase.ModHidden)) === null || _a === void 0 ? void 0 : _a.onlyFadeApproachCircles.value) !== null && _b !== void 0 ? _b : mods.has(osuBase.ModTraceable);
|
|
3608
|
-
// Start from normal curve, rewarding lower AR up to AR 7.
|
|
3609
|
-
// Traceable forcefully requires a lower reading bonus for now as it is post-applied in pp, which make
|
|
3610
|
-
// it multiplicative with the regular AR bonuses.
|
|
3611
|
-
// This means it has an advantage over Hidden, so we decrease the multiplier to compensate.
|
|
3612
|
-
// This should be removed once we are able to apply Traceable bonuses in star rating (requires real-time
|
|
3613
|
-
// difficulty calculations being possible).
|
|
3614
|
-
let readingBonus = (isAlwaysPartiallyVisible ? 0.025 : 0.04) *
|
|
3615
|
-
(12 - Math.max(approachRate, 7));
|
|
3616
|
-
readingBonus *= visibilityFactor;
|
|
3617
|
-
// We want to reward slideraim on low AR less.
|
|
3618
|
-
const sliderVisibilityFactor = Math.pow(sliderFactor, 3);
|
|
3619
|
-
// For AR up to 0, reduce reward for very low ARs when object is visible.
|
|
3620
|
-
if (approachRate < 7) {
|
|
3621
|
-
readingBonus +=
|
|
3622
|
-
(isAlwaysPartiallyVisible ? 0.02 : 0.045) *
|
|
3623
|
-
(7 - Math.max(approachRate, 0)) *
|
|
3624
|
-
sliderVisibilityFactor;
|
|
3625
|
-
}
|
|
3626
|
-
// Starting from AR 0, cap values so they won't grow to infinity.
|
|
3627
|
-
if (approachRate < 0) {
|
|
3628
|
-
readingBonus +=
|
|
3629
|
-
(isAlwaysPartiallyVisible ? 0.01 : 0.1) *
|
|
3630
|
-
(1 - Math.pow(1.5, approachRate)) *
|
|
3631
|
-
sliderVisibilityFactor;
|
|
3632
|
-
}
|
|
3633
|
-
return readingBonus;
|
|
3634
|
-
}
|
|
3635
|
-
static calculateDifficultyRating(difficultyValue) {
|
|
3636
|
-
return Math.sqrt(difficultyValue) * this.difficultyMultiplier;
|
|
3637
|
-
}
|
|
3638
|
-
}
|
|
3639
|
-
OsuRatingCalculator.difficultyMultiplier = 0.0675;
|
|
3640
|
-
|
|
3641
3638
|
/**
|
|
3642
3639
|
* A performance points calculator that calculates performance points for osu!standard gamemode.
|
|
3643
3640
|
*/
|
|
@@ -3660,6 +3657,11 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3660
3657
|
* The flashlight performance value.
|
|
3661
3658
|
*/
|
|
3662
3659
|
this.flashlight = 0;
|
|
3660
|
+
this.greatWindow = 0;
|
|
3661
|
+
this.okWindow = 0;
|
|
3662
|
+
this.mehWindow = 0;
|
|
3663
|
+
this.approachRate = 0;
|
|
3664
|
+
this.overallDifficulty = 0;
|
|
3663
3665
|
this._effectiveMissCount = 0;
|
|
3664
3666
|
this.speedDeviation = 0;
|
|
3665
3667
|
}
|
|
@@ -3672,16 +3674,15 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3672
3674
|
calculateValues() {
|
|
3673
3675
|
this._effectiveMissCount = osuBase.MathUtils.clamp(this.calculateComboBasedEstimatedMissCount(), this.computedAccuracy.nmiss, this.totalHits);
|
|
3674
3676
|
let { finalMultiplier } = OsuPerformanceCalculator;
|
|
3677
|
+
const { spinnerCount, approachRate: ar, overallDifficulty: od, clockRate, } = this.difficultyAttributes;
|
|
3675
3678
|
if (this.mods.has(osuBase.ModNoFail)) {
|
|
3676
3679
|
finalMultiplier *= Math.max(0.9, 1 - 0.02 * this._effectiveMissCount);
|
|
3677
3680
|
}
|
|
3678
3681
|
if (this.mods.has(osuBase.ModSpunOut)) {
|
|
3679
3682
|
finalMultiplier *=
|
|
3680
|
-
1 -
|
|
3681
|
-
Math.pow(this.difficultyAttributes.spinnerCount / this.totalHits, 0.85);
|
|
3683
|
+
1 - Math.pow(spinnerCount / this.totalHits, 0.85);
|
|
3682
3684
|
}
|
|
3683
3685
|
if (this.mods.has(osuBase.ModRelax)) {
|
|
3684
|
-
const { overallDifficulty: od } = this.difficultyAttributes;
|
|
3685
3686
|
// Graph: https://www.desmos.com/calculator/vspzsop6td
|
|
3686
3687
|
// We use OD13.3 as maximum since it's the value at which great hit window becomes 0.
|
|
3687
3688
|
const n100Multiplier = 0.75 * Math.max(0, od > 0 ? 1 - od / 13.33 : 1);
|
|
@@ -3692,6 +3693,14 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3692
3693
|
this.computedAccuracy.n100 * n100Multiplier +
|
|
3693
3694
|
this.computedAccuracy.n50 * n50Multiplier, this.totalHits);
|
|
3694
3695
|
}
|
|
3696
|
+
const hitWindow = new osuBase.OsuHitWindow(od);
|
|
3697
|
+
this.greatWindow = hitWindow.greatWindow / clockRate;
|
|
3698
|
+
this.okWindow = hitWindow.okWindow / clockRate;
|
|
3699
|
+
this.mehWindow = hitWindow.mehWindow / clockRate;
|
|
3700
|
+
this.approachRate =
|
|
3701
|
+
OsuDifficultyCalculator.calculateRateAdjustedApproachRate(ar, clockRate);
|
|
3702
|
+
this.overallDifficulty =
|
|
3703
|
+
OsuDifficultyCalculator.calculateRateAdjustedOverallDifficulty(od, clockRate);
|
|
3695
3704
|
this.speedDeviation = this.calculateSpeedDeviation();
|
|
3696
3705
|
this.aim = this.calculateAimValue();
|
|
3697
3706
|
this.speed = this.calculateSpeedValue();
|
|
@@ -3759,7 +3768,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3759
3768
|
else if (this.mods.has(osuBase.ModTraceable)) {
|
|
3760
3769
|
aimValue *=
|
|
3761
3770
|
1 +
|
|
3762
|
-
OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.
|
|
3771
|
+
OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.approachRate, undefined, this.difficultyAttributes.sliderFactor);
|
|
3763
3772
|
}
|
|
3764
3773
|
// Scale the aim value with accuracy.
|
|
3765
3774
|
aimValue *= this.computedAccuracy.value();
|
|
@@ -3794,7 +3803,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3794
3803
|
else if (this.mods.has(osuBase.ModTraceable)) {
|
|
3795
3804
|
speedValue *=
|
|
3796
3805
|
1 +
|
|
3797
|
-
OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.
|
|
3806
|
+
OsuRatingCalculator.calculateVisibilityBonus(this.mods, this.approachRate, undefined, this.difficultyAttributes.sliderFactor);
|
|
3798
3807
|
}
|
|
3799
3808
|
// Calculate accuracy assuming the worst case scenario.
|
|
3800
3809
|
const countGreat = this.computedAccuracy.n300;
|
|
@@ -3812,7 +3821,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3812
3821
|
{ n300: 0, nobjects: 1 });
|
|
3813
3822
|
speedValue *= this.calculateSpeedHighDeviationNerf();
|
|
3814
3823
|
// Scale the speed value with accuracy and OD.
|
|
3815
|
-
speedValue *= Math.pow((this.computedAccuracy.value() + relevantAccuracy.value()) / 2, (14.5 - this.
|
|
3824
|
+
speedValue *= Math.pow((this.computedAccuracy.value() + relevantAccuracy.value()) / 2, (14.5 - this.overallDifficulty) / 2);
|
|
3816
3825
|
return speedValue;
|
|
3817
3826
|
}
|
|
3818
3827
|
/**
|
|
@@ -3831,7 +3840,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3831
3840
|
const realAccuracy = new osuBase.Accuracy(Object.assign(Object.assign({}, this.computedAccuracy), { n300: this.computedAccuracy.n300 - (this.totalHits - ncircles) }));
|
|
3832
3841
|
// Lots of arbitrary values from testing.
|
|
3833
3842
|
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
|
|
3834
|
-
let accuracyValue = Math.pow(1.52163, this.
|
|
3843
|
+
let accuracyValue = Math.pow(1.52163, this.overallDifficulty) *
|
|
3835
3844
|
// It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points.
|
|
3836
3845
|
Math.pow(realAccuracy.n300 < 0 ? 0 : realAccuracy.value(), 24) *
|
|
3837
3846
|
2.83;
|
|
@@ -3845,8 +3854,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3845
3854
|
// Decrease bonus for AR > 10.
|
|
3846
3855
|
accuracyValue *=
|
|
3847
3856
|
1 +
|
|
3848
|
-
0.08 *
|
|
3849
|
-
osuBase.Interpolation.reverseLerp(this.difficultyAttributes.approachRate, 11.5, 10);
|
|
3857
|
+
0.08 * osuBase.Interpolation.reverseLerp(this.approachRate, 11.5, 10);
|
|
3850
3858
|
}
|
|
3851
3859
|
if (this.mods.has(osuBase.ModFlashlight)) {
|
|
3852
3860
|
accuracyValue *= 1.02;
|
|
@@ -3931,14 +3939,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3931
3939
|
if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0) {
|
|
3932
3940
|
return Number.POSITIVE_INFINITY;
|
|
3933
3941
|
}
|
|
3934
|
-
|
|
3935
|
-
const { clockRate, overallDifficulty } = this.difficultyAttributes;
|
|
3936
|
-
const hitWindow = new osuBase.OsuHitWindow(osuBase.OsuHitWindow.greatWindowToOD(
|
|
3937
|
-
// Convert current OD to non clock rate-adjusted OD.
|
|
3938
|
-
new osuBase.OsuHitWindow(overallDifficulty).greatWindow * clockRate));
|
|
3939
|
-
const greatWindow = hitWindow.greatWindow / clockRate;
|
|
3940
|
-
const okWindow = hitWindow.okWindow / clockRate;
|
|
3941
|
-
const mehWindow = hitWindow.mehWindow / clockRate;
|
|
3942
|
+
const { greatWindow, okWindow, mehWindow } = this;
|
|
3942
3943
|
// The sample proportion of successful hits.
|
|
3943
3944
|
const n = Math.max(1, relevantCountGreat + relevantCountOk);
|
|
3944
3945
|
const p = relevantCountGreat / n;
|
|
@@ -4334,7 +4335,7 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
4334
4335
|
}
|
|
4335
4336
|
static calculateRateAdjustedOverallDifficulty(overallDifficulty, clockRate) {
|
|
4336
4337
|
const greatWindow = new osuBase.OsuHitWindow(overallDifficulty).greatWindow / clockRate;
|
|
4337
|
-
return
|
|
4338
|
+
return (79.5 - greatWindow) / 6;
|
|
4338
4339
|
}
|
|
4339
4340
|
}
|
|
4340
4341
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rian8337/osu-difficulty-calculator",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.89",
|
|
4
4
|
"description": "A module for calculating osu!standard beatmap difficulty and performance value with respect to the current difficulty and performance algorithm.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"osu",
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"url": "https://github.com/Rian8337/osu-droid-module/issues"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@rian8337/osu-base": "4.0.0-beta.
|
|
36
|
+
"@rian8337/osu-base": "4.0.0-beta.89"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "70d91858f02b78f847d784609bae069c8e1679c9"
|
|
42
42
|
}
|
package/typings/index.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ interface IDifficultyAttributes {
|
|
|
43
43
|
*/
|
|
44
44
|
clockRate: number;
|
|
45
45
|
/**
|
|
46
|
-
* The perceived overall difficulty
|
|
46
|
+
* The perceived overall difficulty **exclusive** of rate-adjusting mods (DT/HT/etc).
|
|
47
47
|
*
|
|
48
48
|
* Rate-adjusting mods don't directly affect the overall difficulty value, but have a perceived effect as a result of adjusting audio timing.
|
|
49
49
|
*/
|
|
@@ -1143,7 +1143,7 @@ declare class DroidPerformanceCalculator extends PerformanceCalculator<IDroidDif
|
|
|
1143
1143
|
* [Graph](https://www.desmos.com/calculator/z5l9ebrwpi)
|
|
1144
1144
|
*/
|
|
1145
1145
|
private calculateTapHighDeviationNerf;
|
|
1146
|
-
private
|
|
1146
|
+
private getHitWindow;
|
|
1147
1147
|
toString(): string;
|
|
1148
1148
|
}
|
|
1149
1149
|
|
|
@@ -1316,7 +1316,7 @@ declare abstract class DroidTapEvaluator {
|
|
|
1316
1316
|
*/
|
|
1317
1317
|
interface IOsuDifficultyAttributes extends IDifficultyAttributes {
|
|
1318
1318
|
/**
|
|
1319
|
-
* The perceived approach rate
|
|
1319
|
+
* The perceived approach rate **exclusive** of rate-adjusting mods (DT/HT/etc).
|
|
1320
1320
|
*
|
|
1321
1321
|
* Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
|
|
1322
1322
|
*/
|
|
@@ -1531,6 +1531,11 @@ declare class OsuPerformanceCalculator extends PerformanceCalculator<IOsuDifficu
|
|
|
1531
1531
|
*/
|
|
1532
1532
|
get effectiveMissCount(): number;
|
|
1533
1533
|
static readonly finalMultiplier = 1.14;
|
|
1534
|
+
private greatWindow;
|
|
1535
|
+
private okWindow;
|
|
1536
|
+
private mehWindow;
|
|
1537
|
+
private approachRate;
|
|
1538
|
+
private overallDifficulty;
|
|
1534
1539
|
private _effectiveMissCount;
|
|
1535
1540
|
private speedDeviation;
|
|
1536
1541
|
protected calculateValues(): void;
|