@rian8337/osu-difficulty-calculator 4.0.0-beta.0 → 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
  /**
@@ -1273,6 +1253,7 @@ class DroidFlashlight extends DroidSkill {
1273
1253
  * This class should be considered an "evaluating" class and not persisted.
1274
1254
  */
1275
1255
  class RhythmEvaluator {
1256
+ static rhythmMultiplier = 0.75;
1276
1257
  static historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
1277
1258
  }
1278
1259
 
@@ -1290,7 +1271,7 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
1290
1271
  static evaluateDifficultyOf(current, greatWindow) {
1291
1272
  if (current.object instanceof osuBase.Spinner ||
1292
1273
  // Exclude overlapping objects that can be tapped at once.
1293
- current.deltaTime < 5) {
1274
+ current.isOverlapping(false)) {
1294
1275
  return 1;
1295
1276
  }
1296
1277
  let previousIslandSize = 0;
@@ -1308,7 +1289,7 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
1308
1289
  if (!object) {
1309
1290
  break;
1310
1291
  }
1311
- if (object.deltaTime >= 5) {
1292
+ if (!object.isOverlapping(false)) {
1312
1293
  validPrevious.push(object);
1313
1294
  }
1314
1295
  }
@@ -1402,20 +1383,7 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
1402
1383
  const windowRatio = Math.pow(Math.min(1, currentDeltaTime / (greatWindow * 2)), 2);
1403
1384
  doubletapness = Math.pow(speedRatio, 1 - windowRatio);
1404
1385
  }
1405
- return (Math.sqrt(4 +
1406
- rhythmComplexitySum *
1407
- this.calculateRhythmMultiplier(greatWindow) *
1408
- doubletapness) / 2);
1409
- }
1410
- /**
1411
- * Calculates the rhythm multiplier of a given hit window.
1412
- *
1413
- * @param greatWindow The great hit window.
1414
- */
1415
- static calculateRhythmMultiplier(greatWindow) {
1416
- const od = osuBase.OsuHitWindow.hitWindow300ToOD(greatWindow);
1417
- const odScaling = Math.pow(od, 2) / 400;
1418
- return 0.75 + (od >= 0 ? odScaling : -odScaling);
1386
+ return (Math.sqrt(4 + rhythmComplexitySum * this.rhythmMultiplier * doubletapness) / 2);
1419
1387
  }
1420
1388
  }
1421
1389
 
@@ -1468,7 +1436,8 @@ class DroidVisualEvaluator {
1468
1436
  static evaluateDifficultyOf(current, isHiddenMod, withSliders) {
1469
1437
  if (current.object instanceof osuBase.Spinner ||
1470
1438
  // Exclude overlapping objects that can be tapped at once.
1471
- current.isOverlapping(true)) {
1439
+ current.isOverlapping(true) ||
1440
+ current.index === 0) {
1472
1441
  return 0;
1473
1442
  }
1474
1443
  // Start with base density and give global bonus for Hidden.
@@ -1495,7 +1464,7 @@ class DroidVisualEvaluator {
1495
1464
  }
1496
1465
  strain +=
1497
1466
  (1 -
1498
- current.opacityAt(previous.object.startTime, isHiddenMod, osuBase.Modes.droid)) /
1467
+ current.opacityAt(previous.object.startTime, isHiddenMod)) /
1499
1468
  4;
1500
1469
  }
1501
1470
  // Scale the value with overlapping factor.
@@ -1531,44 +1500,6 @@ class DroidVisualEvaluator {
1531
1500
  Math.min(1, 300 / cumulativeStrainTime);
1532
1501
  }
1533
1502
  }
