@rian8337/osu-difficulty-calculator 4.0.0-beta.1 → 4.0.0-beta.10

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
@@ -207,10 +207,9 @@ class DifficultyHitObject {
207
207
  *
208
208
  * @param time The time to calculate the hitobject's opacity at.
209
209
  * @param isHidden Whether Hidden mod is used.
210
- * @param mode The gamemode to calculate the opacity for.
211
210
  * @returns The opacity of the hitobject at the given time.
212
211
  */
213
- opacityAt(time, isHidden, mode) {
212
+ opacityAt(time, isHidden) {
214
213
  if (time > this.object.startTime) {
215
214
  // Consider a hitobject as being invisible when its start time is passed.
216
215
  // In reality the hitobject will be visible beyond its start time up until its hittable window has passed,
@@ -221,10 +220,7 @@ class DifficultyHitObject {
221
220
  const fadeInDuration = this.timeFadeIn;
222
221
  if (isHidden) {
223
222
  const fadeOutStartTime = fadeInStartTime + fadeInDuration;
224
- const fadeOutDuration = this.baseTimePreempt *
225
- (mode === osuBase.Modes.droid
226
- ? 0.35
227
- : osuBase.ModHidden.fadeOutDurationMultiplier);
223
+ const fadeOutDuration = this.baseTimePreempt * osuBase.ModHidden.fadeOutDurationMultiplier;
228
224
  return Math.min(osuBase.MathUtils.clamp((time - fadeInStartTime) / fadeInDuration, 0, 1), 1 -
229
225
  osuBase.MathUtils.clamp((time - fadeOutStartTime) / fadeOutDuration, 0, 1));
230
226
  }
@@ -297,34 +293,16 @@ class DifficultyHitObjectCreator {
297
293
  if (this.mode === osuBase.Modes.droid) {
298
294
  this.maximumSliderRadius = this.normalizedRadius * 2;
299
295
  }
300
- const droidCircleSize = new osuBase.MapStats({
301
- cs: params.circleSize,
302
- mods: params.mods,
303
- }).calculate({ mode: osuBase.Modes.droid }).cs;
304
- const droidScale = (1 - (0.7 * (droidCircleSize - 5)) / 5) / 2;
305
- const osuCircleSize = new osuBase.MapStats({
306
- cs: params.circleSize,
307
- mods: params.mods,
308
- }).calculate({ mode: osuBase.Modes.osu }).cs;
309
- const osuScale = (1 - (0.7 * (osuCircleSize - 5)) / 5) / 2;
310
- params.objects[0].droidScale = droidScale;
311
- params.objects[0].osuScale = osuScale;
312
296
  const scalingFactor = this.getScalingFactor(params.objects[0].getRadius(this.mode));
313
297
  const difficultyObjects = [];
314
298
  for (let i = 0; i < params.objects.length; ++i) {
315
299
  const object = new DifficultyHitObject(params.objects[i], difficultyObjects);
316
300
  object.index = difficultyObjects.length - 1;
317
- object.object.droidScale = droidScale;
318
- object.object.osuScale = osuScale;
319
301
  object.timePreempt = params.preempt;
320
302
  object.baseTimePreempt = params.preempt * params.speedMultiplier;
321
303
  if (object.object instanceof osuBase.Slider) {
322
304
  object.velocity =
323
305
  object.object.velocity * params.speedMultiplier;
324
- object.object.nestedHitObjects.forEach((o) => {
325
- o.droidScale = droidScale;
326
- o.osuScale = osuScale;
327
- });
328
306
  this.calculateSliderCursorPosition(object.object);
329
307
  object.travelDistance = object.object.lazyTravelDistance;
330
308
  // Bonus for repeat sliders until a better per nested object strain system can be achieved.
@@ -367,9 +345,6 @@ class DifficultyHitObjectCreator {
367
345
  object.endTime + object.timePreempt) {
368
346
  break;
369
347
  }
370
- // Future objects do not have their scales set, so we set them here.
371
- o.droidScale = droidScale;
372
- o.osuScale = osuScale;
373
348
  nextVisibleObjects.push(o);
374
349
  }
375
350
  for (let j = 0; j < object.index; ++j) {
@@ -620,7 +595,6 @@ class DifficultyCalculator {
620
595
  speed: [],
621
596
  flashlight: [],
622
597
  };
623
- sectionLength = 400;
624
598
  /**
625
599
  * Constructs a new instance of the calculator.
626
600
  *
@@ -655,6 +629,7 @@ class DifficultyCalculator {
655
629
  speedMultiplier: options?.stats?.speedMultiplier,
656
630
  oldStatistics: options?.stats?.oldStatistics,
657
631
  }).calculate({ mode: this.mode });
632
+ this.preProcess();
658
633
  this.populateDifficultyAttributes();
659
634
  this.generateDifficultyHitObjects();
660
635
  this.calculateAll();
@@ -674,6 +649,11 @@ class DifficultyCalculator {
674
649
  preempt: osuBase.MapStats.arToMS(this.stats.ar),
675
650
  }));
676
651
  }
652
+ /**
653
+ * Performs some pre-processing before proceeding with difficulty calculation.
654
+ */
655
+ preProcess() {
656
+ }
677
657
  /**
678
658
  * Calculates the skills provided.
679
659
  *
@@ -702,6 +682,7 @@ class DifficultyCalculator {
702
682
  this.attributes.overallDifficulty = this.stats.od;
703
683
  this.attributes.sliderCount = this.beatmap.hitObjects.sliders;
704
684
  this.attributes.spinnerCount = this.beatmap.hitObjects.spinners;
685
+ this.attributes.clockRate = this.stats.speedMultiplier;
705
686
  }
706
687
  /**
707
688
  * Calculates the star rating value of a difficulty.
@@ -861,7 +842,8 @@ class DroidAimEvaluator extends AimEvaluator {
861
842
  velocityChangeBonus * this.velocityChangeMultiplier);
862
843
  // Add in additional slider velocity bonus.
863
844
  if (withSliders) {
864
- strain += sliderBonus * this.sliderMultiplier;
845
+ strain +=
846
+ Math.pow(1 + sliderBonus * this.sliderMultiplier, 1.25) - 1;
865
847
  }
866
848
  return strain;
867
849
  }
@@ -918,7 +900,6 @@ class StrainSkill extends Skill {
918
900
  strainPeaks = [];
919
901
  sectionLength = 400;
920
902
  currentSectionEnd = 0;
921
- isFirstObject = true;
922
903
  /**
923
904
  * Calculates the strain value of a hitobject and stores the value in it. This value is affected by previously processed objects.
924
905
  *
@@ -926,11 +907,10 @@ class StrainSkill extends Skill {
926
907
  */
927
908
  process(current) {
928
909
  // The first object doesn't generate a strain, so we begin with an incremented section end
929
- if (this.isFirstObject) {
910
+ if (current.index === 0) {
930
911
  this.currentSectionEnd =
931
912
  Math.ceil(current.startTime / this.sectionLength) *
932
913
  this.sectionLength;
933
- this.isFirstObject = false;
934
914
  }
935
915
  while (current.startTime > this.currentSectionEnd) {
936
916
  this.saveCurrentPeak();
@@ -1065,7 +1045,6 @@ class DroidTapEvaluator extends SpeedEvaluator {
1065
1045
  current.isOverlapping(false)) {
1066
1046
  return 0;
1067
1047
  }
1068
- let strainTime = current.strainTime;
1069
1048
  let doubletapness = 1;
1070
1049
  if (considerCheesability) {
1071
1050
  const greatWindowFull = greatWindow * 2;
@@ -1080,16 +1059,14 @@ class DroidTapEvaluator extends SpeedEvaluator {
1080
1059
  const windowRatio = Math.pow(Math.min(1, currentDeltaTime / greatWindowFull), 2);
1081
1060
  doubletapness = Math.pow(speedRatio, 1 - windowRatio);
1082
1061
  }
1083
- // Cap deltatime to the OD 300 hitwindow.
1084
- // 0.58 is derived from making sure 260 BPM 1/4 OD5 streams aren't nerfed harshly, whilst 0.91 limits the effect of the cap.
1085
- strainTime /= osuBase.MathUtils.clamp(strainTime / greatWindowFull / 0.58, 0.91, 1);
1086
1062
  }
1087
1063
  let speedBonus = 1;
1088
- if (strainTime < this.minSpeedBonus) {
1064
+ if (current.strainTime < this.minSpeedBonus) {
1089
1065
  speedBonus +=
1090
- 0.75 * Math.pow((this.minSpeedBonus - strainTime) / 40, 2);
1066
+ 0.75 *
1067
+ Math.pow(osuBase.ErrorFunction.erf((this.minSpeedBonus - current.strainTime) / 40), 2);
1091
1068
  }
1092
- return (speedBonus * doubletapness) / strainTime;
1069
+ return (speedBonus * Math.pow(doubletapness, 1.5)) / current.strainTime;
1093
1070
  }
1094
1071
  }
1095
1072
 
@@ -1193,7 +1170,7 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
1193
1170
  const opacityBonus = 1 +
1194
1171
  this.maxOpacityBonus *
1195
1172
  (1 -
1196
- current.opacityAt(currentObject.object.startTime, isHiddenMod, osuBase.Modes.droid));
1173
+ current.opacityAt(currentObject.object.startTime, isHiddenMod));
1197
1174
  result +=
1198
1175
  (stackNerf * opacityBonus * scalingFactor * jumpDistance) /
1199
1176
  cumulativeStrainTime;
@@ -1236,11 +1213,11 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
1236
1213
  * Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
1237
1214
  */
1238
1215
  class DroidFlashlight extends DroidSkill {
1239
- skillMultiplier = 0.125;
1216
+ skillMultiplier = 0.052;
1240
1217
  strainDecayBase = 0.15;
1241
1218
  reducedSectionCount = 0;
1242
1219
  reducedSectionBaseline = 1;
1243
- starsPerDouble = 1.05;
1220
+ starsPerDouble = 1.06;
1244
1221
  isHidden;
1245
1222
  withSliders;
1246
1223
  constructor(mods, withSliders) {
@@ -1265,6 +1242,9 @@ class DroidFlashlight extends DroidSkill {
1265
1242
  current.flashlightStrainWithoutSliders = this.currentStrain;
1266
1243
  }
1267
1244
  }
1245
+ difficultyValue() {
1246
+ return Math.pow(this.strainPeaks.reduce((a, v) => a + v, 0) * this.starsPerDouble, 0.8);
1247
+ }
1268
1248
  }
1269
1249
 
1270
1250
  /**
@@ -1456,7 +1436,8 @@ class DroidVisualEvaluator {
1456
1436
  static evaluateDifficultyOf(current, isHiddenMod, withSliders) {
1457
1437
  if (current.object instanceof osuBase.Spinner ||
1458
1438
  // Exclude overlapping objects that can be tapped at once.
1459
- current.isOverlapping(true)) {
1439
+ current.isOverlapping(true) ||
1440
+ current.index === 0) {
1460
1441
  return 0;
1461
1442
  }
1462
1443
  // Start with base density and give global bonus for Hidden.
@@ -1483,7 +1464,7 @@ class DroidVisualEvaluator {
1483
1464
  }
1484
1465
  strain +=
1485
1466
  (1 -
1486
- current.opacityAt(previous.object.startTime, isHiddenMod, osuBase.Modes.droid)) /
1467
+ current.opacityAt(previous.object.startTime, isHiddenMod)) /
1487
1468
  4;
1488
1469
  }
1489
1470
  // Scale the value with overlapping factor.
@@ -1519,44 +1500,6 @@ class DroidVisualEvaluator {
1519
1500
  Math.min(1, 300 / cumulativeStrainTime);
1520
1501
  }
1521
1502
  }
1522
- // Reward for rhythm changes.
1523
- if (current.rhythmMultiplier > 1) {
1524
- let rhythmBonus = (current.rhythmMultiplier - 1) / 20;
1525
- // Rhythm changes are harder to read in Hidden.
1526
- // Add additional bonus for Hidden.
1527
- if (isHiddenMod) {
1528
- rhythmBonus += (current.rhythmMultiplier - 1) / 25;
1529
- }
1530
- // Rhythm changes are harder to read when objects are stacked together.
1531
- // Scale rhythm bonus based on the stack of past objects.
1532
- const diameter = 2 * current.object.getRadius(osuBase.Modes.droid);
1533
- let cumulativeStrainTime = 0;
1534
- for (let i = 0; i < Math.min(current.index, 5); ++i) {
1535
- const previous = current.previous(i);
1536
- if (previous.object instanceof osuBase.Spinner ||
1537
- // Exclude overlapping objects that can be tapped at once.
1538
- previous.isOverlapping(true)) {
1539
- continue;
1540
- }
1541
- const jumpDistance = current.object
1542
- .getStackedPosition(osuBase.Modes.droid)
1543
- .getDistance(previous.object.getStackedEndPosition(osuBase.Modes.droid));
1544
- cumulativeStrainTime += previous.strainTime;
1545
- rhythmBonus +=
1546
- // Scale the bonus with diameter.
1547
- osuBase.MathUtils.clamp((0.5 - jumpDistance / diameter) / 10, 0, 0.05) *
1548
- // Scale with cumulative strain time to avoid overbuffing past objects.
1549
- Math.min(1, 300 / cumulativeStrainTime);
1550
- // Give a larger bonus for Hidden.
1551
- if (isHiddenMod) {
1552
- rhythmBonus +=
1553
- (1 -
1554
- current.opacityAt(previous.object.startTime, isHiddenMod, osuBase.Modes.droid)) /
1555
- 20;
1556
- }
1557
- }
1558
- strain += rhythmBonus;
1559
- }
1560
1503
  return strain;
1561
1504
  }
1562
1505
  }
@@ -1571,20 +1514,20 @@ class DroidVisual extends DroidSkill {
1571
1514
  skillMultiplier = 10;
1572
1515
  strainDecayBase = 0.1;
1573
1516
  isHidden;
1574
- withSliders;
1517
+ withsliders;
1575
1518
  constructor(mods, withSliders) {
1576
1519
  super(mods);
1577
1520
  this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
1578
- this.withSliders = withSliders;
1521
+ this.withsliders = withSliders;
1579
1522
  }
1580
1523
  strainValueAt(current) {
1581
1524
  this.currentStrain *= this.strainDecay(current.deltaTime);
1582
1525
  this.currentStrain +=
1583
- DroidVisualEvaluator.evaluateDifficultyOf(current, this.isHidden, this.withSliders) * this.skillMultiplier;
1584
- return this.currentStrain;
1526
+ DroidVisualEvaluator.evaluateDifficultyOf(current, this.isHidden, this.withsliders) * this.skillMultiplier;
1527
+ return this.currentStrain * (1 + (current.rhythmMultiplier - 1) / 5);
1585
1528
  }
1586
1529
  saveToHitObject(current) {
1587
- if (this.withSliders) {
1530
+ if (this.withsliders) {
1588
1531
  current.visualStrainWithSliders = this.currentStrain;
1589
1532
  }
1590
1533
  else {
@@ -1635,15 +1578,21 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1635
1578
  flashlightDifficulty: 0,
1636
1579
  speedNoteCount: 0,
1637
1580
  sliderFactor: 0,
1581
+ clockRate: 1,
1638
1582
  approachRate: 0,
1639
1583
  overallDifficulty: 0,
1640
1584
  hitCircleCount: 0,
1641
1585
  sliderCount: 0,
1642
1586
  spinnerCount: 0,
1587
+ aimDifficultStrainCount: 0,
1588
+ tapDifficultStrainCount: 0,
1589
+ flashlightDifficultStrainCount: 0,
1590
+ visualDifficultStrainCount: 0,
1643
1591
  flashlightSliderFactor: 0,
1644
1592
  visualSliderFactor: 0,
1645
1593
  possibleThreeFingeredSections: [],
1646
1594
  difficultSliders: [],
1595
+ averageSpeedDeltaTime: 0,
1647
1596
  };
1648
1597
  difficultyMultiplier = 0.18;
1649
1598
  mode = osuBase.Modes.droid;
@@ -1765,9 +1714,6 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1765
1714
  }
1766
1715
  this.calculateTotal();
1767
1716
  }
1768
- /**
1769
- * Returns a string representative of the class.
1770
- */
1771
1717
  toString() {
1772
1718
  return (this.total.toFixed(2) +
1773
1719
  " stars (" +
@@ -1782,9 +1728,13 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1782
1728
  this.visual.toFixed(2) +
1783
1729
  " visual)");
1784
1730
  }
1785
- /**
1786
- * Creates skills to be calculated.
1787
- */
1731
+ preProcess() {
1732
+ const scale = osuBase.CircleSizeCalculator.standardCSToStandardScale(this.stats.cs);
1733
+ for (const object of this.beatmap.hitObjects.objects) {
1734
+ object.droidScale = scale;
1735
+ }
1736
+ osuBase.HitObjectStackEvaluator.applyDroidStacking(this.beatmap.hitObjects.objects, this.beatmap.general.stackLeniency);
1737
+ }
1788
1738
  createSkills() {
1789
1739
  return [
1790
1740
  new DroidAim(this.mods, true),
@@ -1825,7 +1775,6 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1825
1775
  calculateAimAttributes() {
1826
1776
  const objectStrains = [];
1827
1777
  let maxStrain = 0;
1828
- // Take the top 15% most difficult sliders based on velocity.
1829
1778
  const topDifficultSliders = [];
1830
1779
  for (let i = 0; i < this.objects.length; ++i) {
1831
1780
  const object = this.objects[i];
@@ -1837,22 +1786,28 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1837
1786
  index: i,
1838
1787
  velocity: velocity,
1839
1788
  });
1840
- topDifficultSliders.sort((a, b) => b.velocity - a.velocity);
1841
- while (topDifficultSliders.length >
1842
- Math.ceil(0.15 * this.beatmap.hitObjects.sliders)) {
1843
- topDifficultSliders.pop();
1844
- }
1845
1789
  }
1846
1790
  }
1847
1791
  if (maxStrain) {
1848
1792
  this.attributes.aimNoteCount = objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
1793
+ this.attributes.aimDifficultStrainCount = objectStrains.reduce((total, next) => total + Math.pow(next / maxStrain, 4), 0);
1849
1794
  }
1850
1795
  const velocitySum = topDifficultSliders.reduce((a, v) => a + v.velocity, 0);
1851
1796
  for (const slider of topDifficultSliders) {
1852
- this.attributes.difficultSliders.push({
1853
- index: slider.index,
1854
- difficultyRating: slider.velocity / velocitySum,
1855
- });
1797
+ const difficultyRating = slider.velocity / velocitySum;
1798
+ // Only consider sliders that are fast enough.
1799
+ if (difficultyRating > 0.02) {
1800
+ this.attributes.difficultSliders.push({
1801
+ index: slider.index,
1802
+ difficultyRating: slider.velocity / velocitySum,
1803
+ });
1804
+ }
1805
+ }
1806
+ this.attributes.difficultSliders.sort((a, b) => b.difficultyRating - a.difficultyRating);
1807
+ // Take the top 15% most difficult sliders.
1808
+ while (this.attributes.difficultSliders.length >
1809
+ Math.ceil(0.15 * this.beatmap.hitObjects.sliders)) {
1810
+ this.attributes.difficultSliders.pop();
1856
1811
  }
1857
1812
  }
1858
1813
  /**
@@ -1871,6 +1826,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1871
1826
  this.attributes.possibleThreeFingeredSections = [];
1872
1827
  const tempSections = [];
1873
1828
  const objectStrains = [];
1829
+ const objectDeltaTimes = [];
1874
1830
  let maxStrain = 0;
1875
1831
  const maxSectionDeltaTime = 2000;
1876
1832
  const minSectionObjectCount = 5;
@@ -1880,8 +1836,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1880
1836
  const next = this.objects[i + 1];
1881
1837
  if (i === 0) {
1882
1838
  objectStrains.push(current.tapStrain);
1839
+ objectDeltaTimes.push(current.deltaTime);
1883
1840
  }
1884
1841
  objectStrains.push(next.tapStrain);
1842
+ objectDeltaTimes.push(next.deltaTime);
1885
1843
  maxStrain = Math.max(current.tapStrain, maxStrain);
1886
1844
  const realDeltaTime = next.object.startTime - current.object.endTime;
1887
1845
  if (realDeltaTime >= maxSectionDeltaTime) {
@@ -1920,6 +1878,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1920
1878
  if (inSpeedSection &&
1921
1879
  current.originalTapStrain < threeFingerStrainThreshold) {
1922
1880
  inSpeedSection = false;
1881
+ // Ignore sections that don't meet object count requirement.
1882
+ if (i - newFirstObjectIndex < minSectionObjectCount) {
1883
+ continue;
1884
+ }
1923
1885
  this.attributes.possibleThreeFingeredSections.push({
1924
1886
  firstObjectIndex: newFirstObjectIndex,
1925
1887
  lastObjectIndex: i,
@@ -1928,7 +1890,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1928
1890
  }
1929
1891
  }
1930
1892
  // Don't forget to manually add the last beatmap section, which would otherwise be ignored.
1931
- if (inSpeedSection) {
1893
+ // Ignore sections that don't meet object count requirement.
1894
+ if (inSpeedSection &&
1895
+ section.lastObjectIndex - newFirstObjectIndex >=
1896
+ minSectionObjectCount) {
1932
1897
  this.attributes.possibleThreeFingeredSections.push({
1933
1898
  firstObjectIndex: newFirstObjectIndex,
1934
1899
  lastObjectIndex: section.lastObjectIndex,
@@ -1938,6 +1903,16 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1938
1903
  }
1939
1904
  if (maxStrain) {
1940
1905
  this.attributes.speedNoteCount = objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
1906
+ this.attributes.averageSpeedDeltaTime =
1907
+ objectDeltaTimes.reduce((total, next, index) => total +
1908
+ (next * 1) /
1909
+ (1 +
1910
+ Math.exp(-((objectStrains[index] / maxStrain) *
1911
+ 25 -
1912
+ 20))), 0) /
1913
+ objectStrains.reduce((total, next) => total +
1914
+ 1 / (1 + Math.exp(-((next / maxStrain) * 25 - 20))), 0);
1915
+ this.attributes.tapDifficultStrainCount = objectStrains.reduce((total, next) => total + Math.pow(next / maxStrain, 4), 0);
1941
1916
  }
1942
1917
  }
1943
1918
  /**
@@ -1980,6 +1955,12 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1980
1955
  if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
1981
1956
  this.flashlight *= 0.7;
1982
1957
  }
1958
+ const objectStrains = this.objects.map((v) => v.flashlightStrainWithSliders);
1959
+ const maxStrain = Math.max(...objectStrains);
1960
+ if (maxStrain) {
1961
+ this.attributes.flashlightDifficultStrainCount =
1962
+ objectStrains.reduce((total, next) => total + Math.pow(next / maxStrain, 4), 0);
1963
+ }
1983
1964
  this.attributes.flashlightDifficulty = this.flashlight;
1984
1965
  }
1985
1966
  /**
@@ -1997,6 +1978,11 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1997
1978
  this.starValue(visualSkillWithoutSliders.difficultyValue()) /
1998
1979
  this.visual;
1999
1980
  }
1981
+ const objectStrains = this.objects.map((v) => v.flashlightStrainWithSliders);
1982
+ const maxStrain = Math.max(...objectStrains);
1983
+ if (maxStrain) {
1984
+ this.attributes.visualDifficultStrainCount = objectStrains.reduce((total, next) => total + Math.pow(next / maxStrain, 4), 0);
1985
+ }
2000
1986
  }
2001
1987
  }
2002
1988
 
@@ -2119,7 +2105,7 @@ class PerformanceCalculator {
2119
2105
  if (this.difficultyAttributes.sliderCount > 0) {
2120
2106
  // We assume 15% of sliders in a beatmap are difficult since there's no way to tell from the performance calculator.
2121
2107
  const estimateDifficultSliders = this.difficultyAttributes.sliderCount * 0.15;
2122
- const estimateSliderEndsDropped = osuBase.MathUtils.clamp(Math.min(this.computedAccuracy.n300 +
2108
+ const estimateSliderEndsDropped = osuBase.MathUtils.clamp(Math.min(this.computedAccuracy.n100 +
2123
2109
  this.computedAccuracy.n50 +
2124
2110
  this.computedAccuracy.nmiss, maxCombo - combo), 0, estimateDifficultSliders);
2125
2111
  this.sliderNerfFactor =
@@ -2256,11 +2242,11 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2256
2242
  *
2257
2243
  * The aim and total performance value will be recalculated afterwards.
2258
2244
  *
2259
- * @param value The slider cheese penalty value. Must be between than 0 (exclusive) and 1 (inclusive).
2245
+ * @param value The slider cheese penalty value. Must be between than 0 and 1.
2260
2246
  */
2261
2247
  applyAimSliderCheesePenalty(value) {
2262
- if (value <= 0) {
2263
- throw new RangeError("New aim slider cheese penalty must be greater than zero.");
2248
+ if (value < 0) {
2249
+ throw new RangeError("New aim slider cheese penalty must be greater than or equal to zero.");
2264
2250
  }
2265
2251
  if (value > 1) {
2266
2252
  throw new RangeError("New aim slider cheese penalty must be less than or equal to one.");
@@ -2268,8 +2254,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2268
2254
  if (value === this._aimSliderCheesePenalty) {
2269
2255
  return;
2270
2256
  }
2271
- this.aim *= value / this._aimSliderCheesePenalty;
2272
2257
  this._aimSliderCheesePenalty = value;
2258
+ this.calculateAimValue();
2273
2259
  this.calculateTotalValue();
2274
2260
  }
2275
2261
  /**
@@ -2277,11 +2263,11 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2277
2263
  *
2278
2264
  * The flashlight and total performance value will be recalculated afterwards.
2279
2265
  *
2280
- * @param value The slider cheese penalty value. Must be between 0 (exclusive) and 1 (inclusive).
2266
+ * @param value The slider cheese penalty value. Must be between 0 and 1.
2281
2267
  */
2282
2268
  applyFlashlightSliderCheesePenalty(value) {
2283
- if (value <= 0) {
2284
- throw new RangeError("New flashlight slider cheese penalty must be greater than zero.");
2269
+ if (value < 0) {
2270
+ throw new RangeError("New flashlight slider cheese penalty must be greater than or equal to zero.");
2285
2271
  }
2286
2272
  if (value > 1) {
2287
2273
  throw new RangeError("New flashlight slider cheese penalty must be less than or equal to one.");
@@ -2289,8 +2275,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2289
2275
  if (value === this._flashlightSliderCheesePenalty) {
2290
2276
  return;
2291
2277
  }
2292
- this.flashlight *= value / this._flashlightSliderCheesePenalty;
2293
2278
  this._flashlightSliderCheesePenalty = value;
2279
+ this.calculateFlashlightValue();
2294
2280
  this.calculateTotalValue();
2295
2281
  }
2296
2282
  /**
@@ -2298,11 +2284,11 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2298
2284
  *
2299
2285
  * The visual and total performance value will be recalculated afterwards.
2300
2286
  *
2301
- * @param value The slider cheese penalty value. Must be between 0 (exclusive) and 1 (inclusive).
2287
+ * @param value The slider cheese penalty value. Must be between 0 and 1.
2302
2288
  */
2303
2289
  applyVisualSliderCheesePenalty(value) {
2304
- if (value <= 0) {
2305
- throw new RangeError("New visual slider cheese penalty must be greater than zero.");
2290
+ if (value < 0) {
2291
+ throw new RangeError("New visual slider cheese penalty must be greater than or equal to zero.");
2306
2292
  }
2307
2293
  if (value > 1) {
2308
2294
  throw new RangeError("New visual slider cheese penalty must be less than or equal to one.");
@@ -2310,8 +2296,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2310
2296
  if (value === this._visualSliderCheesePenalty) {
2311
2297
  return;
2312
2298
  }
2313
- this.visual *= value / this._visualSliderCheesePenalty;
2314
2299
  this._visualSliderCheesePenalty = value;
2300
+ this.calculateVisualValue();
2315
2301
  this.calculateTotalValue();
2316
2302
  }
2317
2303
  calculateValues() {
@@ -2345,16 +2331,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2345
2331
  */
2346
2332
  calculateAimValue() {
2347
2333
  this.aim = this.baseValue(Math.pow(this.difficultyAttributes.aimDifficulty, 0.8));
2348
- if (this.effectiveMissCount > 0) {
2349
- // Penalize misses by assessing # of misses relative to the total # of objects.
2350
- // Default a 3% reduction for any # of misses.
2351
- this.aim *=
2352
- 0.97 *
2353
- Math.pow(1 -
2354
- Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
2355
- }
2356
- // Combo scaling
2357
- this.aim *= this.comboPenalty;
2334
+ this.aim *= this.calculateMissPenalty(this.difficultyAttributes.aimDifficultStrainCount);
2358
2335
  // Scale the aim value with slider factor to nerf very likely dropped sliderends.
2359
2336
  this.aim *= this.sliderNerfFactor;
2360
2337
  // Scale the aim value with slider cheese penalty.
@@ -2369,20 +2346,23 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2369
2346
  */
2370
2347
  calculateTapValue() {
2371
2348
  this.tap = this.baseValue(this.difficultyAttributes.tapDifficulty);
2372
- if (this.effectiveMissCount > 0) {
2373
- // Penalize misses by assessing # of misses relative to the total # of objects.
2374
- // Default a 3% reduction for any # of misses.
2375
- this.tap *=
2376
- 0.97 *
2377
- Math.pow(1 -
2378
- Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
2379
- }
2380
- // Combo scaling
2381
- this.tap *= this.comboPenalty;
2349
+ this.tap *= this.calculateMissPenalty(this.difficultyAttributes.tapDifficultStrainCount);
2350
+ // Normalize the deviation to 300 BPM.
2351
+ const normalizedDeviation = this.tapDeviation *
2352
+ Math.max(1, 50 / this.difficultyAttributes.averageSpeedDeltaTime);
2353
+ // We expect the player to get 7500/x deviation when doubletapping x BPM.
2354
+ // Using this expectation, we penalize scores with deviation above 25.
2355
+ const averageBPM = 60000 / 4 / this.difficultyAttributes.averageSpeedDeltaTime;
2356
+ const adjustedDeviation = normalizedDeviation *
2357
+ (1 +
2358
+ 1 /
2359
+ (1 +
2360
+ Math.exp(-(normalizedDeviation - 7500 / averageBPM) /
2361
+ ((2 * 300) / averageBPM))));
2382
2362
  // Scale the tap value with tap deviation.
2383
2363
  this.tap *=
2384
2364
  1.1 *
2385
- Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * this._tapDeviation)), 1.25);
2365
+ Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * adjustedDeviation)), 1.25);
2386
2366
  // Scale the tap value with three-fingered penalty.
2387
2367
  this.tap /= this._tapPenalty;
2388
2368
  }
@@ -2397,7 +2377,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2397
2377
  }
2398
2378
  this.accuracy =
2399
2379
  650 *
2400
- Math.exp(-0.125 * this._deviation) *
2380
+ Math.exp(-0.1 * this._deviation) *
2401
2381
  // The following function is to give higher reward for deviations lower than 25 (250 UR).
2402
2382
  (15 / (this._deviation + 15) + 0.65);
2403
2383
  // Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
@@ -2424,15 +2404,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2424
2404
  }
2425
2405
  this.flashlight =
2426
2406
  Math.pow(this.difficultyAttributes.flashlightDifficulty, 1.6) * 25;
2427
- // Combo scaling
2428
- this.flashlight *= this.comboPenalty;
2429
- if (this.effectiveMissCount > 0) {
2430
- // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
2431
- this.flashlight *=
2432
- 0.97 *
2433
- Math.pow(1 -
2434
- Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
2435
- }
2407
+ this.flashlight *= this.calculateMissPenalty(this.difficultyAttributes.flashlightDifficultStrainCount);
2436
2408
  // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
2437
2409
  this.flashlight *=
2438
2410
  0.7 +
@@ -2451,15 +2423,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2451
2423
  calculateVisualValue() {
2452
2424
  this.visual =
2453
2425
  Math.pow(this.difficultyAttributes.visualDifficulty, 1.6) * 22.5;
2454
- if (this.effectiveMissCount > 0) {
2455
- // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
2456
- this.visual *=
2457
- 0.97 *
2458
- Math.pow(1 -
2459
- Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
2460
- }
2461
- // Combo scaling
2462
- this.visual *= this.comboPenalty;
2426
+ this.visual *= this.calculateMissPenalty(this.difficultyAttributes.visualDifficultStrainCount);
2463
2427
  // Scale the visual value with object count to penalize short maps.
2464
2428
  this.visual *= Math.min(1, 1.650668 +
2465
2429
  (0.4845796 - 1.650668) /
@@ -2471,6 +2435,21 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2471
2435
  1.065 *
2472
2436
  Math.pow(osuBase.ErrorFunction.erf(30 / (Math.SQRT2 * this._deviation)), 1.75);
2473
2437
  }
2438
+ /**
2439
+ * Calculates miss penalty.
2440
+ *
2441
+ * Miss penalty assumes that a player will miss on the hardest parts of a map,
2442
+ * so we use the amount of relatively difficult sections to adjust miss penalty
2443
+ * to make it more punishing on maps with lower amount of hard sections.
2444
+ */
2445
+ calculateMissPenalty(difficultStrainCount) {
2446
+ if (this.effectiveMissCount === 0) {
2447
+ return 1;
2448
+ }
2449
+ return (0.94 /
2450
+ (this.effectiveMissCount / (2 * Math.sqrt(difficultStrainCount)) +
2451
+ 1));
2452
+ }
2474
2453
  /**
2475
2454
  * Estimates the player's tap deviation based on the OD, number of circles and sliders,
2476
2455
  * and number of 300s, 100s, 50s, and misses, assuming the player's mean hit error is 0.
@@ -2490,17 +2469,16 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2490
2469
  return Number.POSITIVE_INFINITY;
2491
2470
  }
2492
2471
  const hitWindow300 = new osuBase.OsuHitWindow(this.difficultyAttributes.overallDifficulty).hitWindowFor300();
2493
- // Obtain the 50 hit window for droid.
2494
- const clockRate = new osuBase.MapStats({
2495
- mods: this.difficultyAttributes.mods,
2496
- }).calculate().speedMultiplier;
2497
- const realHitWindow300 = hitWindow300 * clockRate;
2472
+ // Obtain the 50 and 100 hit window for droid.
2473
+ const realHitWindow300 = hitWindow300 * this.difficultyAttributes.clockRate;
2498
2474
  const droidHitWindow = new osuBase.DroidHitWindow(osuBase.OsuHitWindow.hitWindow300ToOD(realHitWindow300));
2499
- const hitWindow50 = droidHitWindow.hitWindowFor50(this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise)) / clockRate;
2500
- const greatCountOnCircles = this.difficultyAttributes.hitCircleCount -
2501
- this.computedAccuracy.n100 -
2502
- this.computedAccuracy.n50 -
2503
- this.computedAccuracy.nmiss;
2475
+ const isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
2476
+ const hitWindow50 = droidHitWindow.hitWindowFor50(isPrecise) /
2477
+ this.difficultyAttributes.clockRate;
2478
+ const hitWindow100 = droidHitWindow.hitWindowFor100(isPrecise) /
2479
+ this.difficultyAttributes.clockRate;
2480
+ const { n300, n100, n50, nmiss } = this.computedAccuracy;
2481
+ const greatCountOnCircles = this.difficultyAttributes.hitCircleCount - n100 - n50 - nmiss;
2504
2482
  // The probability that a player hits a circle is unknown, but we can estimate it to be
2505
2483
  // the number of greats on circles divided by the number of circles, and then add one
2506
2484
  // to the number of circles as a bias correction / bayesian prior.
@@ -2519,10 +2497,32 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2519
2497
  if (greatProbabilityCircle === 0 && greatProbabilitySlider === 0) {
2520
2498
  return Number.POSITIVE_INFINITY;
2521
2499
  }
2522
- const deviationOnCircles = hitWindow300 /
2523
- (Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbabilityCircle));
2524
- const deviationOnSliders = hitWindow50 /
2525
- (Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbabilitySlider));
2500
+ const calculateDeviation = (mainHitWindow, probability) => {
2501
+ if (probability === 0) {
2502
+ return Number.POSITIVE_INFINITY;
2503
+ }
2504
+ // Start with normal deviation.
2505
+ const normalDeviation = mainHitWindow /
2506
+ (Math.SQRT2 * osuBase.ErrorFunction.erfInv(probability));
2507
+ // Get the variance of the truncated variable.
2508
+ const truncatedVariance = Math.pow(normalDeviation, 2) -
2509
+ (Math.SQRT2 *
2510
+ hitWindow100 *
2511
+ normalDeviation *
2512
+ Math.exp(-0.5 * Math.pow(hitWindow100 / normalDeviation, 2))) /
2513
+ (Math.sqrt(Math.PI) *
2514
+ osuBase.ErrorFunction.erf(hitWindow100 / (Math.SQRT2 * normalDeviation)));
2515
+ // Add 50s by assuming they are uniformly distributed.
2516
+ return Math.sqrt((1 / (n300 + n100 + n50)) *
2517
+ ((n300 + n100) * truncatedVariance +
2518
+ (n50 *
2519
+ (Math.pow(hitWindow50, 2) +
2520
+ hitWindow100 * hitWindow50 +
2521
+ Math.pow(hitWindow100, 2))) /
2522
+ 3));
2523
+ };
2524
+ const deviationOnCircles = calculateDeviation(hitWindow300, greatProbabilityCircle);
2525
+ const deviationOnSliders = calculateDeviation(hitWindow50, greatProbabilitySlider);
2526
2526
  return Math.min(deviationOnCircles, deviationOnSliders);
2527
2527
  }
2528
2528
  /**
@@ -2536,13 +2536,48 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2536
2536
  return Number.POSITIVE_INFINITY;
2537
2537
  }
2538
2538
  const hitWindow300 = new osuBase.OsuHitWindow(this.difficultyAttributes.overallDifficulty).hitWindowFor300();
2539
- const relevantTotalDiff = this.totalHits - this.difficultyAttributes.speedNoteCount;
2540
- const relevantCountGreat = Math.max(0, this.computedAccuracy.n300 - relevantTotalDiff);
2539
+ // Obtain the 50 and 100 hit window for droid.
2540
+ const realHitWindow300 = hitWindow300 * this.difficultyAttributes.clockRate;
2541
+ const droidHitWindow = new osuBase.DroidHitWindow(osuBase.OsuHitWindow.hitWindow300ToOD(realHitWindow300));
2542
+ const isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
2543
+ const hitWindow50 = droidHitWindow.hitWindowFor50(isPrecise) /
2544
+ this.difficultyAttributes.clockRate;
2545
+ const hitWindow100 = droidHitWindow.hitWindowFor100(isPrecise) /
2546
+ this.difficultyAttributes.clockRate;
2547
+ const { n300, n100, n50, nmiss } = this.computedAccuracy;
2548
+ // Assume a fixed ratio of non-300s hit in speed notes based on speed note count ratio and OD.
2549
+ // Graph: https://www.desmos.com/calculator/31argjcxqc
2550
+ const speedNoteRatio = this.difficultyAttributes.speedNoteCount / this.totalHits;
2551
+ const nonGreatCount = n100 + n50 + nmiss;
2552
+ const nonGreatRatio = 1 -
2553
+ (Math.pow(Math.exp(Math.sqrt(hitWindow300)) + 1, 1 - speedNoteRatio) -
2554
+ 1) /
2555
+ Math.exp(Math.sqrt(hitWindow300));
2556
+ const relevantCountGreat = Math.max(0, this.difficultyAttributes.speedNoteCount -
2557
+ nonGreatCount * nonGreatRatio);
2541
2558
  if (relevantCountGreat === 0) {
2542
2559
  return Number.POSITIVE_INFINITY;
2543
2560
  }
2544
2561
  const greatProbability = relevantCountGreat / (this.difficultyAttributes.speedNoteCount + 1);
2545
- return (hitWindow300 / (Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbability)));
2562
+ // Start with normal deviation.
2563
+ const normalDeviation = hitWindow300 /
2564
+ (Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbability));
2565
+ // Get the variance of the truncated variable.
2566
+ const truncatedVariance = Math.pow(normalDeviation, 2) -
2567
+ (Math.SQRT2 *
2568
+ hitWindow100 *
2569
+ normalDeviation *
2570
+ Math.exp(-0.5 * Math.pow(hitWindow100 / normalDeviation, 2))) /
2571
+ (Math.sqrt(Math.PI) *
2572
+ osuBase.ErrorFunction.erf(hitWindow100 / (Math.SQRT2 * normalDeviation)));
2573
+ // Add 50s by assuming they are uniformly distributed.
2574
+ return Math.sqrt((1 / (n300 + n100 + n50)) *
2575
+ ((n300 + n100) * truncatedVariance +
2576
+ (n50 *
2577
+ (Math.pow(hitWindow50, 2) +
2578
+ hitWindow100 * hitWindow50 +
2579
+ Math.pow(hitWindow100, 2))) /
2580
+ 3));
2546
2581
  }
2547
2582
  toString() {
2548
2583
  return (this.total.toFixed(2) +
@@ -3006,7 +3041,7 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
3006
3041
  const opacityBonus = 1 +
3007
3042
  this.maxOpacityBonus *
3008
3043
  (1 -
3009
- current.opacityAt(currentObject.object.startTime, isHiddenMod, osuBase.Modes.osu));
3044
+ current.opacityAt(currentObject.object.startTime, isHiddenMod));
3010
3045
  result +=
3011
3046
  (stackNerf * opacityBonus * scalingFactor * jumpDistance) /
3012
3047
  cumulativeStrainTime;
@@ -3098,6 +3133,7 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3098
3133
  flashlightDifficulty: 0,
3099
3134
  speedNoteCount: 0,
3100
3135
  sliderFactor: 0,
3136
+ clockRate: 1,
3101
3137
  approachRate: 0,
3102
3138
  overallDifficulty: 0,
3103
3139
  hitCircleCount: 0,
@@ -3178,9 +3214,6 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3178
3214
  this.postCalculateFlashlight(flashlightSkill);
3179
3215
  this.calculateTotal();
3180
3216
  }
3181
- /**
3182
- * Returns a string representative of the class.
3183
- */
3184
3217
  toString() {
3185
3218
  return (this.total.toFixed(2) +
3186
3219
  " stars (" +
@@ -3191,9 +3224,17 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3191
3224
  this.flashlight.toFixed(2) +
3192
3225
  " flashlight)");
3193
3226
  }
3194
- /**
3195
- * Creates skills to be calculated.
3196
- */
3227
+ preProcess() {
3228
+ const scale = osuBase.CircleSizeCalculator.standardCSToStandardScale(this.stats.cs);
3229
+ for (const object of this.beatmap.hitObjects.objects) {
3230
+ object.osuScale = scale;
3231
+ }
3232
+ const ar = new osuBase.MapStats({
3233
+ ar: this.beatmap.difficulty.ar,
3234
+ mods: osuBase.ModUtil.removeSpeedChangingMods(this.mods),
3235
+ }).calculate().ar;
3236
+ osuBase.HitObjectStackEvaluator.applyStandardStacking(this.beatmap.formatVersion, this.beatmap.hitObjects.objects, ar, this.beatmap.general.stackLeniency);
3237
+ }
3197
3238
  createSkills() {
3198
3239
  return [
3199
3240
  new OsuAim(this.mods, true),
@@ -3386,7 +3427,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
3386
3427
  // Scale the aim value with slider factor to nerf very likely dropped sliderends.
3387
3428
  this.aim *= this.sliderNerfFactor;
3388
3429
  // Scale the aim value with accuracy.
3389
- this.aim *= this.computedAccuracy.value(this.totalHits);
3430
+ this.aim *= this.computedAccuracy.value();
3390
3431
  // It is also important to consider accuracy difficulty when doing that.
3391
3432
  const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
3392
3433
  this.aim *= 0.98 + odScaling;
@@ -3434,15 +3475,14 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
3434
3475
  n300: Math.max(0, countGreat - relevantTotalDiff),
3435
3476
  n100: Math.max(0, countOk - Math.max(0, relevantTotalDiff - countGreat)),
3436
3477
  n50: Math.max(0, countMeh - Math.max(0, relevantTotalDiff - countGreat - countOk)),
3437
- nmiss: this.effectiveMissCount,
3438
3478
  });
3439
3479
  // Scale the speed value with accuracy and OD.
3440
3480
  this.speed *=
3441
3481
  (0.95 +
3442
3482
  Math.pow(this.difficultyAttributes.overallDifficulty, 2) /
3443
3483
  750) *
3444
- Math.pow((this.computedAccuracy.value(this.totalHits) +
3445
- relevantAccuracy.value()) /
3484
+ Math.pow((this.computedAccuracy.value() +
3485
+ relevantAccuracy.value(this.difficultyAttributes.speedNoteCount)) /
3446
3486
  2, (14.5 -
3447
3487
  Math.max(this.difficultyAttributes.overallDifficulty, 8)) /
3448
3488
  2);
@@ -3472,7 +3512,8 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
3472
3512
  // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
3473
3513
  this.accuracy =
3474
3514
  Math.pow(1.52163, this.difficultyAttributes.overallDifficulty) *
3475
- Math.pow(realAccuracy.value(ncircles), 24) *
3515
+ // It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points.
3516
+ Math.pow(realAccuracy.n300 < 0 ? 0 : realAccuracy.value(), 24) *
3476
3517
  2.83;
3477
3518
  // Bonus for many hitcircles - it's harder to keep good accuracy up for longer
3478
3519
  this.accuracy *= Math.min(1.15, Math.pow(ncircles / 1000, 0.3));
@@ -3511,8 +3552,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
3511
3552
  ? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
3512
3553
  : 0);
3513
3554
  // Scale the flashlight value with accuracy slightly.
3514
- this.flashlight *=
3515
- 0.5 + this.computedAccuracy.value(this.totalHits) / 2;
3555
+ this.flashlight *= 0.5 + this.computedAccuracy.value() / 2;
3516
3556
  // It is also important to consider accuracy difficulty when doing that.
3517
3557
  const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
3518
3558
  this.flashlight *= 0.98 + odScaling;