@rian8337/osu-difficulty-calculator 4.0.0-beta.50 → 4.0.0-beta.52
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 +443 -225
- package/package.json +3 -3
- package/typings/index.d.ts +84 -83
package/dist/index.js
CHANGED
|
@@ -2,33 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
var osuBase = require('@rian8337/osu-base');
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* An evaluator for calculating aim skill.
|
|
7
|
-
*
|
|
8
|
-
* This class should be considered an "evaluating" class and not persisted.
|
|
9
|
-
*/
|
|
10
|
-
class AimEvaluator {
|
|
11
|
-
/**
|
|
12
|
-
* Calculates the bonus of wide angles.
|
|
13
|
-
*/
|
|
14
|
-
static calculateWideAngleBonus(angle) {
|
|
15
|
-
return Math.pow(Math.sin((3 / 4) *
|
|
16
|
-
(Math.min((5 / 6) * Math.PI, Math.max(Math.PI / 6, angle)) -
|
|
17
|
-
Math.PI / 6)), 2);
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Calculates the bonus of acute angles.
|
|
21
|
-
*/
|
|
22
|
-
static calculateAcuteAngleBonus(angle) {
|
|
23
|
-
return 1 - this.calculateWideAngleBonus(angle);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
AimEvaluator.wideAngleMultiplier = 1.5;
|
|
27
|
-
AimEvaluator.acuteAngleMultiplier = 1.95;
|
|
28
|
-
AimEvaluator.sliderMultiplier = 1.35;
|
|
29
|
-
AimEvaluator.velocityChangeMultiplier = 0.75;
|
|
30
|
-
AimEvaluator.wiggleMultiplier = 1.02;
|
|
31
|
-
|
|
32
5
|
/**
|
|
33
6
|
* The base of a difficulty calculator.
|
|
34
7
|
*/
|
|
@@ -314,10 +287,10 @@ class DifficultyHitObject {
|
|
|
314
287
|
* Calculates the opacity of the hitobject at a given time.
|
|
315
288
|
*
|
|
316
289
|
* @param time The time to calculate the hitobject's opacity at.
|
|
317
|
-
* @param
|
|
290
|
+
* @param mods The mods used.
|
|
318
291
|
* @returns The opacity of the hitobject at the given time.
|
|
319
292
|
*/
|
|
320
|
-
opacityAt(time,
|
|
293
|
+
opacityAt(time, mods) {
|
|
321
294
|
if (time > this.object.startTime) {
|
|
322
295
|
// Consider a hitobject as being invisible when its start time is passed.
|
|
323
296
|
// In reality the hitobject will be visible beyond its start time up until its hittable window has passed,
|
|
@@ -326,7 +299,7 @@ class DifficultyHitObject {
|
|
|
326
299
|
}
|
|
327
300
|
const fadeInStartTime = this.object.startTime - this.object.timePreempt;
|
|
328
301
|
const fadeInDuration = this.object.timeFadeIn;
|
|
329
|
-
if (
|
|
302
|
+
if (mods.some((m) => m instanceof osuBase.ModHidden)) {
|
|
330
303
|
const fadeOutStartTime = fadeInStartTime + fadeInDuration;
|
|
331
304
|
const fadeOutDuration = this.object.timePreempt * osuBase.ModHidden.fadeOutDurationMultiplier;
|
|
332
305
|
return Math.min(osuBase.MathUtils.clamp((time - fadeInStartTime) / fadeInDuration, 0, 1), 1 -
|
|
@@ -424,6 +397,36 @@ class DifficultyHitObject {
|
|
|
424
397
|
if (slider.lazyEndPosition) {
|
|
425
398
|
return;
|
|
426
399
|
}
|
|
400
|
+
let trackingEndTime = slider.endTime;
|
|
401
|
+
let { nestedHitObjects: nestedObjects } = slider;
|
|
402
|
+
if (this.mode === osuBase.Modes.osu) {
|
|
403
|
+
trackingEndTime = Math.max(slider.endTime - osuBase.Slider.legacyLastTickOffset, slider.startTime + slider.duration / 2);
|
|
404
|
+
let lastRealTick = null;
|
|
405
|
+
for (let i = nestedObjects.length - 2; i > 0; --i) {
|
|
406
|
+
const current = nestedObjects[i];
|
|
407
|
+
if (current instanceof osuBase.SliderTick) {
|
|
408
|
+
lastRealTick = current;
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
if (current instanceof osuBase.SliderRepeat) {
|
|
412
|
+
// A repeat means the slider does not have a slider tick.
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (lastRealTick && lastRealTick.startTime > trackingEndTime) {
|
|
417
|
+
trackingEndTime = lastRealTick.startTime;
|
|
418
|
+
// When the last tick falls after the tracking end time, we need to re-sort the nested objects
|
|
419
|
+
// based on time. This creates a somewhat weird ordering which is counter to how a user would
|
|
420
|
+
// understand the slider, but allows a zero-diff with known diffcalc output.
|
|
421
|
+
//
|
|
422
|
+
// To reiterate, this is definitely not correct from a difficulty calculation perspective
|
|
423
|
+
// and should be revisited at a later date.
|
|
424
|
+
const reordered = nestedObjects.slice();
|
|
425
|
+
reordered.splice(reordered.indexOf(lastRealTick), 1);
|
|
426
|
+
reordered.push(lastRealTick);
|
|
427
|
+
nestedObjects = reordered;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
427
430
|
if (this.mode === osuBase.Modes.droid) {
|
|
428
431
|
// Temporary lazy end position until a real result can be derived.
|
|
429
432
|
slider.lazyEndPosition = slider.getStackedPosition(this.mode);
|
|
@@ -433,9 +436,7 @@ class DifficultyHitObject {
|
|
|
433
436
|
return;
|
|
434
437
|
}
|
|
435
438
|
}
|
|
436
|
-
|
|
437
|
-
slider.lazyTravelTime =
|
|
438
|
-
slider.nestedHitObjects.at(-1).startTime - slider.startTime;
|
|
439
|
+
slider.lazyTravelTime = trackingEndTime - slider.startTime;
|
|
439
440
|
let endTimeMin = slider.lazyTravelTime / slider.spanDuration;
|
|
440
441
|
if (endTimeMin % 2 >= 1) {
|
|
441
442
|
endTimeMin = 1 - (endTimeMin % 1);
|
|
@@ -449,15 +450,15 @@ class DifficultyHitObject {
|
|
|
449
450
|
.add(slider.path.positionAt(endTimeMin));
|
|
450
451
|
let currentCursorPosition = slider.getStackedPosition(this.mode);
|
|
451
452
|
const scalingFactor = DifficultyHitObject.normalizedRadius / slider.radius;
|
|
452
|
-
for (let i = 1; i <
|
|
453
|
-
const currentMovementObject =
|
|
453
|
+
for (let i = 1; i < nestedObjects.length; ++i) {
|
|
454
|
+
const currentMovementObject = nestedObjects[i];
|
|
454
455
|
let currentMovement = currentMovementObject
|
|
455
456
|
.getStackedPosition(this.mode)
|
|
456
457
|
.subtract(currentCursorPosition);
|
|
457
458
|
let currentMovementLength = scalingFactor * currentMovement.length;
|
|
458
459
|
// The amount of movement required so that the cursor position needs to be updated.
|
|
459
460
|
let requiredMovement = this.assumedSliderRadius;
|
|
460
|
-
if (i ===
|
|
461
|
+
if (i === nestedObjects.length - 1) {
|
|
461
462
|
// The end of a slider has special aim rules due to the relaxed time constraint on position.
|
|
462
463
|
// There is both a lazy end position as well as the actual end slider position. We assume the player takes the simpler movement.
|
|
463
464
|
// For sliders that are circular, the lazy end position may actually be farther away than the sliders' true end.
|
|
@@ -482,7 +483,7 @@ class DifficultyHitObject {
|
|
|
482
483
|
currentMovementLength;
|
|
483
484
|
slider.lazyTravelDistance += currentMovementLength;
|
|
484
485
|
}
|
|
485
|
-
if (i ===
|
|
486
|
+
if (i === nestedObjects.length - 1) {
|
|
486
487
|
slider.lazyEndPosition = currentCursorPosition;
|
|
487
488
|
}
|
|
488
489
|
}
|
|
@@ -581,6 +582,14 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
|
|
|
581
582
|
super.computeProperties(clockRate, hitObjects);
|
|
582
583
|
this.setVisuals(clockRate, hitObjects);
|
|
583
584
|
}
|
|
585
|
+
opacityAt(time, mods) {
|
|
586
|
+
// Traceable hides the primary piece of a hit circle (that is, its body), so consider it as fully invisible.
|
|
587
|
+
if (this.object instanceof osuBase.Circle &&
|
|
588
|
+
mods.some((m) => m instanceof osuBase.ModTraceable)) {
|
|
589
|
+
return 0;
|
|
590
|
+
}
|
|
591
|
+
return super.opacityAt(time, mods);
|
|
592
|
+
}
|
|
584
593
|
/**
|
|
585
594
|
* Determines whether this hitobject is considered overlapping with the hitobject before it.
|
|
586
595
|
*
|
|
@@ -705,7 +714,7 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
|
|
|
705
714
|
/**
|
|
706
715
|
* An evaluator for calculating osu!droid Aim skill.
|
|
707
716
|
*/
|
|
708
|
-
class DroidAimEvaluator
|
|
717
|
+
class DroidAimEvaluator {
|
|
709
718
|
/**
|
|
710
719
|
* Evaluates the difficulty of aiming the current object, based on:
|
|
711
720
|
*
|
|
@@ -773,8 +782,7 @@ class DroidAimEvaluator extends AimEvaluator {
|
|
|
773
782
|
Math.max(current.strainTime, last.strainTime) <
|
|
774
783
|
1.25 * Math.min(current.strainTime, last.strainTime) &&
|
|
775
784
|
current.angle !== null &&
|
|
776
|
-
last.angle !== null
|
|
777
|
-
lastLast.angle !== null) {
|
|
785
|
+
last.angle !== null) {
|
|
778
786
|
const currentAngle = current.angle;
|
|
779
787
|
const lastAngle = last.angle;
|
|
780
788
|
// Rewarding angles, take the smaller velocity as base.
|
|
@@ -869,6 +877,7 @@ DroidAimEvaluator.wideAngleMultiplier = 1.5;
|
|
|
869
877
|
DroidAimEvaluator.acuteAngleMultiplier = 2.6;
|
|
870
878
|
DroidAimEvaluator.sliderMultiplier = 1.35;
|
|
871
879
|
DroidAimEvaluator.velocityChangeMultiplier = 0.75;
|
|
880
|
+
DroidAimEvaluator.wiggleMultiplier = 1.02;
|
|
872
881
|
DroidAimEvaluator.singleSpacingThreshold = 100;
|
|
873
882
|
// 200 1/4 BPM delta time
|
|
874
883
|
DroidAimEvaluator.minSpeedBonus = 75;
|
|
@@ -1034,7 +1043,7 @@ class DroidAim extends DroidSkill {
|
|
|
1034
1043
|
if (this.sliderStrains.length === 0) {
|
|
1035
1044
|
return 0;
|
|
1036
1045
|
}
|
|
1037
|
-
const maxSliderStrain =
|
|
1046
|
+
const maxSliderStrain = osuBase.MathUtils.max(this.sliderStrains);
|
|
1038
1047
|
if (maxSliderStrain === 0) {
|
|
1039
1048
|
return 0;
|
|
1040
1049
|
}
|
|
@@ -1072,20 +1081,10 @@ class DroidAim extends DroidSkill {
|
|
|
1072
1081
|
}
|
|
1073
1082
|
}
|
|
1074
1083
|
|
|
1075
|
-
/**
|
|
1076
|
-
* An evaluator for calculating speed or tap skill.
|
|
1077
|
-
*
|
|
1078
|
-
* This class should be considered an "evaluating" class and not persisted.
|
|
1079
|
-
*/
|
|
1080
|
-
class SpeedEvaluator {
|
|
1081
|
-
}
|
|
1082
|
-
// ~200 1/4 BPM streams
|
|
1083
|
-
SpeedEvaluator.minSpeedBonus = 75;
|
|
1084
|
-
|
|
1085
1084
|
/**
|
|
1086
1085
|
* An evaluator for calculating osu!droid tap skill.
|
|
1087
1086
|
*/
|
|
1088
|
-
class DroidTapEvaluator
|
|
1087
|
+
class DroidTapEvaluator {
|
|
1089
1088
|
/**
|
|
1090
1089
|
* Evaluates the difficulty of tapping the current object, based on:
|
|
1091
1090
|
*
|
|
@@ -1122,6 +1121,8 @@ class DroidTapEvaluator extends SpeedEvaluator {
|
|
|
1122
1121
|
return (speedBonus * Math.pow(doubletapness, 1.5) * 1000) / strainTime;
|
|
1123
1122
|
}
|
|
1124
1123
|
}
|
|
1124
|
+
// ~200 1/4 BPM streams
|
|
1125
|
+
DroidTapEvaluator.minSpeedBonus = 75;
|
|
1125
1126
|
|
|
1126
1127
|
/**
|
|
1127
1128
|
* Represents the skill required to press keys or tap with regards to keeping up with the speed at which objects need to be hit.
|
|
@@ -1153,7 +1154,7 @@ class DroidTap extends DroidSkill {
|
|
|
1153
1154
|
if (this._objectStrains.length === 0) {
|
|
1154
1155
|
return 0;
|
|
1155
1156
|
}
|
|
1156
|
-
const maxStrain =
|
|
1157
|
+
const maxStrain = osuBase.MathUtils.max(this._objectStrains);
|
|
1157
1158
|
if (maxStrain === 0) {
|
|
1158
1159
|
return 0;
|
|
1159
1160
|
}
|
|
@@ -1166,7 +1167,7 @@ class DroidTap extends DroidSkill {
|
|
|
1166
1167
|
if (this._objectStrains.length === 0) {
|
|
1167
1168
|
return 0;
|
|
1168
1169
|
}
|
|
1169
|
-
const maxStrain =
|
|
1170
|
+
const maxStrain = osuBase.MathUtils.max(this._objectStrains);
|
|
1170
1171
|
if (maxStrain === 0) {
|
|
1171
1172
|
return 0;
|
|
1172
1173
|
}
|
|
@@ -1212,23 +1213,10 @@ class DroidTap extends DroidSkill {
|
|
|
1212
1213
|
}
|
|
1213
1214
|
}
|
|
1214
1215
|
|
|
1215
|
-
/**
|
|
1216
|
-
* An evaluator for calculating flashlight skill.
|
|
1217
|
-
*
|
|
1218
|
-
* This class should be considered an "evaluating" class and not persisted.
|
|
1219
|
-
*/
|
|
1220
|
-
class FlashlightEvaluator {
|
|
1221
|
-
}
|
|
1222
|
-
FlashlightEvaluator.maxOpacityBonus = 0.4;
|
|
1223
|
-
FlashlightEvaluator.hiddenBonus = 0.2;
|
|
1224
|
-
FlashlightEvaluator.minVelocity = 0.5;
|
|
1225
|
-
FlashlightEvaluator.sliderMultiplier = 1.3;
|
|
1226
|
-
FlashlightEvaluator.minAngleMultiplier = 0.2;
|
|
1227
|
-
|
|
1228
1216
|
/**
|
|
1229
1217
|
* An evaluator for calculating osu!droid Flashlight skill.
|
|
1230
1218
|
*/
|
|
1231
|
-
class DroidFlashlightEvaluator
|
|
1219
|
+
class DroidFlashlightEvaluator {
|
|
1232
1220
|
/**
|
|
1233
1221
|
* Evaluates the difficulty of memorizing and hitting the current object, based on:
|
|
1234
1222
|
*
|
|
@@ -1239,10 +1227,10 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
1239
1227
|
* - and whether Hidden mod is enabled.
|
|
1240
1228
|
*
|
|
1241
1229
|
* @param current The current object.
|
|
1242
|
-
* @param
|
|
1230
|
+
* @param mods The mods used.
|
|
1243
1231
|
* @param withSliders Whether to take slider difficulty into account.
|
|
1244
1232
|
*/
|
|
1245
|
-
static evaluateDifficultyOf(current,
|
|
1233
|
+
static evaluateDifficultyOf(current, mods, withSliders) {
|
|
1246
1234
|
if (current.object instanceof osuBase.Spinner ||
|
|
1247
1235
|
// Exclude overlapping objects that can be tapped at once.
|
|
1248
1236
|
current.isOverlapping(true)) {
|
|
@@ -1273,7 +1261,7 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
1273
1261
|
const opacityBonus = 1 +
|
|
1274
1262
|
this.maxOpacityBonus *
|
|
1275
1263
|
(1 -
|
|
1276
|
-
current.opacityAt(currentObject.object.startTime,
|
|
1264
|
+
current.opacityAt(currentObject.object.startTime, mods));
|
|
1277
1265
|
result +=
|
|
1278
1266
|
(stackNerf * opacityBonus * scalingFactor * jumpDistance) /
|
|
1279
1267
|
cumulativeStrainTime;
|
|
@@ -1288,9 +1276,20 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
1288
1276
|
}
|
|
1289
1277
|
result = Math.pow(smallDistNerf * result, 2);
|
|
1290
1278
|
// Additional bonus for Hidden due to there being no approach circles.
|
|
1291
|
-
if (
|
|
1279
|
+
if (mods.some((m) => m instanceof osuBase.ModHidden)) {
|
|
1292
1280
|
result *= 1 + this.hiddenBonus;
|
|
1293
1281
|
}
|
|
1282
|
+
else if (mods.some((m) => m instanceof osuBase.ModTraceable)) {
|
|
1283
|
+
// Additional bonus for Traceable due to there being no primary or secondary object pieces.
|
|
1284
|
+
if (current.object instanceof osuBase.Circle) {
|
|
1285
|
+
// Additional bonus for hit circles due to there being no circle piece, which is the primary piece.
|
|
1286
|
+
result *= 1 + this.traceableCircleBonus;
|
|
1287
|
+
}
|
|
1288
|
+
else {
|
|
1289
|
+
// The rest of the objects only hide secondary pieces.
|
|
1290
|
+
result *= 1 + this.traceableObjectBonus;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1294
1293
|
// Nerf patterns with repeated angles.
|
|
1295
1294
|
result *=
|
|
1296
1295
|
this.minAngleMultiplier +
|
|
@@ -1311,6 +1310,13 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
1311
1310
|
return result;
|
|
1312
1311
|
}
|
|
1313
1312
|
}
|
|
1313
|
+
DroidFlashlightEvaluator.maxOpacityBonus = 0.4;
|
|
1314
|
+
DroidFlashlightEvaluator.hiddenBonus = 0.2;
|
|
1315
|
+
DroidFlashlightEvaluator.traceableCircleBonus = 0.15;
|
|
1316
|
+
DroidFlashlightEvaluator.traceableObjectBonus = 0.1;
|
|
1317
|
+
DroidFlashlightEvaluator.minVelocity = 0.5;
|
|
1318
|
+
DroidFlashlightEvaluator.sliderMultiplier = 1.3;
|
|
1319
|
+
DroidFlashlightEvaluator.minAngleMultiplier = 0.2;
|
|
1314
1320
|
|
|
1315
1321
|
/**
|
|
1316
1322
|
* Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
|
|
@@ -1324,13 +1330,12 @@ class DroidFlashlight extends DroidSkill {
|
|
|
1324
1330
|
this.starsPerDouble = 1.06;
|
|
1325
1331
|
this.skillMultiplier = 0.02;
|
|
1326
1332
|
this.currentFlashlightStrain = 0;
|
|
1327
|
-
this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
|
|
1328
1333
|
this.withSliders = withSliders;
|
|
1329
1334
|
}
|
|
1330
1335
|
strainValueAt(current) {
|
|
1331
1336
|
this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
|
|
1332
1337
|
this.currentFlashlightStrain +=
|
|
1333
|
-
DroidFlashlightEvaluator.evaluateDifficultyOf(current, this.
|
|
1338
|
+
DroidFlashlightEvaluator.evaluateDifficultyOf(current, this.mods, this.withSliders) * this.skillMultiplier;
|
|
1334
1339
|
return this.currentFlashlightStrain;
|
|
1335
1340
|
}
|
|
1336
1341
|
calculateInitialStrain(time, current) {
|
|
@@ -1597,22 +1602,31 @@ class DroidVisualEvaluator {
|
|
|
1597
1602
|
* - and whether the Hidden mod is enabled.
|
|
1598
1603
|
*
|
|
1599
1604
|
* @param current The current object.
|
|
1600
|
-
* @param
|
|
1605
|
+
* @param mods The mods used.
|
|
1601
1606
|
* @param withSliders Whether to take slider difficulty into account.
|
|
1602
1607
|
*/
|
|
1603
|
-
static evaluateDifficultyOf(current,
|
|
1608
|
+
static evaluateDifficultyOf(current, mods, withSliders) {
|
|
1604
1609
|
if (current.object instanceof osuBase.Spinner ||
|
|
1605
1610
|
// Exclude overlapping objects that can be tapped at once.
|
|
1606
1611
|
current.isOverlapping(true) ||
|
|
1607
1612
|
current.index === 0) {
|
|
1608
1613
|
return 0;
|
|
1609
1614
|
}
|
|
1610
|
-
// Start with base density and give global bonus for Hidden.
|
|
1615
|
+
// Start with base density and give global bonus for Hidden and Traceable.
|
|
1611
1616
|
// Add density caps for sanity.
|
|
1612
1617
|
let strain;
|
|
1613
|
-
if (
|
|
1618
|
+
if (mods.some((m) => m instanceof osuBase.ModHidden)) {
|
|
1614
1619
|
strain = Math.min(30, Math.pow(current.noteDensity, 3));
|
|
1615
1620
|
}
|
|
1621
|
+
else if (mods.some((m) => m instanceof osuBase.ModTraceable)) {
|
|
1622
|
+
// Give more bonus for hit circles due to there being no circle piece.
|
|
1623
|
+
if (current.object instanceof osuBase.Circle) {
|
|
1624
|
+
strain = Math.min(25, Math.pow(current.noteDensity, 2.5));
|
|
1625
|
+
}
|
|
1626
|
+
else {
|
|
1627
|
+
strain = Math.min(22.5, Math.pow(current.noteDensity, 2.25));
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1616
1630
|
else {
|
|
1617
1631
|
strain = Math.min(20, Math.pow(current.noteDensity, 2));
|
|
1618
1632
|
}
|
|
@@ -1630,9 +1644,7 @@ class DroidVisualEvaluator {
|
|
|
1630
1644
|
break;
|
|
1631
1645
|
}
|
|
1632
1646
|
strain +=
|
|
1633
|
-
(1 -
|
|
1634
|
-
current.opacityAt(previous.object.startTime, isHiddenMod)) /
|
|
1635
|
-
4;
|
|
1647
|
+
(1 - current.opacityAt(previous.object.startTime, mods)) / 4;
|
|
1636
1648
|
}
|
|
1637
1649
|
if (current.timePreempt < 400) {
|
|
1638
1650
|
// Give bonus for AR higher than 10.33.
|
|
@@ -1692,13 +1704,12 @@ class DroidVisual extends DroidSkill {
|
|
|
1692
1704
|
this.currentVisualStrain = 0;
|
|
1693
1705
|
this.currentRhythmMultiplier = 1;
|
|
1694
1706
|
this.skillMultiplier = 10;
|
|
1695
|
-
this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
|
|
1696
1707
|
this.withSliders = withSliders;
|
|
1697
1708
|
}
|
|
1698
1709
|
strainValueAt(current) {
|
|
1699
1710
|
this.currentVisualStrain *= this.strainDecay(current.deltaTime);
|
|
1700
1711
|
this.currentVisualStrain +=
|
|
1701
|
-
DroidVisualEvaluator.evaluateDifficultyOf(current, this.
|
|
1712
|
+
DroidVisualEvaluator.evaluateDifficultyOf(current, this.mods, this.withSliders) * this.skillMultiplier;
|
|
1702
1713
|
this.currentRhythmMultiplier = current.rhythmMultiplier;
|
|
1703
1714
|
return this.currentVisualStrain * this.currentRhythmMultiplier;
|
|
1704
1715
|
}
|
|
@@ -1802,6 +1813,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1802
1813
|
* Calculates the aim star rating of the beatmap and stores it in this instance.
|
|
1803
1814
|
*/
|
|
1804
1815
|
calculateAim() {
|
|
1816
|
+
if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
|
|
1817
|
+
this.attributes.aimDifficulty = 0;
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1805
1820
|
const aimSkill = new DroidAim(this.mods, true);
|
|
1806
1821
|
const aimSkillWithoutSliders = new DroidAim(this.mods, false);
|
|
1807
1822
|
this.calculateSkills(aimSkill, aimSkillWithoutSliders);
|
|
@@ -1811,6 +1826,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1811
1826
|
* Calculates the tap star rating of the beatmap and stores it in this instance.
|
|
1812
1827
|
*/
|
|
1813
1828
|
calculateTap() {
|
|
1829
|
+
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
1830
|
+
this.attributes.tapDifficulty = 0;
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1814
1833
|
const tapSkillCheese = new DroidTap(this.mods, true);
|
|
1815
1834
|
const tapSkillNoCheese = new DroidTap(this.mods, false);
|
|
1816
1835
|
this.calculateSkills(tapSkillCheese, tapSkillNoCheese);
|
|
@@ -1830,6 +1849,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1830
1849
|
* Calculates the flashlight star rating of the beatmap and stores it in this instance.
|
|
1831
1850
|
*/
|
|
1832
1851
|
calculateFlashlight() {
|
|
1852
|
+
if (!this.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
|
|
1853
|
+
this.attributes.flashlightDifficulty = 0;
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1833
1856
|
const flashlightSkill = new DroidFlashlight(this.mods, true);
|
|
1834
1857
|
const flashlightSkillWithoutSliders = new DroidFlashlight(this.mods, false);
|
|
1835
1858
|
this.calculateSkills(flashlightSkill, flashlightSkillWithoutSliders);
|
|
@@ -1874,21 +1897,29 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1874
1897
|
calculateAll() {
|
|
1875
1898
|
const skills = this.createSkills();
|
|
1876
1899
|
this.calculateSkills(...skills);
|
|
1877
|
-
const aimSkill = skills
|
|
1878
|
-
const aimSkillWithoutSliders = skills
|
|
1879
|
-
const rhythmSkill = skills
|
|
1880
|
-
const tapSkillCheese = skills
|
|
1881
|
-
const flashlightSkill = skills
|
|
1882
|
-
const flashlightSkillWithoutSliders = skills
|
|
1883
|
-
const visualSkill = skills
|
|
1884
|
-
const visualSkillWithoutSliders = skills
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1900
|
+
const aimSkill = skills.find((s) => s instanceof DroidAim && s.withSliders);
|
|
1901
|
+
const aimSkillWithoutSliders = skills.find((s) => s instanceof DroidAim && !s.withSliders);
|
|
1902
|
+
const rhythmSkill = skills.find((s) => s instanceof DroidRhythm);
|
|
1903
|
+
const tapSkillCheese = skills.find((s) => s instanceof DroidTap && s.considerCheesability);
|
|
1904
|
+
const flashlightSkill = skills.find((s) => s instanceof DroidFlashlight && s.withSliders);
|
|
1905
|
+
const flashlightSkillWithoutSliders = skills.find((s) => s instanceof DroidFlashlight && !s.withSliders);
|
|
1906
|
+
const visualSkill = skills.find((s) => s instanceof DroidVisual && s.withSliders);
|
|
1907
|
+
const visualSkillWithoutSliders = skills.find((s) => s instanceof DroidVisual && !s.withSliders);
|
|
1908
|
+
if (aimSkill && aimSkillWithoutSliders) {
|
|
1909
|
+
this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
|
|
1910
|
+
}
|
|
1911
|
+
if (tapSkillCheese) {
|
|
1912
|
+
const tapSkillVibro = new DroidTap(this.mods, true, tapSkillCheese.relevantDeltaTime());
|
|
1913
|
+
this.calculateSkills(tapSkillVibro);
|
|
1914
|
+
this.postCalculateTap(tapSkillCheese, tapSkillVibro);
|
|
1915
|
+
}
|
|
1889
1916
|
this.postCalculateRhythm(rhythmSkill);
|
|
1890
|
-
|
|
1891
|
-
|
|
1917
|
+
if (flashlightSkill && flashlightSkillWithoutSliders) {
|
|
1918
|
+
this.postCalculateFlashlight(flashlightSkill, flashlightSkillWithoutSliders);
|
|
1919
|
+
}
|
|
1920
|
+
if (visualSkill && visualSkillWithoutSliders) {
|
|
1921
|
+
this.postCalculateVisual(visualSkill, visualSkillWithoutSliders);
|
|
1922
|
+
}
|
|
1892
1923
|
this.calculateTotal();
|
|
1893
1924
|
}
|
|
1894
1925
|
toString() {
|
|
@@ -1917,20 +1948,24 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1917
1948
|
return difficultyObjects;
|
|
1918
1949
|
}
|
|
1919
1950
|
createSkills() {
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
new DroidAim(this.mods,
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
new DroidTap(this.mods,
|
|
1929
|
-
new
|
|
1930
|
-
new
|
|
1931
|
-
new DroidVisual(this.mods,
|
|
1932
|
-
|
|
1933
|
-
|
|
1951
|
+
const skills = [];
|
|
1952
|
+
if (!this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
|
|
1953
|
+
skills.push(new DroidAim(this.mods, true));
|
|
1954
|
+
skills.push(new DroidAim(this.mods, false));
|
|
1955
|
+
}
|
|
1956
|
+
if (!this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
1957
|
+
// Tap and visual skills depend on rhythm skill, so we put it first
|
|
1958
|
+
skills.push(new DroidRhythm(this.mods));
|
|
1959
|
+
skills.push(new DroidTap(this.mods, true));
|
|
1960
|
+
skills.push(new DroidTap(this.mods, false));
|
|
1961
|
+
skills.push(new DroidVisual(this.mods, true));
|
|
1962
|
+
skills.push(new DroidVisual(this.mods, false));
|
|
1963
|
+
}
|
|
1964
|
+
if (this.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
|
|
1965
|
+
skills.push(new DroidFlashlight(this.mods, true));
|
|
1966
|
+
skills.push(new DroidFlashlight(this.mods, false));
|
|
1967
|
+
}
|
|
1968
|
+
return skills;
|
|
1934
1969
|
}
|
|
1935
1970
|
calculateClockRate(options) {
|
|
1936
1971
|
var _a, _b;
|
|
@@ -2882,10 +2917,40 @@ class OsuSkill extends StrainSkill {
|
|
|
2882
2917
|
}
|
|
2883
2918
|
}
|
|
2884
2919
|
|
|
2920
|
+
/**
|
|
2921
|
+
* Represents an osu!standard hit object with difficulty calculation values.
|
|
2922
|
+
*/
|
|
2923
|
+
class OsuDifficultyHitObject extends DifficultyHitObject {
|
|
2924
|
+
constructor() {
|
|
2925
|
+
super(...arguments);
|
|
2926
|
+
/**
|
|
2927
|
+
* The speed strain generated by the hitobject.
|
|
2928
|
+
*/
|
|
2929
|
+
this.speedStrain = 0;
|
|
2930
|
+
/**
|
|
2931
|
+
* The flashlight strain generated by this hitobject.
|
|
2932
|
+
*/
|
|
2933
|
+
this.flashlightStrain = 0;
|
|
2934
|
+
this.radiusBuffThreshold = 30;
|
|
2935
|
+
this.mode = osuBase.Modes.osu;
|
|
2936
|
+
}
|
|
2937
|
+
get scalingFactor() {
|
|
2938
|
+
const radius = this.object.radius;
|
|
2939
|
+
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
|
2940
|
+
let scalingFactor = DifficultyHitObject.normalizedRadius / radius;
|
|
2941
|
+
// High circle size (small CS) bonus
|
|
2942
|
+
if (radius < this.radiusBuffThreshold) {
|
|
2943
|
+
scalingFactor *=
|
|
2944
|
+
1 + Math.min(this.radiusBuffThreshold - radius, 5) / 50;
|
|
2945
|
+
}
|
|
2946
|
+
return scalingFactor;
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2885
2950
|
/**
|
|
2886
2951
|
* An evaluator for calculating osu!standard Aim skill.
|
|
2887
2952
|
*/
|
|
2888
|
-
class OsuAimEvaluator
|
|
2953
|
+
class OsuAimEvaluator {
|
|
2889
2954
|
/**
|
|
2890
2955
|
* Evaluates the difficulty of aiming the current object, based on:
|
|
2891
2956
|
*
|
|
@@ -2905,6 +2970,8 @@ class OsuAimEvaluator extends AimEvaluator {
|
|
|
2905
2970
|
return 0;
|
|
2906
2971
|
}
|
|
2907
2972
|
const lastLast = current.previous(1);
|
|
2973
|
+
const radius = OsuDifficultyHitObject.normalizedRadius;
|
|
2974
|
+
const diameter = OsuDifficultyHitObject.normalizedDiameter;
|
|
2908
2975
|
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
|
|
2909
2976
|
let currentVelocity = current.lazyJumpDistance / current.strainTime;
|
|
2910
2977
|
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
|
|
@@ -2927,6 +2994,7 @@ class OsuAimEvaluator extends AimEvaluator {
|
|
|
2927
2994
|
let acuteAngleBonus = 0;
|
|
2928
2995
|
let sliderBonus = 0;
|
|
2929
2996
|
let velocityChangeBonus = 0;
|
|
2997
|
+
let wiggleBonus = 0;
|
|
2930
2998
|
// Start strain with regular velocity.
|
|
2931
2999
|
let strain = currentVelocity;
|
|
2932
3000
|
if (
|
|
@@ -2934,42 +3002,41 @@ class OsuAimEvaluator extends AimEvaluator {
|
|
|
2934
3002
|
Math.max(current.strainTime, last.strainTime) <
|
|
2935
3003
|
1.25 * Math.min(current.strainTime, last.strainTime) &&
|
|
2936
3004
|
current.angle !== null &&
|
|
2937
|
-
last.angle !== null
|
|
2938
|
-
|
|
3005
|
+
last.angle !== null) {
|
|
3006
|
+
const currentAngle = current.angle;
|
|
3007
|
+
const lastAngle = last.angle;
|
|
2939
3008
|
// Rewarding angles, take the smaller velocity as base.
|
|
2940
3009
|
const angleBonus = Math.min(currentVelocity, prevVelocity);
|
|
2941
3010
|
wideAngleBonus = this.calculateWideAngleBonus(current.angle);
|
|
2942
3011
|
acuteAngleBonus = this.calculateAcuteAngleBonus(current.angle);
|
|
2943
|
-
//
|
|
2944
|
-
if (current.strainTime > 100) {
|
|
2945
|
-
acuteAngleBonus = 0;
|
|
2946
|
-
}
|
|
2947
|
-
else {
|
|
2948
|
-
acuteAngleBonus *=
|
|
2949
|
-
// Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
|
|
2950
|
-
this.calculateAcuteAngleBonus(last.angle) *
|
|
2951
|
-
// The maximum velocity we buff is equal to 125 / strainTime.
|
|
2952
|
-
Math.min(angleBonus, 125 / current.strainTime) *
|
|
2953
|
-
// Scale buff from 300 BPM 1/2 to 400 BPM 1/2.
|
|
2954
|
-
Math.pow(Math.sin((Math.PI / 2) *
|
|
2955
|
-
Math.min(1, (100 - current.strainTime) / 25)), 2) *
|
|
2956
|
-
// Buff distance exceeding 50 (radius) up to 100 (diameter).
|
|
2957
|
-
Math.pow(Math.sin(((Math.PI / 2) *
|
|
2958
|
-
(osuBase.MathUtils.clamp(current.lazyJumpDistance, 50, 100) -
|
|
2959
|
-
50)) /
|
|
2960
|
-
50), 2);
|
|
2961
|
-
}
|
|
2962
|
-
// Penalize wide angles if they're repeated, reducing the penalty as last.angle gets more acute.
|
|
3012
|
+
// Penalize angle repetition.
|
|
2963
3013
|
wideAngleBonus *=
|
|
2964
|
-
|
|
2965
|
-
(
|
|
2966
|
-
Math.min(wideAngleBonus, Math.pow(this.calculateWideAngleBonus(last.angle), 3)));
|
|
2967
|
-
// Penalize acute angles if they're repeated, reducing the penalty as lastLast.angle gets more obtuse.
|
|
3014
|
+
1 -
|
|
3015
|
+
Math.min(wideAngleBonus, Math.pow(this.calculateWideAngleBonus(lastAngle), 3));
|
|
2968
3016
|
acuteAngleBonus *=
|
|
2969
|
-
0.
|
|
2970
|
-
0.
|
|
3017
|
+
0.08 +
|
|
3018
|
+
0.92 *
|
|
2971
3019
|
(1 -
|
|
2972
|
-
Math.min(acuteAngleBonus, Math.pow(this.calculateAcuteAngleBonus(
|
|
3020
|
+
Math.min(acuteAngleBonus, Math.pow(this.calculateAcuteAngleBonus(lastAngle), 3)));
|
|
3021
|
+
// Apply full wide angle bonus for distance more than one diameter
|
|
3022
|
+
wideAngleBonus *=
|
|
3023
|
+
angleBonus *
|
|
3024
|
+
osuBase.MathUtils.smootherstep(current.lazyJumpDistance, 0, diameter);
|
|
3025
|
+
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
|
3026
|
+
acuteAngleBonus *=
|
|
3027
|
+
angleBonus *
|
|
3028
|
+
osuBase.MathUtils.smootherstep(osuBase.MathUtils.millisecondsToBPM(current.strainTime, 2), 300, 400) *
|
|
3029
|
+
osuBase.MathUtils.smootherstep(current.lazyJumpDistance, diameter, diameter * 2);
|
|
3030
|
+
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
|
3031
|
+
// https://www.desmos.com/calculator/dp0v0nvowc
|
|
3032
|
+
wiggleBonus =
|
|
3033
|
+
angleBonus *
|
|
3034
|
+
osuBase.MathUtils.smootherstep(current.lazyJumpDistance, radius, diameter) *
|
|
3035
|
+
Math.pow(osuBase.MathUtils.reverseLerp(current.lazyJumpDistance, diameter * 3, diameter), 1.8) *
|
|
3036
|
+
osuBase.MathUtils.smootherstep(currentAngle, osuBase.MathUtils.degreesToRadians(110), osuBase.MathUtils.degreesToRadians(60)) *
|
|
3037
|
+
osuBase.MathUtils.smootherstep(last.lazyJumpDistance, radius, diameter) *
|
|
3038
|
+
Math.pow(osuBase.MathUtils.reverseLerp(last.lazyJumpDistance, diameter * 3, diameter), 1.8) *
|
|
3039
|
+
osuBase.MathUtils.smootherstep(lastAngle, osuBase.MathUtils.degreesToRadians(110), osuBase.MathUtils.degreesToRadians(60));
|
|
2973
3040
|
}
|
|
2974
3041
|
if (Math.max(prevVelocity, currentVelocity)) {
|
|
2975
3042
|
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
|
|
@@ -2993,6 +3060,7 @@ class OsuAimEvaluator extends AimEvaluator {
|
|
|
2993
3060
|
// Reward sliders based on velocity.
|
|
2994
3061
|
sliderBonus = last.travelDistance / last.travelTime;
|
|
2995
3062
|
}
|
|
3063
|
+
strain += wiggleBonus * this.wiggleMultiplier;
|
|
2996
3064
|
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
|
|
2997
3065
|
strain += Math.max(acuteAngleBonus * this.acuteAngleMultiplier, wideAngleBonus * this.wideAngleMultiplier +
|
|
2998
3066
|
velocityChangeBonus * this.velocityChangeMultiplier);
|
|
@@ -3002,7 +3070,18 @@ class OsuAimEvaluator extends AimEvaluator {
|
|
|
3002
3070
|
}
|
|
3003
3071
|
return strain;
|
|
3004
3072
|
}
|
|
3073
|
+
static calculateWideAngleBonus(angle) {
|
|
3074
|
+
return osuBase.MathUtils.smoothstep(angle, osuBase.MathUtils.degreesToRadians(40), osuBase.MathUtils.degreesToRadians(140));
|
|
3075
|
+
}
|
|
3076
|
+
static calculateAcuteAngleBonus(angle) {
|
|
3077
|
+
return osuBase.MathUtils.smoothstep(angle, osuBase.MathUtils.degreesToRadians(140), osuBase.MathUtils.degreesToRadians(40));
|
|
3078
|
+
}
|
|
3005
3079
|
}
|
|
3080
|
+
OsuAimEvaluator.wideAngleMultiplier = 1.5;
|
|
3081
|
+
OsuAimEvaluator.acuteAngleMultiplier = 2.6;
|
|
3082
|
+
OsuAimEvaluator.sliderMultiplier = 1.35;
|
|
3083
|
+
OsuAimEvaluator.velocityChangeMultiplier = 0.75;
|
|
3084
|
+
OsuAimEvaluator.wiggleMultiplier = 1.02;
|
|
3006
3085
|
|
|
3007
3086
|
/**
|
|
3008
3087
|
* Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
|
@@ -3015,15 +3094,33 @@ class OsuAim extends OsuSkill {
|
|
|
3015
3094
|
this.reducedSectionBaseline = 0.75;
|
|
3016
3095
|
this.decayWeight = 0.9;
|
|
3017
3096
|
this.currentAimStrain = 0;
|
|
3018
|
-
this.skillMultiplier = 25.
|
|
3097
|
+
this.skillMultiplier = 25.6;
|
|
3098
|
+
this.sliderStrains = [];
|
|
3019
3099
|
this.withSliders = withSliders;
|
|
3020
3100
|
}
|
|
3101
|
+
/**
|
|
3102
|
+
* Obtains the amount of sliders that are considered difficult in terms of relative strain.
|
|
3103
|
+
*/
|
|
3104
|
+
countDifficultSliders() {
|
|
3105
|
+
if (this.sliderStrains.length === 0) {
|
|
3106
|
+
return 0;
|
|
3107
|
+
}
|
|
3108
|
+
const maxSliderStrain = osuBase.MathUtils.max(this.sliderStrains);
|
|
3109
|
+
if (maxSliderStrain === 0) {
|
|
3110
|
+
return 0;
|
|
3111
|
+
}
|
|
3112
|
+
return this.sliderStrains.reduce((total, strain) => total +
|
|
3113
|
+
1 / (1 + Math.exp(-((strain / maxSliderStrain) * 12 - 6))), 0);
|
|
3114
|
+
}
|
|
3021
3115
|
strainValueAt(current) {
|
|
3022
3116
|
this.currentAimStrain *= this.strainDecay(current.deltaTime);
|
|
3023
3117
|
this.currentAimStrain +=
|
|
3024
3118
|
OsuAimEvaluator.evaluateDifficultyOf(current, this.withSliders) *
|
|
3025
3119
|
this.skillMultiplier;
|
|
3026
3120
|
this._objectStrains.push(this.currentAimStrain);
|
|
3121
|
+
if (current.object instanceof osuBase.Slider) {
|
|
3122
|
+
this.sliderStrains.push(this.currentAimStrain);
|
|
3123
|
+
}
|
|
3027
3124
|
return this.currentAimStrain;
|
|
3028
3125
|
}
|
|
3029
3126
|
calculateInitialStrain(time, current) {
|
|
@@ -3047,7 +3144,7 @@ class OsuAim extends OsuSkill {
|
|
|
3047
3144
|
/**
|
|
3048
3145
|
* An evaluator for calculating osu!standard speed skill.
|
|
3049
3146
|
*/
|
|
3050
|
-
class OsuSpeedEvaluator
|
|
3147
|
+
class OsuSpeedEvaluator {
|
|
3051
3148
|
/**
|
|
3052
3149
|
* Evaluates the difficulty of tapping the current object, based on:
|
|
3053
3150
|
*
|
|
@@ -3056,8 +3153,9 @@ class OsuSpeedEvaluator extends SpeedEvaluator {
|
|
|
3056
3153
|
* - and how easily they can be cheesed.
|
|
3057
3154
|
*
|
|
3058
3155
|
* @param current The current object.
|
|
3156
|
+
* @param mods The mods applied.
|
|
3059
3157
|
*/
|
|
3060
|
-
static evaluateDifficultyOf(current) {
|
|
3158
|
+
static evaluateDifficultyOf(current, mods) {
|
|
3061
3159
|
var _a;
|
|
3062
3160
|
if (current.object instanceof osuBase.Spinner) {
|
|
3063
3161
|
return 0;
|
|
@@ -3080,8 +3178,11 @@ class OsuSpeedEvaluator extends SpeedEvaluator {
|
|
|
3080
3178
|
// Cap distance at spacing threshold
|
|
3081
3179
|
const distance = Math.min(this.SINGLE_SPACING_THRESHOLD, travelDistance + current.minimumJumpDistance);
|
|
3082
3180
|
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
|
|
3083
|
-
|
|
3181
|
+
let distanceBonus = Math.pow(distance / this.SINGLE_SPACING_THRESHOLD, 3.95) *
|
|
3084
3182
|
this.DISTANCE_MULTIPLIER;
|
|
3183
|
+
if (mods.some((m) => m instanceof osuBase.ModAutopilot)) {
|
|
3184
|
+
distanceBonus = 0;
|
|
3185
|
+
}
|
|
3085
3186
|
// Base difficulty with all bonuses
|
|
3086
3187
|
const difficulty = ((1 + speedBonus + distanceBonus) * 1000) / strainTime;
|
|
3087
3188
|
// Apply penalty if there's doubletappable doubles
|
|
@@ -3094,7 +3195,9 @@ class OsuSpeedEvaluator extends SpeedEvaluator {
|
|
|
3094
3195
|
* About 1.25 circles distance between hitobject centers.
|
|
3095
3196
|
*/
|
|
3096
3197
|
OsuSpeedEvaluator.SINGLE_SPACING_THRESHOLD = 125;
|
|
3097
|
-
|
|
3198
|
+
// ~200 1/4 BPM streams
|
|
3199
|
+
OsuSpeedEvaluator.minSpeedBonus = 75;
|
|
3200
|
+
OsuSpeedEvaluator.DISTANCE_MULTIPLIER = 0.9;
|
|
3098
3201
|
|
|
3099
3202
|
/**
|
|
3100
3203
|
* An evaluator for calculating osu!standard Rhythm skill.
|
|
@@ -3196,7 +3299,7 @@ class OsuRhythmEvaluator {
|
|
|
3196
3299
|
}
|
|
3197
3300
|
// Repeated island (ex: triplet -> triplet).
|
|
3198
3301
|
// Graph: https://www.desmos.com/calculator/pj7an56zwf
|
|
3199
|
-
effectiveRatio *= Math.min(3 / islandCount, Math.pow(1 / islandCount,
|
|
3302
|
+
effectiveRatio *= Math.min(3 / islandCount, Math.pow(1 / islandCount, osuBase.MathUtils.offsetLogistic(island.delta, 58.33, 0.24, 2.75)));
|
|
3200
3303
|
break;
|
|
3201
3304
|
}
|
|
3202
3305
|
if (!islandFound) {
|
|
@@ -3256,7 +3359,7 @@ class OsuSpeed extends OsuSkill {
|
|
|
3256
3359
|
this.decayWeight = 0.9;
|
|
3257
3360
|
this.currentSpeedStrain = 0;
|
|
3258
3361
|
this.currentRhythm = 0;
|
|
3259
|
-
this.skillMultiplier = 1.
|
|
3362
|
+
this.skillMultiplier = 1.46;
|
|
3260
3363
|
}
|
|
3261
3364
|
/**
|
|
3262
3365
|
* @param current The hitobject to calculate.
|
|
@@ -3264,7 +3367,7 @@ class OsuSpeed extends OsuSkill {
|
|
|
3264
3367
|
strainValueAt(current) {
|
|
3265
3368
|
this.currentSpeedStrain *= this.strainDecay(current.strainTime);
|
|
3266
3369
|
this.currentSpeedStrain +=
|
|
3267
|
-
OsuSpeedEvaluator.evaluateDifficultyOf(current) *
|
|
3370
|
+
OsuSpeedEvaluator.evaluateDifficultyOf(current, this.mods) *
|
|
3268
3371
|
this.skillMultiplier;
|
|
3269
3372
|
this.currentRhythm = OsuRhythmEvaluator.evaluateDifficultyOf(current);
|
|
3270
3373
|
const strain = this.currentSpeedStrain * this.currentRhythm;
|
|
@@ -3289,7 +3392,7 @@ class OsuSpeed extends OsuSkill {
|
|
|
3289
3392
|
/**
|
|
3290
3393
|
* An evaluator for calculating osu!standard Flashlight skill.
|
|
3291
3394
|
*/
|
|
3292
|
-
class OsuFlashlightEvaluator
|
|
3395
|
+
class OsuFlashlightEvaluator {
|
|
3293
3396
|
/**
|
|
3294
3397
|
* Evaluates the difficulty of memorizing and hitting the current object, based on:
|
|
3295
3398
|
*
|
|
@@ -3300,9 +3403,9 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
3300
3403
|
* - and whether Hidden mod is enabled.
|
|
3301
3404
|
*
|
|
3302
3405
|
* @param current The current object.
|
|
3303
|
-
* @param
|
|
3406
|
+
* @param mods The mods used.
|
|
3304
3407
|
*/
|
|
3305
|
-
static evaluateDifficultyOf(current,
|
|
3408
|
+
static evaluateDifficultyOf(current, mods) {
|
|
3306
3409
|
if (current.object instanceof osuBase.Spinner) {
|
|
3307
3410
|
return 0;
|
|
3308
3411
|
}
|
|
@@ -3314,11 +3417,11 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
3314
3417
|
let angleRepeatCount = 0;
|
|
3315
3418
|
for (let i = 0; i < Math.min(current.index, 10); ++i) {
|
|
3316
3419
|
const currentObject = current.previous(i);
|
|
3420
|
+
cumulativeStrainTime += last.strainTime;
|
|
3317
3421
|
if (!(currentObject.object instanceof osuBase.Spinner)) {
|
|
3318
3422
|
const jumpDistance = current.object
|
|
3319
3423
|
.getStackedPosition(osuBase.Modes.osu)
|
|
3320
3424
|
.subtract(currentObject.object.getStackedEndPosition(osuBase.Modes.osu)).length;
|
|
3321
|
-
cumulativeStrainTime += last.strainTime;
|
|
3322
3425
|
// We want to nerf objects that can be easily seen within the Flashlight circle radius.
|
|
3323
3426
|
if (i === 0) {
|
|
3324
3427
|
smallDistNerf = Math.min(1, jumpDistance / 75);
|
|
@@ -3329,7 +3432,7 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
3329
3432
|
const opacityBonus = 1 +
|
|
3330
3433
|
this.maxOpacityBonus *
|
|
3331
3434
|
(1 -
|
|
3332
|
-
current.opacityAt(currentObject.object.startTime,
|
|
3435
|
+
current.opacityAt(currentObject.object.startTime, mods));
|
|
3333
3436
|
result +=
|
|
3334
3437
|
(stackNerf * opacityBonus * scalingFactor * jumpDistance) /
|
|
3335
3438
|
cumulativeStrainTime;
|
|
@@ -3344,7 +3447,7 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
3344
3447
|
}
|
|
3345
3448
|
result = Math.pow(smallDistNerf * result, 2);
|
|
3346
3449
|
// Additional bonus for Hidden due to there being no approach circles.
|
|
3347
|
-
if (
|
|
3450
|
+
if (mods.some((m) => m instanceof osuBase.ModHidden)) {
|
|
3348
3451
|
result *= 1 + this.hiddenBonus;
|
|
3349
3452
|
}
|
|
3350
3453
|
// Nerf patterns with repeated angles.
|
|
@@ -3367,20 +3470,24 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
3367
3470
|
return result;
|
|
3368
3471
|
}
|
|
3369
3472
|
}
|
|
3473
|
+
OsuFlashlightEvaluator.maxOpacityBonus = 0.4;
|
|
3474
|
+
OsuFlashlightEvaluator.hiddenBonus = 0.2;
|
|
3475
|
+
OsuFlashlightEvaluator.minVelocity = 0.5;
|
|
3476
|
+
OsuFlashlightEvaluator.sliderMultiplier = 1.3;
|
|
3477
|
+
OsuFlashlightEvaluator.minAngleMultiplier = 0.2;
|
|
3370
3478
|
|
|
3371
3479
|
/**
|
|
3372
3480
|
* Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
|
|
3373
3481
|
*/
|
|
3374
3482
|
class OsuFlashlight extends OsuSkill {
|
|
3375
|
-
constructor(
|
|
3376
|
-
super(
|
|
3483
|
+
constructor() {
|
|
3484
|
+
super(...arguments);
|
|
3377
3485
|
this.strainDecayBase = 0.15;
|
|
3378
3486
|
this.reducedSectionCount = 0;
|
|
3379
3487
|
this.reducedSectionBaseline = 1;
|
|
3380
3488
|
this.decayWeight = 1;
|
|
3381
3489
|
this.currentFlashlightStrain = 0;
|
|
3382
3490
|
this.skillMultiplier = 0.05512;
|
|
3383
|
-
this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
|
|
3384
3491
|
}
|
|
3385
3492
|
difficultyValue() {
|
|
3386
3493
|
return this.strainPeaks.reduce((a, b) => a + b, 0);
|
|
@@ -3388,7 +3495,8 @@ class OsuFlashlight extends OsuSkill {
|
|
|
3388
3495
|
strainValueAt(current) {
|
|
3389
3496
|
this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
|
|
3390
3497
|
this.currentFlashlightStrain +=
|
|
3391
|
-
OsuFlashlightEvaluator.evaluateDifficultyOf(current, this.
|
|
3498
|
+
OsuFlashlightEvaluator.evaluateDifficultyOf(current, this.mods) *
|
|
3499
|
+
this.skillMultiplier;
|
|
3392
3500
|
return this.currentFlashlightStrain;
|
|
3393
3501
|
}
|
|
3394
3502
|
calculateInitialStrain(time, current) {
|
|
@@ -3401,36 +3509,6 @@ class OsuFlashlight extends OsuSkill {
|
|
|
3401
3509
|
}
|
|
3402
3510
|
}
|
|
3403
3511
|
|
|
3404
|
-
/**
|
|
3405
|
-
* Represents an osu!standard hit object with difficulty calculation values.
|
|
3406
|
-
*/
|
|
3407
|
-
class OsuDifficultyHitObject extends DifficultyHitObject {
|
|
3408
|
-
constructor() {
|
|
3409
|
-
super(...arguments);
|
|
3410
|
-
/**
|
|
3411
|
-
* The speed strain generated by the hitobject.
|
|
3412
|
-
*/
|
|
3413
|
-
this.speedStrain = 0;
|
|
3414
|
-
/**
|
|
3415
|
-
* The flashlight strain generated by this hitobject.
|
|
3416
|
-
*/
|
|
3417
|
-
this.flashlightStrain = 0;
|
|
3418
|
-
this.radiusBuffThreshold = 30;
|
|
3419
|
-
this.mode = osuBase.Modes.osu;
|
|
3420
|
-
}
|
|
3421
|
-
get scalingFactor() {
|
|
3422
|
-
const radius = this.object.radius;
|
|
3423
|
-
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
|
3424
|
-
let scalingFactor = DifficultyHitObject.normalizedRadius / radius;
|
|
3425
|
-
// High circle size (small CS) bonus
|
|
3426
|
-
if (radius < this.radiusBuffThreshold) {
|
|
3427
|
-
scalingFactor *=
|
|
3428
|
-
1 + Math.min(this.radiusBuffThreshold - radius, 5) / 50;
|
|
3429
|
-
}
|
|
3430
|
-
return scalingFactor;
|
|
3431
|
-
}
|
|
3432
|
-
}
|
|
3433
|
-
|
|
3434
3512
|
/**
|
|
3435
3513
|
* A difficulty calculator for osu!standard gamemode.
|
|
3436
3514
|
*/
|
|
@@ -3452,6 +3530,7 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3452
3530
|
hitCircleCount: 0,
|
|
3453
3531
|
sliderCount: 0,
|
|
3454
3532
|
spinnerCount: 0,
|
|
3533
|
+
aimDifficultSliderCount: 0,
|
|
3455
3534
|
aimDifficultStrainCount: 0,
|
|
3456
3535
|
speedDifficultStrainCount: 0,
|
|
3457
3536
|
};
|
|
@@ -3483,6 +3562,10 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3483
3562
|
* Calculates the aim star rating of the beatmap and stores it in this instance.
|
|
3484
3563
|
*/
|
|
3485
3564
|
calculateAim() {
|
|
3565
|
+
if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
|
|
3566
|
+
this.attributes.aimDifficulty = 0;
|
|
3567
|
+
return;
|
|
3568
|
+
}
|
|
3486
3569
|
const aimSkill = new OsuAim(this.mods, true);
|
|
3487
3570
|
const aimSkillWithoutSliders = new OsuAim(this.mods, false);
|
|
3488
3571
|
this.calculateSkills(aimSkill, aimSkillWithoutSliders);
|
|
@@ -3504,6 +3587,10 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3504
3587
|
* Calculates the flashlight star rating of the beatmap and stores it in this instance.
|
|
3505
3588
|
*/
|
|
3506
3589
|
calculateFlashlight() {
|
|
3590
|
+
if (!this.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
|
|
3591
|
+
this.attributes.flashlightDifficulty = 0;
|
|
3592
|
+
return;
|
|
3593
|
+
}
|
|
3507
3594
|
const flashlightSkill = new OsuFlashlight(this.mods);
|
|
3508
3595
|
this.calculateSkills(flashlightSkill);
|
|
3509
3596
|
this.postCalculateFlashlight(flashlightSkill);
|
|
@@ -3533,21 +3620,20 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3533
3620
|
}
|
|
3534
3621
|
calculateAll() {
|
|
3535
3622
|
const skills = this.createSkills();
|
|
3536
|
-
const isRelax = this.mods.some((m) => m instanceof osuBase.ModRelax);
|
|
3537
3623
|
this.calculateSkills(...skills);
|
|
3538
|
-
const aimSkill = skills
|
|
3539
|
-
const aimSkillWithoutSliders = skills
|
|
3540
|
-
const speedSkill = skills
|
|
3541
|
-
const flashlightSkill = skills
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
else {
|
|
3624
|
+
const aimSkill = skills.find((s) => s instanceof OsuAim && s.withSliders);
|
|
3625
|
+
const aimSkillWithoutSliders = skills.find((s) => s instanceof OsuAim && !s.withSliders);
|
|
3626
|
+
const speedSkill = skills.find((s) => s instanceof OsuSpeed);
|
|
3627
|
+
const flashlightSkill = skills.find((s) => s instanceof OsuFlashlight);
|
|
3628
|
+
if (aimSkill && aimSkillWithoutSliders) {
|
|
3629
|
+
this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
|
|
3630
|
+
}
|
|
3631
|
+
if (speedSkill) {
|
|
3547
3632
|
this.postCalculateSpeed(speedSkill);
|
|
3548
3633
|
}
|
|
3549
|
-
|
|
3550
|
-
|
|
3634
|
+
if (flashlightSkill) {
|
|
3635
|
+
this.postCalculateFlashlight(flashlightSkill);
|
|
3636
|
+
}
|
|
3551
3637
|
this.calculateTotal();
|
|
3552
3638
|
}
|
|
3553
3639
|
toString() {
|
|
@@ -3572,12 +3658,18 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3572
3658
|
return difficultyObjects;
|
|
3573
3659
|
}
|
|
3574
3660
|
createSkills() {
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
new OsuAim(this.mods,
|
|
3578
|
-
new
|
|
3579
|
-
|
|
3580
|
-
|
|
3661
|
+
const skills = [];
|
|
3662
|
+
if (!this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
|
|
3663
|
+
skills.push(new OsuAim(this.mods, true));
|
|
3664
|
+
skills.push(new OsuAim(this.mods, false));
|
|
3665
|
+
}
|
|
3666
|
+
if (!this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3667
|
+
skills.push(new OsuSpeed(this.mods));
|
|
3668
|
+
}
|
|
3669
|
+
if (this.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
|
|
3670
|
+
skills.push(new OsuFlashlight(this.mods));
|
|
3671
|
+
}
|
|
3672
|
+
return skills;
|
|
3581
3673
|
}
|
|
3582
3674
|
populateDifficultyAttributes(beatmap, clockRate) {
|
|
3583
3675
|
super.populateDifficultyAttributes(beatmap, clockRate);
|
|
@@ -3605,8 +3697,13 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3605
3697
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3606
3698
|
this.attributes.aimDifficulty *= 0.9;
|
|
3607
3699
|
}
|
|
3700
|
+
else if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
|
|
3701
|
+
this.attributes.aimDifficulty = 0;
|
|
3702
|
+
}
|
|
3608
3703
|
this.attributes.aimDifficultStrainCount =
|
|
3609
3704
|
aimSkill.countDifficultStrains();
|
|
3705
|
+
this.attributes.aimDifficultSliderCount =
|
|
3706
|
+
aimSkill.countDifficultSliders();
|
|
3610
3707
|
}
|
|
3611
3708
|
/**
|
|
3612
3709
|
* Called after speed skill calculation.
|
|
@@ -3615,16 +3712,16 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3615
3712
|
*/
|
|
3616
3713
|
postCalculateSpeed(speedSkill) {
|
|
3617
3714
|
this.strainPeaks.speed = speedSkill.strainPeaks;
|
|
3618
|
-
this.attributes.speedDifficulty = this.
|
|
3715
|
+
this.attributes.speedDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
|
|
3716
|
+
? 0
|
|
3717
|
+
: this.starValue(speedSkill.difficultyValue());
|
|
3718
|
+
if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
|
|
3719
|
+
this.attributes.speedDifficulty *= 0.5;
|
|
3720
|
+
}
|
|
3619
3721
|
this.attributes.speedDifficultStrainCount =
|
|
3620
3722
|
speedSkill.countDifficultStrains();
|
|
3621
|
-
}
|
|
3622
|
-
/**
|
|
3623
|
-
* Calculates speed-related attributes.
|
|
3624
|
-
*/
|
|
3625
|
-
calculateSpeedAttributes() {
|
|
3626
3723
|
const objectStrains = this.objects.map((v) => v.speedStrain);
|
|
3627
|
-
const maxStrain =
|
|
3724
|
+
const maxStrain = osuBase.MathUtils.max(objectStrains);
|
|
3628
3725
|
if (maxStrain) {
|
|
3629
3726
|
this.attributes.speedNoteCount = objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
|
|
3630
3727
|
}
|
|
@@ -3643,6 +3740,9 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3643
3740
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3644
3741
|
this.attributes.flashlightDifficulty *= 0.7;
|
|
3645
3742
|
}
|
|
3743
|
+
else if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
|
|
3744
|
+
this.attributes.flashlightDifficulty *= 0.4;
|
|
3745
|
+
}
|
|
3646
3746
|
}
|
|
3647
3747
|
}
|
|
3648
3748
|
|
|
@@ -3671,8 +3771,10 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3671
3771
|
this.finalMultiplier = 1.15;
|
|
3672
3772
|
this.mode = osuBase.Modes.osu;
|
|
3673
3773
|
this.comboPenalty = 1;
|
|
3774
|
+
this.speedDeviation = 0;
|
|
3674
3775
|
}
|
|
3675
3776
|
calculateValues() {
|
|
3777
|
+
this.speedDeviation = this.calculateSpeedDeviation();
|
|
3676
3778
|
this.aim = this.calculateAimValue();
|
|
3677
3779
|
this.speed = this.calculateSpeedValue();
|
|
3678
3780
|
this.accuracy = this.calculateAccuracyValue();
|
|
@@ -3696,6 +3798,9 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3696
3798
|
* Calculates the aim performance value of the beatmap.
|
|
3697
3799
|
*/
|
|
3698
3800
|
calculateAimValue() {
|
|
3801
|
+
if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
|
|
3802
|
+
return 0;
|
|
3803
|
+
}
|
|
3699
3804
|
let aimValue = this.baseValue(this.difficultyAttributes.aimDifficulty);
|
|
3700
3805
|
// Longer maps are worth more
|
|
3701
3806
|
let lengthBonus = 0.95 + 0.4 * Math.min(1, this.totalHits / 2000);
|
|
@@ -3703,6 +3808,14 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3703
3808
|
lengthBonus += Math.log10(this.totalHits / 2000) * 0.5;
|
|
3704
3809
|
}
|
|
3705
3810
|
aimValue *= lengthBonus;
|
|
3811
|
+
if (this.effectiveMissCount > 0) {
|
|
3812
|
+
// Penalize misses by assessing # of misses relative to the total # of objects.
|
|
3813
|
+
// Default a 3% reduction for any # of misses.
|
|
3814
|
+
aimValue *=
|
|
3815
|
+
0.97 *
|
|
3816
|
+
Math.pow(1 -
|
|
3817
|
+
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
|
|
3818
|
+
}
|
|
3706
3819
|
aimValue *= this.calculateStrainBasedMissPenalty(this.difficultyAttributes.aimDifficultStrainCount);
|
|
3707
3820
|
const calculatedAR = this.difficultyAttributes.approachRate;
|
|
3708
3821
|
if (!this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
@@ -3718,7 +3831,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3718
3831
|
aimValue *= 1 + arFactor * lengthBonus;
|
|
3719
3832
|
}
|
|
3720
3833
|
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
|
3721
|
-
if (this.mods.some((m) => m instanceof osuBase.ModHidden)) {
|
|
3834
|
+
if (this.mods.some((m) => m instanceof osuBase.ModHidden || m instanceof osuBase.ModTraceable)) {
|
|
3722
3835
|
aimValue *= 1 + 0.04 * (12 - calculatedAR);
|
|
3723
3836
|
}
|
|
3724
3837
|
// Scale the aim value with slider factor to nerf very likely dropped sliderends.
|
|
@@ -3734,7 +3847,8 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3734
3847
|
* Calculates the speed performance value of the beatmap.
|
|
3735
3848
|
*/
|
|
3736
3849
|
calculateSpeedValue() {
|
|
3737
|
-
if (this.mods.some((m) => m instanceof osuBase.ModRelax)
|
|
3850
|
+
if (this.mods.some((m) => m instanceof osuBase.ModRelax) ||
|
|
3851
|
+
this.speedDeviation === Number.POSITIVE_INFINITY) {
|
|
3738
3852
|
return 0;
|
|
3739
3853
|
}
|
|
3740
3854
|
let speedValue = this.baseValue(this.difficultyAttributes.speedDifficulty);
|
|
@@ -3747,11 +3861,12 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3747
3861
|
speedValue *= this.calculateStrainBasedMissPenalty(this.difficultyAttributes.speedDifficultStrainCount);
|
|
3748
3862
|
// AR scaling
|
|
3749
3863
|
const calculatedAR = this.difficultyAttributes.approachRate;
|
|
3750
|
-
if (calculatedAR > 10.33
|
|
3864
|
+
if (calculatedAR > 10.33 &&
|
|
3865
|
+
!this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
|
|
3751
3866
|
// Buff for longer maps with high AR.
|
|
3752
3867
|
speedValue *= 1 + 0.3 * (calculatedAR - 10.33) * lengthBonus;
|
|
3753
3868
|
}
|
|
3754
|
-
if (this.mods.some((m) => m instanceof osuBase.ModHidden)) {
|
|
3869
|
+
if (this.mods.some((m) => m instanceof osuBase.ModHidden || m instanceof osuBase.ModTraceable)) {
|
|
3755
3870
|
speedValue *= 1 + 0.04 * (12 - calculatedAR);
|
|
3756
3871
|
}
|
|
3757
3872
|
// Calculate accuracy assuming the worst case scenario.
|
|
@@ -3768,16 +3883,15 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3768
3883
|
}
|
|
3769
3884
|
: // Set accuracy to 0.
|
|
3770
3885
|
{ n300: 0, nobjects: 1 });
|
|
3886
|
+
speedValue *= this.calculateSpeedHighDeviationNerf();
|
|
3771
3887
|
// Scale the speed value with accuracy and OD.
|
|
3772
3888
|
speedValue *=
|
|
3773
3889
|
(0.95 +
|
|
3774
|
-
Math.pow(this.difficultyAttributes.overallDifficulty, 2) /
|
|
3890
|
+
Math.pow(Math.max(0, this.difficultyAttributes.overallDifficulty), 2) /
|
|
3775
3891
|
750) *
|
|
3776
3892
|
Math.pow((this.computedAccuracy.value() +
|
|
3777
3893
|
relevantAccuracy.value(this.difficultyAttributes.speedNoteCount)) /
|
|
3778
3894
|
2, (14.5 - this.difficultyAttributes.overallDifficulty) / 2);
|
|
3779
|
-
// Scale the speed value with # of 50s to punish doubletapping.
|
|
3780
|
-
speedValue *= Math.pow(0.99, Math.max(0, this.computedAccuracy.n50 - this.totalHits / 500));
|
|
3781
3895
|
return speedValue;
|
|
3782
3896
|
}
|
|
3783
3897
|
/**
|
|
@@ -3802,7 +3916,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3802
3916
|
2.83;
|
|
3803
3917
|
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
|
|
3804
3918
|
accuracyValue *= Math.min(1.15, Math.pow(ncircles / 1000, 0.3));
|
|
3805
|
-
if (this.mods.some((m) => m instanceof osuBase.ModHidden)) {
|
|
3919
|
+
if (this.mods.some((m) => m instanceof osuBase.ModHidden || m instanceof osuBase.ModTraceable)) {
|
|
3806
3920
|
accuracyValue *= 1.08;
|
|
3807
3921
|
}
|
|
3808
3922
|
if (this.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
|
|
@@ -3841,6 +3955,113 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3841
3955
|
flashlightValue *= 0.98 + odScaling;
|
|
3842
3956
|
return flashlightValue;
|
|
3843
3957
|
}
|
|
3958
|
+
/**
|
|
3959
|
+
* Estimates a player's deviation on speed notes using {@link calculateDeviation}, assuming worst-case.
|
|
3960
|
+
*
|
|
3961
|
+
* Treats all speed notes as hit circles.
|
|
3962
|
+
*/
|
|
3963
|
+
calculateSpeedDeviation() {
|
|
3964
|
+
if (this.totalSuccessfulHits === 0) {
|
|
3965
|
+
return Number.POSITIVE_INFINITY;
|
|
3966
|
+
}
|
|
3967
|
+
// Calculate accuracy assuming the worst case scenario
|
|
3968
|
+
const speedNoteCount = this.difficultyAttributes.speedNoteCount +
|
|
3969
|
+
(this.totalHits - this.difficultyAttributes.speedNoteCount) * 0.1;
|
|
3970
|
+
// Assume worst case: all mistakes were on speed notes
|
|
3971
|
+
const relevantCountMiss = Math.min(this.computedAccuracy.nmiss, speedNoteCount);
|
|
3972
|
+
const relevantCountMeh = Math.min(this.computedAccuracy.n50, speedNoteCount - relevantCountMiss);
|
|
3973
|
+
const relevantCountOk = Math.min(this.computedAccuracy.n100, speedNoteCount - relevantCountMiss - relevantCountMeh);
|
|
3974
|
+
const relevantCountGreat = Math.max(0, speedNoteCount -
|
|
3975
|
+
relevantCountMiss -
|
|
3976
|
+
relevantCountMeh -
|
|
3977
|
+
relevantCountOk);
|
|
3978
|
+
return this.calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss);
|
|
3979
|
+
}
|
|
3980
|
+
/**
|
|
3981
|
+
* Estimates the player's tap deviation based on the OD, given number of greats, oks, mehs and misses,
|
|
3982
|
+
* assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the
|
|
3983
|
+
* same map with the same settings will always return the same deviation.
|
|
3984
|
+
*
|
|
3985
|
+
* Misses are ignored because they are usually due to misaiming.
|
|
3986
|
+
*
|
|
3987
|
+
* Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution.
|
|
3988
|
+
*/
|
|
3989
|
+
calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss) {
|
|
3990
|
+
if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0) {
|
|
3991
|
+
return Number.POSITIVE_INFINITY;
|
|
3992
|
+
}
|
|
3993
|
+
const objectCount = relevantCountGreat +
|
|
3994
|
+
relevantCountOk +
|
|
3995
|
+
relevantCountMeh +
|
|
3996
|
+
relevantCountMiss;
|
|
3997
|
+
// Obtain the great, ok, and meh windows.
|
|
3998
|
+
const hitWindow = new osuBase.OsuHitWindow(osuBase.OsuHitWindow.greatWindowToOD(
|
|
3999
|
+
// Convert current OD to non clock rate-adjusted OD.
|
|
4000
|
+
new osuBase.OsuHitWindow(this.difficultyAttributes.overallDifficulty)
|
|
4001
|
+
.greatWindow * this.difficultyAttributes.clockRate));
|
|
4002
|
+
const { greatWindow, okWindow, mehWindow } = hitWindow;
|
|
4003
|
+
// The probability that a player hits a circle is unknown, but we can estimate it to be
|
|
4004
|
+
// the number of greats on circles divided by the number of circles, and then add one
|
|
4005
|
+
// to the number of circles as a bias correction.
|
|
4006
|
+
const n = Math.max(1, objectCount - relevantCountMiss - relevantCountMeh);
|
|
4007
|
+
// 99% critical value for the normal distribution (one-tailed).
|
|
4008
|
+
const z = 2.32634787404;
|
|
4009
|
+
// Proportion of greats hit on circles, ignoring misses and 50s.
|
|
4010
|
+
const p = relevantCountGreat / n;
|
|
4011
|
+
// We can be 99% confident that p is at least this value.
|
|
4012
|
+
const pLowerBound = (n * p + (z * z) / 2) / (n + z * z) -
|
|
4013
|
+
(z / (n + z * z)) * Math.sqrt(n * p * (1 - p) + (z * z) / 4);
|
|
4014
|
+
// Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed.
|
|
4015
|
+
// Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than:
|
|
4016
|
+
let deviation = greatWindow / (Math.SQRT2 * osuBase.ErrorFunction.erfInv(pLowerBound));
|
|
4017
|
+
const randomValue = (Math.sqrt(2 / Math.PI) *
|
|
4018
|
+
okWindow *
|
|
4019
|
+
Math.pow(Math.exp(-0.5 * (okWindow / deviation)), 2)) /
|
|
4020
|
+
(deviation *
|
|
4021
|
+
osuBase.ErrorFunction.erf(okWindow / (Math.SQRT2 * deviation)));
|
|
4022
|
+
deviation *= Math.sqrt(1 - randomValue);
|
|
4023
|
+
// Value deviation approach as greatCount approaches 0
|
|
4024
|
+
const limitValue = okWindow / Math.sqrt(3);
|
|
4025
|
+
// If precision is not enough to compute true deviation - use limit value
|
|
4026
|
+
if (pLowerBound == 0.0 || randomValue >= 1 || deviation > limitValue) {
|
|
4027
|
+
deviation = limitValue;
|
|
4028
|
+
}
|
|
4029
|
+
// Then compute the variance for mehs.
|
|
4030
|
+
const mehVariance = (Math.pow(mehWindow, 2) +
|
|
4031
|
+
okWindow * mehWindow +
|
|
4032
|
+
Math.pow(okWindow, 2)) /
|
|
4033
|
+
3;
|
|
4034
|
+
// Find the total deviation.
|
|
4035
|
+
deviation = Math.sqrt(((relevantCountGreat + relevantCountOk) * Math.pow(deviation, 2) +
|
|
4036
|
+
relevantCountMeh * mehVariance) /
|
|
4037
|
+
(relevantCountGreat + relevantCountOk + relevantCountMeh));
|
|
4038
|
+
return deviation;
|
|
4039
|
+
}
|
|
4040
|
+
/**
|
|
4041
|
+
* Calculates multiplier for speed to account for improper tapping based on the deviation and speed difficulty.
|
|
4042
|
+
*
|
|
4043
|
+
* [Graph](https://www.desmos.com/calculator/dmogdhzofn)
|
|
4044
|
+
*/
|
|
4045
|
+
calculateSpeedHighDeviationNerf() {
|
|
4046
|
+
if (this.speedDeviation == Number.POSITIVE_INFINITY) {
|
|
4047
|
+
return 0;
|
|
4048
|
+
}
|
|
4049
|
+
const speedValue = this.baseValue(this.difficultyAttributes.speedDifficulty);
|
|
4050
|
+
// Decide a point where the PP value achieved compared to the speed deviation is assumed to be tapped
|
|
4051
|
+
// improperly. Any PP above this point is considered "excess" speed difficulty. This is used to cause
|
|
4052
|
+
// PP above the cutoff to scale logarithmically towards the original speed value thus nerfing the value.
|
|
4053
|
+
const excessSpeedDifficultyCutoff = 100 + 220 * Math.pow(22 / this.speedDeviation, 6.5);
|
|
4054
|
+
if (speedValue <= excessSpeedDifficultyCutoff) {
|
|
4055
|
+
return 1;
|
|
4056
|
+
}
|
|
4057
|
+
const scale = 50;
|
|
4058
|
+
const adjustedSpeedValue = scale *
|
|
4059
|
+
(Math.log((speedValue - excessSpeedDifficultyCutoff) / scale + 1) +
|
|
4060
|
+
excessSpeedDifficultyCutoff / scale);
|
|
4061
|
+
// 220 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible
|
|
4062
|
+
const t = 1 - osuBase.Interpolation.reverseLerp(this.speedDeviation, 22, 27);
|
|
4063
|
+
return (osuBase.Interpolation.lerp(adjustedSpeedValue, speedValue, t) / speedValue);
|
|
4064
|
+
}
|
|
3844
4065
|
toString() {
|
|
3845
4066
|
return (this.total.toFixed(2) +
|
|
3846
4067
|
" pp (" +
|
|
@@ -3855,7 +4076,6 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3855
4076
|
}
|
|
3856
4077
|
}
|
|
3857
4078
|
|
|
3858
|
-
exports.AimEvaluator = AimEvaluator;
|
|
3859
4079
|
exports.DifficultyCalculator = DifficultyCalculator;
|
|
3860
4080
|
exports.DifficultyHitObject = DifficultyHitObject;
|
|
3861
4081
|
exports.DroidAim = DroidAim;
|
|
@@ -3871,7 +4091,6 @@ exports.DroidTap = DroidTap;
|
|
|
3871
4091
|
exports.DroidTapEvaluator = DroidTapEvaluator;
|
|
3872
4092
|
exports.DroidVisual = DroidVisual;
|
|
3873
4093
|
exports.DroidVisualEvaluator = DroidVisualEvaluator;
|
|
3874
|
-
exports.FlashlightEvaluator = FlashlightEvaluator;
|
|
3875
4094
|
exports.OsuAim = OsuAim;
|
|
3876
4095
|
exports.OsuAimEvaluator = OsuAimEvaluator;
|
|
3877
4096
|
exports.OsuDifficultyCalculator = OsuDifficultyCalculator;
|
|
@@ -3883,5 +4102,4 @@ exports.OsuRhythmEvaluator = OsuRhythmEvaluator;
|
|
|
3883
4102
|
exports.OsuSpeed = OsuSpeed;
|
|
3884
4103
|
exports.OsuSpeedEvaluator = OsuSpeedEvaluator;
|
|
3885
4104
|
exports.PerformanceCalculator = PerformanceCalculator;
|
|
3886
|
-
exports.SpeedEvaluator = SpeedEvaluator;
|
|
3887
4105
|
//# sourceMappingURL=index.js.map
|