1534
- // Reward for rhythm changes.
1535
- if (current.rhythmMultiplier > 1) {
1536
- let rhythmBonus = (current.rhythmMultiplier - 1) / 20;
1537
- // Rhythm changes are harder to read in Hidden.
1538
- // Add additional bonus for Hidden.
1539
- if (isHiddenMod) {
1540
- rhythmBonus += (current.rhythmMultiplier - 1) / 25;
1541
- }
1542
- // Rhythm changes are harder to read when objects are stacked together.
1543
- // Scale rhythm bonus based on the stack of past objects.
1544
- const diameter = 2 * current.object.getRadius(osuBase.Modes.droid);
1545
- let cumulativeStrainTime = 0;
1546
- for (let i = 0; i < Math.min(current.index, 5); ++i) {
1547
- const previous = current.previous(i);
1548
- if (previous.object instanceof osuBase.Spinner ||
1549
- // Exclude overlapping objects that can be tapped at once.
1550
- previous.isOverlapping(true)) {
1551
- continue;
1552
- }
1553
- const jumpDistance = current.object
1554
- .getStackedPosition(osuBase.Modes.droid)
1555
- .getDistance(previous.object.getStackedEndPosition(osuBase.Modes.droid));
1556
- cumulativeStrainTime += previous.strainTime;
1557
- rhythmBonus +=
1558
- // Scale the bonus with diameter.
1559
- osuBase.MathUtils.clamp((0.5 - jumpDistance / diameter) / 10, 0, 0.05) *
1560
- // Scale with cumulative strain time to avoid overbuffing past objects.
1561
- Math.min(1, 300 / cumulativeStrainTime);
1562
- // Give a larger bonus for Hidden.
1563
- if (isHiddenMod) {
1564
- rhythmBonus +=
1565
- (1 -
1566
- current.opacityAt(previous.object.startTime, isHiddenMod, osuBase.Modes.droid)) /
1567
- 20;
1568
- }
1569
- }
1570
- strain += rhythmBonus;
1571
- }
1572
1503
  return strain;
1573
1504
  }
1574
1505
  }
