@rian8337/osu-difficulty-calculator 3.0.0-beta.19 → 3.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
@@ -573,13 +573,6 @@ class DifficultyCalculator {
573
573
  speed: [],
574
574
  flashlight: [],
575
575
  };
576
- /**
577
- * Additional data that is used in performance calculation.
578
- */
579
- attributes = {
580
- speedNoteCount: 0,
581
- sliderFactor: 1,
582
- };
583
576
  sectionLength = 400;
584
577
  /**
585
578
  * Constructs a new instance of the calculator.
@@ -615,6 +608,7 @@ class DifficultyCalculator {
615
608
  speedMultiplier: options?.stats?.speedMultiplier,
616
609
  oldStatistics: options?.stats?.oldStatistics,
617
610
  }).calculate({ mode: this.mode });
611
+ this.populateDifficultyAttributes();
618
612
  this.generateDifficultyHitObjects();
619
613
  this.calculateAll();
620
614
  return this;
@@ -650,6 +644,18 @@ class DifficultyCalculator {
650
644
  });
651
645
  });
652
646
  }
647
+ /**
648
+ * Populates the stored difficulty attributes with necessary data.
649
+ */
650
+ populateDifficultyAttributes() {
651
+ this.attributes.approachRate = this.stats.ar;
652
+ this.attributes.hitCircleCount = this.beatmap.hitObjects.circles;
653
+ this.attributes.maxCombo = this.beatmap.maxCombo;
654
+ this.attributes.mods = this.mods.slice();
655
+ this.attributes.overallDifficulty = this.stats.od;
656
+ this.attributes.sliderCount = this.beatmap.hitObjects.sliders;
657
+ this.attributes.spinnerCount = this.beatmap.hitObjects.spinners;
658
+ }
653
659
  /**
654
660
  * Calculates the star rating value of a difficulty.
655
661
  *
@@ -1484,6 +1490,23 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1484
1490
  visual = 0;
1485
1491
  difficultyMultiplier = 0.18;
1486
1492
  mode = osuBase.Modes.droid;
1493
+ attributes = {
1494
+ tapDifficulty: 0,
1495
+ rhythmDifficulty: 0,
1496
+ visualDifficulty: 0,
1497
+ mods: [],
1498
+ starRating: 0,
1499
+ maxCombo: 0,
1500
+ aimDifficulty: 0,
1501
+ flashlightDifficulty: 0,
1502
+ speedNoteCount: 0,
1503
+ sliderFactor: 0,
1504
+ approachRate: 0,
1505
+ overallDifficulty: 0,
1506
+ hitCircleCount: 0,
1507
+ sliderCount: 0,
1508
+ spinnerCount: 0,
1509
+ };
1487
1510
  /**
1488
1511
  * Calculates the aim star rating of the beatmap and stores it in this instance.
1489
1512
  */
@@ -1640,6 +1663,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1640
1663
  if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
1641
1664
  this.aim *= 0.9;
1642
1665
  }
1666
+ this.attributes.aimDifficulty = this.aim;
1643
1667
  }
1644
1668
  /**
1645
1669
  * Called after tap skill calculation.
@@ -1648,7 +1672,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1648
1672
  */
1649
1673
  postCalculateTap(tapSkill) {
1650
1674
  this.strainPeaks.speed = tapSkill.strainPeaks;
1651
- this.tap = this.starValue(tapSkill.difficultyValue());
1675
+ this.tap = this.attributes.tapDifficulty = this.starValue(tapSkill.difficultyValue());
1652
1676
  }
1653
1677
  /**
1654
1678
  * Calculates speed-related attributes.
@@ -1666,7 +1690,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1666
1690
  * @param rhythmSkill The rhythm skill.
1667
1691
  */
1668
1692
  postCalculateRhythm(rhythmSkill) {
1669
- this.rhythm = this.mods.some((m) => m instanceof osuBase.ModRelax)
1693
+ this.rhythm = this.attributes.rhythmDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
1670
1694
  ? 0
1671
1695
  : this.starValue(rhythmSkill.difficultyValue());
1672
1696
  }
@@ -1681,6 +1705,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1681
1705
  if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
1682
1706
  this.flashlight *= 0.7;
1683
1707
  }
1708
+ this.attributes.flashlightDifficulty = this.flashlight;
1684
1709
  }
1685
1710
  /**
1686
1711
  * Called after visual skill calculation.
@@ -1688,7 +1713,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1688
1713
  * @param visualSkill The visual skill.
1689
1714
  */
1690
1715
  postCalculateVisual(visualSkill) {
1691
- this.visual = this.mods.some((m) => m instanceof osuBase.ModRelax)
1716
+ this.visual = this.attributes.visualDifficulty = this.mods.some((m) => m instanceof osuBase.ModRelax)
1692
1717
  ? 0
1693
1718
  : this.starValue(visualSkill.difficultyValue());
1694
1719
  }
