@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 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
- * - and how easily they can be cheesed.
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 (current.strainTime < this.minSpeedBonus) {
831
+ if (strainTime < this.minSpeedBonus) {
786
832
  speedBonus +=
787
833
  0.75 *
788
- Math.pow(osuBase.ErrorFunction.erf((this.minSpeedBonus - current.strainTime) / 40), 2);
834
+ Math.pow(osuBase.ErrorFunction.erf((this.minSpeedBonus - strainTime) / 40), 2);
789
835
  }
790
- return (speedBonus * Math.pow(doubletapness, 1.5)) / current.strainTime;
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
- constructor(mods, overallDifficulty, considerCheesability) {
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
- const decay = this.strainDecay(current.strainTime);
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 Visual skill.
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.3) / 100;
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 / (3 * this.object.radius)) *
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.postCalculateTap(tapSkillCheese);
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
- if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
1765
- this.attributes.tapDifficulty = 0;
1766
- this.attributes.possibleThreeFingeredSections = [];
1767
- }
1768
- else {
1769
- this.attributes.tapDifficulty = this.starValue(tapSkillCheese.difficultyValue());
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 tempSections = [];
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 firstObjectIndex = 0;
1785
- for (let i = 0; i < this.objects.length - 1; ++i) {
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 next = this.objects[i + 1];
1788
- if (i === 0) {
1789
- objectStrains.push(current.tapStrain);
1790
- objectDeltaTimes.push(current.deltaTime);
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
- objectStrains.push(next.tapStrain);
1793
- objectDeltaTimes.push(next.deltaTime);
1794
- maxStrain = Math.max(current.tapStrain, maxStrain);
1795
- const realDeltaTime = next.object.startTime - current.object.endTime;
1796
- if (realDeltaTime >= maxSectionDeltaTime) {
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 - firstObjectIndex < minSectionObjectCount) {
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: newFirstObjectIndex,
1850
- lastObjectIndex: section.lastObjectIndex,
1851
- sumStrain: this.calculateThreeFingerSummedStrain(newFirstObjectIndex, section.lastObjectIndex),
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
- const objectStrains = this.objects.map((v) => v.flashlightStrainWithSliders);
1910
- const maxStrain = Math.max(...objectStrains);
1911
- if (maxStrain) {
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
- const objectStrains = this.objects.map((v) => v.flashlightStrainWithSliders);
1932
- const maxStrain = Math.max(...objectStrains);
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 / bayesian prior.
2518
- const greatProbabilityCircle = Math.max(0, greatCountCircles / (greatCountCircles + okCountCircles + 1));
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
- const deviationOnCircles = hitWindow300 /
2568
+ let deviationOnCircles = hitWindow300 /
2522
2569
  (Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbabilityCircle));
2523
- // Get the variance of the truncated variable.
2524
- const truncatedVariance = Math.pow(deviationOnCircles, 2) -
2570
+ deviationOnCircles *= Math.sqrt(1 -
2525
2571
  (Math.sqrt(2 / Math.PI) *
2526
2572
  hitWindow100 *
2527
- deviationOnCircles *
2528
- Math.exp(-0.5 * Math.pow(hitWindow100 / deviationOnCircles, 2))) /
2529
- osuBase.ErrorFunction.erf(hitWindow100 / (Math.SQRT2 * deviationOnCircles));
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 = (Math.pow(hitWindow50, 2) +
2579
+ const mehVariance = (hitWindow50 * hitWindow50 +
2532
2580
  hitWindow100 * hitWindow50 +
2533
- Math.pow(hitWindow100, 2)) /
2581
+ hitWindow100 * hitWindow100) /
2534
2582
  3;
2535
2583
  // Find the total deviation.
2536
- return Math.sqrt(((greatCountCircles + okCountCircles) * truncatedVariance +
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 hitWindow300 = new osuBase.OsuHitWindow(this.difficultyAttributes.overallDifficulty).hitWindowFor300();
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/31argjcxqc
2569
- const speedNoteRatio = this.difficultyAttributes.speedNoteCount / this.totalHits;
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, this.difficultyAttributes.speedNoteCount -
2576
- nonGreatCount * nonGreatRatio);
2577
- if (relevantCountGreat === 0) {
2578
- return Number.POSITIVE_INFINITY;
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
- const greatProbability = relevantCountGreat / (this.difficultyAttributes.speedNoteCount + 1);
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.19",
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": "45f7c976ba683027dc3a4d29de8bf289aadf7707"
41
+ "gitHead": "8e6d7449a4a949f0158f10b9130378e7368bd8aa"
42
42
  }
@@ -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: number;
601
- protected readonly reducedSectionCount: number;
602
- protected readonly reducedSectionBaseline: number;
603
- protected readonly starsPerDouble: number;
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: number;
621
- protected static readonly sliderMultiplier: number;
622
- protected static readonly velocityChangeMultiplier: number;
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: number;
871
- protected readonly reducedSectionCount: number;
872
- protected readonly reducedSectionBaseline: number;
873
- protected readonly starsPerDouble: number;
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: Modes;
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: number;
1215
- protected readonly reducedSectionBaseline: number;
1216
- protected readonly strainDecayBase: number;
1217
- protected readonly starsPerDouble: number;
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: number;
1256
- protected readonly reducedSectionBaseline: number;
1257
- protected readonly strainDecayBase: number;
1258
- protected readonly starsPerDouble: number;
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
- constructor(mods: Mod[], overallDifficulty: number, considerCheesability: boolean);
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
- * - and how easily they can be cheesed.
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: number;
1305
- protected readonly reducedSectionCount: number;
1306
- protected readonly reducedSectionBaseline: number;
1307
- protected readonly strainDecayBase: number;
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 Visual skill.
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.