@@ -1583,20 +1514,20 @@ class DroidVisual extends DroidSkill {
1583
1514
  skillMultiplier = 10;
1584
1515
  strainDecayBase = 0.1;
1585
1516
  isHidden;
1586
- withSliders;
1517
+ withsliders;
1587
1518
  constructor(mods, withSliders) {
1588
1519
  super(mods);
1589
1520
  this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
1590
- this.withSliders = withSliders;
1521
+ this.withsliders = withSliders;
1591
1522
  }
1592
1523
  strainValueAt(current) {
1593
1524
  this.currentStrain *= this.strainDecay(current.deltaTime);
1594
1525
  this.currentStrain +=
1595
- DroidVisualEvaluator.evaluateDifficultyOf(current, this.isHidden, this.withSliders) * this.skillMultiplier;
1596
- return this.currentStrain;
1526
+ DroidVisualEvaluator.evaluateDifficultyOf(current, this.isHidden, this.withsliders) * this.skillMultiplier;
1527
+ return this.currentStrain * (1 + (current.rhythmMultiplier - 1) / 5);
1597
1528
  }
1598
1529
  saveToHitObject(current) {
1599
- if (this.withSliders) {
1530
+ if (this.withsliders) {
1600
1531
  current.visualStrainWithSliders = this.currentStrain;
1601
1532
  }
1602
1533
  else {
@@ -1647,15 +1578,21 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1647
1578
  flashlightDifficulty: 0,
1648
1579
  speedNoteCount: 0,
1649
1580
  sliderFactor: 0,
1581
+ clockRate: 1,
1650
1582
  approachRate: 0,
1651
1583
  overallDifficulty: 0,
1652
1584
  hitCircleCount: 0,
1653
1585
  sliderCount: 0,
1654
1586
  spinnerCount: 0,
1587
+ aimDifficultStrainCount: 0,
1588
+ tapDifficultStrainCount: 0,
1589
+ flashlightDifficultStrainCount: 0,
1590
+ visualDifficultStrainCount: 0,
1655
1591
  flashlightSliderFactor: 0,
1656
1592
  visualSliderFactor: 0,
1657
1593
  possibleThreeFingeredSections: [],
1658
1594
  difficultSliders: [],
1595
+ averageSpeedDeltaTime: 0,
1659
1596
  };
1660
1597
  difficultyMultiplier = 0.18;
1661
1598
  mode = osuBase.Modes.droid;
@@ -1777,9 +1714,6 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1777
1714
  }
1778
1715
  this.calculateTotal();
1779
1716
  }
1780
- /**
1781
- * Returns a string representative of the class.
1782
- */
1783
1717
  toString() {
1784
1718
  return (this.total.toFixed(2) +
1785
1719
  " stars (" +
@@ -1794,9 +1728,13 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1794
1728
  this.visual.toFixed(2) +
1795
1729
  " visual)");
1796
1730
  }
1797
- /**
1798
- * Creates skills to be calculated.
1799
- */
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
+ }
1800
1738
  createSkills() {
1801
1739
  return [
1802
1740
  new DroidAim(this.mods, true),
@@ -1837,33 +1775,39 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1837
1775
  calculateAimAttributes() {
1838
1776
  const objectStrains = [];
1839
1777
  let maxStrain = 0;
1840
- // Take the top 15% most difficult sliders based on velocity.
1841
1778
  const topDifficultSliders = [];
1842
1779
  for (let i = 0; i < this.objects.length; ++i) {
1843
1780
  const object = this.objects[i];
1844
1781
  objectStrains.push(object.aimStrainWithSliders);
1845
1782
  maxStrain = Math.max(maxStrain, object.aimStrainWithSliders);
1846
- if (object.object instanceof osuBase.Slider) {
1783
+ const velocity = object.travelDistance / object.travelTime;
1784
+ if (velocity > 0) {
1847
1785
  topDifficultSliders.push({
1848
1786
  index: i,
1849
- velocity: object.travelDistance / object.travelTime,
1787
+ velocity: velocity,
1850
1788
  });
1851
- topDifficultSliders.sort((a, b) => b.velocity - a.velocity);
1852
- while (topDifficultSliders.length >
1853
- Math.floor(0.15 * this.objects.length)) {
1854
- topDifficultSliders.pop();
1855
- }
1856
1789
  }
1857
1790
  }
1858
1791
  if (maxStrain) {
1859
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);
1860
1794
  }
1861
1795
  const velocitySum = topDifficultSliders.reduce((a, v) => a + v.velocity, 0);
1862
1796
  for (const slider of topDifficultSliders) {
1863
- this.attributes.difficultSliders.push({
1864
- index: slider.index,
1865
- difficultyRating: slider.velocity / velocitySum,
1866
- });
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();
1867
1811
  }
1868
1812
  }
1869
1813
  /**
@@ -1882,6 +1826,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1882
1826
  this.attributes.possibleThreeFingeredSections = [];
1883
1827
  const tempSections = [];
1884
1828
  const objectStrains = [];
1829
+ const objectDeltaTimes = [];
1885
1830
  let maxStrain = 0;
1886
1831
  const maxSectionDeltaTime = 2000;
1887
1832
  const minSectionObjectCount = 5;
@@ -1891,8 +1836,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1891
1836
  const next = this.objects[i + 1];
1892
1837
  if (i === 0) {
1893
1838
  objectStrains.push(current.tapStrain);
1839
+ objectDeltaTimes.push(current.deltaTime);
1894
1840
  }
1895
1841
  objectStrains.push(next.tapStrain);
1842
+ objectDeltaTimes.push(next.deltaTime);
1896
1843
  maxStrain = Math.max(current.tapStrain, maxStrain);
1897
1844
  const realDeltaTime = next.object.startTime - current.object.endTime;
1898
1845
  if (realDeltaTime >= maxSectionDeltaTime) {
@@ -1931,6 +1878,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1931
1878
  if (inSpeedSection &&
1932
1879
  current.originalTapStrain < threeFingerStrainThreshold) {
1933
1880
  inSpeedSection = false;
1881
+ // Ignore sections that don't meet object count requirement.
1882
+ if (i - newFirstObjectIndex < minSectionObjectCount) {
1883
+ continue;
1884
+ }
1934
1885
  this.attributes.possibleThreeFingeredSections.push({
1935
1886
  firstObjectIndex: newFirstObjectIndex,
1936
1887
  lastObjectIndex: i,
@@ -1939,7 +1890,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1939
1890
  }
1940
1891
  }
1941
1892
  // Don't forget to manually add the last beatmap section, which would otherwise be ignored.
1942
- if (inSpeedSection) {
1893
+ // Ignore sections that don't meet object count requirement.
1894
+ if (inSpeedSection &&
1895
+ section.lastObjectIndex - newFirstObjectIndex >=
1896
+ minSectionObjectCount) {
1943
1897
  this.attributes.possibleThreeFingeredSections.push({
1944
1898
  firstObjectIndex: newFirstObjectIndex,
1945
1899
  lastObjectIndex: section.lastObjectIndex,
@@ -1949,6 +1903,16 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1949
1903
  }
1950
1904
  if (maxStrain) {
1951
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);
1952
1916
  }
1953
1917
  }
1954
1918
  /**
@@ -1991,6 +1955,12 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1991
1955
  if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
1992
1956
  this.flashlight *= 0.7;
1993
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
+ }
1994
1964
  this.attributes.flashlightDifficulty = this.flashlight;
1995
1965
  }
1996
1966
  /**
@@ -2008,6 +1978,11 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
2008
1978
  this.starValue(visualSkillWithoutSliders.difficultyValue()) /
2009
1979
  this.visual;
2010
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
+ }
2011
1986
  }
2012
1987
  }
2013
1988
 
@@ -2130,7 +2105,7 @@ class PerformanceCalculator {
2130
2105
  if (this.difficultyAttributes.sliderCount > 0) {
2131
2106
  // We assume 15% of sliders in a beatmap are difficult since there's no way to tell from the performance calculator.
2132
2107
  const estimateDifficultSliders = this.difficultyAttributes.sliderCount * 0.15;
2133
- const estimateSliderEndsDropped = osuBase.MathUtils.clamp(Math.min(this.computedAccuracy.n300 +
2108
+ const estimateSliderEndsDropped = osuBase.MathUtils.clamp(Math.min(this.computedAccuracy.n100 +
2134
2109
  this.computedAccuracy.n50 +
2135
2110
  this.computedAccuracy.nmiss, maxCombo - combo), 0, estimateDifficultSliders);
2136
2111
  this.sliderNerfFactor =
@@ -2145,6 +2120,7 @@ class PerformanceCalculator {
2145
2120
  * Calculates the amount of misses + sliderbreaks from combo.
2146
2121
  */
2147
2122
  calculateEffectiveMissCount(combo, maxCombo) {
2123
+ // Guess the number of misses + slider breaks from combo.
2148
2124
  let comboBasedMissCount = 0;
2149
2125
  if (this.difficultyAttributes.sliderCount > 0) {
2150
2126
  const fullComboThreshold = maxCombo - 0.1 * this.difficultyAttributes.sliderCount;
@@ -2266,20 +2242,20 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2266
2242
  *
2267
2243
  * The aim and total performance value will be recalculated afterwards.
2268
2244
  *
2269
- * @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.
2270
2246
  */
2271
2247
  applyAimSliderCheesePenalty(value) {
2272
- if (value <= 0) {
2273
- 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.");
2274
2250
  }
2275
2251
  if (value > 1) {
2276
- throw new RangeError("New visual slider cheese penalty must be less than or equal to one.");
2252
+ throw new RangeError("New aim slider cheese penalty must be less than or equal to one.");
2277
2253
  }
2278
2254
  if (value === this._aimSliderCheesePenalty) {
2279
2255
  return;
2280
2256
  }
2281
- this.aim *= this._aimSliderCheesePenalty / value;
2282
2257
  this._aimSliderCheesePenalty = value;
2258
+ this.calculateAimValue();
2283
2259
  this.calculateTotalValue();
2284
2260
  }
2285
2261
  /**
@@ -2287,11 +2263,11 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2287
2263
  *
2288
2264
  * The flashlight and total performance value will be recalculated afterwards.
2289
2265
  *
2290
- * @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.
2291
2267
  */
2292
2268
  applyFlashlightSliderCheesePenalty(value) {
2293
- if (value <= 0) {
2294
- 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.");
2295
2271
  }
2296
2272
  if (value > 1) {
2297
2273
  throw new RangeError("New flashlight slider cheese penalty must be less than or equal to one.");
@@ -2299,8 +2275,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2299
2275
  if (value === this._flashlightSliderCheesePenalty) {
2300
2276
  return;
2301
2277
  }
2302
- this.flashlight *= this._flashlightSliderCheesePenalty / value;
2303
2278
  this._flashlightSliderCheesePenalty = value;
2279
+ this.calculateFlashlightValue();
2304
2280
  this.calculateTotalValue();
2305
2281
  }
2306
2282
  /**
@@ -2308,11 +2284,11 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2308
2284
  *
2309
2285
  * The visual and total performance value will be recalculated afterwards.
2310
2286
  *
2311
- * @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.
2312
2288
  */
2313
2289
  applyVisualSliderCheesePenalty(value) {
2314
- if (value <= 0) {
2315
- 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.");
2316
2292
  }
2317
2293
  if (value > 1) {
2318
2294
  throw new RangeError("New visual slider cheese penalty must be less than or equal to one.");
@@ -2320,8 +2296,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2320
2296
  if (value === this._visualSliderCheesePenalty) {
2321
2297
  return;
2322
2298
  }
2323
- this.visual *= this._visualSliderCheesePenalty / value;
2324
2299
  this._visualSliderCheesePenalty = value;
2300
+ this.calculateVisualValue();
2325
2301
  this.calculateTotalValue();
2326
2302
  }
2327
2303
  calculateValues() {
@@ -2355,16 +2331,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2355
2331
  */
2356
2332
  calculateAimValue() {
2357
2333
  this.aim = this.baseValue(Math.pow(this.difficultyAttributes.aimDifficulty, 0.8));
2358
- if (this.effectiveMissCount > 0) {
2359
- // Penalize misses by assessing # of misses relative to the total # of objects.
2360
- // Default a 3% reduction for any # of misses.
2361
- this.aim *=
2362
- 0.97 *
2363
- Math.pow(1 -
2364
- Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
2365
- }
2366
- // Combo scaling
2367
- this.aim *= this.comboPenalty;
2334
+ this.aim *= this.calculateMissPenalty(this.difficultyAttributes.aimDifficultStrainCount);
2368
2335
  // Scale the aim value with slider factor to nerf very likely dropped sliderends.
2369
2336
  this.aim *= this.sliderNerfFactor;
2370
2337
  // Scale the aim value with slider cheese penalty.
@@ -2379,20 +2346,23 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2379
2346
  */
2380
2347
  calculateTapValue() {
2381
2348
  this.tap = this.baseValue(this.difficultyAttributes.tapDifficulty);
2382
- if (this.effectiveMissCount > 0) {
2383
- // Penalize misses by assessing # of misses relative to the total # of objects.
2384
- // Default a 3% reduction for any # of misses.
2385
- this.tap *=
2386
- 0.97 *
2387
- Math.pow(1 -
2388
- Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
2389
- }
2390
- // Combo scaling
2391
- 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))));
2392
2362
  // Scale the tap value with tap deviation.
2393
2363
  this.tap *=
2394
2364
  1.1 *
2395
- Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * this._tapDeviation)), 1.25);
2365
+ Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * adjustedDeviation)), 1.25);
2396
2366
  // Scale the tap value with three-fingered penalty.
2397
2367
  this.tap /= this._tapPenalty;
2398
2368
  }