@@ -1706,10 +1731,6 @@ class PerformanceCalculator {
1706
1731
  * The calculated accuracy.
1707
1732
  */
1708
1733
  computedAccuracy = new osuBase.Accuracy({});
1709
- /**
1710
- * The difficulty calculator that is being calculated.
1711
- */
1712
- difficultyCalculator;
1713
1734
  /**
1714
1735
  * Penalty for combo breaks.
1715
1736
  */
@@ -1722,12 +1743,6 @@ class PerformanceCalculator {
1722
1743
  * Nerf factor used for nerfing beatmaps with very likely dropped sliderends.
1723
1744
  */
1724
1745
  sliderNerfFactor = 1;
1725
- /**
1726
- * @param difficultyCalculator The difficulty calculator to calculate.
1727
- */
1728
- constructor(difficultyCalculator) {
1729
- this.difficultyCalculator = difficultyCalculator;
1730
- }
1731
1746
  /**
1732
1747
  * Calculates the performance points of the beatmap.
1733
1748
  *
@@ -1740,6 +1755,14 @@ class PerformanceCalculator {
1740
1755
  this.calculateTotalValue();
1741
1756
  return this;
1742
1757
  }
1758
+ /**
1759
+ * The total hits that can be done in the beatmap.
1760
+ */
1761
+ get totalHits() {
1762
+ return (this.difficultyAttributes.hitCircleCount +
1763
+ this.difficultyAttributes.sliderCount +
1764
+ this.difficultyAttributes.spinnerCount);
1765
+ }
1743
1766
  /**
1744
1767
  * Calculates the base performance value of a star rating.
1745
1768
  */
@@ -1752,7 +1775,7 @@ class PerformanceCalculator {
1752
1775
  * @param options Options for performance calculation.
1753
1776
  */
1754
1777
  handleOptions(options) {
1755
- const maxCombo = this.difficultyCalculator.beatmap.maxCombo;
1778
+ const maxCombo = this.difficultyAttributes.maxCombo;
1756
1779
  const miss = this.computedAccuracy.nmiss;
1757
1780
  const combo = options?.combo ?? maxCombo - miss;
1758
1781
  this.comboPenalty = Math.min(Math.pow(combo / maxCombo, 0.8), 1);
@@ -1761,7 +1784,7 @@ class PerformanceCalculator {
1761
1784
  this.computedAccuracy = new osuBase.Accuracy(options.accPercent);
1762
1785
  if (this.computedAccuracy.n300 <= 0) {
1763
1786
  this.computedAccuracy.n300 =
1764
- this.difficultyCalculator.objects.length -
1787
+ this.totalHits -
1765
1788
  this.computedAccuracy.n100 -
1766
1789
  this.computedAccuracy.n50 -
1767
1790
  this.computedAccuracy.nmiss;
@@ -1770,49 +1793,50 @@ class PerformanceCalculator {
1770
1793
  else {
1771
1794
  this.computedAccuracy = new osuBase.Accuracy({
1772
1795
  percent: options?.accPercent,
1773
- nobjects: this.difficultyCalculator.objects.length,
1796
+ nobjects: this.totalHits,
1774
1797
  nmiss: options?.miss || 0,
1775
1798
  });
1776
1799
  }
1777
1800
  this.effectiveMissCount = this.calculateEffectiveMissCount(combo, maxCombo);
1778
- if (this.difficultyCalculator.mods.some((m) => m instanceof osuBase.ModNoFail)) {
1801
+ if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModNoFail)) {
1779
1802
  this.finalMultiplier *= Math.max(0.9, 1 - 0.02 * this.effectiveMissCount);
1780
1803
  }
1781
- if (this.difficultyCalculator.mods.some((m) => m instanceof osuBase.ModSpunOut)) {
1804
+ if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModSpunOut)) {
1782
1805
  this.finalMultiplier *=
1783
1806
  1 -
1784
- Math.pow(this.difficultyCalculator.beatmap.hitObjects.spinners /
1785
- this.difficultyCalculator.objects.length, 0.85);
1807
+ Math.pow(this.difficultyAttributes.spinnerCount / this.totalHits, 0.85);
1786
1808
  }
1787
- if (this.difficultyCalculator.mods.some((m) => m instanceof osuBase.ModRelax)) {
1809
+ if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax)) {
1788
1810
  // Graph: https://www.desmos.com/calculator/bc9eybdthb
1789
1811
  // We use OD13.3 as maximum since it's the value at which great hit window becomes 0.
1790
- const n100Multiplier = Math.max(0, this.difficultyCalculator.stats.od > 0
1812
+ const n100Multiplier = Math.max(0, this.difficultyAttributes.overallDifficulty > 0
1791
1813
  ? 1 -
1792
- Math.pow(this.difficultyCalculator.stats.od / 13.33, 1.8)
1814
+ Math.pow(this.difficultyAttributes.overallDifficulty /
1815
+ 13.33, 1.8)
1793
1816
  : 1);
1794
- const n50Multiplier = Math.max(0, this.difficultyCalculator.stats.od > 0.0
1817
+ const n50Multiplier = Math.max(0, this.difficultyAttributes.overallDifficulty > 0.0
1795
1818
  ? 1 -
1796
- Math.pow(this.difficultyCalculator.stats.od / 13.33, 5)
1819
+ Math.pow(this.difficultyAttributes.overallDifficulty /
1820
+ 13.33, 5)
1797
1821
  : 1);
1798
1822
  // As we're adding 100s and 50s to an approximated number of combo breaks, the result can be higher
1799
1823
  // than total hits in specific scenarios (which breaks some calculations), so we need to clamp it.
1800
1824
  this.effectiveMissCount = Math.min(this.effectiveMissCount +
1801
1825
  this.computedAccuracy.n100 * n100Multiplier +
1802
- this.computedAccuracy.n50 * n50Multiplier, this.difficultyCalculator.objects.length);
1826
+ this.computedAccuracy.n50 * n50Multiplier, this.totalHits);
1803
1827
  }
1804
- if (this.difficultyCalculator.beatmap.hitObjects.sliders > 0) {
1828
+ if (this.difficultyAttributes.sliderCount > 0) {
1805
1829
  // We assume 15% of sliders in a beatmap are difficult since there's no way to tell from the performance calculator.
1806
- const estimateDifficultSliders = this.difficultyCalculator.beatmap.hitObjects.sliders * 0.15;
1830
+ const estimateDifficultSliders = this.difficultyAttributes.sliderCount * 0.15;
1807
1831
  const estimateSliderEndsDropped = osuBase.MathUtils.clamp(Math.min(this.computedAccuracy.n300 +
1808
1832
  this.computedAccuracy.n50 +
1809
1833
  this.computedAccuracy.nmiss, maxCombo - combo), 0, estimateDifficultSliders);
1810
1834
  this.sliderNerfFactor =
1811
- (1 - this.difficultyCalculator.attributes.sliderFactor) *
1835
+ (1 - this.difficultyAttributes.sliderFactor) *
1812
1836
  Math.pow(1 -
1813
1837
  estimateSliderEndsDropped /
1814
1838
  estimateDifficultSliders, 3) +
1815
- this.difficultyCalculator.attributes.sliderFactor;
1839
+ this.difficultyAttributes.sliderFactor;
1816
1840
  }
1817
1841
  }
1818
1842
  /**
@@ -1820,14 +1844,13 @@ class PerformanceCalculator {
1820
1844
  */
1821
1845
  calculateEffectiveMissCount(combo, maxCombo) {
1822
1846
  let comboBasedMissCount = 0;
1823
- if (this.difficultyCalculator.beatmap.hitObjects.sliders > 0) {
1824
- const fullComboThreshold = maxCombo -
1825
- 0.1 * this.difficultyCalculator.beatmap.hitObjects.sliders;
1847
+ if (this.difficultyAttributes.sliderCount > 0) {
1848
+ const fullComboThreshold = maxCombo - 0.1 * this.difficultyAttributes.sliderCount;
1826
1849
  if (combo < fullComboThreshold) {
1827
1850
  comboBasedMissCount = Math.min(fullComboThreshold / Math.max(1, combo), this.mode === osuBase.Modes.droid
1828
1851
  ? // We're clamping miss count because since it's derived from combo, it can
1829
1852
  // be higher than the amount of objects and that breaks some calculations.
1830
- this.difficultyCalculator.objects.length
1853
+ this.totalHits
1831
1854
  : // Clamp miss count to maximum amount of possible breaks.
1832
1855
  this.computedAccuracy.n300 +
1833
1856
  this.computedAccuracy.n100 +
@@ -1871,8 +1894,16 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
1871
1894
  return this._tapPenalty;
1872
1895
  }
1873
1896
  _tapPenalty = 1;
1897
+ difficultyAttributes;
1874
1898
  finalMultiplier = 1.24;
1875
1899
  mode = osuBase.Modes.droid;
1900
+ /**
1901
+ * @param difficultyAttributes The difficulty attributes to calculate.
1902
+ */
1903
+ constructor(difficultyAttributes) {
1904
+ super();
1905
+ this.difficultyAttributes = osuBase.Utils.deepCopy(difficultyAttributes);
1906
+ }
1876
1907
  /**
1877
1908
  * Applies a tap penalty value to this calculator.
1878
1909
  *
@@ -1914,24 +1945,23 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
1914
1945
  * Calculates the aim performance value of the beatmap.
1915
1946
  */
1916
1947
  calculateAimValue() {
1917
- // Global variables
1918
- const objectCount = this.difficultyCalculator.objects.length;
1919
- this.aim = this.baseValue(Math.pow(this.difficultyCalculator.aim, 0.8));
1948
+ this.aim = this.baseValue(Math.pow(this.difficultyAttributes.aimDifficulty, 0.8));
1920
1949
  if (this.effectiveMissCount > 0) {
1921
1950
  // Penalize misses by assessing # of misses relative to the total # of objects.
1922
1951
  // Default a 3% reduction for any # of misses.
1923
1952
  this.aim *=
1924
1953
  0.97 *
1925
- Math.pow(1 - Math.pow(this.effectiveMissCount / objectCount, 0.775), this.effectiveMissCount);
1954
+ Math.pow(1 -
1955
+ Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
1926
1956
  }
1927
1957
  // Combo scaling
1928
1958
  this.aim *= this.comboPenalty;
1929
1959
  // Scale the aim value with slider factor to nerf very likely dropped sliderends.
1930
1960
  this.aim *= this.sliderNerfFactor;
1931
1961
  // Scale the aim value with accuracy.
1932
- this.aim *= this.computedAccuracy.value(objectCount);
1962
+ this.aim *= this.computedAccuracy.value(this.totalHits);
1933
1963
  // It is also important to consider accuracy difficulty when doing that.
1934
- const od = this.difficultyCalculator.stats.od;
1964
+ const od = this.difficultyAttributes.overallDifficulty;
1935
1965
  const odScaling = Math.pow(od, 2) / 2500;
1936
1966
  this.aim *= 0.98 + (od >= 0 ? odScaling : -odScaling);
1937
1967
  }
@@ -1939,15 +1969,14 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
1939
1969
  * Calculates the tap performance value of the beatmap.
1940
1970
  */
1941
1971
  calculateTapValue() {
1942
- // Global variables
1943
- const objectCount = this.difficultyCalculator.objects.length;
1944
- this.tap = this.baseValue(this.difficultyCalculator.tap);
1972
+ this.tap = this.baseValue(this.difficultyAttributes.tapDifficulty);
1945
1973
  if (this.effectiveMissCount > 0) {
1946
1974
  // Penalize misses by assessing # of misses relative to the total # of objects.
1947
1975
  // Default a 3% reduction for any # of misses.
1948
1976
  this.tap *=
1949
1977
  0.97 *
1950
- Math.pow(1 - Math.pow(this.effectiveMissCount / objectCount, 0.775), Math.pow(this.effectiveMissCount, 0.875));
1978
+ Math.pow(1 -
1979
+ Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
1951
1980
  }
1952
1981
  // Combo scaling
1953
1982
  this.tap *= this.comboPenalty;
@@ -1955,7 +1984,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
1955
1984
  const countGreat = this.computedAccuracy.n300;
1956
1985
  const countOk = this.computedAccuracy.n100;
1957
1986
  const countMeh = this.computedAccuracy.n50;
1958
- const relevantTotalDiff = objectCount - this.difficultyCalculator.attributes.speedNoteCount;
1987
+ const relevantTotalDiff = this.totalHits - this.difficultyAttributes.speedNoteCount;
1959
1988
  const relevantAccuracy = new osuBase.Accuracy({
1960
1989
  n300: Math.max(0, countGreat - relevantTotalDiff),
1961
1990
  n100: Math.max(0, countOk - Math.max(0, relevantTotalDiff - countGreat)),
@@ -1963,15 +1992,15 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
1963
1992
  nmiss: this.effectiveMissCount,
1964
1993
  });
1965
1994
  // Scale the tap value with accuracy and OD.
1966
- const od = this.difficultyCalculator.stats.od;
1995
+ const od = this.difficultyAttributes.overallDifficulty;
1967
1996
  const odScaling = Math.pow(od, 2) / 750;
1968
1997
  this.tap *=
1969
1998
  (0.95 + (od > 0 ? odScaling : -odScaling)) *
1970
- Math.pow((this.computedAccuracy.value(objectCount) +
1971
- relevantAccuracy.value(this.difficultyCalculator.attributes.speedNoteCount)) /
1999
+ Math.pow((this.computedAccuracy.value(this.totalHits) +
2000
+ relevantAccuracy.value(this.difficultyAttributes.speedNoteCount)) /
1972
2001
  2, (14 - Math.max(od, 2.5)) / 2);
1973
2002
  // Scale the tap value with # of 50s to punish doubletapping.
1974
- this.tap *= Math.pow(0.99, Math.max(0, this.computedAccuracy.n50 - objectCount / 500));
2003
+ this.tap *= Math.pow(0.99, Math.max(0, this.computedAccuracy.n50 - this.totalHits / 500));
1975
2004
  // Scale the tap value with three-fingered penalty.
1976
2005
  this.tap /= this._tapPenalty;
1977
2006
  }
@@ -1979,34 +2008,35 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
1979
2008
  * Calculates the accuracy performance value of the beatmap.
1980
2009
  */
1981
2010
  calculateAccuracyValue() {
1982
- if (this.difficultyCalculator.mods.some((m) => m instanceof osuBase.ModRelax)) {
2011
+ if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax)) {
2012
+ this.accuracy = 0;
1983
2013
  return;
1984
2014
  }
1985
- // Global variables
1986
- const objectCount = this.difficultyCalculator.objects.length;
1987
- const ncircles = this.difficultyCalculator.mods.some((m) => m instanceof osuBase.ModScoreV2)
1988
- ? objectCount -
1989
- this.difficultyCalculator.beatmap.hitObjects.spinners
1990
- : this.difficultyCalculator.beatmap.hitObjects.circles;
2015
+ const ncircles = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModScoreV2)
2016
+ ? this.totalHits - this.difficultyAttributes.spinnerCount
2017
+ : this.difficultyAttributes.hitCircleCount;
1991
2018
  if (ncircles === 0) {
2019
+ this.accuracy = 0;
1992
2020
  return;
1993
2021
  }
1994
2022
  const realAccuracy = new osuBase.Accuracy({
1995
2023
  ...this.computedAccuracy,
1996
- n300: this.computedAccuracy.n300 - (objectCount - ncircles),
2024
+ n300: this.computedAccuracy.n300 - (this.totalHits - ncircles),
1997
2025
  });
1998
2026
  // Lots of arbitrary values from testing.
1999
2027
  // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
2000
2028
  this.accuracy =
2001
- Math.pow(1.4, this.difficultyCalculator.stats.od) *
2029
+ Math.pow(1.4, this.difficultyAttributes.overallDifficulty) *
2002
2030
  Math.pow(realAccuracy.value(ncircles), 12) *
2003
2031
  10;
2004
2032
  // Bonus for many hitcircles - it's harder to keep good accuracy up for longer
2005
2033
  this.accuracy *= Math.min(1.15, Math.pow(ncircles / 1000, 0.3));
2006
2034
  // Scale the accuracy value with rhythm complexity.
2007
2035
  this.accuracy *=
2008
- 1.5 / (1 + Math.exp(-(this.difficultyCalculator.rhythm - 1) / 2));
2009
- if (this.difficultyCalculator.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
2036
+ 1.5 /
2037
+ (1 +
2038
+ Math.exp(-(this.difficultyAttributes.rhythmDifficulty - 1) / 2));
2039
+ if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
2010
2040
  this.accuracy *= 1.02;
2011
2041
  }
2012
2042
  }
@@ -2014,32 +2044,33 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2014
2044
  * Calculates the flashlight performance value of the beatmap.
2015
2045
  */
2016
2046
  calculateFlashlightValue() {
2017
- if (!this.difficultyCalculator.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
2047
+ if (!this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
2048
+ this.flashlight = 0;
2018
2049
  return;
2019
2050
  }
2020
- // Global variables
2021
- const objectCount = this.difficultyCalculator.objects.length;
2022
2051
  this.flashlight =
2023
- Math.pow(this.difficultyCalculator.flashlight, 1.6) * 25;
2052
+ Math.pow(this.difficultyAttributes.flashlightDifficulty, 1.6) * 25;
2024
2053
  // Combo scaling
2025
2054
  this.flashlight *= this.comboPenalty;
2026
2055
  if (this.effectiveMissCount > 0) {
2027
2056
  // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
2028
2057
  this.flashlight *=
2029
2058
  0.97 *
2030
- Math.pow(1 - Math.pow(this.effectiveMissCount / objectCount, 0.775), Math.pow(this.effectiveMissCount, 0.875));
2059
+ Math.pow(1 -
2060
+ Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
2031
2061
  }
2032
2062
  // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
2033
2063
  this.flashlight *=
2034
2064
  0.7 +
2035
- 0.1 * Math.min(1, objectCount / 200) +
2036
- (objectCount > 200
2037
- ? 0.2 * Math.min(1, (objectCount - 200) / 200)
2065
+ 0.1 * Math.min(1, this.totalHits / 200) +
2066
+ (this.totalHits > 200
2067
+ ? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
2038
2068
  : 0);
2039
2069
  // Scale the flashlight value with accuracy slightly.
2040
- this.flashlight *= 0.5 + this.computedAccuracy.value(objectCount) / 2;
2070
+ this.flashlight *=
2071
+ 0.5 + this.computedAccuracy.value(this.totalHits) / 2;
2041
2072
  // It is also important to consider accuracy difficulty when doing that.
2042
- const od = this.difficultyCalculator.stats.od;
2073
+ const od = this.difficultyAttributes.overallDifficulty;
2043
2074
  const odScaling = Math.pow(od, 2) / 2500;
2044
2075
  this.flashlight *= 0.98 + (od >= 0 ? odScaling : -odScaling);
2045
2076
  }
@@ -2047,25 +2078,25 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2047
2078
  * Calculates the visual performance value of the beatmap.
2048
2079
  */
2049
2080
  calculateVisualValue() {
2050
- // Global variables
2051
- const objectCount = this.difficultyCalculator.objects.length;
2052
- this.visual = Math.pow(this.difficultyCalculator.visual, 1.6) * 22.5;
2081
+ this.visual =
2082
+ Math.pow(this.difficultyAttributes.visualDifficulty, 1.6) * 22.5;
2053
2083
  if (this.effectiveMissCount > 0) {
2054
2084
  // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
2055
2085
  this.visual *=
2056
2086
  0.97 *
2057
- Math.pow(1 - Math.pow(this.effectiveMissCount / objectCount, 0.775), this.effectiveMissCount);
2087
+ Math.pow(1 -
2088
+ Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
2058
2089
  }
2059
2090
  // Combo scaling
2060
2091
  this.visual *= this.comboPenalty;
2061
2092
  // Scale the visual value with object count to penalize short maps.
2062
2093
  this.visual *= Math.min(1, 1.650668 +
2063
2094
  (0.4845796 - 1.650668) /
2064
- (1 + Math.pow(objectCount / 817.9306, 1.147469)));
2095
+ (1 + Math.pow(this.totalHits / 817.9306, 1.147469)));
2065
2096
  // Scale the visual value with accuracy harshly.
2066
2097
  this.visual *= Math.pow(this.computedAccuracy.value(), 8);
2067
2098
  // It is also important to consider accuracy difficulty when doing that.
2068
- const od = this.difficultyCalculator.stats.od;
2099
+ const od = this.difficultyAttributes.overallDifficulty;
2069
2100
  const odScaling = Math.pow(od, 2) / 2500;
2070
2101
  this.visual *= 0.98 + (od >= 0 ? odScaling : -odScaling);
2071
2102
  }
@@ -2620,6 +2651,21 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
2620
2651
  * The flashlight star rating of the beatmap.
2621
2652
  */
2622
2653
  flashlight = 0;
2654
+ attributes = {
2655
+ speedDifficulty: 0,
2656
+ mods: [],
2657
+ starRating: 0,
2658
+ maxCombo: 0,
2659
+ aimDifficulty: 0,
2660
+ flashlightDifficulty: 0,
2661
+ speedNoteCount: 0,
2662
+ sliderFactor: 0,
2663
+ approachRate: 0,
2664
+ overallDifficulty: 0,
2665
+ hitCircleCount: 0,
2666
+ sliderCount: 0,
2667
+ spinnerCount: 0,
2668
+ };
2623
2669
  difficultyMultiplier = 0.0675;
2624
2670
  mode = osuBase.Modes.osu;
2625
2671
  /**
@@ -2734,6 +2780,7 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
2734
2780
  if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
2735
2781
  this.aim *= 0.9;
2736
2782
  }
2783
+ this.attributes.aimDifficulty = this.aim;
2737
2784
  }
2738
2785
  /**
2739
2786
  * Called after speed skill calculation.
@@ -2742,7 +2789,7 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
2742
2789
  */
2743
2790
  postCalculateSpeed(speedSkill) {
2744
2791
  this.strainPeaks.speed = speedSkill.strainPeaks;
2745
- this.speed = this.starValue(speedSkill.difficultyValue());
2792
+ this.speed = this.attributes.speedDifficulty = this.starValue(speedSkill.difficultyValue());
2746
2793
  }
2747
2794
  /**
2748
2795
  * Calculates speed-related attributes.
@@ -2768,6 +2815,7 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
2768
2815
  if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
2769
2816
  this.flashlight *= 0.7;
2770
2817
  }
2818
+ this.attributes.flashlightDifficulty = this.flashlight;
2771
2819
  }
2772
2820
  }
2773
2821
 
@@ -2832,8 +2880,16 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
2832
2880
  * The flashlight performance value.
2833
2881
  */
2834
2882
  flashlight = 0;
2883
+ difficultyAttributes;
2835
2884
  finalMultiplier = 1.14;
2836
2885
  mode = osuBase.Modes.osu;
2886
+ /**
2887
+ * @param difficultyAttributes The difficulty attributes to calculate.
2888
+ */
2889
+ constructor(difficultyAttributes) {
2890
+ super();
2891
+ this.difficultyAttributes = osuBase.Utils.deepCopy(difficultyAttributes);
2892
+ }
2837
2893
  calculateValues() {
2838
2894
  this.calculateAimValue();
2839
2895
  this.calculateSpeedValue();
@@ -2851,25 +2907,24 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
2851
2907
  * Calculates the aim performance value of the beatmap.
2852
2908
  */
2853
2909
  calculateAimValue() {
2854
- // Global variables
2855
- const objectCount = this.difficultyCalculator.objects.length;
2856
- const calculatedAR = this.difficultyCalculator.stats.ar;
2857
- this.aim = this.baseValue(this.difficultyCalculator.aim);
2910
+ this.aim = this.baseValue(this.difficultyAttributes.aimDifficulty);
2858
2911
  // Longer maps are worth more
2859
- let lengthBonus = 0.95 + 0.4 * Math.min(1, objectCount / 2000);
2860
- if (objectCount > 2000) {
2861
- lengthBonus += Math.log10(objectCount / 2000) * 0.5;
2912
+ let lengthBonus = 0.95 + 0.4 * Math.min(1, this.totalHits / 2000);
2913
+ if (this.totalHits > 2000) {
2914
+ lengthBonus += Math.log10(this.totalHits / 2000) * 0.5;
2862
2915
  }
2863
2916
  this.aim *= lengthBonus;
2864
2917
  if (this.effectiveMissCount > 0) {
2865
2918
  // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
2866
2919
  this.aim *=
2867
2920
  0.97 *
2868
- Math.pow(1 - Math.pow(this.effectiveMissCount / objectCount, 0.775), this.effectiveMissCount);
2921
+ Math.pow(1 -
2922
+ Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
2869
2923
  }
2870
2924
  // Combo scaling
2871
2925
  this.aim *= this.comboPenalty;
2872
- if (!this.difficultyCalculator.mods.some((m) => m instanceof osuBase.ModRelax)) {
2926
+ const calculatedAR = this.difficultyAttributes.approachRate;
2927
+ if (!this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax)) {
2873
2928
  // AR scaling
2874
2929
  let arFactor = 0;
2875
2930
  if (calculatedAR > 10.33) {
@@ -2882,57 +2937,56 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
2882
2937
  this.aim *= 1 + arFactor * lengthBonus;
2883
2938
  }
2884
2939
  // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
2885
- if (this.difficultyCalculator.mods.some((m) => m instanceof osuBase.ModHidden)) {
2940
+ if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHidden)) {
2886
2941
  this.aim *= 1 + 0.04 * (12 - calculatedAR);
2887
2942
  }
2888
2943
  // Scale the aim value with slider factor to nerf very likely dropped sliderends.
2889
2944
  this.aim *= this.sliderNerfFactor;
2890
2945
  // Scale the aim value with accuracy.
2891
- this.aim *= this.computedAccuracy.value(objectCount);
2946
+ this.aim *= this.computedAccuracy.value(this.totalHits);
2892
2947
  // It is also important to consider accuracy difficulty when doing that.
2893
- const odScaling = Math.pow(this.difficultyCalculator.stats.od, 2) / 2500;
2948
+ const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
2894
2949
  this.aim *= 0.98 + odScaling;
2895
2950
  }
2896
2951
  /**
2897
2952
  * Calculates the speed performance value of the beatmap.
2898
2953
  */
2899
2954
  calculateSpeedValue() {
2900
- if (this.difficultyCalculator.mods.some((m) => m instanceof osuBase.ModRelax)) {
2955
+ if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax)) {
2901
2956
  this.speed = 0;
2902
2957
  return;
2903
2958
  }
2904
2959
  // Global variables
2905
- const objectCount = this.difficultyCalculator.objects.length;
2906
- const calculatedAR = this.difficultyCalculator.stats.ar;
2907
- const n50 = this.computedAccuracy.n50;
2908
- this.speed = this.baseValue(this.difficultyCalculator.speed);
2960
+ this.speed = this.baseValue(this.difficultyAttributes.speedDifficulty);
2909
2961
  // Longer maps are worth more
2910
- let lengthBonus = 0.95 + 0.4 * Math.min(1, objectCount / 2000);
2911
- if (objectCount > 2000) {
2912
- lengthBonus += Math.log10(objectCount / 2000) * 0.5;
2962
+ let lengthBonus = 0.95 + 0.4 * Math.min(1, this.totalHits / 2000);
2963
+ if (this.totalHits > 2000) {
2964
+ lengthBonus += Math.log10(this.totalHits / 2000) * 0.5;
2913
2965
  }
2914
2966
  this.speed *= lengthBonus;
2915
2967
  if (this.effectiveMissCount > 0) {
2916
2968
  // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
2917
2969
  this.speed *=
2918
2970
  0.97 *
2919
- Math.pow(1 - Math.pow(this.effectiveMissCount / objectCount, 0.775), Math.pow(this.effectiveMissCount, 0.875));
2971
+ Math.pow(1 -
2972
+ Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
2920
2973
  }
2921
2974
  // Combo scaling
2922
2975
  this.speed *= this.comboPenalty;
2923
2976
  // AR scaling
2977
+ const calculatedAR = this.difficultyAttributes.approachRate;
2924
2978
  if (calculatedAR > 10.33) {
2925
2979
  // Buff for longer maps with high AR.
2926
2980
  this.speed *= 1 + 0.3 * (calculatedAR - 10.33) * lengthBonus;
2927
2981
  }
2928
- if (this.difficultyCalculator.mods.some((m) => m instanceof osuBase.ModHidden)) {
2982
+ if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHidden)) {
2929
2983
  this.speed *= 1 + 0.04 * (12 - calculatedAR);
2930
2984
  }
2931
2985
  // Calculate accuracy assuming the worst case scenario.
2932
2986
  const countGreat = this.computedAccuracy.n300;
2933
2987
  const countOk = this.computedAccuracy.n100;
2934
2988
  const countMeh = this.computedAccuracy.n50;
2935
- const relevantTotalDiff = objectCount - this.difficultyCalculator.attributes.speedNoteCount;
2989
+ const relevantTotalDiff = this.totalHits - this.difficultyAttributes.speedNoteCount;
2936
2990
  const relevantAccuracy = new osuBase.Accuracy({
2937
2991
  n300: Math.max(0, countGreat - relevantTotalDiff),
2938
2992
  n100: Math.max(0, countOk - Math.max(0, relevantTotalDiff - countGreat)),
@@ -2941,45 +2995,48 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
2941
2995
  });
2942
2996
  // Scale the speed value with accuracy and OD.
2943
2997
  this.speed *=
2944
- (0.95 + Math.pow(this.difficultyCalculator.stats.od, 2) / 750) *
2945
- Math.pow((this.computedAccuracy.value(objectCount) +
2998
+ (0.95 +
2999
+ Math.pow(this.difficultyAttributes.overallDifficulty, 2) /
3000
+ 750) *
3001
+ Math.pow((this.computedAccuracy.value(this.totalHits) +
2946
3002
  relevantAccuracy.value()) /
2947
- 2, (14.5 - Math.max(this.difficultyCalculator.stats.od, 8)) / 2);
3003
+ 2, (14.5 -
3004
+ Math.max(this.difficultyAttributes.overallDifficulty, 8)) /
3005
+ 2);
2948
3006
  // Scale the speed value with # of 50s to punish doubletapping.
2949
- this.speed *= Math.pow(0.99, Math.max(0, n50 - objectCount / 500));
3007
+ this.speed *= Math.pow(0.99, Math.max(0, this.computedAccuracy.n50 - this.totalHits / 500));
2950
3008
  }
2951
3009
  /**
2952
3010
  * Calculates the accuracy performance value of the beatmap.
2953
3011
  */
2954
3012
  calculateAccuracyValue() {
2955
- if (this.difficultyCalculator.mods.some((m) => m instanceof osuBase.ModRelax)) {
3013
+ if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax)) {
3014
+ this.accuracy = 0;
2956
3015
  return;
2957
3016
  }
2958
- // Global variables
2959
- const nobjects = this.difficultyCalculator.objects.length;
2960
- const ncircles = this.difficultyCalculator.mods.some((m) => m instanceof osuBase.ModScoreV2)
2961
- ? nobjects - this.difficultyCalculator.beatmap.hitObjects.spinners
2962
- : this.difficultyCalculator.beatmap.hitObjects.circles;
3017
+ const ncircles = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModScoreV2)
3018
+ ? this.totalHits - this.difficultyAttributes.spinnerCount
3019
+ : this.difficultyAttributes.hitCircleCount;
2963
3020
  if (ncircles === 0) {
3021
+ this.accuracy = 0;
2964
3022
  return;
2965
3023
  }
2966
3024
  const realAccuracy = new osuBase.Accuracy({
2967
3025
  ...this.computedAccuracy,
2968
- n300: this.computedAccuracy.n300 -
2969
- (this.difficultyCalculator.objects.length - ncircles),
3026
+ n300: this.computedAccuracy.n300 - (this.totalHits - ncircles),
2970
3027
  });
2971
3028
  // Lots of arbitrary values from testing.
2972
3029
  // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
2973
3030
  this.accuracy =
2974
- Math.pow(1.52163, this.difficultyCalculator.stats.od) *
3031
+ Math.pow(1.52163, this.difficultyAttributes.overallDifficulty) *
2975
3032
  Math.pow(realAccuracy.value(ncircles), 24) *
2976
3033
  2.83;
2977
3034
  // Bonus for many hitcircles - it's harder to keep good accuracy up for longer
2978
3035
  this.accuracy *= Math.min(1.15, Math.pow(ncircles / 1000, 0.3));
2979
- if (this.difficultyCalculator.mods.some((m) => m instanceof osuBase.ModHidden)) {
3036
+ if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHidden)) {
2980
3037
  this.accuracy *= 1.08;
2981
3038
  }
2982
- if (this.difficultyCalculator.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
3039
+ if (this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
2983
3040
  this.accuracy *= 1.02;
2984
3041
  }
2985
3042
  }
@@ -2987,32 +3044,34 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
2987
3044
  * Calculates the flashlight performance value of the beatmap.
2988
3045
  */
2989
3046
  calculateFlashlightValue() {
2990
- if (!this.difficultyCalculator.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
3047
+ if (!this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModFlashlight)) {
3048
+ this.flashlight = 0;
2991
3049
  return;
2992
3050
  }
2993
3051
  // Global variables
2994
- const objectCount = this.difficultyCalculator.objects.length;
2995
3052
  this.flashlight =
2996
- Math.pow(this.difficultyCalculator.flashlight, 2) * 25;
3053
+ Math.pow(this.difficultyAttributes.flashlightDifficulty, 2) * 25;
2997
3054
  // Combo scaling
2998
3055
  this.flashlight *= this.comboPenalty;
2999
3056
  if (this.effectiveMissCount > 0) {
3000
3057
  // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
3001
3058
  this.flashlight *=
3002
3059
  0.97 *
3003
- Math.pow(1 - Math.pow(this.effectiveMissCount / objectCount, 0.775), Math.pow(this.effectiveMissCount, 0.875));
3060
+ Math.pow(1 -
3061
+ Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
3004
3062
  }
3005
3063
  // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
3006
3064
  this.flashlight *=
3007
3065
  0.7 +
3008
- 0.1 * Math.min(1, objectCount / 200) +
3009
- (objectCount > 200
3010
- ? 0.2 * Math.min(1, (objectCount - 200) / 200)
3066
+ 0.1 * Math.min(1, this.totalHits / 200) +
3067
+ (this.totalHits > 200
3068
+ ? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
3011
3069
  : 0);
3012
3070
  // Scale the flashlight value with accuracy slightly.
3013
- this.flashlight *= 0.5 + this.computedAccuracy.value(objectCount) / 2;
3071
+ this.flashlight *=
3072
+ 0.5 + this.computedAccuracy.value(this.totalHits) / 2;
3014
3073
  // It is also important to consider accuracy difficulty when doing that.
3015
- const odScaling = Math.pow(this.difficultyCalculator.stats.od, 2) / 2500;
3074
+ const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
3016
3075
  this.flashlight *= 0.98 + odScaling;
3017
3076
  }
3018
3077
  toString() {