@rian8337/osu-difficulty-calculator 4.0.0-beta.11 → 4.0.0-beta.13
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 +411 -380
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
- package/typings/index.d.ts +147 -143
package/dist/index.js
CHANGED
|
@@ -563,7 +563,7 @@ class DifficultyHitObjectCreator {
|
|
|
563
563
|
}
|
|
564
564
|
|
|
565
565
|
/**
|
|
566
|
-
* The base of difficulty
|
|
566
|
+
* The base of a difficulty calculator.
|
|
567
567
|
*/
|
|
568
568
|
class DifficultyCalculator {
|
|
569
569
|
/**
|
|
@@ -626,8 +626,8 @@ class DifficultyCalculator {
|
|
|
626
626
|
od: this.beatmap.difficulty.od,
|
|
627
627
|
hp: this.beatmap.difficulty.hp,
|
|
628
628
|
mods: options?.mods,
|
|
629
|
-
speedMultiplier: options?.stats?.speedMultiplier,
|
|
630
|
-
oldStatistics: options?.stats?.oldStatistics,
|
|
629
|
+
speedMultiplier: options?.stats?.speedMultiplier ?? 1,
|
|
630
|
+
oldStatistics: options?.stats?.oldStatistics ?? false,
|
|
631
631
|
}).calculate({ mode: this.mode });
|
|
632
632
|
this.preProcess();
|
|
633
633
|
this.populateDifficultyAttributes();
|
|
@@ -661,15 +661,11 @@ class DifficultyCalculator {
|
|
|
661
661
|
*/
|
|
662
662
|
calculateSkills(...skills) {
|
|
663
663
|
// The first object doesn't generate a strain, so we begin calculating from the second object.
|
|
664
|
-
this.objects.slice(1)
|
|
665
|
-
|
|
666
|
-
skill.process(
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
skill.saveCurrentPeak();
|
|
670
|
-
}
|
|
671
|
-
});
|
|
672
|
-
});
|
|
664
|
+
for (const object of this.objects.slice(1)) {
|
|
665
|
+
for (const skill of skills) {
|
|
666
|
+
skill.process(object);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
673
669
|
}
|
|
674
670
|
/**
|
|
675
671
|
* Populates the stored difficulty attributes with necessary data.
|
|
@@ -709,12 +705,9 @@ class DroidAimEvaluator extends AimEvaluator {
|
|
|
709
705
|
static wideAngleMultiplier = 1.65;
|
|
710
706
|
static sliderMultiplier = 1.5;
|
|
711
707
|
static velocityChangeMultiplier = 0.85;
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
static SINGLE_SPACING_THRESHOLD = 175;
|
|
716
|
-
// ~200 1/2 BPM jumps
|
|
717
|
-
static minSpeedBonus = 150;
|
|
708
|
+
static singleSpacingThreshold = 100;
|
|
709
|
+
// 200 1/4 BPM delta time
|
|
710
|
+
static minSpeedBonus = 75;
|
|
718
711
|
/**
|
|
719
712
|
* Evaluates the difficulty of aiming the current object, based on:
|
|
720
713
|
*
|
|
@@ -732,13 +725,13 @@ class DroidAimEvaluator extends AimEvaluator {
|
|
|
732
725
|
current.isOverlapping(true)) {
|
|
733
726
|
return 0;
|
|
734
727
|
}
|
|
735
|
-
return (this.
|
|
736
|
-
this.
|
|
728
|
+
return (this.snapAimStrainOf(current, withSliders) +
|
|
729
|
+
this.flowAimStrainOf(current));
|
|
737
730
|
}
|
|
738
731
|
/**
|
|
739
|
-
* Calculates the aim strain of a hitobject.
|
|
732
|
+
* Calculates the snap aim strain of a hitobject.
|
|
740
733
|
*/
|
|
741
|
-
static
|
|
734
|
+
static snapAimStrainOf(current, withSliders) {
|
|
742
735
|
if (current.index <= 1 ||
|
|
743
736
|
current.previous(0)?.object instanceof osuBase.Spinner) {
|
|
744
737
|
return 0;
|
|
@@ -848,21 +841,18 @@ class DroidAimEvaluator extends AimEvaluator {
|
|
|
848
841
|
return strain;
|
|
849
842
|
}
|
|
850
843
|
/**
|
|
851
|
-
* Calculates the
|
|
844
|
+
* Calculates the flow aim strain of a hitobject.
|
|
852
845
|
*/
|
|
853
|
-
static
|
|
846
|
+
static flowAimStrainOf(current) {
|
|
854
847
|
let speedBonus = 1;
|
|
855
848
|
if (current.strainTime < this.minSpeedBonus) {
|
|
856
849
|
speedBonus +=
|
|
857
850
|
0.75 *
|
|
858
|
-
Math.pow((this.minSpeedBonus - current.strainTime) /
|
|
851
|
+
Math.pow((this.minSpeedBonus - current.strainTime) / 40, 2);
|
|
859
852
|
}
|
|
860
853
|
const travelDistance = current.previous(0)?.travelDistance ?? 0;
|
|
861
|
-
const
|
|
862
|
-
return (
|
|
863
|
-
speedBonus *
|
|
864
|
-
Math.pow(distance / this.SINGLE_SPACING_THRESHOLD, 5)) /
|
|
865
|
-
current.strainTime);
|
|
854
|
+
const shortDistancePenalty = Math.pow(Math.min(this.singleSpacingThreshold, travelDistance + current.minimumJumpDistance) / this.singleSpacingThreshold, 3.5);
|
|
855
|
+
return (200 * speedBonus * shortDistancePenalty) / current.strainTime;
|
|
866
856
|
}
|
|
867
857
|
}
|
|
868
858
|
|
|
@@ -886,25 +876,14 @@ class Skill {
|
|
|
886
876
|
* and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
|
|
887
877
|
*/
|
|
888
878
|
class StrainSkill extends Skill {
|
|
889
|
-
/**
|
|
890
|
-
* The strain of currently calculated hitobject.
|
|
891
|
-
*/
|
|
892
|
-
currentStrain = 0;
|
|
893
|
-
/**
|
|
894
|
-
* The current section's strain peak.
|
|
895
|
-
*/
|
|
896
|
-
currentSectionPeak = 0;
|
|
897
879
|
/**
|
|
898
880
|
* Strain peaks are stored here.
|
|
899
881
|
*/
|
|
900
882
|
strainPeaks = [];
|
|
901
883
|
sectionLength = 400;
|
|
884
|
+
currentStrain = 0;
|
|
885
|
+
currentSectionPeak = 0;
|
|
902
886
|
currentSectionEnd = 0;
|
|
903
|
-
/**
|
|
904
|
-
* Calculates the strain value of a hitobject and stores the value in it. This value is affected by previously processed objects.
|
|
905
|
-
*
|
|
906
|
-
* @param current The hitobject to process.
|
|
907
|
-
*/
|
|
908
887
|
process(current) {
|
|
909
888
|
// The first object doesn't generate a strain, so we begin with an incremented section end
|
|
910
889
|
if (current.index === 0) {
|
|
@@ -921,6 +900,10 @@ class StrainSkill extends Skill {
|
|
|
921
900
|
this.currentStrain = this.strainValueAt(current);
|
|
922
901
|
this.saveToHitObject(current);
|
|
923
902
|
this.currentSectionPeak = Math.max(this.currentStrain, this.currentSectionPeak);
|
|
903
|
+
if (!current.next(0)) {
|
|
904
|
+
// Don't forget to save the last strain peak, which would otherwise be ignored.
|
|
905
|
+
this.saveCurrentPeak();
|
|
906
|
+
}
|
|
924
907
|
}
|
|
925
908
|
/**
|
|
926
909
|
* Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty.
|
|
@@ -939,15 +922,13 @@ class StrainSkill extends Skill {
|
|
|
939
922
|
/**
|
|
940
923
|
* Sets the initial strain level for a new section.
|
|
941
924
|
*
|
|
942
|
-
* @param
|
|
925
|
+
* @param time The beginning of the new section in milliseconds.
|
|
943
926
|
* @param current The current hitobject.
|
|
944
927
|
*/
|
|
945
|
-
startNewSectionFrom(
|
|
946
|
-
// The maximum strain of the new section is not zero by default
|
|
928
|
+
startNewSectionFrom(time, current) {
|
|
929
|
+
// The maximum strain of the new section is not zero by default.
|
|
947
930
|
// This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level.
|
|
948
|
-
this.currentSectionPeak =
|
|
949
|
-
this.currentStrain *
|
|
950
|
-
this.strainDecay(offset - current.previous(0).startTime);
|
|
931
|
+
this.currentSectionPeak = this.calculateInitialStrain(time, current);
|
|
951
932
|
}
|
|
952
933
|
}
|
|
953
934
|
|
|
@@ -981,35 +962,37 @@ class DroidSkill extends StrainSkill {
|
|
|
981
962
|
* Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
|
982
963
|
*/
|
|
983
964
|
class DroidAim extends DroidSkill {
|
|
984
|
-
skillMultiplier = 24.55;
|
|
985
965
|
strainDecayBase = 0.15;
|
|
986
966
|
reducedSectionCount = 10;
|
|
987
967
|
reducedSectionBaseline = 0.75;
|
|
988
968
|
starsPerDouble = 1.05;
|
|
969
|
+
skillMultiplier = 24.55;
|
|
989
970
|
withSliders;
|
|
971
|
+
currentAimStrain = 0;
|
|
990
972
|
constructor(mods, withSliders) {
|
|
991
973
|
super(mods);
|
|
992
974
|
this.withSliders = withSliders;
|
|
993
975
|
}
|
|
994
|
-
/**
|
|
995
|
-
* @param current The hitobject to calculate.
|
|
996
|
-
*/
|
|
997
976
|
strainValueAt(current) {
|
|
998
|
-
this.
|
|
999
|
-
this.
|
|
977
|
+
this.currentAimStrain *= this.strainDecay(current.deltaTime);
|
|
978
|
+
this.currentAimStrain +=
|
|
1000
979
|
DroidAimEvaluator.evaluateDifficultyOf(current, this.withSliders) *
|
|
1001
980
|
this.skillMultiplier;
|
|
1002
|
-
return this.
|
|
981
|
+
return this.currentAimStrain;
|
|
982
|
+
}
|
|
983
|
+
calculateInitialStrain(time, current) {
|
|
984
|
+
return (this.currentAimStrain *
|
|
985
|
+
this.strainDecay(time - (current.previous(0)?.startTime ?? 0)));
|
|
1003
986
|
}
|
|
1004
987
|
/**
|
|
1005
988
|
* @param current The hitobject to save to.
|
|
1006
989
|
*/
|
|
1007
990
|
saveToHitObject(current) {
|
|
1008
991
|
if (this.withSliders) {
|
|
1009
|
-
current.aimStrainWithSliders = this.
|
|
992
|
+
current.aimStrainWithSliders = this.currentAimStrain;
|
|
1010
993
|
}
|
|
1011
994
|
else {
|
|
1012
|
-
current.aimStrainWithoutSliders = this.
|
|
995
|
+
current.aimStrainWithoutSliders = this.currentAimStrain;
|
|
1013
996
|
}
|
|
1014
997
|
}
|
|
1015
998
|
}
|
|
@@ -1047,10 +1030,10 @@ class DroidTapEvaluator extends SpeedEvaluator {
|
|
|
1047
1030
|
}
|
|
1048
1031
|
let doubletapness = 1;
|
|
1049
1032
|
if (considerCheesability) {
|
|
1050
|
-
const greatWindowFull = greatWindow * 2;
|
|
1051
1033
|
// Nerf doubletappable doubles.
|
|
1052
1034
|
const next = current.next(0);
|
|
1053
1035
|
if (next) {
|
|
1036
|
+
const greatWindowFull = greatWindow * 2;
|
|
1054
1037
|
const currentDeltaTime = Math.max(1, current.deltaTime);
|
|
1055
1038
|
const nextDeltaTime = Math.max(1, next.deltaTime);
|
|
1056
1039
|
const deltaDifference = Math.abs(nextDeltaTime - currentDeltaTime);
|
|
@@ -1074,38 +1057,44 @@ class DroidTapEvaluator extends SpeedEvaluator {
|
|
|
1074
1057
|
* Represents the skill required to press keys or tap with regards to keeping up with the speed at which objects need to be hit.
|
|
1075
1058
|
*/
|
|
1076
1059
|
class DroidTap extends DroidSkill {
|
|
1077
|
-
skillMultiplier = 1375;
|
|
1078
1060
|
reducedSectionCount = 10;
|
|
1079
1061
|
reducedSectionBaseline = 0.75;
|
|
1080
1062
|
strainDecayBase = 0.3;
|
|
1081
1063
|
starsPerDouble = 1.1;
|
|
1082
1064
|
currentTapStrain = 0;
|
|
1083
|
-
|
|
1065
|
+
currentRhythmMultiplier = 0;
|
|
1066
|
+
skillMultiplier = 1375;
|
|
1084
1067
|
greatWindow;
|
|
1085
|
-
|
|
1068
|
+
considerCheesability;
|
|
1069
|
+
constructor(mods, overallDifficulty, considerCheesability) {
|
|
1086
1070
|
super(mods);
|
|
1087
1071
|
this.greatWindow = new osuBase.OsuHitWindow(overallDifficulty).hitWindowFor300();
|
|
1072
|
+
this.considerCheesability = considerCheesability;
|
|
1088
1073
|
}
|
|
1089
|
-
/**
|
|
1090
|
-
* @param current The hitobject to calculate.
|
|
1091
|
-
*/
|
|
1092
1074
|
strainValueAt(current) {
|
|
1093
1075
|
const decay = this.strainDecay(current.strainTime);
|
|
1094
1076
|
this.currentTapStrain *= decay;
|
|
1095
1077
|
this.currentTapStrain +=
|
|
1096
|
-
DroidTapEvaluator.evaluateDifficultyOf(current, this.greatWindow,
|
|
1097
|
-
this.
|
|
1098
|
-
this.currentOriginalTapStrain +=
|
|
1099
|
-
DroidTapEvaluator.evaluateDifficultyOf(current, this.greatWindow, false) * this.skillMultiplier;
|
|
1100
|
-
this.currentOriginalTapStrain *= current.rhythmMultiplier;
|
|
1078
|
+
DroidTapEvaluator.evaluateDifficultyOf(current, this.greatWindow, this.considerCheesability) * this.skillMultiplier;
|
|
1079
|
+
this.currentRhythmMultiplier = current.rhythmMultiplier;
|
|
1101
1080
|
return this.currentTapStrain * current.rhythmMultiplier;
|
|
1102
1081
|
}
|
|
1082
|
+
calculateInitialStrain(time, current) {
|
|
1083
|
+
return (this.currentTapStrain *
|
|
1084
|
+
this.currentRhythmMultiplier *
|
|
1085
|
+
this.strainDecay(time - (current.previous(0)?.startTime ?? 0)));
|
|
1086
|
+
}
|
|
1103
1087
|
/**
|
|
1104
1088
|
* @param current The hitobject to save to.
|
|
1105
1089
|
*/
|
|
1106
1090
|
saveToHitObject(current) {
|
|
1107
|
-
|
|
1108
|
-
|
|
1091
|
+
const strain = this.currentTapStrain * this.currentRhythmMultiplier;
|
|
1092
|
+
if (this.considerCheesability) {
|
|
1093
|
+
current.tapStrain = strain;
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
current.originalTapStrain = strain;
|
|
1097
|
+
}
|
|
1109
1098
|
}
|
|
1110
1099
|
}
|
|
1111
1100
|
|
|
@@ -1213,33 +1202,36 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
1213
1202
|
* Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
|
|
1214
1203
|
*/
|
|
1215
1204
|
class DroidFlashlight extends DroidSkill {
|
|
1216
|
-
skillMultiplier = 0.052;
|
|
1217
1205
|
strainDecayBase = 0.15;
|
|
1218
1206
|
reducedSectionCount = 0;
|
|
1219
1207
|
reducedSectionBaseline = 1;
|
|
1220
1208
|
starsPerDouble = 1.06;
|
|
1209
|
+
skillMultiplier = 0.052;
|
|
1221
1210
|
isHidden;
|
|
1222
1211
|
withSliders;
|
|
1212
|
+
currentFlashlightStrain = 0;
|
|
1223
1213
|
constructor(mods, withSliders) {
|
|
1224
1214
|
super(mods);
|
|
1225
1215
|
this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
|
|
1226
1216
|
this.withSliders = withSliders;
|
|
1227
1217
|
}
|
|
1228
|
-
/**
|
|
1229
|
-
* @param current The hitobject to calculate.
|
|
1230
|
-
*/
|
|
1231
1218
|
strainValueAt(current) {
|
|
1232
|
-
this.
|
|
1233
|
-
this.
|
|
1219
|
+
this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
|
|
1220
|
+
this.currentFlashlightStrain +=
|
|
1234
1221
|
DroidFlashlightEvaluator.evaluateDifficultyOf(current, this.isHidden, this.withSliders) * this.skillMultiplier;
|
|
1235
|
-
return this.
|
|
1222
|
+
return this.currentFlashlightStrain;
|
|
1223
|
+
}
|
|
1224
|
+
calculateInitialStrain(time, current) {
|
|
1225
|
+
return (this.currentFlashlightStrain *
|
|
1226
|
+
this.strainDecay(time - (current.previous(0)?.startTime ?? 0)));
|
|
1236
1227
|
}
|
|
1237
1228
|
saveToHitObject(current) {
|
|
1238
1229
|
if (this.withSliders) {
|
|
1239
|
-
current.flashlightStrainWithSliders = this.
|
|
1230
|
+
current.flashlightStrainWithSliders = this.currentFlashlightStrain;
|
|
1240
1231
|
}
|
|
1241
1232
|
else {
|
|
1242
|
-
current.flashlightStrainWithoutSliders =
|
|
1233
|
+
current.flashlightStrainWithoutSliders =
|
|
1234
|
+
this.currentFlashlightStrain;
|
|
1243
1235
|
}
|
|
1244
1236
|
}
|
|
1245
1237
|
difficultyValue() {
|
|
@@ -1313,8 +1305,8 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
|
|
|
1313
1305
|
Math.min(0.5, Math.pow(Math.sin(Math.PI /
|
|
1314
1306
|
(Math.min(prevDelta, currentDelta) /
|
|
1315
1307
|
Math.max(prevDelta, currentDelta))), 2));
|
|
1316
|
-
const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) - greatWindow * 0.
|
|
1317
|
-
(greatWindow * 0.
|
|
1308
|
+
const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) - greatWindow * 0.6) /
|
|
1309
|
+
(greatWindow * 0.6));
|
|
1318
1310
|
let effectiveRatio = windowPenalty * currentRatio;
|
|
1319
1311
|
if (firstDeltaSwitch) {
|
|
1320
1312
|
if (prevDelta <= 1.25 * currentDelta &&
|
|
@@ -1391,26 +1383,31 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
|
|
|
1391
1383
|
* Represents the skill required to properly follow a beatmap's rhythm.
|
|
1392
1384
|
*/
|
|
1393
1385
|
class DroidRhythm extends DroidSkill {
|
|
1394
|
-
skillMultiplier = 1;
|
|
1395
1386
|
reducedSectionCount = 5;
|
|
1396
1387
|
reducedSectionBaseline = 0.75;
|
|
1397
1388
|
strainDecayBase = 0.3;
|
|
1398
1389
|
starsPerDouble = 1.75;
|
|
1399
|
-
|
|
1390
|
+
currentRhythmStrain = 0;
|
|
1391
|
+
currentRhythmMultiplier = 1;
|
|
1400
1392
|
hitWindow;
|
|
1401
1393
|
constructor(mods, overallDifficulty) {
|
|
1402
1394
|
super(mods);
|
|
1403
1395
|
this.hitWindow = new osuBase.OsuHitWindow(overallDifficulty);
|
|
1404
1396
|
}
|
|
1405
1397
|
strainValueAt(current) {
|
|
1406
|
-
this.
|
|
1407
|
-
|
|
1408
|
-
this.
|
|
1409
|
-
|
|
1398
|
+
this.currentRhythmMultiplier =
|
|
1399
|
+
DroidRhythmEvaluator.evaluateDifficultyOf(current, this.hitWindow.hitWindowFor300());
|
|
1400
|
+
this.currentRhythmStrain *= this.strainDecay(current.deltaTime);
|
|
1401
|
+
this.currentRhythmStrain += this.currentRhythmMultiplier - 1;
|
|
1402
|
+
return this.currentRhythmStrain;
|
|
1403
|
+
}
|
|
1404
|
+
calculateInitialStrain(time, current) {
|
|
1405
|
+
return (this.currentRhythmStrain *
|
|
1406
|
+
this.strainDecay(time - (current.previous(0)?.startTime ?? 0)));
|
|
1410
1407
|
}
|
|
1411
1408
|
saveToHitObject(current) {
|
|
1412
|
-
current.rhythmStrain = this.
|
|
1413
|
-
current.rhythmMultiplier = this.
|
|
1409
|
+
current.rhythmStrain = this.currentRhythmStrain;
|
|
1410
|
+
current.rhythmMultiplier = this.currentRhythmMultiplier;
|
|
1414
1411
|
}
|
|
1415
1412
|
}
|
|
1416
1413
|
|
|
@@ -1475,12 +1472,14 @@ class DroidVisualEvaluator {
|
|
|
1475
1472
|
}
|
|
1476
1473
|
if (current.object instanceof osuBase.Slider && withSliders) {
|
|
1477
1474
|
const scalingFactor = 50 / current.object.getRadius(osuBase.Modes.droid);
|
|
1478
|
-
//
|
|
1475
|
+
// Invert the scaling factor to determine the true travel distance independent of circle size.
|
|
1476
|
+
const pixelTravelDistance = current.object.lazyTravelDistance / scalingFactor;
|
|
1477
|
+
const currentVelocity = pixelTravelDistance / current.travelTime;
|
|
1479
1478
|
strain +=
|
|
1480
|
-
//
|
|
1481
|
-
Math.min(6,
|
|
1482
|
-
//
|
|
1483
|
-
|
|
1479
|
+
// Reward sliders based on velocity, while also avoiding overbuffing extremely fast sliders.
|
|
1480
|
+
Math.min(6, currentVelocity * 1.5) *
|
|
1481
|
+
// Longer sliders require more reading.
|
|
1482
|
+
(pixelTravelDistance / 100);
|
|
1484
1483
|
let cumulativeStrainTime = 0;
|
|
1485
1484
|
// Reward for velocity changes based on last few sliders.
|
|
1486
1485
|
for (let i = 0; i < Math.min(current.index, 4); ++i) {
|
|
@@ -1491,12 +1490,16 @@ class DroidVisualEvaluator {
|
|
|
1491
1490
|
last.isOverlapping(true)) {
|
|
1492
1491
|
continue;
|
|
1493
1492
|
}
|
|
1493
|
+
// Invert the scaling factor to determine the true travel distance independent of circle size.
|
|
1494
|
+
const pixelTravelDistance = last.object.lazyTravelDistance / scalingFactor;
|
|
1495
|
+
const lastVelocity = pixelTravelDistance / last.travelTime;
|
|
1494
1496
|
strain +=
|
|
1495
|
-
//
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1497
|
+
// Reward past sliders based on velocity changes, while also
|
|
1498
|
+
// avoiding overbuffing extremely fast velocity changes.
|
|
1499
|
+
Math.min(10, 2.5 * Math.abs(currentVelocity - lastVelocity)) *
|
|
1500
|
+
// Longer sliders require more reading.
|
|
1501
|
+
(pixelTravelDistance / 125) *
|
|
1502
|
+
// Avoid overbuffing past sliders.
|
|
1500
1503
|
Math.min(1, 300 / cumulativeStrainTime);
|
|
1501
1504
|
}
|
|
1502
1505
|
}
|
|
@@ -1511,27 +1514,36 @@ class DroidVisual extends DroidSkill {
|
|
|
1511
1514
|
starsPerDouble = 1.025;
|
|
1512
1515
|
reducedSectionCount = 10;
|
|
1513
1516
|
reducedSectionBaseline = 0.75;
|
|
1514
|
-
skillMultiplier = 10;
|
|
1515
1517
|
strainDecayBase = 0.1;
|
|
1516
1518
|
isHidden;
|
|
1517
|
-
|
|
1519
|
+
withSliders;
|
|
1520
|
+
currentVisualStrain = 0;
|
|
1521
|
+
currentRhythmMultiplier = 1;
|
|
1522
|
+
skillMultiplier = 10;
|
|
1518
1523
|
constructor(mods, withSliders) {
|
|
1519
1524
|
super(mods);
|
|
1520
1525
|
this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
|
|
1521
|
-
this.
|
|
1526
|
+
this.withSliders = withSliders;
|
|
1522
1527
|
}
|
|
1523
1528
|
strainValueAt(current) {
|
|
1524
|
-
this.
|
|
1525
|
-
this.
|
|
1526
|
-
DroidVisualEvaluator.evaluateDifficultyOf(current, this.isHidden, this.
|
|
1527
|
-
|
|
1529
|
+
this.currentVisualStrain *= this.strainDecay(current.deltaTime);
|
|
1530
|
+
this.currentVisualStrain +=
|
|
1531
|
+
DroidVisualEvaluator.evaluateDifficultyOf(current, this.isHidden, this.withSliders) * this.skillMultiplier;
|
|
1532
|
+
this.currentRhythmMultiplier = current.rhythmMultiplier;
|
|
1533
|
+
return this.currentVisualStrain * this.currentRhythmMultiplier;
|
|
1534
|
+
}
|
|
1535
|
+
calculateInitialStrain(time, current) {
|
|
1536
|
+
return (this.currentVisualStrain *
|
|
1537
|
+
this.currentRhythmMultiplier *
|
|
1538
|
+
this.strainDecay(time - (current.previous(0)?.startTime ?? 0)));
|
|
1528
1539
|
}
|
|
1529
1540
|
saveToHitObject(current) {
|
|
1530
|
-
|
|
1531
|
-
|
|
1541
|
+
const strain = this.currentVisualStrain * this.currentRhythmMultiplier;
|
|
1542
|
+
if (this.withSliders) {
|
|
1543
|
+
current.visualStrainWithSliders = strain;
|
|
1532
1544
|
}
|
|
1533
1545
|
else {
|
|
1534
|
-
current.visualStrainWithoutSliders =
|
|
1546
|
+
current.visualStrainWithoutSliders = strain;
|
|
1535
1547
|
}
|
|
1536
1548
|
}
|
|
1537
1549
|
}
|
|
@@ -1609,25 +1621,16 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1609
1621
|
* Calculates the tap star rating of the beatmap and stores it in this instance.
|
|
1610
1622
|
*/
|
|
1611
1623
|
calculateTap() {
|
|
1612
|
-
const
|
|
1613
|
-
this.
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
}
|
|
1618
|
-
else {
|
|
1619
|
-
this.postCalculateTap(tapSkill);
|
|
1620
|
-
}
|
|
1621
|
-
this.calculateSpeedAttributes();
|
|
1624
|
+
const od = this.stats.od;
|
|
1625
|
+
const tapSkillCheese = new DroidTap(this.mods, od, true);
|
|
1626
|
+
const tapSkillNoCheese = new DroidTap(this.mods, od, false);
|
|
1627
|
+
this.calculateSkills(tapSkillCheese, tapSkillNoCheese);
|
|
1628
|
+
this.postCalculateTap(tapSkillCheese);
|
|
1622
1629
|
}
|
|
1623
1630
|
/**
|
|
1624
1631
|
* Calculates the rhythm star rating of the beatmap and stores it in this instance.
|
|
1625
1632
|
*/
|
|
1626
1633
|
calculateRhythm() {
|
|
1627
|
-
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
1628
|
-
this.rhythm = this.attributes.rhythmDifficulty = 0;
|
|
1629
|
-
return;
|
|
1630
|
-
}
|
|
1631
1634
|
const rhythmSkill = new DroidRhythm(this.mods, this.stats.od);
|
|
1632
1635
|
this.calculateSkills(rhythmSkill);
|
|
1633
1636
|
this.postCalculateRhythm(rhythmSkill);
|
|
@@ -1679,39 +1682,21 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1679
1682
|
}
|
|
1680
1683
|
calculateAll() {
|
|
1681
1684
|
const skills = this.createSkills();
|
|
1682
|
-
const isRelax = this.mods.some((m) => m instanceof osuBase.ModRelax);
|
|
1683
|
-
if (isRelax) {
|
|
1684
|
-
// Remove visual skills to reduce overhead.
|
|
1685
|
-
skills.pop();
|
|
1686
|
-
skills.pop();
|
|
1687
|
-
}
|
|
1688
1685
|
this.calculateSkills(...skills);
|
|
1689
1686
|
const aimSkill = skills[0];
|
|
1690
1687
|
const aimSkillWithoutSliders = skills[1];
|
|
1691
1688
|
const rhythmSkill = skills[2];
|
|
1692
|
-
const
|
|
1693
|
-
const flashlightSkill = skills[
|
|
1694
|
-
const flashlightSkillWithoutSliders = skills[
|
|
1695
|
-
const visualSkill = skills[
|
|
1696
|
-
const visualSkillWithoutSliders = skills[
|
|
1689
|
+
const tapSkillCheese = skills[3];
|
|
1690
|
+
const flashlightSkill = skills[5];
|
|
1691
|
+
const flashlightSkillWithoutSliders = skills[6];
|
|
1692
|
+
const visualSkill = skills[7];
|
|
1693
|
+
const visualSkillWithoutSliders = skills[8];
|
|
1697
1694
|
this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
else {
|
|
1702
|
-
this.postCalculateTap(tapSkill);
|
|
1703
|
-
}
|
|
1704
|
-
this.calculateSpeedAttributes();
|
|
1705
|
-
if (!isRelax) {
|
|
1706
|
-
this.postCalculateRhythm(rhythmSkill);
|
|
1707
|
-
}
|
|
1695
|
+
this.postCalculateTap(tapSkillCheese);
|
|
1696
|
+
this.calculateTapAttributes();
|
|
1697
|
+
this.postCalculateRhythm(rhythmSkill);
|
|
1708
1698
|
this.postCalculateFlashlight(flashlightSkill, flashlightSkillWithoutSliders);
|
|
1709
|
-
|
|
1710
|
-
this.postCalculateVisual(visualSkill, visualSkillWithoutSliders);
|
|
1711
|
-
}
|
|
1712
|
-
else {
|
|
1713
|
-
this.visual = this.attributes.visualDifficulty = 0;
|
|
1714
|
-
}
|
|
1699
|
+
this.postCalculateVisual(visualSkill, visualSkillWithoutSliders);
|
|
1715
1700
|
this.calculateTotal();
|
|
1716
1701
|
}
|
|
1717
1702
|
toString() {
|
|
@@ -1736,12 +1721,14 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1736
1721
|
osuBase.HitObjectStackEvaluator.applyDroidStacking(this.beatmap.hitObjects.objects, this.beatmap.general.stackLeniency);
|
|
1737
1722
|
}
|
|
1738
1723
|
createSkills() {
|
|
1724
|
+
const od = this.stats.od;
|
|
1739
1725
|
return [
|
|
1740
1726
|
new DroidAim(this.mods, true),
|
|
1741
1727
|
new DroidAim(this.mods, false),
|
|
1742
1728
|
// Tap skill depends on rhythm skill, so we put it first
|
|
1743
|
-
new DroidRhythm(this.mods,
|
|
1744
|
-
new DroidTap(this.mods,
|
|
1729
|
+
new DroidRhythm(this.mods, od),
|
|
1730
|
+
new DroidTap(this.mods, od, true),
|
|
1731
|
+
new DroidTap(this.mods, od, false),
|
|
1745
1732
|
new DroidFlashlight(this.mods, true),
|
|
1746
1733
|
new DroidFlashlight(this.mods, false),
|
|
1747
1734
|
new DroidVisual(this.mods, true),
|
|
@@ -1813,16 +1800,23 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1813
1800
|
/**
|
|
1814
1801
|
* Called after tap skill calculation.
|
|
1815
1802
|
*
|
|
1816
|
-
* @param
|
|
1803
|
+
* @param tapSkillCheese The tap skill that considers cheesing.
|
|
1817
1804
|
*/
|
|
1818
|
-
postCalculateTap(
|
|
1819
|
-
this.strainPeaks.speed =
|
|
1820
|
-
|
|
1805
|
+
postCalculateTap(tapSkillCheese) {
|
|
1806
|
+
this.strainPeaks.speed = tapSkillCheese.strainPeaks;
|
|
1807
|
+
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
1808
|
+
this.tap = this.attributes.tapDifficulty = 0;
|
|
1809
|
+
this.attributes.possibleThreeFingeredSections = [];
|
|
1810
|
+
}
|
|
1811
|
+
else {
|
|
1812
|
+
this.tap = this.attributes.tapDifficulty = this.starValue(tapSkillCheese.difficultyValue());
|
|
1813
|
+
}
|
|
1814
|
+
this.calculateTapAttributes();
|
|
1821
1815
|
}
|
|
1822
1816
|
/**
|
|
1823
|
-
* Calculates
|
|
1817
|
+
* Calculates tap-related attributes.
|
|
1824
1818
|
*/
|
|
1825
|
-
|
|
1819
|
+
calculateTapAttributes() {
|
|
1826
1820
|
this.attributes.possibleThreeFingeredSections = [];
|
|
1827
1821
|
const tempSections = [];
|
|
1828
1822
|
const objectStrains = [];
|
|
@@ -2019,7 +2013,7 @@ class PerformanceCalculator {
|
|
|
2019
2013
|
calculate(options) {
|
|
2020
2014
|
this.handleOptions(options);
|
|
2021
2015
|
this.calculateValues();
|
|
2022
|
-
this.calculateTotalValue();
|
|
2016
|
+
this.total = this.calculateTotalValue();
|
|
2023
2017
|
return this;
|
|
2024
2018
|
}
|
|
2025
2019
|
/**
|
|
@@ -2233,9 +2227,9 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2233
2227
|
if (value === this._tapPenalty) {
|
|
2234
2228
|
return;
|
|
2235
2229
|
}
|
|
2236
|
-
this.tap *= this._tapPenalty / value;
|
|
2237
2230
|
this._tapPenalty = value;
|
|
2238
|
-
this.
|
|
2231
|
+
this.tap = this.calculateTapValue();
|
|
2232
|
+
this.total = this.calculateTotalValue();
|
|
2239
2233
|
}
|
|
2240
2234
|
/**
|
|
2241
2235
|
* Applies an aim slider cheese penalty value to this calculator.
|
|
@@ -2255,8 +2249,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2255
2249
|
return;
|
|
2256
2250
|
}
|
|
2257
2251
|
this._aimSliderCheesePenalty = value;
|
|
2258
|
-
this.calculateAimValue();
|
|
2259
|
-
this.calculateTotalValue();
|
|
2252
|
+
this.aim = this.calculateAimValue();
|
|
2253
|
+
this.total = this.calculateTotalValue();
|
|
2260
2254
|
}
|
|
2261
2255
|
/**
|
|
2262
2256
|
* Applies a flashlight slider cheese penalty value to this calculator.
|
|
@@ -2276,8 +2270,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2276
2270
|
return;
|
|
2277
2271
|
}
|
|
2278
2272
|
this._flashlightSliderCheesePenalty = value;
|
|
2279
|
-
this.calculateFlashlightValue();
|
|
2280
|
-
this.calculateTotalValue();
|
|
2273
|
+
this.flashlight = this.calculateFlashlightValue();
|
|
2274
|
+
this.total = this.calculateTotalValue();
|
|
2281
2275
|
}
|
|
2282
2276
|
/**
|
|
2283
2277
|
* Applies a visual slider cheese penalty value to this calculator.
|
|
@@ -2297,25 +2291,24 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2297
2291
|
return;
|
|
2298
2292
|
}
|
|
2299
2293
|
this._visualSliderCheesePenalty = value;
|
|
2300
|
-
this.calculateVisualValue();
|
|
2301
|
-
this.calculateTotalValue();
|
|
2294
|
+
this.visual = this.calculateVisualValue();
|
|
2295
|
+
this.total = this.calculateTotalValue();
|
|
2302
2296
|
}
|
|
2303
2297
|
calculateValues() {
|
|
2304
2298
|
this._deviation = this.calculateDeviation();
|
|
2305
2299
|
this._tapDeviation = this.calculateTapDeviation();
|
|
2306
|
-
this.calculateAimValue();
|
|
2307
|
-
this.calculateTapValue();
|
|
2308
|
-
this.calculateAccuracyValue();
|
|
2309
|
-
this.calculateFlashlightValue();
|
|
2310
|
-
this.calculateVisualValue();
|
|
2300
|
+
this.aim = this.calculateAimValue();
|
|
2301
|
+
this.tap = this.calculateTapValue();
|
|
2302
|
+
this.accuracy = this.calculateAccuracyValue();
|
|
2303
|
+
this.flashlight = this.calculateFlashlightValue();
|
|
2304
|
+
this.visual = this.calculateVisualValue();
|
|
2311
2305
|
}
|
|
2312
2306
|
calculateTotalValue() {
|
|
2313
|
-
this.
|
|
2314
|
-
Math.pow(
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
Math.pow(this.visual, 1.1), 1 / 1.1) * this.finalMultiplier;
|
|
2307
|
+
return (Math.pow(Math.pow(this.aim, 1.1) +
|
|
2308
|
+
Math.pow(this.tap, 1.1) +
|
|
2309
|
+
Math.pow(this.accuracy, 1.1) +
|
|
2310
|
+
Math.pow(this.flashlight, 1.1) +
|
|
2311
|
+
Math.pow(this.visual, 1.1), 1 / 1.1) * this.finalMultiplier);
|
|
2319
2312
|
}
|
|
2320
2313
|
handleOptions(options) {
|
|
2321
2314
|
this._tapPenalty = options?.tapPenalty ?? 1;
|
|
@@ -2330,23 +2323,31 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2330
2323
|
* Calculates the aim performance value of the beatmap.
|
|
2331
2324
|
*/
|
|
2332
2325
|
calculateAimValue() {
|
|
2333
|
-
|
|
2334
|
-
|
|
2326
|
+
let aimValue = this.baseValue(Math.pow(this.difficultyAttributes.aimDifficulty, 0.8));
|
|
2327
|
+
aimValue *= this.proportionalMissPenalty;
|
|
2328
|
+
// Scale the aim value with estimated full combo deviation.
|
|
2329
|
+
aimValue *= this.calculateDeviationBasedLengthScaling();
|
|
2335
2330
|
// Scale the aim value with slider factor to nerf very likely dropped sliderends.
|
|
2336
|
-
|
|
2331
|
+
aimValue *= this.sliderNerfFactor;
|
|
2337
2332
|
// Scale the aim value with slider cheese penalty.
|
|
2338
|
-
|
|
2333
|
+
aimValue *= this._aimSliderCheesePenalty;
|
|
2339
2334
|
// Scale the aim value with deviation.
|
|
2340
|
-
|
|
2335
|
+
aimValue *=
|
|
2341
2336
|
1.05 *
|
|
2342
|
-
Math.
|
|
2337
|
+
Math.sqrt(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * this._deviation)));
|
|
2338
|
+
// OD 7 SS stays the same.
|
|
2339
|
+
aimValue *= 0.98 + Math.pow(7, 2) / 2500;
|
|
2340
|
+
return aimValue;
|
|
2343
2341
|
}
|
|
2344
2342
|
/**
|
|
2345
2343
|
* Calculates the tap performance value of the beatmap.
|
|
2346
2344
|
*/
|
|
2347
2345
|
calculateTapValue() {
|
|
2348
|
-
|
|
2349
|
-
|
|
2346
|
+
let tapValue = this.baseValue(this.difficultyAttributes.tapDifficulty);
|
|
2347
|
+
tapValue *= this.calculateStrainBasedMissPenalty(this.difficultyAttributes.tapDifficultStrainCount);
|
|
2348
|
+
// Scale the tap value with estimated full combo deviation.
|
|
2349
|
+
// Require more objects to be present as object count can rack up easily in tap-oriented beatmaps.
|
|
2350
|
+
tapValue *= this.calculateDeviationBasedLengthScaling(this.totalHits / 1.45);
|
|
2350
2351
|
// Normalize the deviation to 300 BPM.
|
|
2351
2352
|
const normalizedDeviation = this.tapDeviation *
|
|
2352
2353
|
Math.max(1, 50 / this.difficultyAttributes.averageSpeedDeltaTime);
|
|
@@ -2360,11 +2361,14 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2360
2361
|
Math.exp(-(normalizedDeviation - 7500 / averageBPM) /
|
|
2361
2362
|
((2 * 300) / averageBPM))));
|
|
2362
2363
|
// Scale the tap value with tap deviation.
|
|
2363
|
-
|
|
2364
|
+
tapValue *=
|
|
2364
2365
|
1.1 *
|
|
2365
|
-
Math.pow(osuBase.ErrorFunction.erf(
|
|
2366
|
+
Math.pow(osuBase.ErrorFunction.erf(20 / (Math.SQRT2 * adjustedDeviation)), 0.625);
|
|
2366
2367
|
// Scale the tap value with three-fingered penalty.
|
|
2367
|
-
|
|
2368
|
+
tapValue /= this._tapPenalty;
|
|
2369
|
+
// OD 8 SS stays the same.
|
|
2370
|
+
tapValue *= 0.95 + Math.pow(8, 2) / 750;
|
|
2371
|
+
return tapValue;
|
|
2368
2372
|
}
|
|
2369
2373
|
/**
|
|
2370
2374
|
* Calculates the accuracy performance value of the beatmap.
|
|
@@ -2372,77 +2376,75 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2372
2376
|
calculateAccuracyValue() {
|
|
2373
2377
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax) ||
|
|
2374
2378
|
this.totalSuccessfulHits === 0) {
|
|
2375
|
-
|
|
2376
|
-
return;
|
|
2379
|
+
return 0;
|
|
2377
2380
|
}
|
|
2378
|
-
this.
|
|
2379
|
-
650 *
|
|
2380
|
-
Math.exp(-0.1 * this._deviation) *
|
|
2381
|
-
// The following function is to give higher reward for deviations lower than 25 (250 UR).
|
|
2382
|
-
(15 / (this._deviation + 15) + 0.65);
|
|
2383
|
-
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
|
|
2381
|
+
let accuracyValue = 800 * Math.exp(-0.1 * this._deviation);
|
|
2384
2382
|
const ncircles = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModScoreV2)
|
|
2385
2383
|
? this.totalHits - this.difficultyAttributes.spinnerCount
|
|
2386
2384
|
: this.difficultyAttributes.hitCircleCount;
|
|
2387
|
-
|
|
2385
|
+
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
|
|
2386
|
+
accuracyValue *= Math.min(1.15, Math.sqrt(Math.log(1 + ((Math.E - 1) * ncircles) / 1000)));
|
|
2388
2387
|
// Scale the accuracy value with rhythm complexity.
|
|
2389
|
-
|
|
2388
|
+
accuracyValue *=
|
|
2390
2389
|
1.5 /
|
|
2391
2390
|
(1 +
|
|
2392
2391
|
Math.exp(-(this.difficultyAttributes.rhythmDifficulty - 1) / 2));
|
|
2392
|
+
// Penalize accuracy pp after the first miss.
|
|
2393
|
+
accuracyValue *= Math.pow(0.97, Math.max(0, this.effectiveMissCount - 1));
|
|
2393
2394
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
|
|
2394
|
-
|
|
2395
|
+
accuracyValue *= 1.02;
|
|
2395
2396
|
}
|
|
2397
|
+
return accuracyValue;
|
|
2396
2398
|
}
|
|
2397
2399
|
/**
|
|
2398
2400
|
* Calculates the flashlight performance value of the beatmap.
|
|
2399
2401
|
*/
|
|
2400
2402
|
calculateFlashlightValue() {
|
|
2401
2403
|
if (!this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
|
|
2402
|
-
|
|
2403
|
-
return;
|
|
2404
|
+
return 0;
|
|
2404
2405
|
}
|
|
2405
|
-
this.
|
|
2406
|
-
|
|
2407
|
-
this.flashlight *= this.calculateMissPenalty(this.difficultyAttributes.flashlightDifficultStrainCount);
|
|
2406
|
+
let flashlightValue = Math.pow(this.difficultyAttributes.flashlightDifficulty, 1.6) * 25;
|
|
2407
|
+
flashlightValue *= this.proportionalMissPenalty;
|
|
2408
2408
|
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
2409
|
-
|
|
2409
|
+
flashlightValue *=
|
|
2410
2410
|
0.7 +
|
|
2411
2411
|
0.1 * Math.min(1, this.totalHits / 200) +
|
|
2412
2412
|
(this.totalHits > 200
|
|
2413
2413
|
? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
|
|
2414
2414
|
: 0);
|
|
2415
2415
|
// Scale the flashlight value with slider cheese penalty.
|
|
2416
|
-
|
|
2416
|
+
flashlightValue *= this._flashlightSliderCheesePenalty;
|
|
2417
2417
|
// Scale the flashlight value with deviation.
|
|
2418
|
-
|
|
2418
|
+
flashlightValue *= osuBase.ErrorFunction.erf(50 / (Math.SQRT2 * this._deviation));
|
|
2419
|
+
return flashlightValue;
|
|
2419
2420
|
}
|
|
2420
2421
|
/**
|
|
2421
2422
|
* Calculates the visual performance value of the beatmap.
|
|
2422
2423
|
*/
|
|
2423
2424
|
calculateVisualValue() {
|
|
2424
|
-
this.
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
//
|
|
2428
|
-
|
|
2429
|
-
(0.4845796 - 1.650668) /
|
|
2430
|
-
(1 + Math.pow(this.totalHits / 817.9306, 1.147469)));
|
|
2425
|
+
let visualValue = Math.pow(this.difficultyAttributes.visualDifficulty, 1.6) * 22.5;
|
|
2426
|
+
visualValue *= this.proportionalMissPenalty;
|
|
2427
|
+
// Scale the visual value with estimated full combo deviation.
|
|
2428
|
+
// As visual is easily "bypassable" with memorization, punish for memorization.
|
|
2429
|
+
visualValue *= this.calculateDeviationBasedLengthScaling(undefined, true);
|
|
2431
2430
|
// Scale the visual value with slider cheese penalty.
|
|
2432
|
-
|
|
2431
|
+
visualValue *= this._visualSliderCheesePenalty;
|
|
2433
2432
|
// Scale the visual value with deviation.
|
|
2434
|
-
|
|
2433
|
+
visualValue *=
|
|
2435
2434
|
1.065 *
|
|
2436
|
-
Math.pow(osuBase.ErrorFunction.erf(
|
|
2435
|
+
Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * this._deviation)), 0.8);
|
|
2436
|
+
// OD 5 SS stays the same.
|
|
2437
|
+
visualValue *= 0.98 + Math.pow(5, 2) / 2500;
|
|
2438
|
+
return visualValue;
|
|
2437
2439
|
}
|
|
2438
2440
|
/**
|
|
2439
|
-
* Calculates miss penalty.
|
|
2441
|
+
* Calculates a strain-based miss penalty.
|
|
2440
2442
|
*
|
|
2441
|
-
*
|
|
2443
|
+
* Strain-based miss penalty assumes that a player will miss on the hardest parts of a map,
|
|
2442
2444
|
* so we use the amount of relatively difficult sections to adjust miss penalty
|
|
2443
2445
|
* to make it more punishing on maps with lower amount of hard sections.
|
|
2444
2446
|
*/
|
|
2445
|
-
|
|
2447
|
+
calculateStrainBasedMissPenalty(difficultStrainCount) {
|
|
2446
2448
|
if (this.effectiveMissCount === 0) {
|
|
2447
2449
|
return 1;
|
|
2448
2450
|
}
|
|
@@ -2450,6 +2452,52 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2450
2452
|
(this.effectiveMissCount / (2 * Math.sqrt(difficultStrainCount)) +
|
|
2451
2453
|
1));
|
|
2452
2454
|
}
|
|
2455
|
+
/**
|
|
2456
|
+
* The object-based proportional miss penalty.
|
|
2457
|
+
*/
|
|
2458
|
+
get proportionalMissPenalty() {
|
|
2459
|
+
if (this.effectiveMissCount === 0) {
|
|
2460
|
+
return 1;
|
|
2461
|
+
}
|
|
2462
|
+
const missProportion = (this.totalHits - this.effectiveMissCount) / (this.totalHits + 1);
|
|
2463
|
+
const noMissProportion = this.totalHits / (this.totalHits + 1);
|
|
2464
|
+
return (
|
|
2465
|
+
// Aim deviation-based scale.
|
|
2466
|
+
(osuBase.ErrorFunction.erfInv(missProportion) /
|
|
2467
|
+
osuBase.ErrorFunction.erfInv(noMissProportion)) *
|
|
2468
|
+
// Cheesing-based scale (i.e. 50% misses is deliberately only hitting each other
|
|
2469
|
+
// note, 90% misses is deliberately only hitting 1 note every 10 notes).
|
|
2470
|
+
Math.pow(missProportion, 8));
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Calculates the object-based length scaling based on the deviation of a player for a full
|
|
2474
|
+
* combo in this beatmap, taking retries into account.
|
|
2475
|
+
*
|
|
2476
|
+
* @param objectCount The amount of objects to be considered. Defaults to the amount of
|
|
2477
|
+
* objects in this beatmap.
|
|
2478
|
+
* @param punishForMemorization Whether to punish the deviation for memorization. Defaults to `false`.
|
|
2479
|
+
*/
|
|
2480
|
+
calculateDeviationBasedLengthScaling(objectCount = this.totalHits, punishForMemorization = false) {
|
|
2481
|
+
// Assume a sample proportion of hits for a full combo to be `(n - 0.5) / n` due to
|
|
2482
|
+
// continuity correction, where `n` is the object count.
|
|
2483
|
+
const calculateProportion = (notes) => (notes - 0.5) / notes;
|
|
2484
|
+
// Keeping `x` notes as the benchmark, assume that a player will retry a beatmap
|
|
2485
|
+
// `max(1, x/n)` times relative to an `x`-note beatmap.
|
|
2486
|
+
const benchmarkNotes = 700;
|
|
2487
|
+
// Calculate the proportion equivalent to the bottom half of retry count percentile of
|
|
2488
|
+
// scores and take it as the player's "real" proportion.
|
|
2489
|
+
const retryProportion = (proportion, notes, tries) => proportion +
|
|
2490
|
+
Math.sqrt((2 * proportion * (1 - proportion)) / notes) *
|
|
2491
|
+
osuBase.ErrorFunction.erfInv(1 / tries - 1);
|
|
2492
|
+
// Using the proportion, we calculate the deviation based off that proportion and again
|
|
2493
|
+
// compared to the hit deviation for proportion `(n - 0.5) / n`.
|
|
2494
|
+
let multiplier = Math.max(0, osuBase.ErrorFunction.erfInv(retryProportion(calculateProportion(objectCount), objectCount, Math.max(1, benchmarkNotes / objectCount))) / osuBase.ErrorFunction.erfInv(calculateProportion(benchmarkNotes)) || 0);
|
|
2495
|
+
// Punish for memorization if needed.
|
|
2496
|
+
if (punishForMemorization) {
|
|
2497
|
+
multiplier *= Math.min(1, Math.sqrt(objectCount / benchmarkNotes));
|
|
2498
|
+
}
|
|
2499
|
+
return multiplier;
|
|
2500
|
+
}
|
|
2453
2501
|
/**
|
|
2454
2502
|
* Estimates the player's tap deviation based on the OD, number of circles and sliders,
|
|
2455
2503
|
* and number of 300s, 100s, 50s, and misses, assuming the player's mean hit error is 0.
|
|
@@ -2470,60 +2518,60 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2470
2518
|
}
|
|
2471
2519
|
const hitWindow300 = new osuBase.OsuHitWindow(this.difficultyAttributes.overallDifficulty).hitWindowFor300();
|
|
2472
2520
|
// Obtain the 50 and 100 hit window for droid.
|
|
2473
|
-
const realHitWindow300 = hitWindow300 * this.difficultyAttributes.clockRate;
|
|
2474
|
-
const droidHitWindow = new osuBase.DroidHitWindow(osuBase.OsuHitWindow.hitWindow300ToOD(realHitWindow300));
|
|
2475
2521
|
const isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
2522
|
+
const droidHitWindow = new osuBase.DroidHitWindow(osuBase.DroidHitWindow.hitWindow300ToOD(hitWindow300 * this.difficultyAttributes.clockRate, isPrecise));
|
|
2476
2523
|
const hitWindow50 = droidHitWindow.hitWindowFor50(isPrecise) /
|
|
2477
2524
|
this.difficultyAttributes.clockRate;
|
|
2478
2525
|
const hitWindow100 = droidHitWindow.hitWindowFor100(isPrecise) /
|
|
2479
2526
|
this.difficultyAttributes.clockRate;
|
|
2480
|
-
const {
|
|
2481
|
-
const
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
const
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
if (greatProbabilityCircle === 0 && greatProbabilitySlider === 0) {
|
|
2498
|
-
return Number.POSITIVE_INFINITY;
|
|
2499
|
-
}
|
|
2500
|
-
const calculateDeviation = (mainHitWindow, probability) => {
|
|
2501
|
-
if (probability === 0) {
|
|
2502
|
-
return Number.POSITIVE_INFINITY;
|
|
2503
|
-
}
|
|
2504
|
-
// Start with normal deviation.
|
|
2505
|
-
const normalDeviation = mainHitWindow /
|
|
2506
|
-
(Math.SQRT2 * osuBase.ErrorFunction.erfInv(probability));
|
|
2527
|
+
const { n100, n50, nmiss } = this.computedAccuracy;
|
|
2528
|
+
const circleCount = this.difficultyAttributes.hitCircleCount;
|
|
2529
|
+
const missCountCircles = Math.min(nmiss, circleCount);
|
|
2530
|
+
const mehCountCircles = Math.min(n50, circleCount - missCountCircles);
|
|
2531
|
+
const okCountCircles = Math.min(n100, circleCount - missCountCircles - mehCountCircles);
|
|
2532
|
+
const greatCountCircles = Math.max(0, circleCount - missCountCircles - mehCountCircles - okCountCircles);
|
|
2533
|
+
// Assume 100s, 50s, and misses happen on circles. If there are less non-300s on circles than 300s,
|
|
2534
|
+
// compute the deviation on circles.
|
|
2535
|
+
if (greatCountCircles > 0) {
|
|
2536
|
+
// The probability that a player hits a circle is unknown, but we can estimate it to be
|
|
2537
|
+
// the number of greats on circles divided by the number of circles, and then add one
|
|
2538
|
+
// to the number of circles as a bias correction / bayesian prior.
|
|
2539
|
+
const greatProbabilityCircle = Math.max(0, greatCountCircles / (greatCountCircles + okCountCircles + 1));
|
|
2540
|
+
// Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed.
|
|
2541
|
+
// Begin with the normal distribution first.
|
|
2542
|
+
const deviationOnCircles = hitWindow300 /
|
|
2543
|
+
(Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbabilityCircle));
|
|
2507
2544
|
// Get the variance of the truncated variable.
|
|
2508
|
-
const truncatedVariance = Math.pow(
|
|
2509
|
-
(Math.
|
|
2545
|
+
const truncatedVariance = Math.pow(deviationOnCircles, 2) -
|
|
2546
|
+
(Math.sqrt(2 / Math.PI) *
|
|
2510
2547
|
hitWindow100 *
|
|
2511
|
-
|
|
2512
|
-
Math.exp(-0.5 * Math.pow(hitWindow100 /
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
(
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
}
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2548
|
+
deviationOnCircles *
|
|
2549
|
+
Math.exp(-0.5 * Math.pow(hitWindow100 / deviationOnCircles, 2))) /
|
|
2550
|
+
osuBase.ErrorFunction.erf(hitWindow100 / (Math.SQRT2 * deviationOnCircles));
|
|
2551
|
+
// Then compute the variance for 50s.
|
|
2552
|
+
const mehVariance = (Math.pow(hitWindow50, 2) +
|
|
2553
|
+
hitWindow100 * hitWindow50 +
|
|
2554
|
+
Math.pow(hitWindow100, 2)) /
|
|
2555
|
+
3;
|
|
2556
|
+
// Find the total deviation.
|
|
2557
|
+
return Math.sqrt(((greatCountCircles + okCountCircles) * truncatedVariance +
|
|
2558
|
+
mehCountCircles * mehVariance) /
|
|
2559
|
+
(greatCountCircles + okCountCircles + mehCountCircles));
|
|
2560
|
+
}
|
|
2561
|
+
// If there are more non-300s than there are circles, compute the deviation on sliders instead.
|
|
2562
|
+
// Here, all that matters is whether or not the slider was missed, since it is impossible
|
|
2563
|
+
// to get a 100 or 50 on a slider by mis-tapping it.
|
|
2564
|
+
const sliderCount = this.difficultyAttributes.sliderCount;
|
|
2565
|
+
const missCountSliders = Math.min(sliderCount, nmiss - missCountCircles);
|
|
2566
|
+
const greatCountSliders = sliderCount - missCountSliders;
|
|
2567
|
+
// We only get here if nothing was hit. In this case, there is no estimate for deviation.
|
|
2568
|
+
// Note that this is never negative, so checking if this is only equal to 0 makes sense.
|
|
2569
|
+
if (greatCountSliders === 0) {
|
|
2570
|
+
return Number.POSITIVE_INFINITY;
|
|
2571
|
+
}
|
|
2572
|
+
const greatProbabilitySlider = greatCountSliders / (sliderCount + 1);
|
|
2573
|
+
return (hitWindow50 /
|
|
2574
|
+
(Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbabilitySlider)));
|
|
2527
2575
|
}
|
|
2528
2576
|
/**
|
|
2529
2577
|
* Does the same as {@link calculateDeviation}, but only for notes and inaccuracies that are relevant to tap difficulty.
|
|
@@ -2536,15 +2584,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2536
2584
|
return Number.POSITIVE_INFINITY;
|
|
2537
2585
|
}
|
|
2538
2586
|
const hitWindow300 = new osuBase.OsuHitWindow(this.difficultyAttributes.overallDifficulty).hitWindowFor300();
|
|
2539
|
-
|
|
2540
|
-
const realHitWindow300 = hitWindow300 * this.difficultyAttributes.clockRate;
|
|
2541
|
-
const droidHitWindow = new osuBase.DroidHitWindow(osuBase.OsuHitWindow.hitWindow300ToOD(realHitWindow300));
|
|
2542
|
-
const isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
2543
|
-
const hitWindow50 = droidHitWindow.hitWindowFor50(isPrecise) /
|
|
2544
|
-
this.difficultyAttributes.clockRate;
|
|
2545
|
-
const hitWindow100 = droidHitWindow.hitWindowFor100(isPrecise) /
|
|
2546
|
-
this.difficultyAttributes.clockRate;
|
|
2547
|
-
const { n300, n100, n50, nmiss } = this.computedAccuracy;
|
|
2587
|
+
const { n100, n50, nmiss } = this.computedAccuracy;
|
|
2548
2588
|
// Assume a fixed ratio of non-300s hit in speed notes based on speed note count ratio and OD.
|
|
2549
2589
|
// Graph: https://www.desmos.com/calculator/31argjcxqc
|
|
2550
2590
|
const speedNoteRatio = this.difficultyAttributes.speedNoteCount / this.totalHits;
|
|
@@ -2559,25 +2599,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2559
2599
|
return Number.POSITIVE_INFINITY;
|
|
2560
2600
|
}
|
|
2561
2601
|
const greatProbability = relevantCountGreat / (this.difficultyAttributes.speedNoteCount + 1);
|
|
2562
|
-
|
|
2563
|
-
const normalDeviation = hitWindow300 /
|
|
2564
|
-
(Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbability));
|
|
2565
|
-
// Get the variance of the truncated variable.
|
|
2566
|
-
const truncatedVariance = Math.pow(normalDeviation, 2) -
|
|
2567
|
-
(Math.SQRT2 *
|
|
2568
|
-
hitWindow100 *
|
|
2569
|
-
normalDeviation *
|
|
2570
|
-
Math.exp(-0.5 * Math.pow(hitWindow100 / normalDeviation, 2))) /
|
|
2571
|
-
(Math.sqrt(Math.PI) *
|
|
2572
|
-
osuBase.ErrorFunction.erf(hitWindow100 / (Math.SQRT2 * normalDeviation)));
|
|
2573
|
-
// Add 50s by assuming they are uniformly distributed.
|
|
2574
|
-
return Math.sqrt((1 / (n300 + n100 + n50)) *
|
|
2575
|
-
((n300 + n100) * truncatedVariance +
|
|
2576
|
-
(n50 *
|
|
2577
|
-
(Math.pow(hitWindow50, 2) +
|
|
2578
|
-
hitWindow100 * hitWindow50 +
|
|
2579
|
-
Math.pow(hitWindow100, 2))) /
|
|
2580
|
-
3));
|
|
2602
|
+
return (hitWindow300 / (Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbability)));
|
|
2581
2603
|
}
|
|
2582
2604
|
toString() {
|
|
2583
2605
|
return (this.total.toFixed(2) +
|
|
@@ -2764,36 +2786,37 @@ class OsuAimEvaluator extends AimEvaluator {
|
|
|
2764
2786
|
* Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
|
2765
2787
|
*/
|
|
2766
2788
|
class OsuAim extends OsuSkill {
|
|
2767
|
-
skillMultiplier = 23.55;
|
|
2768
2789
|
strainDecayBase = 0.15;
|
|
2769
2790
|
reducedSectionCount = 10;
|
|
2770
2791
|
reducedSectionBaseline = 0.75;
|
|
2771
|
-
difficultyMultiplier = 1.06;
|
|
2772
2792
|
decayWeight = 0.9;
|
|
2793
|
+
currentAimStrain = 0;
|
|
2794
|
+
skillMultiplier = 23.55;
|
|
2773
2795
|
withSliders;
|
|
2774
2796
|
constructor(mods, withSliders) {
|
|
2775
2797
|
super(mods);
|
|
2776
2798
|
this.withSliders = withSliders;
|
|
2777
2799
|
}
|
|
2778
|
-
/**
|
|
2779
|
-
* @param current The hitobject to calculate.
|
|
2780
|
-
*/
|
|
2781
2800
|
strainValueAt(current) {
|
|
2782
|
-
this.
|
|
2783
|
-
this.
|
|
2801
|
+
this.currentAimStrain *= this.strainDecay(current.deltaTime);
|
|
2802
|
+
this.currentAimStrain +=
|
|
2784
2803
|
OsuAimEvaluator.evaluateDifficultyOf(current, this.withSliders) *
|
|
2785
2804
|
this.skillMultiplier;
|
|
2786
|
-
return this.
|
|
2805
|
+
return this.currentAimStrain;
|
|
2806
|
+
}
|
|
2807
|
+
calculateInitialStrain(time, current) {
|
|
2808
|
+
return (this.currentAimStrain *
|
|
2809
|
+
this.strainDecay(time - (current.previous(0)?.startTime ?? 0)));
|
|
2787
2810
|
}
|
|
2788
2811
|
/**
|
|
2789
2812
|
* @param current The hitobject to save to.
|
|
2790
2813
|
*/
|
|
2791
2814
|
saveToHitObject(current) {
|
|
2792
2815
|
if (this.withSliders) {
|
|
2793
|
-
current.aimStrainWithSliders = this.
|
|
2816
|
+
current.aimStrainWithSliders = this.currentAimStrain;
|
|
2794
2817
|
}
|
|
2795
2818
|
else {
|
|
2796
|
-
current.aimStrainWithoutSliders = this.
|
|
2819
|
+
current.aimStrainWithoutSliders = this.currentAimStrain;
|
|
2797
2820
|
}
|
|
2798
2821
|
}
|
|
2799
2822
|
}
|
|
@@ -2965,7 +2988,6 @@ class OsuRhythmEvaluator extends RhythmEvaluator {
|
|
|
2965
2988
|
* Represents the skill required to press keys or tap with regards to keeping up with the speed at which objects need to be hit.
|
|
2966
2989
|
*/
|
|
2967
2990
|
class OsuSpeed extends OsuSkill {
|
|
2968
|
-
skillMultiplier = 1375;
|
|
2969
2991
|
strainDecayBase = 0.3;
|
|
2970
2992
|
reducedSectionCount = 5;
|
|
2971
2993
|
reducedSectionBaseline = 0.75;
|
|
@@ -2973,6 +2995,7 @@ class OsuSpeed extends OsuSkill {
|
|
|
2973
2995
|
decayWeight = 0.9;
|
|
2974
2996
|
currentSpeedStrain = 0;
|
|
2975
2997
|
currentRhythm = 0;
|
|
2998
|
+
skillMultiplier = 1375;
|
|
2976
2999
|
greatWindow;
|
|
2977
3000
|
constructor(mods, greatWindow) {
|
|
2978
3001
|
super(mods);
|
|
@@ -2989,11 +3012,16 @@ class OsuSpeed extends OsuSkill {
|
|
|
2989
3012
|
this.currentRhythm = OsuRhythmEvaluator.evaluateDifficultyOf(current, this.greatWindow);
|
|
2990
3013
|
return this.currentSpeedStrain * this.currentRhythm;
|
|
2991
3014
|
}
|
|
3015
|
+
calculateInitialStrain(time, current) {
|
|
3016
|
+
return (this.currentSpeedStrain *
|
|
3017
|
+
this.currentRhythm *
|
|
3018
|
+
this.strainDecay(time - (current.previous(0)?.startTime ?? 0)));
|
|
3019
|
+
}
|
|
2992
3020
|
/**
|
|
2993
3021
|
* @param current The hitobject to save to.
|
|
2994
3022
|
*/
|
|
2995
3023
|
saveToHitObject(current) {
|
|
2996
|
-
current.tapStrain = this.
|
|
3024
|
+
current.tapStrain = this.currentSpeedStrain * this.currentRhythm;
|
|
2997
3025
|
current.rhythmMultiplier = this.currentRhythm;
|
|
2998
3026
|
}
|
|
2999
3027
|
}
|
|
@@ -3084,27 +3112,29 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
3084
3112
|
* Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
|
|
3085
3113
|
*/
|
|
3086
3114
|
class OsuFlashlight extends OsuSkill {
|
|
3087
|
-
skillMultiplier = 0.052;
|
|
3088
3115
|
strainDecayBase = 0.15;
|
|
3089
3116
|
reducedSectionCount = 0;
|
|
3090
3117
|
reducedSectionBaseline = 1;
|
|
3091
3118
|
decayWeight = 1;
|
|
3119
|
+
currentFlashlightStrain = 0;
|
|
3120
|
+
skillMultiplier = 0.052;
|
|
3092
3121
|
isHidden;
|
|
3093
3122
|
constructor(mods) {
|
|
3094
3123
|
super(mods);
|
|
3095
3124
|
this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
|
|
3096
3125
|
}
|
|
3097
|
-
/**
|
|
3098
|
-
* @param current The hitobject to calculate.
|
|
3099
|
-
*/
|
|
3100
3126
|
strainValueAt(current) {
|
|
3101
|
-
this.
|
|
3102
|
-
this.
|
|
3127
|
+
this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
|
|
3128
|
+
this.currentFlashlightStrain +=
|
|
3103
3129
|
OsuFlashlightEvaluator.evaluateDifficultyOf(current, this.isHidden) * this.skillMultiplier;
|
|
3104
|
-
return this.
|
|
3130
|
+
return this.currentFlashlightStrain;
|
|
3131
|
+
}
|
|
3132
|
+
calculateInitialStrain(time, current) {
|
|
3133
|
+
return (this.currentFlashlightStrain *
|
|
3134
|
+
this.strainDecay(time - (current.previous(0)?.startTime ?? 0)));
|
|
3105
3135
|
}
|
|
3106
3136
|
saveToHitObject(current) {
|
|
3107
|
-
current.flashlightStrainWithSliders = this.
|
|
3137
|
+
current.flashlightStrainWithSliders = this.currentFlashlightStrain;
|
|
3108
3138
|
}
|
|
3109
3139
|
}
|
|
3110
3140
|
|
|
@@ -3367,6 +3397,18 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3367
3397
|
difficultyAttributes;
|
|
3368
3398
|
finalMultiplier = 1.14;
|
|
3369
3399
|
mode = osuBase.Modes.osu;
|
|
3400
|
+
calculateValues() {
|
|
3401
|
+
this.aim = this.calculateAimValue();
|
|
3402
|
+
this.speed = this.calculateSpeedValue();
|
|
3403
|
+
this.accuracy = this.calculateAccuracyValue();
|
|
3404
|
+
this.flashlight = this.calculateFlashlightValue();
|
|
3405
|
+
}
|
|
3406
|
+
calculateTotalValue() {
|
|
3407
|
+
return (Math.pow(Math.pow(this.aim, 1.1) +
|
|
3408
|
+
Math.pow(this.speed, 1.1) +
|
|
3409
|
+
Math.pow(this.accuracy, 1.1) +
|
|
3410
|
+
Math.pow(this.flashlight, 1.1), 1 / 1.1) * this.finalMultiplier);
|
|
3411
|
+
}
|
|
3370
3412
|
/**
|
|
3371
3413
|
* @param difficultyAttributes The difficulty attributes to calculate.
|
|
3372
3414
|
*/
|
|
@@ -3374,39 +3416,27 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3374
3416
|
super();
|
|
3375
3417
|
this.difficultyAttributes = osuBase.Utils.deepCopy(difficultyAttributes);
|
|
3376
3418
|
}
|
|
3377
|
-
calculateValues() {
|
|
3378
|
-
this.calculateAimValue();
|
|
3379
|
-
this.calculateSpeedValue();
|
|
3380
|
-
this.calculateAccuracyValue();
|
|
3381
|
-
this.calculateFlashlightValue();
|
|
3382
|
-
}
|
|
3383
|
-
calculateTotalValue() {
|
|
3384
|
-
this.total =
|
|
3385
|
-
Math.pow(Math.pow(this.aim, 1.1) +
|
|
3386
|
-
Math.pow(this.speed, 1.1) +
|
|
3387
|
-
Math.pow(this.accuracy, 1.1) +
|
|
3388
|
-
Math.pow(this.flashlight, 1.1), 1 / 1.1) * this.finalMultiplier;
|
|
3389
|
-
}
|
|
3390
3419
|
/**
|
|
3391
3420
|
* Calculates the aim performance value of the beatmap.
|
|
3392
3421
|
*/
|
|
3393
3422
|
calculateAimValue() {
|
|
3394
|
-
|
|
3423
|
+
let aimValue = this.baseValue(this.difficultyAttributes.aimDifficulty);
|
|
3395
3424
|
// Longer maps are worth more
|
|
3396
3425
|
let lengthBonus = 0.95 + 0.4 * Math.min(1, this.totalHits / 2000);
|
|
3397
3426
|
if (this.totalHits > 2000) {
|
|
3398
3427
|
lengthBonus += Math.log10(this.totalHits / 2000) * 0.5;
|
|
3399
3428
|
}
|
|
3400
|
-
|
|
3429
|
+
aimValue *= lengthBonus;
|
|
3401
3430
|
if (this.effectiveMissCount > 0) {
|
|
3402
|
-
// Penalize misses by assessing # of misses relative to the total # of objects.
|
|
3403
|
-
|
|
3431
|
+
// Penalize misses by assessing # of misses relative to the total # of objects.
|
|
3432
|
+
// Default a 3% reduction for any # of misses.
|
|
3433
|
+
aimValue *=
|
|
3404
3434
|
0.97 *
|
|
3405
3435
|
Math.pow(1 -
|
|
3406
3436
|
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
|
|
3407
3437
|
}
|
|
3408
3438
|
// Combo scaling
|
|
3409
|
-
|
|
3439
|
+
aimValue *= this.comboPenalty;
|
|
3410
3440
|
const calculatedAR = this.difficultyAttributes.approachRate;
|
|
3411
3441
|
if (!this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3412
3442
|
// AR scaling
|
|
@@ -3418,66 +3448,70 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3418
3448
|
arFactor += 0.05 * (8 - calculatedAR);
|
|
3419
3449
|
}
|
|
3420
3450
|
// Buff for longer maps with high AR.
|
|
3421
|
-
|
|
3451
|
+
aimValue *= 1 + arFactor * lengthBonus;
|
|
3422
3452
|
}
|
|
3423
3453
|
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
|
3424
3454
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHidden)) {
|
|
3425
|
-
|
|
3455
|
+
aimValue *= 1 + 0.04 * (12 - calculatedAR);
|
|
3426
3456
|
}
|
|
3427
3457
|
// Scale the aim value with slider factor to nerf very likely dropped sliderends.
|
|
3428
|
-
|
|
3458
|
+
aimValue *= this.sliderNerfFactor;
|
|
3429
3459
|
// Scale the aim value with accuracy.
|
|
3430
|
-
|
|
3460
|
+
aimValue *= this.computedAccuracy.value();
|
|
3431
3461
|
// It is also important to consider accuracy difficulty when doing that.
|
|
3432
3462
|
const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
|
|
3433
|
-
|
|
3463
|
+
aimValue *= 0.98 + odScaling;
|
|
3464
|
+
return aimValue;
|
|
3434
3465
|
}
|
|
3435
3466
|
/**
|
|
3436
3467
|
* Calculates the speed performance value of the beatmap.
|
|
3437
3468
|
*/
|
|
3438
3469
|
calculateSpeedValue() {
|
|
3439
3470
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3440
|
-
|
|
3441
|
-
return;
|
|
3471
|
+
return 0;
|
|
3442
3472
|
}
|
|
3443
|
-
|
|
3444
|
-
this.speed = this.baseValue(this.difficultyAttributes.speedDifficulty);
|
|
3473
|
+
let speedValue = this.baseValue(this.difficultyAttributes.speedDifficulty);
|
|
3445
3474
|
// Longer maps are worth more
|
|
3446
3475
|
let lengthBonus = 0.95 + 0.4 * Math.min(1, this.totalHits / 2000);
|
|
3447
3476
|
if (this.totalHits > 2000) {
|
|
3448
3477
|
lengthBonus += Math.log10(this.totalHits / 2000) * 0.5;
|
|
3449
3478
|
}
|
|
3450
|
-
|
|
3479
|
+
speedValue *= lengthBonus;
|
|
3451
3480
|
if (this.effectiveMissCount > 0) {
|
|
3452
|
-
// Penalize misses by assessing # of misses relative to the total # of objects.
|
|
3453
|
-
|
|
3481
|
+
// Penalize misses by assessing # of misses relative to the total # of objects.
|
|
3482
|
+
// Default a 3% reduction for any # of misses.
|
|
3483
|
+
speedValue *=
|
|
3454
3484
|
0.97 *
|
|
3455
3485
|
Math.pow(1 -
|
|
3456
3486
|
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
|
|
3457
3487
|
}
|
|
3458
3488
|
// Combo scaling
|
|
3459
|
-
|
|
3489
|
+
speedValue *= this.comboPenalty;
|
|
3460
3490
|
// AR scaling
|
|
3461
3491
|
const calculatedAR = this.difficultyAttributes.approachRate;
|
|
3462
3492
|
if (calculatedAR > 10.33) {
|
|
3463
3493
|
// Buff for longer maps with high AR.
|
|
3464
|
-
|
|
3494
|
+
speedValue *= 1 + 0.3 * (calculatedAR - 10.33) * lengthBonus;
|
|
3465
3495
|
}
|
|
3466
3496
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHidden)) {
|
|
3467
|
-
|
|
3497
|
+
speedValue *= 1 + 0.04 * (12 - calculatedAR);
|
|
3468
3498
|
}
|
|
3469
3499
|
// Calculate accuracy assuming the worst case scenario.
|
|
3470
3500
|
const countGreat = this.computedAccuracy.n300;
|
|
3471
3501
|
const countOk = this.computedAccuracy.n100;
|
|
3472
3502
|
const countMeh = this.computedAccuracy.n50;
|
|
3473
3503
|
const relevantTotalDiff = this.totalHits - this.difficultyAttributes.speedNoteCount;
|
|
3474
|
-
const relevantAccuracy = new osuBase.Accuracy(
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3504
|
+
const relevantAccuracy = new osuBase.Accuracy(this.difficultyAttributes.speedNoteCount > 0
|
|
3505
|
+
? {
|
|
3506
|
+
n300: Math.max(0, countGreat - relevantTotalDiff),
|
|
3507
|
+
n100: Math.max(0, countOk - Math.max(0, relevantTotalDiff - countGreat)),
|
|
3508
|
+
n50: Math.max(0, countMeh -
|
|
3509
|
+
Math.max(0, relevantTotalDiff - countGreat - countOk)),
|
|
3510
|
+
}
|
|
3511
|
+
: // Set accuracy to 0.
|
|
3512
|
+
{ n300: 0, nobjects: 1 });
|
|
3479
3513
|
// Scale the speed value with accuracy and OD.
|
|
3480
|
-
|
|
3514
|
+
speedValue *=
|
|
3481
3515
|
(0.95 +
|
|
3482
3516
|
Math.pow(this.difficultyAttributes.overallDifficulty, 2) /
|
|
3483
3517
|
750) *
|
|
@@ -3487,22 +3521,21 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3487
3521
|
Math.max(this.difficultyAttributes.overallDifficulty, 8)) /
|
|
3488
3522
|
2);
|
|
3489
3523
|
// Scale the speed value with # of 50s to punish doubletapping.
|
|
3490
|
-
|
|
3524
|
+
speedValue *= Math.pow(0.99, Math.max(0, this.computedAccuracy.n50 - this.totalHits / 500));
|
|
3525
|
+
return speedValue;
|
|
3491
3526
|
}
|
|
3492
3527
|
/**
|
|
3493
3528
|
* Calculates the accuracy performance value of the beatmap.
|
|
3494
3529
|
*/
|
|
3495
3530
|
calculateAccuracyValue() {
|
|
3496
3531
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
3497
|
-
|
|
3498
|
-
return;
|
|
3532
|
+
return 0;
|
|
3499
3533
|
}
|
|
3500
3534
|
const ncircles = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModScoreV2)
|
|
3501
3535
|
? this.totalHits - this.difficultyAttributes.spinnerCount
|
|
3502
3536
|
: this.difficultyAttributes.hitCircleCount;
|
|
3503
3537
|
if (ncircles === 0) {
|
|
3504
|
-
|
|
3505
|
-
return;
|
|
3538
|
+
return 0;
|
|
3506
3539
|
}
|
|
3507
3540
|
const realAccuracy = new osuBase.Accuracy({
|
|
3508
3541
|
...this.computedAccuracy,
|
|
@@ -3510,52 +3543,50 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3510
3543
|
});
|
|
3511
3544
|
// Lots of arbitrary values from testing.
|
|
3512
3545
|
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
|
|
3513
|
-
this.
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
2.83;
|
|
3546
|
+
let accuracyValue = Math.pow(1.52163, this.difficultyAttributes.overallDifficulty) *
|
|
3547
|
+
// It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points.
|
|
3548
|
+
Math.pow(realAccuracy.n300 < 0 ? 0 : realAccuracy.value(), 24) *
|
|
3549
|
+
2.83;
|
|
3518
3550
|
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
|
|
3519
|
-
|
|
3551
|
+
accuracyValue *= Math.min(1.15, Math.pow(ncircles / 1000, 0.3));
|
|
3520
3552
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHidden)) {
|
|
3521
|
-
|
|
3553
|
+
accuracyValue *= 1.08;
|
|
3522
3554
|
}
|
|
3523
3555
|
if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
|
|
3524
|
-
|
|
3556
|
+
accuracyValue *= 1.02;
|
|
3525
3557
|
}
|
|
3558
|
+
return accuracyValue;
|
|
3526
3559
|
}
|
|
3527
3560
|
/**
|
|
3528
3561
|
* Calculates the flashlight performance value of the beatmap.
|
|
3529
3562
|
*/
|
|
3530
3563
|
calculateFlashlightValue() {
|
|
3531
3564
|
if (!this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
|
|
3532
|
-
|
|
3533
|
-
return;
|
|
3565
|
+
return 0;
|
|
3534
3566
|
}
|
|
3535
|
-
|
|
3536
|
-
this.flashlight =
|
|
3537
|
-
Math.pow(this.difficultyAttributes.flashlightDifficulty, 2) * 25;
|
|
3567
|
+
let flashlightValue = Math.pow(this.difficultyAttributes.flashlightDifficulty, 2) * 25;
|
|
3538
3568
|
// Combo scaling
|
|
3539
|
-
|
|
3569
|
+
flashlightValue *= this.comboPenalty;
|
|
3540
3570
|
if (this.effectiveMissCount > 0) {
|
|
3541
3571
|
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
3542
|
-
|
|
3572
|
+
flashlightValue *=
|
|
3543
3573
|
0.97 *
|
|
3544
3574
|
Math.pow(1 -
|
|
3545
3575
|
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
|
|
3546
3576
|
}
|
|
3547
3577
|
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
3548
|
-
|
|
3578
|
+
flashlightValue *=
|
|
3549
3579
|
0.7 +
|
|
3550
3580
|
0.1 * Math.min(1, this.totalHits / 200) +
|
|
3551
3581
|
(this.totalHits > 200
|
|
3552
3582
|
? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
|
|
3553
3583
|
: 0);
|
|
3554
3584
|
// Scale the flashlight value with accuracy slightly.
|
|
3555
|
-
|
|
3585
|
+
flashlightValue *= 0.5 + this.computedAccuracy.value() / 2;
|
|
3556
3586
|
// It is also important to consider accuracy difficulty when doing that.
|
|
3557
3587
|
const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
|
|
3558
|
-
|
|
3588
|
+
flashlightValue *= 0.98 + odScaling;
|
|
3589
|
+
return flashlightValue;
|
|
3559
3590
|
}
|
|
3560
3591
|
toString() {
|
|
3561
3592
|
return (this.total.toFixed(2) +
|