@@ -2407,9 +2377,14 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2407
2377
  }
2408
2378
  this.accuracy =
2409
2379
  650 *
2410
- Math.exp(-0.125 * this._deviation) *
2380
+ Math.exp(-0.1 * this._deviation) *
2411
2381
  // The following function is to give higher reward for deviations lower than 25 (250 UR).
2412
2382
  (15 / (this._deviation + 15) + 0.65);
2383
+ // Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
2384
+ const ncircles = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModScoreV2)
2385
+ ? this.totalHits - this.difficultyAttributes.spinnerCount
2386
+ : this.difficultyAttributes.hitCircleCount;
2387
+ this.accuracy *= Math.min(1.15, Math.sqrt(Math.log(1 + ((Math.E - 1) * ncircles) / 1000)));
2413
2388
  // Scale the accuracy value with rhythm complexity.
2414
2389
  this.accuracy *=
2415
2390
  1.5 /
@@ -2429,15 +2404,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2429
2404
  }
2430
2405
  this.flashlight =
2431
2406
  Math.pow(this.difficultyAttributes.flashlightDifficulty, 1.6) * 25;
2432
- // Combo scaling
2433
- this.flashlight *= this.comboPenalty;
2434
- if (this.effectiveMissCount > 0) {
2435
- // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
2436
- this.flashlight *=
2437
- 0.97 *
2438
- Math.pow(1 -
2439
- Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
2440
- }
2407
+ this.flashlight *= this.calculateMissPenalty(this.difficultyAttributes.flashlightDifficultStrainCount);
2441
2408
  // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
2442
2409
  this.flashlight *=
2443
2410
  0.7 +
@@ -2447,9 +2414,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2447
2414
  : 0);
