@rian8337/osu-difficulty-calculator 4.0.0-beta.19 → 4.0.0-beta.20
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 +258 -172
- package/package.json +2 -2
- package/typings/index.d.ts +88 -43
package/dist/index.js
CHANGED
|
@@ -152,16 +152,13 @@ class DifficultyCalculator {
|
|
|
152
152
|
class DifficultyHitObject {
|
|
153
153
|
/**
|
|
154
154
|
* Note: You **must** call `computeProperties` at some point due to how TypeScript handles
|
|
155
|
-
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue
|
|
155
|
+
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue).
|
|
156
156
|
*
|
|
157
157
|
* @param object The underlying hitobject.
|
|
158
158
|
* @param lastObject The hitobject before this hitobject.
|
|
159
159
|
* @param lastLastObject The hitobject before the last hitobject.
|
|
160
160
|
* @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
|
|
161
161
|
* @param clockRate The clock rate of the beatmap.
|
|
162
|
-
* @param timePreempt The time preempt with clock rate.
|
|
163
|
-
* @param isForceAR Whether force AR is enabled.
|
|
164
|
-
* @param mode The gamemode to compute properties for.
|
|
165
162
|
*/
|
|
166
163
|
constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate) {
|
|
167
164
|
/**
|
|
@@ -625,9 +622,7 @@ class StrainSkill extends Skill {
|
|
|
625
622
|
process(current) {
|
|
626
623
|
// The first object doesn't generate a strain, so we begin with an incremented section end
|
|
627
624
|
if (current.index === 0) {
|
|
628
|
-
this.currentSectionEnd =
|
|
629
|
-
Math.ceil(current.startTime / this.sectionLength) *
|
|
630
|
-
this.sectionLength;
|
|
625
|
+
this.currentSectionEnd = this.calculateCurrentSectionStart(current);
|
|
631
626
|
}
|
|
632
627
|
while (current.startTime > this.currentSectionEnd) {
|
|
633
628
|
this.saveCurrentPeak();
|
|
@@ -657,6 +652,16 @@ class StrainSkill extends Skill {
|
|
|
657
652
|
strainDecay(ms) {
|
|
658
653
|
return Math.pow(this.strainDecayBase, ms / 1000);
|
|
659
654
|
}
|
|
655
|
+
/**
|
|
656
|
+
* Calculates the starting time of a strain section at an object.
|
|
657
|
+
*
|
|
658
|
+
* @param current The object at which the strain section starts.
|
|
659
|
+
* @returns The start time of the strain section.
|
|
660
|
+
*/
|
|
661
|
+
calculateCurrentSectionStart(current) {
|
|
662
|
+
return (Math.ceil(current.startTime / this.sectionLength) *
|
|
663
|
+
this.sectionLength);
|
|
664
|
+
}
|
|
660
665
|
/**
|
|
661
666
|
* Sets the initial strain level for a new section.
|
|
662
667
|
*
|
|
@@ -675,6 +680,35 @@ class StrainSkill extends Skill {
|
|
|
675
680
|
* and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
|
|
676
681
|
*/
|
|
677
682
|
class DroidSkill extends StrainSkill {
|
|
683
|
+
constructor() {
|
|
684
|
+
super(...arguments);
|
|
685
|
+
this._objectStrains = [];
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* The strains of hitobjects.
|
|
689
|
+
*/
|
|
690
|
+
get objectStrains() {
|
|
691
|
+
return this._objectStrains;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Returns the number of strains weighed against the top strain.
|
|
695
|
+
*
|
|
696
|
+
* The result is scaled by clock rate as it affects the total number of strains.
|
|
697
|
+
*/
|
|
698
|
+
countDifficultStrains() {
|
|
699
|
+
if (this._objectStrains.length === 0) {
|
|
700
|
+
return 0;
|
|
701
|
+
}
|
|
702
|
+
const maxStrain = Math.max(...this._objectStrains);
|
|
703
|
+
if (maxStrain === 0) {
|
|
704
|
+
return 0;
|
|
705
|
+
}
|
|
706
|
+
return this._objectStrains.reduce((total, next) => total + Math.pow(next / maxStrain, 4), 0);
|
|
707
|
+
}
|
|
708
|
+
process(current) {
|
|
709
|
+
super.process(current);
|
|
710
|
+
this._objectStrains.push(this.getObjectStrain(current));
|
|
711
|
+
}
|
|
678
712
|
difficultyValue() {
|
|
679
713
|
const strains = this.strainPeaks.slice();
|
|
680
714
|
if (this.reducedSectionCount > 0) {
|
|
@@ -694,6 +728,9 @@ class DroidSkill extends StrainSkill {
|
|
|
694
728
|
return a + Math.pow(v, 1 / Math.log2(this.starsPerDouble));
|
|
695
729
|
}, 0), Math.log2(this.starsPerDouble));
|
|
696
730
|
}
|
|
731
|
+
calculateCurrentSectionStart(current) {
|
|
732
|
+
return current.startTime;
|
|
733
|
+
}
|
|
697
734
|
}
|
|
698
735
|
|
|
699
736
|
/**
|
|
@@ -722,6 +759,9 @@ class DroidAim extends DroidSkill {
|
|
|
722
759
|
return (this.currentAimStrain *
|
|
723
760
|
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
724
761
|
}
|
|
762
|
+
getObjectStrain() {
|
|
763
|
+
return this.currentAimStrain;
|
|
764
|
+
}
|
|
725
765
|
/**
|
|
726
766
|
* @param current The hitobject to save to.
|
|
727
767
|
*/
|
|
@@ -754,13 +794,15 @@ class DroidTapEvaluator extends SpeedEvaluator {
|
|
|
754
794
|
*
|
|
755
795
|
* - time between pressing the previous and current object,
|
|
756
796
|
* - distance between those objects,
|
|
757
|
-
* -
|
|
797
|
+
* - how easily they can be cheesed,
|
|
798
|
+
* - and the strain time cap.
|
|
758
799
|
*
|
|
759
800
|
* @param current The current object.
|
|
760
801
|
* @param greatWindow The great hit window of the current object.
|
|
761
802
|
* @param considerCheesability Whether to consider cheesability.
|
|
803
|
+
* @param strainTimeCap The strain time to cap the object's strain time to.
|
|
762
804
|
*/
|
|
763
|
-
static evaluateDifficultyOf(current, greatWindow, considerCheesability) {
|
|
805
|
+
static evaluateDifficultyOf(current, greatWindow, considerCheesability, strainTimeCap) {
|
|
764
806
|
if (current.object instanceof osuBase.Spinner ||
|
|
765
807
|
// Exclude overlapping objects that can be tapped at once.
|
|
766
808
|
current.isOverlapping(false)) {
|
|
@@ -781,13 +823,17 @@ class DroidTapEvaluator extends SpeedEvaluator {
|
|
|
781
823
|
doubletapness = Math.pow(speedRatio, 1 - windowRatio);
|
|
782
824
|
}
|
|
783
825
|
}
|
|
826
|
+
const strainTime = strainTimeCap !== undefined
|
|
827
|
+
? // We cap the strain time to 50 here as the chance of vibro is higher in any BPM higher than 300.
|
|
828
|
+
Math.max(50, strainTimeCap, current.strainTime)
|
|
829
|
+
: current.strainTime;
|
|
784
830
|
let speedBonus = 1;
|
|
785
|
-
if (
|
|
831
|
+
if (strainTime < this.minSpeedBonus) {
|
|
786
832
|
speedBonus +=
|
|
787
833
|
0.75 *
|
|
788
|
-
Math.pow(osuBase.ErrorFunction.erf((this.minSpeedBonus -
|
|
834
|
+
Math.pow(osuBase.ErrorFunction.erf((this.minSpeedBonus - strainTime) / 40), 2);
|
|
789
835
|
}
|
|
790
|
-
return (speedBonus * Math.pow(doubletapness, 1.5)) /
|
|
836
|
+
return (speedBonus * Math.pow(doubletapness, 1.5)) / strainTime;
|
|
791
837
|
}
|
|
792
838
|
}
|
|
793
839
|
|
|
@@ -795,7 +841,13 @@ class DroidTapEvaluator extends SpeedEvaluator {
|
|
|
795
841
|
* Represents the skill required to press keys or tap with regards to keeping up with the speed at which objects need to be hit.
|
|
796
842
|
*/
|
|
797
843
|
class DroidTap extends DroidSkill {
|
|
798
|
-
|
|
844
|
+
/**
|
|
845
|
+
* The delta time of hitobjects.
|
|
846
|
+
*/
|
|
847
|
+
get objectDeltaTimes() {
|
|
848
|
+
return this._objectDeltaTimes;
|
|
849
|
+
}
|
|
850
|
+
constructor(mods, overallDifficulty, considerCheesability, strainTimeCap) {
|
|
799
851
|
super(mods);
|
|
800
852
|
this.reducedSectionCount = 10;
|
|
801
853
|
this.reducedSectionBaseline = 0.75;
|
|
@@ -804,15 +856,49 @@ class DroidTap extends DroidSkill {
|
|
|
804
856
|
this.currentTapStrain = 0;
|
|
805
857
|
this.currentRhythmMultiplier = 0;
|
|
806
858
|
this.skillMultiplier = 1375;
|
|
859
|
+
this._objectDeltaTimes = [];
|
|
807
860
|
this.greatWindow = new osuBase.OsuHitWindow(overallDifficulty).hitWindowFor300();
|
|
808
861
|
this.considerCheesability = considerCheesability;
|
|
862
|
+
this.strainTimeCap = strainTimeCap;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* The amount of notes that are relevant to the difficulty.
|
|
866
|
+
*/
|
|
867
|
+
relevantNoteCount() {
|
|
868
|
+
if (this._objectStrains.length === 0) {
|
|
869
|
+
return 0;
|
|
870
|
+
}
|
|
871
|
+
const maxStrain = Math.max(...this._objectStrains);
|
|
872
|
+
if (maxStrain === 0) {
|
|
873
|
+
return 0;
|
|
874
|
+
}
|
|
875
|
+
return this._objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* The delta time relevant to the difficulty.
|
|
879
|
+
*/
|
|
880
|
+
relevantDeltaTime() {
|
|
881
|
+
if (this._objectStrains.length === 0) {
|
|
882
|
+
return 0;
|
|
883
|
+
}
|
|
884
|
+
const maxStrain = Math.max(...this._objectStrains);
|
|
885
|
+
if (maxStrain === 0) {
|
|
886
|
+
return 0;
|
|
887
|
+
}
|
|
888
|
+
return (this._objectDeltaTimes.reduce((total, next, index) => total +
|
|
889
|
+
(next * 1) /
|
|
890
|
+
(1 +
|
|
891
|
+
Math.exp(-((this._objectStrains[index] / maxStrain) *
|
|
892
|
+
25 -
|
|
893
|
+
20))), 0) /
|
|
894
|
+
this._objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 25 - 20))), 0));
|
|
809
895
|
}
|
|
810
896
|
strainValueAt(current) {
|
|
811
|
-
|
|
812
|
-
this.currentTapStrain *= decay;
|
|
897
|
+
this.currentTapStrain *= this.strainDecay(current.strainTime);
|
|
813
898
|
this.currentTapStrain +=
|
|
814
|
-
DroidTapEvaluator.evaluateDifficultyOf(current, this.greatWindow, this.considerCheesability) * this.skillMultiplier;
|
|
899
|
+
DroidTapEvaluator.evaluateDifficultyOf(current, this.greatWindow, this.considerCheesability, this.strainTimeCap) * this.skillMultiplier;
|
|
815
900
|
this.currentRhythmMultiplier = current.rhythmMultiplier;
|
|
901
|
+
this._objectDeltaTimes.push(current.deltaTime);
|
|
816
902
|
return this.currentTapStrain * current.rhythmMultiplier;
|
|
817
903
|
}
|
|
818
904
|
calculateInitialStrain(time, current) {
|
|
@@ -821,10 +907,16 @@ class DroidTap extends DroidSkill {
|
|
|
821
907
|
this.currentRhythmMultiplier *
|
|
822
908
|
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
823
909
|
}
|
|
910
|
+
getObjectStrain() {
|
|
911
|
+
return this.currentTapStrain * this.currentRhythmMultiplier;
|
|
912
|
+
}
|
|
824
913
|
/**
|
|
825
914
|
* @param current The hitobject to save to.
|
|
826
915
|
*/
|
|
827
916
|
saveToHitObject(current) {
|
|
917
|
+
if (this.strainTimeCap !== undefined) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
828
920
|
const strain = this.currentTapStrain * this.currentRhythmMultiplier;
|
|
829
921
|
if (this.considerCheesability) {
|
|
830
922
|
current.tapStrain = strain;
|
|
@@ -961,6 +1053,9 @@ class DroidFlashlight extends DroidSkill {
|
|
|
961
1053
|
return (this.currentFlashlightStrain *
|
|
962
1054
|
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
963
1055
|
}
|
|
1056
|
+
getObjectStrain() {
|
|
1057
|
+
return this.currentFlashlightStrain;
|
|
1058
|
+
}
|
|
964
1059
|
saveToHitObject(current) {
|
|
965
1060
|
if (this.withSliders) {
|
|
966
1061
|
current.flashlightStrainWithSliders = this.currentFlashlightStrain;
|
|
@@ -1141,6 +1236,9 @@ class DroidRhythm extends DroidSkill {
|
|
|
1141
1236
|
return (this.currentRhythmStrain *
|
|
1142
1237
|
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
1143
1238
|
}
|
|
1239
|
+
getObjectStrain() {
|
|
1240
|
+
return this.currentRhythmStrain;
|
|
1241
|
+
}
|
|
1144
1242
|
saveToHitObject(current) {
|
|
1145
1243
|
current.rhythmStrain = this.currentRhythmStrain;
|
|
1146
1244
|
current.rhythmMultiplier = this.currentRhythmMultiplier;
|
|
@@ -1148,7 +1246,7 @@ class DroidRhythm extends DroidSkill {
|
|
|
1148
1246
|
}
|
|
1149
1247
|
|
|
1150
1248
|
/**
|
|
1151
|
-
* An evaluator for calculating osu!droid
|
|
1249
|
+
* An evaluator for calculating osu!droid visual skill.
|
|
1152
1250
|
*/
|
|
1153
1251
|
class DroidVisualEvaluator {
|
|
1154
1252
|
/**
|
|
@@ -1200,12 +1298,12 @@ class DroidVisualEvaluator {
|
|
|
1200
1298
|
current.opacityAt(previous.object.startTime, isHiddenMod)) /
|
|
1201
1299
|
4;
|
|
1202
1300
|
}
|
|
1203
|
-
// Scale the value with overlapping factor.
|
|
1204
|
-
strain /= 10 * (1 + current.overlappingFactor);
|
|
1205
1301
|
if (current.timePreempt < 400) {
|
|
1206
1302
|
// Give bonus for AR higher than 10.33.
|
|
1207
|
-
strain += Math.pow(400 - current.timePreempt, 1.
|
|
1303
|
+
strain += Math.pow(400 - current.timePreempt, 1.35) / 100;
|
|
1208
1304
|
}
|
|
1305
|
+
// Scale the value with overlapping factor.
|
|
1306
|
+
strain /= 10 * (1 + current.overlappingFactor);
|
|
1209
1307
|
if (current.object instanceof osuBase.Slider && withSliders) {
|
|
1210
1308
|
const scalingFactor = 50 / current.object.radius;
|
|
1211
1309
|
// Invert the scaling factor to determine the true travel distance independent of circle size.
|
|
@@ -1272,6 +1370,9 @@ class DroidVisual extends DroidSkill {
|
|
|
1272
1370
|
this.currentRhythmMultiplier *
|
|
1273
1371
|
this.strainDecay(time - ((_b = (_a = current.previous(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : 0)));
|
|
1274
1372
|
}
|
|
1373
|
+
getObjectStrain() {
|
|
1374
|
+
return this.currentVisualStrain * this.currentRhythmMultiplier;
|
|
1375
|
+
}
|
|
1275
1376
|
saveToHitObject(current) {
|
|
1276
1377
|
const strain = this.currentVisualStrain * this.currentRhythmMultiplier;
|
|
1277
1378
|
if (this.withSliders) {
|
|
@@ -1300,7 +1401,7 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
|
|
|
1300
1401
|
}
|
|
1301
1402
|
/**
|
|
1302
1403
|
* Note: You **must** call `computeProperties` at some point due to how TypeScript handles
|
|
1303
|
-
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue
|
|
1404
|
+
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue).
|
|
1304
1405
|
*
|
|
1305
1406
|
* @param object The underlying hitobject.
|
|
1306
1407
|
* @param lastObject The hitobject before this hitobject.
|
|
@@ -1446,7 +1547,7 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
|
|
|
1446
1547
|
// Penalize objects that are too close to the object in both distance
|
|
1447
1548
|
// and delta time to prevent stream maps from being overweighted.
|
|
1448
1549
|
this.overlappingFactor +=
|
|
1449
|
-
Math.max(0, 1 - distance / (
|
|
1550
|
+
Math.max(0, 1 - distance / (2.5 * this.object.radius)) *
|
|
1450
1551
|
(7.5 /
|
|
1451
1552
|
(1 +
|
|
1452
1553
|
Math.exp(0.15 * (Math.max(deltaTime, this.minDeltaTime) - 75))));
|
|
@@ -1487,6 +1588,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1487
1588
|
possibleThreeFingeredSections: [],
|
|
1488
1589
|
difficultSliders: [],
|
|
1489
1590
|
averageSpeedDeltaTime: 0,
|
|
1591
|
+
vibroFactor: 1,
|
|
1490
1592
|
};
|
|
1491
1593
|
this.difficultyMultiplier = 0.18;
|
|
1492
1594
|
this.mode = osuBase.Modes.droid;
|
|
@@ -1544,11 +1646,9 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1544
1646
|
flashlightDifficultStrainCount: this.attributes.flashlightDifficultStrainCount,
|
|
1545
1647
|
visualDifficultStrainCount: this.attributes.visualDifficultStrainCount,
|
|
1546
1648
|
averageSpeedDeltaTime: this.attributes.averageSpeedDeltaTime,
|
|
1649
|
+
vibroFactor: this.attributes.vibroFactor,
|
|
1547
1650
|
};
|
|
1548
1651
|
}
|
|
1549
|
-
calculate(options) {
|
|
1550
|
-
return super.calculate(options);
|
|
1551
|
-
}
|
|
1552
1652
|
/**
|
|
1553
1653
|
* Calculates the aim star rating of the beatmap and stores it in this instance.
|
|
1554
1654
|
*/
|
|
@@ -1566,7 +1666,9 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1566
1666
|
const tapSkillCheese = new DroidTap(this.mods, od, true);
|
|
1567
1667
|
const tapSkillNoCheese = new DroidTap(this.mods, od, false);
|
|
1568
1668
|
this.calculateSkills(tapSkillCheese, tapSkillNoCheese);
|
|
1569
|
-
this.
|
|
1669
|
+
const tapSkillVibro = new DroidTap(this.mods, od, true, tapSkillCheese.relevantDeltaTime());
|
|
1670
|
+
this.calculateSkills(tapSkillVibro);
|
|
1671
|
+
this.postCalculateTap(tapSkillCheese, tapSkillVibro);
|
|
1570
1672
|
}
|
|
1571
1673
|
/**
|
|
1572
1674
|
* Calculates the rhythm star rating of the beatmap and stores it in this instance.
|
|
@@ -1632,8 +1734,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1632
1734
|
const flashlightSkillWithoutSliders = skills[6];
|
|
1633
1735
|
const visualSkill = skills[7];
|
|
1634
1736
|
const visualSkillWithoutSliders = skills[8];
|
|
1737
|
+
const tapSkillVibro = new DroidTap(this.mods, this.difficultyStatistics.overallDifficulty, true, tapSkillCheese.relevantDeltaTime());
|
|
1738
|
+
this.calculateSkills(tapSkillVibro);
|
|
1635
1739
|
this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
|
|
1636
|
-
this.postCalculateTap(tapSkillCheese);
|
|
1740
|
+
this.postCalculateTap(tapSkillCheese, tapSkillVibro);
|
|
1637
1741
|
this.postCalculateRhythm(rhythmSkill);
|
|
1638
1742
|
this.postCalculateFlashlight(flashlightSkill, flashlightSkillWithoutSliders);
|
|
1639
1743
|
this.postCalculateVisual(visualSkill, visualSkillWithoutSliders);
|
|
@@ -1685,7 +1789,9 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1685
1789
|
new DroidAim(this.mods, false),
|
|
1686
1790
|
// Tap skill depends on rhythm skill, so we put it first
|
|
1687
1791
|
new DroidRhythm(this.mods, od),
|
|
1792
|
+
// Cheesability tap
|
|
1688
1793
|
new DroidTap(this.mods, od, true),
|
|
1794
|
+
// Non-cheesability tap
|
|
1689
1795
|
new DroidTap(this.mods, od, false),
|
|
1690
1796
|
new DroidFlashlight(this.mods, true),
|
|
1691
1797
|
new DroidFlashlight(this.mods, false),
|
|
@@ -1711,19 +1817,17 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1711
1817
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
1712
1818
|
this.attributes.aimDifficulty *= 0.9;
|
|
1713
1819
|
}
|
|
1820
|
+
this.attributes.aimDifficultStrainCount =
|
|
1821
|
+
aimSkill.countDifficultStrains();
|
|
1714
1822
|
this.calculateAimAttributes();
|
|
1715
1823
|
}
|
|
1716
1824
|
/**
|
|
1717
1825
|
* Calculates aim-related attributes.
|
|
1718
1826
|
*/
|
|
1719
1827
|
calculateAimAttributes() {
|
|
1720
|
-
const objectStrains = [];
|
|
1721
|
-
let maxStrain = 0;
|
|
1722
1828
|
const topDifficultSliders = [];
|
|
1723
1829
|
for (let i = 0; i < this.objects.length; ++i) {
|
|
1724
1830
|
const object = this.objects[i];
|
|
1725
|
-
objectStrains.push(object.aimStrainWithSliders);
|
|
1726
|
-
maxStrain = Math.max(maxStrain, object.aimStrainWithSliders);
|
|
1727
1831
|
const velocity = object.travelDistance / object.travelTime;
|
|
1728
1832
|
if (velocity > 0) {
|
|
1729
1833
|
topDifficultSliders.push({
|
|
@@ -1732,10 +1836,6 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1732
1836
|
});
|
|
1733
1837
|
}
|
|
1734
1838
|
}
|
|
1735
|
-
if (maxStrain) {
|
|
1736
|
-
this.attributes.aimNoteCount = objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
|
|
1737
|
-
this.attributes.aimDifficultStrainCount = objectStrains.reduce((total, next) => total + Math.pow(next / maxStrain, 4), 0);
|
|
1738
|
-
}
|
|
1739
1839
|
const velocitySum = topDifficultSliders.reduce((a, v) => a + v.velocity, 0);
|
|
1740
1840
|
for (const slider of topDifficultSliders) {
|
|
1741
1841
|
const difficultyRating = slider.velocity / velocitySum;
|
|
@@ -1758,16 +1858,22 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1758
1858
|
* Called after tap skill calculation.
|
|
1759
1859
|
*
|
|
1760
1860
|
* @param tapSkillCheese The tap skill that considers cheesing.
|
|
1861
|
+
* @param tapSkillVibro The tap skill that considers vibro.
|
|
1761
1862
|
*/
|
|
1762
|
-
postCalculateTap(tapSkillCheese) {
|
|
1863
|
+
postCalculateTap(tapSkillCheese, tapSkillVibro) {
|
|
1763
1864
|
this.strainPeaks.speed = tapSkillCheese.strainPeaks;
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
this.
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
}
|
|
1865
|
+
this.attributes.tapDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
|
|
1866
|
+
? 0
|
|
1867
|
+
: this.starValue(tapSkillCheese.difficultyValue());
|
|
1868
|
+
if (this.tap) {
|
|
1869
|
+
this.attributes.vibroFactor =
|
|
1870
|
+
this.starValue(tapSkillVibro.difficultyValue()) / this.tap;
|
|
1871
|
+
}
|
|
1872
|
+
this.attributes.speedNoteCount = tapSkillCheese.relevantNoteCount();
|
|
1873
|
+
this.attributes.averageSpeedDeltaTime =
|
|
1874
|
+
tapSkillCheese.relevantDeltaTime();
|
|
1875
|
+
this.attributes.tapDifficultStrainCount =
|
|
1876
|
+
tapSkillCheese.countDifficultStrains();
|
|
1771
1877
|
this.calculateTapAttributes();
|
|
1772
1878
|
}
|
|
1773
1879
|
/**
|
|
@@ -1775,110 +1881,46 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1775
1881
|
*/
|
|
1776
1882
|
calculateTapAttributes() {
|
|
1777
1883
|
this.attributes.possibleThreeFingeredSections = [];
|
|
1778
|
-
const
|
|
1779
|
-
const objectStrains = [];
|
|
1780
|
-
const objectDeltaTimes = [];
|
|
1781
|
-
let maxStrain = 0;
|
|
1782
|
-
const maxSectionDeltaTime = 2000;
|
|
1884
|
+
const { threeFingerStrainThreshold } = DroidDifficultyCalculator;
|
|
1783
1885
|
const minSectionObjectCount = 5;
|
|
1784
|
-
let
|
|
1785
|
-
|
|
1886
|
+
let inSpeedSection = false;
|
|
1887
|
+
let firstSpeedObjectIndex = 0;
|
|
1888
|
+
for (let i = 2; i < this.objects.length; ++i) {
|
|
1786
1889
|
const current = this.objects[i];
|
|
1787
|
-
const
|
|
1788
|
-
if (
|
|
1789
|
-
|
|
1790
|
-
|
|
1890
|
+
const prev = this.objects[i - 1];
|
|
1891
|
+
if (!inSpeedSection &&
|
|
1892
|
+
current.originalTapStrain >= threeFingerStrainThreshold) {
|
|
1893
|
+
inSpeedSection = true;
|
|
1894
|
+
firstSpeedObjectIndex = i;
|
|
1895
|
+
continue;
|
|
1791
1896
|
}
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
if (
|
|
1897
|
+
const currentDelta = current.deltaTime;
|
|
1898
|
+
const prevDelta = prev.deltaTime;
|
|
1899
|
+
const deltaRatio = Math.min(prevDelta, currentDelta) /
|
|
1900
|
+
Math.max(prevDelta, currentDelta);
|
|
1901
|
+
if (inSpeedSection &&
|
|
1902
|
+
(current.originalTapStrain < threeFingerStrainThreshold ||
|
|
1903
|
+
// Stop speed section on slowing down 1/2 rhythm change or anything slower.
|
|
1904
|
+
(prevDelta < currentDelta && deltaRatio <= 0.5) ||
|
|
1905
|
+
// Don't forget to manually add the last section, which would otherwise be ignored.
|
|
1906
|
+
i === this.objects.length - 1)) {
|
|
1907
|
+
const lastSpeedObjectIndex = i - (i === this.objects.length - 1 ? 0 : 1);
|
|
1908
|
+
inSpeedSection = false;
|
|
1797
1909
|
// Ignore sections that don't meet object count requirement.
|
|
1798
|
-
if (i -
|
|
1799
|
-
firstObjectIndex = i + 1;
|
|
1910
|
+
if (i - firstSpeedObjectIndex < minSectionObjectCount) {
|
|
1800
1911
|
continue;
|
|
1801
1912
|
}
|
|
1802
|
-
tempSections.push({
|
|
1803
|
-
firstObjectIndex,
|
|
1804
|
-
lastObjectIndex: i,
|
|
1805
|
-
});
|
|
1806
|
-
firstObjectIndex = i + 1;
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
// Don't forget to manually add the last beatmap section, which would otherwise be ignored.
|
|
1810
|
-
if (this.objects.length - firstObjectIndex > minSectionObjectCount) {
|
|
1811
|
-
tempSections.push({
|
|
1812
|
-
firstObjectIndex,
|
|
1813
|
-
lastObjectIndex: this.objects.length - 1,
|
|
1814
|
-
});
|
|
1815
|
-
}
|
|
1816
|
-
// Refilter with tap strain in mind.
|
|
1817
|
-
const { threeFingerStrainThreshold } = DroidDifficultyCalculator;
|
|
1818
|
-
for (const section of tempSections) {
|
|
1819
|
-
let inSpeedSection = false;
|
|
1820
|
-
let newFirstObjectIndex = section.firstObjectIndex;
|
|
1821
|
-
for (let i = section.firstObjectIndex; i <= section.lastObjectIndex; ++i) {
|
|
1822
|
-
const current = this.objects[i];
|
|
1823
|
-
if (!inSpeedSection &&
|
|
1824
|
-
current.originalTapStrain >= threeFingerStrainThreshold) {
|
|
1825
|
-
inSpeedSection = true;
|
|
1826
|
-
newFirstObjectIndex = i;
|
|
1827
|
-
continue;
|
|
1828
|
-
}
|
|
1829
|
-
if (inSpeedSection &&
|
|
1830
|
-
current.originalTapStrain < threeFingerStrainThreshold) {
|
|
1831
|
-
inSpeedSection = false;
|
|
1832
|
-
// Ignore sections that don't meet object count requirement.
|
|
1833
|
-
if (i - newFirstObjectIndex < minSectionObjectCount) {
|
|
1834
|
-
continue;
|
|
1835
|
-
}
|
|
1836
|
-
this.attributes.possibleThreeFingeredSections.push({
|
|
1837
|
-
firstObjectIndex: newFirstObjectIndex,
|
|
1838
|
-
lastObjectIndex: i,
|
|
1839
|
-
sumStrain: this.calculateThreeFingerSummedStrain(newFirstObjectIndex, i),
|
|
1840
|
-
});
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
// Don't forget to manually add the last beatmap section, which would otherwise be ignored.
|
|
1844
|
-
// Ignore sections that don't meet object count requirement.
|
|
1845
|
-
if (inSpeedSection &&
|
|
1846
|
-
section.lastObjectIndex - newFirstObjectIndex >=
|
|
1847
|
-
minSectionObjectCount) {
|
|
1848
1913
|
this.attributes.possibleThreeFingeredSections.push({
|
|
1849
|
-
firstObjectIndex:
|
|
1850
|
-
lastObjectIndex:
|
|
1851
|
-
sumStrain:
|
|
1914
|
+
firstObjectIndex: firstSpeedObjectIndex,
|
|
1915
|
+
lastObjectIndex: lastSpeedObjectIndex,
|
|
1916
|
+
sumStrain: Math.pow(this.objects
|
|
1917
|
+
.slice(firstSpeedObjectIndex, lastSpeedObjectIndex + 1)
|
|
1918
|
+
.reduce((a, v) => a +
|
|
1919
|
+
v.originalTapStrain /
|
|
1920
|
+
threeFingerStrainThreshold, 0), 0.75),
|
|
1852
1921
|
});
|
|
1853
1922
|
}
|
|
1854
1923
|
}
|
|
1855
|
-
if (maxStrain) {
|
|
1856
|
-
this.attributes.speedNoteCount = objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
|
|
1857
|
-
this.attributes.averageSpeedDeltaTime =
|
|
1858
|
-
objectDeltaTimes.reduce((total, next, index) => total +
|
|
1859
|
-
(next * 1) /
|
|
1860
|
-
(1 +
|
|
1861
|
-
Math.exp(-((objectStrains[index] / maxStrain) *
|
|
1862
|
-
25 -
|
|
1863
|
-
20))), 0) /
|
|
1864
|
-
objectStrains.reduce((total, next) => total +
|
|
1865
|
-
1 / (1 + Math.exp(-((next / maxStrain) * 25 - 20))), 0);
|
|
1866
|
-
this.attributes.tapDifficultStrainCount = objectStrains.reduce((total, next) => total + Math.pow(next / maxStrain, 4), 0);
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
/**
|
|
1870
|
-
* Calculates the sum of strains for possible three-fingered sections.
|
|
1871
|
-
*
|
|
1872
|
-
* @param firstObjectIndex The index of the first object in the section.
|
|
1873
|
-
* @param lastObjectIndex The index of the last object in the section.
|
|
1874
|
-
* @returns The summed strain of the section.
|
|
1875
|
-
*/
|
|
1876
|
-
calculateThreeFingerSummedStrain(firstObjectIndex, lastObjectIndex) {
|
|
1877
|
-
return Math.pow(this.objects
|
|
1878
|
-
.slice(firstObjectIndex, lastObjectIndex)
|
|
1879
|
-
.reduce((a, v) => a +
|
|
1880
|
-
v.originalTapStrain /
|
|
1881
|
-
DroidDifficultyCalculator.threeFingerStrainThreshold, 0), 0.75);
|
|
1882
1924
|
}
|
|
1883
1925
|
/**
|
|
1884
1926
|
* Called after rhythm skill calculation.
|
|
@@ -1906,12 +1948,9 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1906
1948
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
1907
1949
|
this.attributes.flashlightDifficulty *= 0.7;
|
|
1908
1950
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
this.attributes.flashlightDifficultStrainCount =
|
|
1913
|
-
objectStrains.reduce((total, next) => total + Math.pow(next / maxStrain, 4), 0);
|
|
1914
|
-
}
|
|
1951
|
+
this.attributes.flashlightDifficulty = this.flashlight;
|
|
1952
|
+
this.attributes.flashlightDifficultStrainCount =
|
|
1953
|
+
flashlightSkill.countDifficultStrains();
|
|
1915
1954
|
}
|
|
1916
1955
|
/**
|
|
1917
1956
|
* Called after visual skill calculation.
|
|
@@ -1928,11 +1967,8 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1928
1967
|
this.starValue(visualSkillWithoutSliders.difficultyValue()) /
|
|
1929
1968
|
this.visual;
|
|
1930
1969
|
}
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
if (maxStrain) {
|
|
1934
|
-
this.attributes.visualDifficultStrainCount = objectStrains.reduce((total, next) => total + Math.pow(next / maxStrain, 4), 0);
|
|
1935
|
-
}
|
|
1970
|
+
this.attributes.visualDifficultStrainCount =
|
|
1971
|
+
visualSkillWithSliders.countDifficultStrains();
|
|
1936
1972
|
}
|
|
1937
1973
|
}
|
|
1938
1974
|
/**
|
|
@@ -2303,7 +2339,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2303
2339
|
*/
|
|
2304
2340
|
calculateAimValue() {
|
|
2305
2341
|
let aimValue = this.baseValue(Math.pow(this.difficultyAttributes.aimDifficulty, 0.8));
|
|
2306
|
-
aimValue *= this.proportionalMissPenalty;
|
|
2342
|
+
aimValue *= Math.min(this.calculateStrainBasedMissPenalty(this.difficultyAttributes.aimDifficultStrainCount), this.proportionalMissPenalty);
|
|
2307
2343
|
// Scale the aim value with estimated full combo deviation.
|
|
2308
2344
|
aimValue *= this.calculateDeviationBasedLengthScaling();
|
|
2309
2345
|
// Scale the aim value with slider factor to nerf very likely dropped sliderends.
|
|
@@ -2343,6 +2379,16 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2343
2379
|
tapValue *=
|
|
2344
2380
|
1.1 *
|
|
2345
2381
|
Math.pow(osuBase.ErrorFunction.erf(20 / (Math.SQRT2 * adjustedDeviation)), 0.625);
|
|
2382
|
+
// Additional scaling for tap value based on average BPM and how "vibroable" the beatmap is.
|
|
2383
|
+
// Higher BPMs require more precise tapping. When the deviation is too high,
|
|
2384
|
+
// it can be assumed that the player taps invariant to rhythm.
|
|
2385
|
+
// We harshen the punishment for such scenario.
|
|
2386
|
+
tapValue *=
|
|
2387
|
+
(1 - Math.pow(this.difficultyAttributes.vibroFactor, 6)) /
|
|
2388
|
+
(1 +
|
|
2389
|
+
Math.exp((this._tapDeviation - 7500 / averageBPM) /
|
|
2390
|
+
((2 * 300) / averageBPM))) +
|
|
2391
|
+
Math.pow(this.difficultyAttributes.vibroFactor, 6);
|
|
2346
2392
|
// Scale the tap value with three-fingered penalty.
|
|
2347
2393
|
tapValue /= this._tapPenalty;
|
|
2348
2394
|
// OD 8 SS stays the same.
|
|
@@ -2383,7 +2429,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2383
2429
|
return 0;
|
|
2384
2430
|
}
|
|
2385
2431
|
let flashlightValue = Math.pow(this.difficultyAttributes.flashlightDifficulty, 1.6) * 25;
|
|
2386
|
-
flashlightValue *= this.proportionalMissPenalty;
|
|
2432
|
+
flashlightValue *= Math.min(this.calculateStrainBasedMissPenalty(this.difficultyAttributes.flashlightDifficultStrainCount), this.proportionalMissPenalty);
|
|
2387
2433
|
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
2388
2434
|
flashlightValue *=
|
|
2389
2435
|
0.7 +
|
|
@@ -2402,7 +2448,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2402
2448
|
*/
|
|
2403
2449
|
calculateVisualValue() {
|
|
2404
2450
|
let visualValue = Math.pow(this.difficultyAttributes.visualDifficulty, 1.6) * 22.5;
|
|
2405
|
-
visualValue *= this.proportionalMissPenalty;
|
|
2451
|
+
visualValue *= Math.min(this.calculateStrainBasedMissPenalty(this.difficultyAttributes.visualDifficultStrainCount), this.proportionalMissPenalty);
|
|
2406
2452
|
// Scale the visual value with estimated full combo deviation.
|
|
2407
2453
|
// As visual is easily "bypassable" with memorization, punish for memorization.
|
|
2408
2454
|
visualValue *= this.calculateDeviationBasedLengthScaling(undefined, true);
|
|
@@ -2514,28 +2560,32 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2514
2560
|
if (greatCountCircles > 0) {
|
|
2515
2561
|
// The probability that a player hits a circle is unknown, but we can estimate it to be
|
|
2516
2562
|
// the number of greats on circles divided by the number of circles, and then add one
|
|
2517
|
-
// to the number of circles as a bias correction
|
|
2518
|
-
const greatProbabilityCircle =
|
|
2563
|
+
// to the number of circles as a bias correction.
|
|
2564
|
+
const greatProbabilityCircle = greatCountCircles /
|
|
2565
|
+
(circleCount - missCountCircles - mehCountCircles + 1);
|
|
2519
2566
|
// Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed.
|
|
2520
2567
|
// Begin with the normal distribution first.
|
|
2521
|
-
|
|
2568
|
+
let deviationOnCircles = hitWindow300 /
|
|
2522
2569
|
(Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbabilityCircle));
|
|
2523
|
-
|
|
2524
|
-
const truncatedVariance = Math.pow(deviationOnCircles, 2) -
|
|
2570
|
+
deviationOnCircles *= Math.sqrt(1 -
|
|
2525
2571
|
(Math.sqrt(2 / Math.PI) *
|
|
2526
2572
|
hitWindow100 *
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2573
|
+
Math.exp(-0.5 *
|
|
2574
|
+
Math.pow(hitWindow100 / deviationOnCircles, 2))) /
|
|
2575
|
+
(deviationOnCircles *
|
|
2576
|
+
osuBase.ErrorFunction.erf(hitWindow100 /
|
|
2577
|
+
(Math.SQRT2 * deviationOnCircles))));
|
|
2530
2578
|
// Then compute the variance for 50s.
|
|
2531
|
-
const mehVariance = (
|
|
2579
|
+
const mehVariance = (hitWindow50 * hitWindow50 +
|
|
2532
2580
|
hitWindow100 * hitWindow50 +
|
|
2533
|
-
|
|
2581
|
+
hitWindow100 * hitWindow100) /
|
|
2534
2582
|
3;
|
|
2535
2583
|
// Find the total deviation.
|
|
2536
|
-
|
|
2584
|
+
deviationOnCircles = Math.sqrt(((greatCountCircles + okCountCircles) *
|
|
2585
|
+
Math.pow(deviationOnCircles, 2) +
|
|
2537
2586
|
mehCountCircles * mehVariance) /
|
|
2538
2587
|
(greatCountCircles + okCountCircles + mehCountCircles));
|
|
2588
|
+
return deviationOnCircles;
|
|
2539
2589
|
}
|
|
2540
2590
|
// If there are more non-300s than there are circles, compute the deviation on sliders instead.
|
|
2541
2591
|
// Here, all that matters is whether or not the slider was missed, since it is impossible
|
|
@@ -2562,23 +2612,59 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2562
2612
|
if (this.totalSuccessfulHits === 0) {
|
|
2563
2613
|
return Number.POSITIVE_INFINITY;
|
|
2564
2614
|
}
|
|
2565
|
-
const
|
|
2615
|
+
const { speedNoteCount, clockRate, overallDifficulty } = this.difficultyAttributes;
|
|
2616
|
+
const hitWindow300 = new osuBase.OsuHitWindow(overallDifficulty).hitWindowFor300();
|
|
2617
|
+
// Obtain the 50 and 100 hit window for droid.
|
|
2618
|
+
const isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
2619
|
+
const droidHitWindow = new osuBase.DroidHitWindow(osuBase.DroidHitWindow.hitWindow300ToOD(hitWindow300 * clockRate, isPrecise));
|
|
2620
|
+
const hitWindow50 = droidHitWindow.hitWindowFor50(isPrecise) / clockRate;
|
|
2621
|
+
const hitWindow100 = droidHitWindow.hitWindowFor100(isPrecise) / clockRate;
|
|
2566
2622
|
const { n100, n50, nmiss } = this.computedAccuracy;
|
|
2567
2623
|
// Assume a fixed ratio of non-300s hit in speed notes based on speed note count ratio and OD.
|
|
2568
|
-
// Graph: https://www.desmos.com/calculator/
|
|
2569
|
-
const speedNoteRatio =
|
|
2624
|
+
// Graph: https://www.desmos.com/calculator/iskvgjkxr4
|
|
2625
|
+
const speedNoteRatio = speedNoteCount / this.totalHits;
|
|
2570
2626
|
const nonGreatCount = n100 + n50 + nmiss;
|
|
2571
2627
|
const nonGreatRatio = 1 -
|
|
2572
2628
|
(Math.pow(Math.exp(Math.sqrt(hitWindow300)) + 1, 1 - speedNoteRatio) -
|
|
2573
2629
|
1) /
|
|
2574
2630
|
Math.exp(Math.sqrt(hitWindow300));
|
|
2575
|
-
const relevantCountGreat = Math.max(0,
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2631
|
+
const relevantCountGreat = Math.max(0, speedNoteCount - nonGreatCount * nonGreatRatio);
|
|
2632
|
+
const relevantCountOk = n100 * nonGreatRatio;
|
|
2633
|
+
const relevantCountMeh = n50 * nonGreatRatio;
|
|
2634
|
+
const relevantCountMiss = nmiss * nonGreatRatio;
|
|
2635
|
+
// Assume 100s, 50s, and misses happen on circles. If there are less non-300s on circles than 300s,
|
|
2636
|
+
// compute the deviation on circles.
|
|
2637
|
+
if (relevantCountGreat > 0) {
|
|
2638
|
+
// The probability that a player hits a circle is unknown, but we can estimate it to be
|
|
2639
|
+
// the number of greats on circles divided by the number of circles, and then add one
|
|
2640
|
+
// to the number of circles as a bias correction.
|
|
2641
|
+
const greatProbabilityCircle = relevantCountGreat /
|
|
2642
|
+
(speedNoteCount - relevantCountMiss - relevantCountMeh + 1);
|
|
2643
|
+
// Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed.
|
|
2644
|
+
// Begin with the normal distribution first.
|
|
2645
|
+
let deviationOnCircles = hitWindow300 /
|
|
2646
|
+
(Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbabilityCircle));
|
|
2647
|
+
deviationOnCircles *= Math.sqrt(1 -
|
|
2648
|
+
(Math.sqrt(2 / Math.PI) *
|
|
2649
|
+
hitWindow100 *
|
|
2650
|
+
Math.exp(-0.5 *
|
|
2651
|
+
Math.pow(hitWindow100 / deviationOnCircles, 2))) /
|
|
2652
|
+
(deviationOnCircles *
|
|
2653
|
+
osuBase.ErrorFunction.erf(hitWindow100 /
|
|
2654
|
+
(Math.SQRT2 * deviationOnCircles))));
|
|
2655
|
+
// Then compute the variance for 50s.
|
|
2656
|
+
const mehVariance = (hitWindow50 * hitWindow50 +
|
|
2657
|
+
hitWindow100 * hitWindow50 +
|
|
2658
|
+
hitWindow100 * hitWindow100) /
|
|
2659
|
+
3;
|
|
2660
|
+
// Find the total deviation.
|
|
2661
|
+
deviationOnCircles = Math.sqrt(((relevantCountGreat + relevantCountOk) *
|
|
2662
|
+
Math.pow(deviationOnCircles, 2) +
|
|
2663
|
+
relevantCountMeh * mehVariance) /
|
|
2664
|
+
(relevantCountGreat + relevantCountOk + relevantCountMeh));
|
|
2665
|
+
return deviationOnCircles;
|
|
2579
2666
|
}
|
|
2580
|
-
|
|
2581
|
-
return (hitWindow300 / (Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbability)));
|
|
2667
|
+
return Number.POSITIVE_INFINITY;
|
|
2582
2668
|
}
|
|
2583
2669
|
toString() {
|
|
2584
2670
|
return (this.total.toFixed(2) +
|
|
@@ -3138,7 +3224,7 @@ class OsuDifficultyHitObject extends DifficultyHitObject {
|
|
|
3138
3224
|
}
|
|
3139
3225
|
/**
|
|
3140
3226
|
* Note: You **must** call `computeProperties` at some point due to how TypeScript handles
|
|
3141
|
-
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue
|
|
3227
|
+
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue).
|
|
3142
3228
|
*
|
|
3143
3229
|
* @param object The underlying hitobject.
|
|
3144
3230
|
* @param lastObject The hitobject before this hitobject.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rian8337/osu-difficulty-calculator",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.20",
|
|
4
4
|
"description": "A module for calculating osu!standard beatmap difficulty and performance value with respect to the current difficulty and performance algorithm.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"osu",
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "8e6d7449a4a949f0158f10b9130378e7368bd8aa"
|
|
42
42
|
}
|
package/typings/index.d.ts
CHANGED
|
@@ -204,16 +204,13 @@ declare abstract class DifficultyHitObject {
|
|
|
204
204
|
private readonly lastLastObject;
|
|
205
205
|
/**
|
|
206
206
|
* Note: You **must** call `computeProperties` at some point due to how TypeScript handles
|
|
207
|
-
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue
|
|
207
|
+
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue).
|
|
208
208
|
*
|
|
209
209
|
* @param object The underlying hitobject.
|
|
210
210
|
* @param lastObject The hitobject before this hitobject.
|
|
211
211
|
* @param lastLastObject The hitobject before the last hitobject.
|
|
212
212
|
* @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
|
|
213
213
|
* @param clockRate The clock rate of the beatmap.
|
|
214
|
-
* @param timePreempt The time preempt with clock rate.
|
|
215
|
-
* @param isForceAR Whether force AR is enabled.
|
|
216
|
-
* @param mode The gamemode to compute properties for.
|
|
217
214
|
*/
|
|
218
215
|
protected constructor(object: PlaceableHitObject, lastObject: PlaceableHitObject | null, lastLastObject: PlaceableHitObject | null, difficultyHitObjects: readonly DifficultyHitObject[], clockRate: number);
|
|
219
216
|
/**
|
|
@@ -476,6 +473,13 @@ declare abstract class StrainSkill extends Skill {
|
|
|
476
473
|
* @param ms The time frame to calculate.
|
|
477
474
|
*/
|
|
478
475
|
protected strainDecay(ms: number): number;
|
|
476
|
+
/**
|
|
477
|
+
* Calculates the starting time of a strain section at an object.
|
|
478
|
+
*
|
|
479
|
+
* @param current The object at which the strain section starts.
|
|
480
|
+
* @returns The start time of the strain section.
|
|
481
|
+
*/
|
|
482
|
+
protected calculateCurrentSectionStart(current: DifficultyHitObject): number;
|
|
479
483
|
/**
|
|
480
484
|
* Calculates the strain value at a hitobject.
|
|
481
485
|
*
|
|
@@ -512,7 +516,27 @@ declare abstract class DroidSkill extends StrainSkill {
|
|
|
512
516
|
* The bonus multiplier that is given for a sequence of notes of equal difficulty.
|
|
513
517
|
*/
|
|
514
518
|
protected abstract readonly starsPerDouble: number;
|
|
519
|
+
protected readonly _objectStrains: number[];
|
|
520
|
+
/**
|
|
521
|
+
* The strains of hitobjects.
|
|
522
|
+
*/
|
|
523
|
+
get objectStrains(): readonly number[];
|
|
524
|
+
/**
|
|
525
|
+
* Returns the number of strains weighed against the top strain.
|
|
526
|
+
*
|
|
527
|
+
* The result is scaled by clock rate as it affects the total number of strains.
|
|
528
|
+
*/
|
|
529
|
+
countDifficultStrains(): number;
|
|
530
|
+
process(current: DifficultyHitObject): void;
|
|
515
531
|
difficultyValue(): number;
|
|
532
|
+
/**
|
|
533
|
+
* Gets the strain of a hitobject.
|
|
534
|
+
*
|
|
535
|
+
* @param current The hitobject to get the strain from.
|
|
536
|
+
* @returns The strain of the hitobject.
|
|
537
|
+
*/
|
|
538
|
+
protected abstract getObjectStrain(current: DifficultyHitObject): number;
|
|
539
|
+
protected calculateCurrentSectionStart(current: DifficultyHitObject): number;
|
|
516
540
|
}
|
|
517
541
|
|
|
518
542
|
/**
|
|
@@ -568,7 +592,7 @@ declare class DroidDifficultyHitObject extends DifficultyHitObject {
|
|
|
568
592
|
protected get scalingFactor(): number;
|
|
569
593
|
/**
|
|
570
594
|
* Note: You **must** call `computeProperties` at some point due to how TypeScript handles
|
|
571
|
-
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue
|
|
595
|
+
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue).
|
|
572
596
|
*
|
|
573
597
|
* @param object The underlying hitobject.
|
|
574
598
|
* @param lastObject The hitobject before this hitobject.
|
|
@@ -597,16 +621,17 @@ declare class DroidDifficultyHitObject extends DifficultyHitObject {
|
|
|
597
621
|
* Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
|
598
622
|
*/
|
|
599
623
|
declare class DroidAim extends DroidSkill {
|
|
600
|
-
protected readonly strainDecayBase
|
|
601
|
-
protected readonly reducedSectionCount
|
|
602
|
-
protected readonly reducedSectionBaseline
|
|
603
|
-
protected readonly starsPerDouble
|
|
624
|
+
protected readonly strainDecayBase = 0.15;
|
|
625
|
+
protected readonly reducedSectionCount = 10;
|
|
626
|
+
protected readonly reducedSectionBaseline = 0.75;
|
|
627
|
+
protected readonly starsPerDouble = 1.05;
|
|
604
628
|
private readonly skillMultiplier;
|
|
605
629
|
private readonly withSliders;
|
|
606
630
|
private currentAimStrain;
|
|
607
631
|
constructor(mods: Mod[], withSliders: boolean);
|
|
608
632
|
protected strainValueAt(current: DroidDifficultyHitObject): number;
|
|
609
633
|
protected calculateInitialStrain(time: number, current: DroidDifficultyHitObject): number;
|
|
634
|
+
protected getObjectStrain(): number;
|
|
610
635
|
/**
|
|
611
636
|
* @param current The hitobject to save to.
|
|
612
637
|
*/
|
|
@@ -617,9 +642,9 @@ declare class DroidAim extends DroidSkill {
|
|
|
617
642
|
* An evaluator for calculating osu!droid Aim skill.
|
|
618
643
|
*/
|
|
619
644
|
declare abstract class DroidAimEvaluator extends AimEvaluator {
|
|
620
|
-
protected static readonly wideAngleMultiplier
|
|
621
|
-
protected static readonly sliderMultiplier
|
|
622
|
-
protected static readonly velocityChangeMultiplier
|
|
645
|
+
protected static readonly wideAngleMultiplier = 1.65;
|
|
646
|
+
protected static readonly sliderMultiplier = 1.5;
|
|
647
|
+
protected static readonly velocityChangeMultiplier = 0.85;
|
|
623
648
|
private static readonly singleSpacingThreshold;
|
|
624
649
|
private static readonly minSpeedBonus;
|
|
625
650
|
/**
|
|
@@ -680,6 +705,14 @@ interface DroidDifficultyAttributes extends DifficultyAttributes {
|
|
|
680
705
|
* The average delta time of speed objects.
|
|
681
706
|
*/
|
|
682
707
|
averageSpeedDeltaTime: number;
|
|
708
|
+
/**
|
|
709
|
+
* Describes how much of tap difficulty is contributed by notes that are "vibroable".
|
|
710
|
+
*
|
|
711
|
+
* A value closer to 1 indicates most of tap difficulty is contributed by notes that are not "vibroable".
|
|
712
|
+
*
|
|
713
|
+
* A value closer to 0 indicates most of tap difficulty is contributed by notes that are "vibroable".
|
|
714
|
+
*/
|
|
715
|
+
vibroFactor: number;
|
|
683
716
|
}
|
|
684
717
|
|
|
685
718
|
/**
|
|
@@ -785,7 +818,6 @@ declare class DroidDifficultyCalculator extends DifficultyCalculator<DroidDiffic
|
|
|
785
818
|
get cacheableAttributes(): CacheableDifficultyAttributes<DroidDifficultyAttributes>;
|
|
786
819
|
protected readonly difficultyMultiplier = 0.18;
|
|
787
820
|
protected readonly mode = Modes.droid;
|
|
788
|
-
calculate(options?: DroidDifficultyCalculationOptions): this;
|
|
789
821
|
/**
|
|
790
822
|
* Calculates the aim star rating of the beatmap and stores it in this instance.
|
|
791
823
|
*/
|
|
@@ -827,20 +859,13 @@ declare class DroidDifficultyCalculator extends DifficultyCalculator<DroidDiffic
|
|
|
827
859
|
* Called after tap skill calculation.
|
|
828
860
|
*
|
|
829
861
|
* @param tapSkillCheese The tap skill that considers cheesing.
|
|
862
|
+
* @param tapSkillVibro The tap skill that considers vibro.
|
|
830
863
|
*/
|
|
831
864
|
private postCalculateTap;
|
|
832
865
|
/**
|
|
833
866
|
* Calculates tap-related attributes.
|
|
834
867
|
*/
|
|
835
868
|
private calculateTapAttributes;
|
|
836
|
-
/**
|
|
837
|
-
* Calculates the sum of strains for possible three-fingered sections.
|
|
838
|
-
*
|
|
839
|
-
* @param firstObjectIndex The index of the first object in the section.
|
|
840
|
-
* @param lastObjectIndex The index of the last object in the section.
|
|
841
|
-
* @returns The summed strain of the section.
|
|
842
|
-
*/
|
|
843
|
-
private calculateThreeFingerSummedStrain;
|
|
844
869
|
/**
|
|
845
870
|
* Called after rhythm skill calculation.
|
|
846
871
|
*
|
|
@@ -867,10 +892,10 @@ declare class DroidDifficultyCalculator extends DifficultyCalculator<DroidDiffic
|
|
|
867
892
|
* Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
|
|
868
893
|
*/
|
|
869
894
|
declare class DroidFlashlight extends DroidSkill {
|
|
870
|
-
protected readonly strainDecayBase
|
|
871
|
-
protected readonly reducedSectionCount
|
|
872
|
-
protected readonly reducedSectionBaseline
|
|
873
|
-
protected readonly starsPerDouble
|
|
895
|
+
protected readonly strainDecayBase = 0.15;
|
|
896
|
+
protected readonly reducedSectionCount = 0;
|
|
897
|
+
protected readonly reducedSectionBaseline = 1;
|
|
898
|
+
protected readonly starsPerDouble = 1.06;
|
|
874
899
|
private readonly skillMultiplier;
|
|
875
900
|
private readonly isHidden;
|
|
876
901
|
private readonly withSliders;
|
|
@@ -878,6 +903,7 @@ declare class DroidFlashlight extends DroidSkill {
|
|
|
878
903
|
constructor(mods: Mod[], withSliders: boolean);
|
|
879
904
|
protected strainValueAt(current: DroidDifficultyHitObject): number;
|
|
880
905
|
protected calculateInitialStrain(time: number, current: DifficultyHitObject): number;
|
|
906
|
+
protected getObjectStrain(): number;
|
|
881
907
|
protected saveToHitObject(current: DroidDifficultyHitObject): void;
|
|
882
908
|
difficultyValue(): number;
|
|
883
909
|
}
|
|
@@ -1099,7 +1125,7 @@ declare class DroidPerformanceCalculator extends PerformanceCalculator<DroidDiff
|
|
|
1099
1125
|
*/
|
|
1100
1126
|
get visualSliderCheesePenalty(): number;
|
|
1101
1127
|
protected finalMultiplier: number;
|
|
1102
|
-
protected readonly mode
|
|
1128
|
+
protected readonly mode = Modes.droid;
|
|
1103
1129
|
private _aimSliderCheesePenalty;
|
|
1104
1130
|
private _flashlightSliderCheesePenalty;
|
|
1105
1131
|
private _visualSliderCheesePenalty;
|
|
@@ -1211,16 +1237,17 @@ declare class DroidPerformanceCalculator extends PerformanceCalculator<DroidDiff
|
|
|
1211
1237
|
* Represents the skill required to properly follow a beatmap's rhythm.
|
|
1212
1238
|
*/
|
|
1213
1239
|
declare class DroidRhythm extends DroidSkill {
|
|
1214
|
-
protected readonly reducedSectionCount
|
|
1215
|
-
protected readonly reducedSectionBaseline
|
|
1216
|
-
protected readonly strainDecayBase
|
|
1217
|
-
protected readonly starsPerDouble
|
|
1240
|
+
protected readonly reducedSectionCount = 5;
|
|
1241
|
+
protected readonly reducedSectionBaseline = 0.75;
|
|
1242
|
+
protected readonly strainDecayBase = 0.3;
|
|
1243
|
+
protected readonly starsPerDouble = 1.75;
|
|
1218
1244
|
private currentRhythmStrain;
|
|
1219
1245
|
private currentRhythmMultiplier;
|
|
1220
1246
|
private readonly hitWindow;
|
|
1221
1247
|
constructor(mods: Mod[], overallDifficulty: number);
|
|
1222
1248
|
protected strainValueAt(current: DroidDifficultyHitObject): number;
|
|
1223
1249
|
protected calculateInitialStrain(time: number, current: DroidDifficultyHitObject): number;
|
|
1250
|
+
protected getObjectStrain(): number;
|
|
1224
1251
|
protected saveToHitObject(current: DroidDifficultyHitObject): void;
|
|
1225
1252
|
}
|
|
1226
1253
|
|
|
@@ -1252,18 +1279,33 @@ declare abstract class DroidRhythmEvaluator extends RhythmEvaluator {
|
|
|
1252
1279
|
* Represents the skill required to press keys or tap with regards to keeping up with the speed at which objects need to be hit.
|
|
1253
1280
|
*/
|
|
1254
1281
|
declare class DroidTap extends DroidSkill {
|
|
1255
|
-
protected readonly reducedSectionCount
|
|
1256
|
-
protected readonly reducedSectionBaseline
|
|
1257
|
-
protected readonly strainDecayBase
|
|
1258
|
-
protected readonly starsPerDouble
|
|
1282
|
+
protected readonly reducedSectionCount = 10;
|
|
1283
|
+
protected readonly reducedSectionBaseline = 0.75;
|
|
1284
|
+
protected readonly strainDecayBase = 0.3;
|
|
1285
|
+
protected readonly starsPerDouble = 1.1;
|
|
1259
1286
|
private currentTapStrain;
|
|
1260
1287
|
private currentRhythmMultiplier;
|
|
1261
1288
|
private readonly skillMultiplier;
|
|
1262
1289
|
private readonly greatWindow;
|
|
1263
1290
|
private readonly considerCheesability;
|
|
1264
|
-
|
|
1291
|
+
private readonly strainTimeCap?;
|
|
1292
|
+
private readonly _objectDeltaTimes;
|
|
1293
|
+
/**
|
|
1294
|
+
* The delta time of hitobjects.
|
|
1295
|
+
*/
|
|
1296
|
+
get objectDeltaTimes(): readonly number[];
|
|
1297
|
+
constructor(mods: Mod[], overallDifficulty: number, considerCheesability: boolean, strainTimeCap?: number);
|
|
1298
|
+
/**
|
|
1299
|
+
* The amount of notes that are relevant to the difficulty.
|
|
1300
|
+
*/
|
|
1301
|
+
relevantNoteCount(): number;
|
|
1302
|
+
/**
|
|
1303
|
+
* The delta time relevant to the difficulty.
|
|
1304
|
+
*/
|
|
1305
|
+
relevantDeltaTime(): number;
|
|
1265
1306
|
protected strainValueAt(current: DroidDifficultyHitObject): number;
|
|
1266
1307
|
protected calculateInitialStrain(time: number, current: DroidDifficultyHitObject): number;
|
|
1308
|
+
protected getObjectStrain(): number;
|
|
1267
1309
|
/**
|
|
1268
1310
|
* @param current The hitobject to save to.
|
|
1269
1311
|
*/
|
|
@@ -1288,23 +1330,25 @@ declare abstract class DroidTapEvaluator extends SpeedEvaluator {
|
|
|
1288
1330
|
*
|
|
1289
1331
|
* - time between pressing the previous and current object,
|
|
1290
1332
|
* - distance between those objects,
|
|
1291
|
-
* -
|
|
1333
|
+
* - how easily they can be cheesed,
|
|
1334
|
+
* - and the strain time cap.
|
|
1292
1335
|
*
|
|
1293
1336
|
* @param current The current object.
|
|
1294
1337
|
* @param greatWindow The great hit window of the current object.
|
|
1295
1338
|
* @param considerCheesability Whether to consider cheesability.
|
|
1339
|
+
* @param strainTimeCap The strain time to cap the object's strain time to.
|
|
1296
1340
|
*/
|
|
1297
|
-
static evaluateDifficultyOf(current: DroidDifficultyHitObject, greatWindow: number, considerCheesability: boolean): number;
|
|
1341
|
+
static evaluateDifficultyOf(current: DroidDifficultyHitObject, greatWindow: number, considerCheesability: boolean, strainTimeCap?: number): number;
|
|
1298
1342
|
}
|
|
1299
1343
|
|
|
1300
1344
|
/**
|
|
1301
1345
|
* Represents the skill required to read every object in the map.
|
|
1302
1346
|
*/
|
|
1303
1347
|
declare class DroidVisual extends DroidSkill {
|
|
1304
|
-
protected readonly starsPerDouble
|
|
1305
|
-
protected readonly reducedSectionCount
|
|
1306
|
-
protected readonly reducedSectionBaseline
|
|
1307
|
-
protected readonly strainDecayBase
|
|
1348
|
+
protected readonly starsPerDouble = 1.025;
|
|
1349
|
+
protected readonly reducedSectionCount = 10;
|
|
1350
|
+
protected readonly reducedSectionBaseline = 0.75;
|
|
1351
|
+
protected readonly strainDecayBase = 0.1;
|
|
1308
1352
|
private readonly isHidden;
|
|
1309
1353
|
private readonly withSliders;
|
|
1310
1354
|
private currentVisualStrain;
|
|
@@ -1313,11 +1357,12 @@ declare class DroidVisual extends DroidSkill {
|
|
|
1313
1357
|
constructor(mods: Mod[], withSliders: boolean);
|
|
1314
1358
|
protected strainValueAt(current: DroidDifficultyHitObject): number;
|
|
1315
1359
|
protected calculateInitialStrain(time: number, current: DroidDifficultyHitObject): number;
|
|
1360
|
+
protected getObjectStrain(): number;
|
|
1316
1361
|
protected saveToHitObject(current: DroidDifficultyHitObject): void;
|
|
1317
1362
|
}
|
|
1318
1363
|
|
|
1319
1364
|
/**
|
|
1320
|
-
* An evaluator for calculating osu!droid
|
|
1365
|
+
* An evaluator for calculating osu!droid visual skill.
|
|
1321
1366
|
*/
|
|
1322
1367
|
declare abstract class DroidVisualEvaluator {
|
|
1323
1368
|
/**
|
|
@@ -1377,7 +1422,7 @@ declare class OsuDifficultyHitObject extends DifficultyHitObject {
|
|
|
1377
1422
|
protected get scalingFactor(): number;
|
|
1378
1423
|
/**
|
|
1379
1424
|
* Note: You **must** call `computeProperties` at some point due to how TypeScript handles
|
|
1380
|
-
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue
|
|
1425
|
+
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue).
|
|
1381
1426
|
*
|
|
1382
1427
|
* @param object The underlying hitobject.
|
|
1383
1428
|
* @param lastObject The hitobject before this hitobject.
|