2448
2415
  // Scale the flashlight value with slider cheese penalty.
2449
2416
  this.flashlight *= this._flashlightSliderCheesePenalty;
2450
- this.flashlight *=
2451
- // Scale the flashlight value with deviation.
2452
- this.flashlight *= osuBase.ErrorFunction.erf(50 / (Math.SQRT2 * this._deviation));
2417
+ // Scale the flashlight value with deviation.
2418
+ this.flashlight *= osuBase.ErrorFunction.erf(50 / (Math.SQRT2 * this._deviation));
2453
2419
  }
2454
2420
  /**
2455
2421
  * Calculates the visual performance value of the beatmap.
@@ -2457,15 +2423,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2457
2423
  calculateVisualValue() {
2458
2424
  this.visual =
2459
2425
  Math.pow(this.difficultyAttributes.visualDifficulty, 1.6) * 22.5;
2460
- if (this.effectiveMissCount > 0) {
2461
- // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
2462
- this.visual *=
2463
- 0.97 *
2464
- Math.pow(1 -
2465
- Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
2466
- }
2467
- // Combo scaling
2468
- this.visual *= this.comboPenalty;
2426
+ this.visual *= this.calculateMissPenalty(this.difficultyAttributes.visualDifficultStrainCount);
2469
2427
  // Scale the visual value with object count to penalize short maps.
2470
2428
  this.visual *= Math.min(1, 1.650668 +
2471
2429
  (0.4845796 - 1.650668) /
@@ -2477,6 +2435,21 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2477
2435
  1.065 *
2478
2436
  Math.pow(osuBase.ErrorFunction.erf(30 / (Math.SQRT2 * this._deviation)), 1.75);
2479
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
+ }
2480
2453
  /**
2481
2454
  * Estimates the player's tap deviation based on the OD, number of circles and sliders,
2482
2455
  * and number of 300s, 100s, 50s, and misses, assuming the player's mean hit error is 0.
@@ -2496,17 +2469,16 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2496
2469
  return Number.POSITIVE_INFINITY;
2497
2470
  }
2498
2471
  const hitWindow300 = new osuBase.OsuHitWindow(this.difficultyAttributes.overallDifficulty).hitWindowFor300();
2499
- // Obtain the 50 hit window for droid.
2500
- const clockRate = new osuBase.MapStats({
2501
- mods: this.difficultyAttributes.mods,
2502
- }).calculate().speedMultiplier;
2503
- const realHitWindow300 = hitWindow300 * clockRate;
2472
+ // Obtain the 50 and 100 hit window for droid.
2473
+ const realHitWindow300 = hitWindow300 * this.difficultyAttributes.clockRate;
2504
2474
  const droidHitWindow = new osuBase.DroidHitWindow(osuBase.OsuHitWindow.hitWindow300ToOD(realHitWindow300));
2505
- const hitWindow50 = droidHitWindow.hitWindowFor50(this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise)) / clockRate;
2506
- const greatCountOnCircles = this.difficultyAttributes.hitCircleCount -
2507
- this.computedAccuracy.n100 -
2508
- this.computedAccuracy.n50 -
2509
- 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;
2510
2482
  // The probability that a player hits a circle is unknown, but we can estimate it to be
2511
2483
  // the number of greats on circles divided by the number of circles, and then add one
2512
2484
  // to the number of circles as a bias correction / bayesian prior.
@@ -2525,10 +2497,32 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2525
2497
  if (greatProbabilityCircle === 0 && greatProbabilitySlider === 0) {
2526
2498
  return Number.POSITIVE_INFINITY;
2527
2499
  }
2528
- const deviationOnCircles = hitWindow300 /
2529
- (Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbabilityCircle));
2530
- const deviationOnSliders = hitWindow50 /
2531
- (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);
2532
2526
  return Math.min(deviationOnCircles, deviationOnSliders);
2533
2527
  }
2534
2528
  /**
@@ -2542,13 +2536,48 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2542
2536
  return Number.POSITIVE_INFINITY;
2543
2537
  }
2544
2538
  const hitWindow300 = new osuBase.OsuHitWindow(this.difficultyAttributes.overallDifficulty).hitWindowFor300();
2545
- const relevantTotalDiff = this.totalHits - this.difficultyAttributes.speedNoteCount;
2546
- 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);
2547
2558
  if (relevantCountGreat === 0) {
2548
2559
  return Number.POSITIVE_INFINITY;
2549
2560
  }
2550
2561
  const greatProbability = relevantCountGreat / (this.difficultyAttributes.speedNoteCount + 1);
2551
- 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));
2552
2581
  }
2553
2582
  toString() {
2554
2583
  return (this.total.toFixed(2) +
@@ -2827,7 +2856,6 @@ class OsuSpeedEvaluator extends SpeedEvaluator {
2827
2856
  * An evaluator for calculating osu!standard Rhythm skill.
2828
2857
  */
2829
2858
  class OsuRhythmEvaluator extends RhythmEvaluator {
2830
- static rhythmMultiplier = 0.75;
2831
2859
  /**
2832
2860
  * Calculates a rhythm multiplier for the difficulty of the tap associated
2833
2861
  * with historic data of the current object.
@@ -2945,8 +2973,6 @@ class OsuSpeed extends OsuSkill {
2945
2973
  decayWeight = 0.9;
2946
2974
  currentSpeedStrain = 0;
2947
2975
  currentRhythm = 0;
2948
- // ~200 1/4 BPM streams
2949
- minSpeedBonus = 75;
2950
2976
  greatWindow;
2951
2977
  constructor(mods, greatWindow) {
2952
2978
  super(mods);
@@ -3015,7 +3041,7 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
3015
3041
  const opacityBonus = 1 +
3016
3042
  this.maxOpacityBonus *
3017
3043
  (1 -
3018
- current.opacityAt(currentObject.object.startTime, isHiddenMod, osuBase.Modes.osu));
3044
+ current.opacityAt(currentObject.object.startTime, isHiddenMod));
3019
3045
  result +=
3020
3046
  (stackNerf * opacityBonus * scalingFactor * jumpDistance) /
3021
3047
  cumulativeStrainTime;
@@ -3107,6 +3133,7 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3107
3133
  flashlightDifficulty: 0,
3108
3134
  speedNoteCount: 0,
3109
3135
  sliderFactor: 0,
3136
+ clockRate: 1,
3110
3137
  approachRate: 0,
3111
3138
  overallDifficulty: 0,
3112
3139
  hitCircleCount: 0,
@@ -3187,9 +3214,6 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3187
3214
  this.postCalculateFlashlight(flashlightSkill);
3188
3215
  this.calculateTotal();
3189
3216
  }
3190
- /**
3191
- * Returns a string representative of the class.
3192
- */
3193
3217
  toString() {
3194
3218
  return (this.total.toFixed(2) +
3195
3219
  " stars (" +
@@ -3200,9 +3224,17 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3200
3224
  this.flashlight.toFixed(2) +
3201
3225
  " flashlight)");
3202
3226
  }
3203
- /**
3204
- * Creates skills to be calculated.
3205
- */
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
+ }
3206
3238
  createSkills() {
3207
3239
  return [
3208
3240
  new OsuAim(this.mods, true),
@@ -3395,7 +3427,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
3395
3427
  // Scale the aim value with slider factor to nerf very likely dropped sliderends.
3396
3428
  this.aim *= this.sliderNerfFactor;
3397
3429
  // Scale the aim value with accuracy.
3398
- this.aim *= this.computedAccuracy.value(this.totalHits);
3430
+ this.aim *= this.computedAccuracy.value();
3399
3431
  // It is also important to consider accuracy difficulty when doing that.
3400
3432
  const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
3401
3433
  this.aim *= 0.98 + odScaling;
@@ -3443,15 +3475,14 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
3443
3475
  n300: Math.max(0, countGreat - relevantTotalDiff),
3444
3476
  n100: Math.max(0, countOk - Math.max(0, relevantTotalDiff - countGreat)),
3445
3477
  n50: Math.max(0, countMeh - Math.max(0, relevantTotalDiff - countGreat - countOk)),
3446
- nmiss: this.effectiveMissCount,
3447
3478
  });
3448
3479
  // Scale the speed value with accuracy and OD.
3449
3480
  this.speed *=
3450
3481
  (0.95 +
3451
3482
  Math.pow(this.difficultyAttributes.overallDifficulty, 2) /
3452
3483
  750) *
3453
- Math.pow((this.computedAccuracy.value(this.totalHits) +
3454
- relevantAccuracy.value()) /
3484
+ Math.pow((this.computedAccuracy.value() +
3485
+ relevantAccuracy.value(this.difficultyAttributes.speedNoteCount)) /
3455
3486
  2, (14.5 -
3456
3487
  Math.max(this.difficultyAttributes.overallDifficulty, 8)) /
3457
3488
  2);
@@ -3481,7 +3512,8 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
3481
3512
  // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
3482
3513
  this.accuracy =
3483
3514
  Math.pow(1.52163, this.difficultyAttributes.overallDifficulty) *
3484
- 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) *
3485
3517
  2.83;
3486
3518
  // Bonus for many hitcircles - it's harder to keep good accuracy up for longer
3487
3519
  this.accuracy *= Math.min(1.15, Math.pow(ncircles / 1000, 0.3));
@@ -3520,8 +3552,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
3520
3552
  ? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
3521
3553
  : 0);
3522
3554
  // Scale the flashlight value with accuracy slightly.
3523
- this.flashlight *=
3524
- 0.5 + this.computedAccuracy.value(this.totalHits) / 2;
3555
+ this.flashlight *= 0.5 + this.computedAccuracy.value() / 2;
3525
3556
  // It is also important to consider accuracy difficulty when doing that.
3526
3557
  const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
3527
3558
  this.flashlight *= 0.98 + odScaling;