@rian8337/osu-base 3.0.0 → 3.3.0

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
@@ -1,17 +1,10 @@
1
1
  'use strict';
2
2
 
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
3
  var request = require('request');
6
4
  var cloneDeep = require('lodash.clonedeep');
7
- var path = require('path');
5
+ var node_path = require('node:path');
8
6
  var dotenv = require('dotenv');
9
7
 
10
- function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
11
-
12
- var request__default = /*#__PURE__*/_interopDefaultLegacy(request);
13
- var cloneDeep__default = /*#__PURE__*/_interopDefaultLegacy(cloneDeep);
14
-
15
8
  /**
16
9
  * Some math utility functions.
17
10
  */
@@ -211,7 +204,7 @@ class Utils {
211
204
  * @param obj The object to deep copy.
212
205
  */
213
206
  static deepCopy(obj) {
214
- return cloneDeep__default["default"](obj);
207
+ return cloneDeep(obj);
215
208
  }
216
209
  /**
217
210
  * Creates an array with specific length that's prefilled with an initial value.
@@ -305,7 +298,7 @@ class APIRequestBuilder {
305
298
  return new Promise((resolve) => {
306
299
  const url = this.buildURL();
307
300
  const dataArray = [];
308
- request__default["default"](url)
301
+ request(url)
309
302
  .on("data", (chunk) => {
310
303
  dataArray.push(Buffer.from(chunk));
311
304
  })
@@ -514,6 +507,10 @@ class Vector2 {
514
507
  * Represents a hitobject in a beatmap.
515
508
  */
516
509
  class HitObject {
510
+ /**
511
+ * The base radius of all hitobjects.
512
+ */
513
+ static baseRadius = 64;
517
514
  /**
518
515
  * The start time of the hitobject in milliseconds.
519
516
  */
@@ -558,15 +555,51 @@ class HitObject {
558
555
  /**
559
556
  * The stack height of the hitobject.
560
557
  */
561
- stackHeight = 0;
558
+ _stackHeight = 0;
559
+ /**
560
+ * The stack height of the hitobject.
561
+ */
562
+ get stackHeight() {
563
+ return this._stackHeight;
564
+ }
565
+ /**
566
+ * The stack height of the hitobject.
567
+ */
568
+ set stackHeight(value) {
569
+ this._stackHeight = value;
570
+ }
571
+ /**
572
+ * The osu!droid scale used to calculate stacked position and radius.
573
+ */
574
+ _droidScale = 1;
575
+ /**
576
+ * The osu!droid scale used to calculate stacked position and radius.
577
+ */
578
+ get droidScale() {
579
+ return this._droidScale;
580
+ }
562
581
  /**
563
582
  * The osu!droid scale used to calculate stacked position and radius.
564
583
  */
565
- droidScale = 1;
584
+ set droidScale(value) {
585
+ this._droidScale = value;
586
+ }
566
587
  /**
567
588
  * The osu!standard scale used to calculate stacked position and radius.
568
589
  */
569
- osuScale = 1;
590
+ _osuScale = 1;
591
+ /**
592
+ * The osu!standard scale used to calculate stacked position and radius.
593
+ */
594
+ get osuScale() {
595
+ return this._osuScale;
596
+ }
597
+ /**
598
+ * The osu!standard scale used to calculate stacked position and radius.
599
+ */
600
+ set osuScale(value) {
601
+ this._osuScale = value;
602
+ }
570
603
  /**
571
604
  * The hitobject type (circle, slider, or spinner).
572
605
  */
@@ -599,16 +632,12 @@ class HitObject {
599
632
  * @returns The radius of the hitobject with respect to the gamemode.
600
633
  */
601
634
  getRadius(mode) {
602
- let radius = 64;
603
635
  switch (mode) {
604
636
  case exports.Modes.droid:
605
- radius *= this.droidScale;
606
- break;
637
+ return HitObject.baseRadius * this._droidScale;
607
638
  case exports.Modes.osu:
608
- radius *= this.osuScale;
609
- break;
639
+ return HitObject.baseRadius * this._osuScale;
610
640
  }
611
- return radius;
612
641
  }
613
642
  /**
614
643
  * Evaluates the stack offset vector of the hitobject.
@@ -619,13 +648,13 @@ class HitObject {
619
648
  * @returns The stack offset with respect to the gamemode.
620
649
  */
621
650
  getStackOffset(mode) {
622
- let coordinate = this.stackHeight;
651
+ let coordinate = this._stackHeight;
623
652
  switch (mode) {
624
653
  case exports.Modes.droid:
625
- coordinate *= this.droidScale * -4;
654
+ coordinate *= this._droidScale * 4;
626
655
  break;
627
656
  case exports.Modes.osu:
628
- coordinate *= this.osuScale * -6.4;
657
+ coordinate *= this._osuScale * -6.4;
629
658
  break;
630
659
  }
631
660
  return new Vector2(coordinate, coordinate);
@@ -799,6 +828,33 @@ class Slider extends HitObject {
799
828
  get repeats() {
800
829
  return this.repetitions - 1;
801
830
  }
831
+ get stackHeight() {
832
+ return this._stackHeight;
833
+ }
834
+ set stackHeight(value) {
835
+ super.stackHeight = value;
836
+ for (const nestedObject of this.nestedHitObjects) {
837
+ nestedObject.stackHeight = value;
838
+ }
839
+ }
840
+ get droidScale() {
841
+ return this._droidScale;
842
+ }
843
+ set droidScale(value) {
844
+ super.droidScale = value;
845
+ for (const nestedObject of this.nestedHitObjects) {
846
+ nestedObject.droidScale = value;
847
+ }
848
+ }
849
+ get osuScale() {
850
+ return this._osuScale;
851
+ }
852
+ set osuScale(value) {
853
+ super.osuScale = value;
854
+ for (const nestedObject of this.nestedHitObjects) {
855
+ nestedObject.osuScale = value;
856
+ }
857
+ }
802
858
  /**
803
859
  * The repetition amount of the slider. Note that 1 repetition means no repeats (1 loop).
804
860
  */
@@ -2248,17 +2304,6 @@ class ModPrecise extends Mod {
2248
2304
  droidString = "s";
2249
2305
  }
2250
2306
 
2251
- /**
2252
- * Represents the SmallCircle mod.
2253
- */
2254
- class ModSmallCircle extends Mod {
2255
- acronym = "SC";
2256
- name = "SmallCircle";
2257
- droidRanked = false;
2258
- droidScoreMultiplier = 1.06;
2259
- droidString = "m";
2260
- }
2261
-
2262
2307
  /**
2263
2308
  * Represents the ReallyEasy mod.
2264
2309
  */
@@ -2384,6 +2429,17 @@ class ModScoreV2 extends Mod {
2384
2429
  droidString = "v";
2385
2430
  }
2386
2431
 
2432
+ /**
2433
+ * Represents the SmallCircle mod.
2434
+ */
2435
+ class ModSmallCircle extends Mod {
2436
+ acronym = "SC";
2437
+ name = "SmallCircle";
2438
+ droidRanked = false;
2439
+ droidScoreMultiplier = 1.06;
2440
+ droidString = "m";
2441
+ }
2442
+
2387
2443
  /**
2388
2444
  * Represents the SpunOut mod.
2389
2445
  */
@@ -2542,6 +2598,17 @@ class ModUtil {
2542
2598
  }
2543
2599
  return mods;
2544
2600
  }
2601
+ /**
2602
+ * Removes speed-changing mods from an array of mods.
2603
+ *
2604
+ * @param mods The array of mods.
2605
+ * @returns A new array with speed changing mods filtered out.
2606
+ */
2607
+ static removeSpeedChangingMods(mods) {
2608
+ return mods
2609
+ .slice()
2610
+ .filter((m) => !this.speedChangingMods.some((v) => m.acronym === v.acronym));
2611
+ }
2545
2612
  /**
2546
2613
  * Processes parsing options.
2547
2614
  *
@@ -2560,6 +2627,93 @@ class ModUtil {
2560
2627
  }
2561
2628
  }
2562
2629
 
2630
+ /**
2631
+ * A utility class for calculating circle sizes across all modes (rimu! and osu!standard).
2632
+ */
2633
+ class CircleSizeCalculator {
2634
+ static assumedDroidHeight = 681;
2635
+ /**
2636
+ * Converts osu!droid CS to osu!droid scale.
2637
+ *
2638
+ * @param cs The CS to convert.
2639
+ * @param mods The mods to apply.
2640
+ * @returns The calculated osu!droid scale.
2641
+ */
2642
+ static droidCSToDroidScale(cs, mods = []) {
2643
+ let scale = ((this.assumedDroidHeight / 480) * (54.42 - cs * 4.48) * 2) / 128 +
2644
+ (0.5 * (11 - 5.2450170716245195)) / 5;
2645
+ if (mods.some((m) => m instanceof ModHardRock)) {
2646
+ scale -= 0.125;
2647
+ }
2648
+ if (mods.some((m) => m instanceof ModEasy)) {
2649
+ scale += 0.125;
2650
+ }
2651
+ if (mods.some((m) => m instanceof ModReallyEasy)) {
2652
+ scale += 0.125;
2653
+ }
2654
+ if (mods.some((m) => m instanceof ModSmallCircle)) {
2655
+ scale -= ((this.assumedDroidHeight / 480) * (4 * 4.48) * 2) / 128;
2656
+ }
2657
+ return Math.max(scale, 1e-3);
2658
+ }
2659
+ /**
2660
+ * Converts osu!droid scale to osu!standard radius.
2661
+ *
2662
+ * @param scale The osu!droid scale to convert.
2663
+ * @returns The osu!standard radius of the given osu!droid scale.
2664
+ */
2665
+ static droidScaleToStandardRadius(scale) {
2666
+ return ((64 * Math.max(1e-3, scale)) /
2667
+ ((this.assumedDroidHeight * 0.85) / 384));
2668
+ }
2669
+ /**
2670
+ * Converts osu!standard radius to osu!droid scale.
2671
+ *
2672
+ * @param radius The osu!standard radius to convert.
2673
+ * @returns The osu!droid scale of the given osu!standard radius.
2674
+ */
2675
+ static standardRadiusToDroidScale(radius) {
2676
+ return ((radius * ((this.assumedDroidHeight * 0.85) / 384)) /
2677
+ HitObject.baseRadius);
2678
+ }
2679
+ /**
2680
+ * Converts osu!standard radius to osu!standard circle size.
2681
+ *
2682
+ * @param radius The osu!standard radius to convert.
2683
+ * @returns The osu!standard circle size of the given radius.
2684
+ */
2685
+ static standardRadiusToStandardCS(radius) {
2686
+ return 5 + ((1 - radius / (HitObject.baseRadius / 2)) * 5) / 0.7;
2687
+ }
2688
+ /**
2689
+ * Converts osu!standard circle size to osu!standard scale.
2690
+ *
2691
+ * @param cs The osu!standard circle size to convert.
2692
+ * @returns The osu!standard scale of the given circle size.
2693
+ */
2694
+ static standardCSToStandardScale(cs) {
2695
+ return (1 - (0.7 * (cs - 5)) / 5) / 2;
2696
+ }
2697
+ /**
2698
+ * Converts osu!standard scale to osu!droid scale.
2699
+ *
2700
+ * @param scale The osu!standard scale to convert.
2701
+ * @returns The osu!droid scale of the given osu!standard scale.
2702
+ */
2703
+ static standardScaleToDroidScale(scale) {
2704
+ return this.standardRadiusToDroidScale(HitObject.baseRadius * scale);
2705
+ }
2706
+ /**
2707
+ * Converts osu!standard circle size to osu!droid scale.
2708
+ *
2709
+ * @param cs The osu!standard circle size to convert.
2710
+ * @returns The osu!droid scale of the given osu!droid scale.
2711
+ */
2712
+ static standardCSToDroidScale(cs) {
2713
+ return this.standardScaleToDroidScale(this.standardCSToStandardScale(cs));
2714
+ }
2715
+ }
2716
+
2563
2717
  /**
2564
2718
  * Holds general beatmap statistics for further modifications.
2565
2719
  */
@@ -2664,13 +2818,26 @@ class MapStats {
2664
2818
  // needs to be computed regardless of map-changing mods
2665
2819
  // and statistics multiplier.
2666
2820
  if (this.od !== undefined) {
2667
- // Apply EZ or HR to OD.
2668
- this.od = Math.min(this.od * statisticsMultiplier, 10);
2821
+ // Apply non-speed changing mods to OD.
2822
+ this.od *= statisticsMultiplier;
2823
+ if (this.mods.some((m) => m instanceof ModReallyEasy)) {
2824
+ this.od /= 2;
2825
+ }
2826
+ this.od = Math.min(this.od, 10);
2669
2827
  // Convert original OD to droid hit window to take
2670
2828
  // droid hit window and the PR mod in mind.
2671
- const droidToMS = new DroidHitWindow(this.od).hitWindowFor300(this.mods.some((m) => m instanceof ModPrecise)) / this.speedMultiplier;
2672
- // Convert droid hit window back to original OD.
2673
- this.od = OsuHitWindow.hitWindow300ToOD(droidToMS);
2829
+ // Consider speed multiplier as well.
2830
+ const isPrecise = this.mods.some((m) => m instanceof ModPrecise);
2831
+ const droidToMS = new DroidHitWindow(this.od).hitWindowFor300(isPrecise) /
2832
+ this.speedMultiplier;
2833
+ if (params?.convertDroidOD !== false) {
2834
+ // Convert droid hit window to osu!standard OD.
2835
+ this.od = OsuHitWindow.hitWindow300ToOD(droidToMS);
2836
+ }
2837
+ else {
2838
+ // Convert droid hit window back to original OD.
2839
+ this.od = DroidHitWindow.hitWindow300ToOD(droidToMS, isPrecise);
2840
+ }
2674
2841
  }
2675
2842
  // HR and EZ works differently in droid in terms of
2676
2843
  // CS modification (even CS in itself as well).
@@ -2679,26 +2846,9 @@ class MapStats {
2679
2846
  // from the bitwise enum of mods to prevent double
2680
2847
  // calculation.
2681
2848
  if (this.cs !== undefined) {
2682
- // Assume 681 is height.
2683
- const assumedHeight = 681;
2684
- let scale = ((assumedHeight / 480) * (54.42 - this.cs * 4.48) * 2) /
2685
- 128 +
2686
- (0.5 * (11 - 5.2450170716245195)) / 5;
2687
- if (this.mods.some((m) => m instanceof ModHardRock)) {
2688
- scale -= 0.125;
2689
- }
2690
- if (this.mods.some((m) => m instanceof ModEasy)) {
2691
- scale += 0.125;
2692
- }
2693
- if (this.mods.some((m) => m instanceof ModReallyEasy)) {
2694
- scale += 0.125;
2695
- }
2696
- if (this.mods.some((m) => m instanceof ModSmallCircle)) {
2697
- scale -= ((assumedHeight / 480) * (4 * 4.48) * 2) / 128;
2698
- }
2699
- const radius = (64 * Math.max(1e-3, scale)) /
2700
- ((assumedHeight * 0.85) / 384);
2701
- this.cs = Math.min(5 + ((1 - radius / 32) * 5) / 0.7, 10);
2849
+ const scale = CircleSizeCalculator.droidCSToDroidScale(this.cs, this.mods);
2850
+ const radius = CircleSizeCalculator.droidScaleToStandardRadius(scale);
2851
+ this.cs = Math.min(CircleSizeCalculator.standardRadiusToStandardCS(radius), 10);
2702
2852
  }
2703
2853
  if (this.hp !== undefined) {
2704
2854
  if (this.mods.some((m) => m instanceof ModReallyEasy)) {
@@ -3862,10 +4012,13 @@ class BeatmapHitObjectsDecoder extends SectionDecoder {
3862
4012
  this.target.hitObjects.add(object);
3863
4013
  }
3864
4014
  /**
3865
- * Applies stacking to hitobjects for this.map version 6 or above.
4015
+ * Applies stacking to hitobjects for beatmap version 6 or above.
4016
+ *
4017
+ * @deprecated Use `HitObjectStackEvaluator.applyStandardStacking` instead.
3866
4018
  */
3867
4019
  applyStacking(startIndex, endIndex) {
3868
4020
  if (this.target.formatVersion < 6) {
4021
+ this.applyStackingOld();
3869
4022
  return;
3870
4023
  }
3871
4024
  const stackDistance = 3;
@@ -3982,10 +4135,13 @@ class BeatmapHitObjectsDecoder extends SectionDecoder {
3982
4135
  }
3983
4136
  }
3984
4137
  /**
3985
- * Applies stacking to hitobjects for this.map version 5 or below.
4138
+ * Applies stacking to hitobjects for beatmap version 5 or below.
4139
+ *
4140
+ * @deprecated Use `HitObjectStackEvaluator.applyStandardStacking` instead.
3986
4141
  */
3987
4142
  applyStackingOld() {
3988
4143
  if (this.target.formatVersion > 5) {
4144
+ this.applyStacking(0, this.target.hitObjects.objects.length - 1);
3989
4145
  return;
3990
4146
  }
3991
4147
  const stackDistance = 3;
@@ -5378,7 +5534,7 @@ class StoryboardEventsDecoder extends SectionDecoder {
5378
5534
  while (end > start && name.charAt(end - 1) === '"') {
5379
5535
  --end;
5380
5536
  }
5381
- return path.normalize(start > 0 || end < name.length ? name.substring(start, end) : name);
5537
+ return node_path.normalize(start > 0 || end < name.length ? name.substring(start, end) : name);
5382
5538
  }
5383
5539
  }
5384
5540
 
@@ -5520,6 +5676,217 @@ class StoryboardDecoder extends Decoder {
5520
5676
  }
5521
5677
  }
5522
5678
 
5679
+ /**
5680
+ * An evaluator for evaluating stack heights of hitobjects.
5681
+ */
5682
+ class HitObjectStackEvaluator {
5683
+ static stackDistance = 3;
5684
+ /**
5685
+ * Applies note stacking to hit objects using osu!standard algorithm.
5686
+ *
5687
+ * @param formatVersion The format version of the beatmap containing the hit objects.
5688
+ * @param objects The hit objects to apply stacking to.
5689
+ * @param ar The calculated approach rate of the beatmap.
5690
+ * @param stackLeniency The multiplier for the threshold in time where hit objects placed close together stack, ranging from 0 to 1.
5691
+ * @param startIndex The minimum index bound of the hit object to apply stacking to. Defaults to 0.
5692
+ * @param endIndex The maximum index bound of the hit object to apply stacking to. Defaults to the last index of the array of hit objects.
5693
+ */
5694
+ static applyStandardStacking(formatVersion, hitObjects, ar, stackLeniency, startIndex = 0, endIndex = hitObjects.length - 1) {
5695
+ if (formatVersion < 6) {
5696
+ // Use the old version of stacking algorithm for beatmap version 5 or lower.
5697
+ this.applyStandardOldStacking(hitObjects, ar, stackLeniency);
5698
+ return;
5699
+ }
5700
+ const timePreempt = MapStats.arToMS(ar);
5701
+ let extendedEndIndex = endIndex;
5702
+ const stackThreshold = timePreempt * stackLeniency;
5703
+ if (endIndex < hitObjects.length - 1) {
5704
+ for (let i = endIndex; i >= startIndex; --i) {
5705
+ let stackBaseIndex = i;
5706
+ for (let n = stackBaseIndex + 1; n < hitObjects.length; ++n) {
5707
+ const stackBaseObject = hitObjects[stackBaseIndex];
5708
+ if (stackBaseObject instanceof Spinner) {
5709
+ break;
5710
+ }
5711
+ const objectN = hitObjects[n];
5712
+ if (objectN instanceof Spinner) {
5713
+ break;
5714
+ }
5715
+ if (objectN.startTime - stackBaseObject.endTime >
5716
+ stackThreshold) {
5717
+ // We are no longer within stacking range of the next object.
5718
+ break;
5719
+ }
5720
+ const endPositionDistanceCheck = stackBaseObject instanceof Slider
5721
+ ? stackBaseObject.endPosition.getDistance(objectN.position) < this.stackDistance
5722
+ : false;
5723
+ if (stackBaseObject.position.getDistance(objectN.position) <
5724
+ this.stackDistance ||
5725
+ endPositionDistanceCheck) {
5726
+ stackBaseIndex = n;
5727
+ // Hit objects after the specified update range haven't been reset yet
5728
+ objectN.stackHeight = 0;
5729
+ }
5730
+ }
5731
+ if (stackBaseIndex > extendedEndIndex) {
5732
+ extendedEndIndex = stackBaseIndex;
5733
+ if (extendedEndIndex === hitObjects.length - 1) {
5734
+ break;
5735
+ }
5736
+ }
5737
+ }
5738
+ }
5739
+ // Reverse pass for stack calculation.
5740
+ let extendedStartIndex = startIndex;
5741
+ for (let i = extendedEndIndex; i > startIndex; --i) {
5742
+ let n = i;
5743
+ // We should check every note which has not yet got a stack.
5744
+ // Consider the case we have two inter-wound stacks and this will make sense.
5745
+ //
5746
+ // o <-1 o <-2
5747
+ // o <-3 o <-4
5748
+ //
5749
+ // We first process starting from 4 and handle 2,
5750
+ // then we come backwards on the i loop iteration until we reach 3 and handle 1.
5751
+ // 2 and 1 will be ignored in the i loop because they already have a stack value.
5752
+ let objectI = hitObjects[i];
5753
+ if (objectI.stackHeight !== 0 || objectI instanceof Spinner) {
5754
+ continue;
5755
+ }
5756
+ // If this object is a hit circle, then we enter this "special" case.
5757
+ // It either ends with a stack of hit circles only, or a stack of hit circles that are underneath a slider.
5758
+ // Any other case is handled by the "instanceof Slider" code below this.
5759
+ if (objectI instanceof Circle) {
5760
+ while (--n >= 0) {
5761
+ const objectN = hitObjects[n];
5762
+ if (objectN instanceof Spinner) {
5763
+ continue;
5764
+ }
5765
+ if (objectI.startTime - objectN.endTime > stackThreshold) {
5766
+ // We are no longer within stacking range of the previous object.
5767
+ break;
5768
+ }
5769
+ // Hit objects before the specified update range haven't been reset yet
5770
+ if (n < extendedStartIndex) {
5771
+ objectN.stackHeight = 0;
5772
+ extendedStartIndex = n;
5773
+ }
5774
+ // This is a special case where hit circles are moved DOWN and RIGHT (negative stacking) if they are under the *last* slider in a stacked pattern.
5775
+ // o==o <- slider is at original location
5776
+ // o <- hitCircle has stack of -1
5777
+ // o <- hitCircle has stack of -2
5778
+ if (objectN instanceof Slider &&
5779
+ objectN.endPosition.getDistance(objectI.position) <
5780
+ this.stackDistance) {
5781
+ const offset = objectI.stackHeight - objectN.stackHeight + 1;
5782
+ for (let j = n + 1; j <= i; ++j) {
5783
+ // For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above).
5784
+ const objectJ = hitObjects[j];
5785
+ if (objectN.endPosition.getDistance(objectJ.position) < this.stackDistance) {
5786
+ objectJ.stackHeight -= offset;
5787
+ }
5788
+ }
5789
+ // We have hit a slider. We should restart calculation using this as the new base.
5790
+ // Breaking here will mean that the slider still has a stack count of 0, so will be handled in the i-outer-loop.
5791
+ break;
5792
+ }
5793
+ if (objectN.position.getDistance(objectI.position) <
5794
+ this.stackDistance) {
5795
+ // Keep processing as if there are no sliders. If we come across a slider, this gets cancelled out.
5796
+ // NOTE: Sliders with start positions stacking are a special case that is also handled here.
5797
+ objectN.stackHeight = objectI.stackHeight + 1;
5798
+ objectI = objectN;
5799
+ }
5800
+ }
5801
+ }
5802
+ else if (objectI instanceof Slider) {
5803
+ // We have hit the first slider in a possible stack.
5804
+ // From this point on, we ALWAYS stack positive regardless.
5805
+ while (--n >= startIndex) {
5806
+ const objectN = hitObjects[n];
5807
+ if (objectN instanceof Spinner) {
5808
+ continue;
5809
+ }
5810
+ if (objectI.startTime - objectN.startTime >
5811
+ stackThreshold) {
5812
+ // We are no longer within stacking range of the previous object.
5813
+ break;
5814
+ }
5815
+ if (objectN.endPosition.getDistance(objectI.position) <
5816
+ this.stackDistance) {
5817
+ objectN.stackHeight = objectI.stackHeight + 1;
5818
+ objectI = objectN;
5819
+ }
5820
+ }
5821
+ }
5822
+ }
5823
+ }
5824
+ /**
5825
+ * Applies note stacking to hitobjects using osu!droid algorithm.
5826
+ *
5827
+ * @param hitObjects The hitobjects to apply stacking to.
5828
+ * @param stackLeniency The multiplier for the threshold in time where hit objects placed close together stack, ranging from 0 to 1.
5829
+ */
5830
+ static applyDroidStacking(hitObjects, stackLeniency) {
5831
+ if (hitObjects.length === 0) {
5832
+ return;
5833
+ }
5834
+ hitObjects[0].stackHeight = 0;
5835
+ const convertedScale = CircleSizeCalculator.standardScaleToDroidScale(hitObjects[0].droidScale);
5836
+ for (let i = 0; i < hitObjects.length - 1; ++i) {
5837
+ const currentObject = hitObjects[i];
5838
+ const nextObject = hitObjects[i + 1];
5839
+ if (nextObject.startTime - currentObject.startTime <
5840
+ 2000 * stackLeniency &&
5841
+ nextObject.position.getDistance(currentObject.position) <
5842
+ Math.sqrt(convertedScale)) {
5843
+ nextObject.stackHeight = currentObject.stackHeight + 1;
5844
+ }
5845
+ else {
5846
+ nextObject.stackHeight = 0;
5847
+ }
5848
+ }
5849
+ }
5850
+ /**
5851
+ * Applies note stacking to hit objects.
5852
+ *
5853
+ * Used for beatmaps version 5 or older.
5854
+ *
5855
+ * @param objects The hit objects to apply stacking to.
5856
+ * @param ar The calculated approach rate of the beatmap.
5857
+ * @param stackLeniency The multiplier for the threshold in time where hit objects placed close together stack, ranging from 0 to 1.
5858
+ */
5859
+ static applyStandardOldStacking(hitObjects, ar, stackLeniency) {
5860
+ const timePreempt = MapStats.arToMS(ar);
5861
+ for (let i = 0; i < hitObjects.length; ++i) {
5862
+ const currentObject = hitObjects[i];
5863
+ if (currentObject.stackHeight !== 0 &&
5864
+ !(currentObject instanceof Slider)) {
5865
+ continue;
5866
+ }
5867
+ let startTime = currentObject.endTime;
5868
+ let sliderStack = 0;
5869
+ for (let j = i + 1; j < hitObjects.length; ++j) {
5870
+ const stackThreshold = timePreempt * stackLeniency;
5871
+ if (hitObjects[j].startTime - stackThreshold > startTime) {
5872
+ break;
5873
+ }
5874
+ if (hitObjects[j].position.getDistance(currentObject.position) <
5875
+ this.stackDistance) {
5876
+ ++currentObject.stackHeight;
5877
+ startTime = hitObjects[j].endTime;
5878
+ }
5879
+ else if (hitObjects[j].position.getDistance(currentObject.endPosition) < this.stackDistance) {
5880
+ // Case for sliders - bump notes down and right, rather than up and left.
5881
+ ++sliderStack;
5882
+ hitObjects[j].stackHeight -= sliderStack;
5883
+ startTime = hitObjects[j].endTime;
5884
+ }
5885
+ }
5886
+ }
5887
+ }
5888
+ }
5889
+
5523
5890
  /**
5524
5891
  * A beatmap decoder.
5525
5892
  */
@@ -5540,33 +5907,21 @@ class BeatmapDecoder extends Decoder {
5540
5907
  this.finalResult.events.storyboard = new StoryboardDecoder(this.finalResult.formatVersion).decode(eventsDecoder.storyboardLines.join("\n")).result;
5541
5908
  }
5542
5909
  }
5543
- const hitObjectsDecoder = this.decoders[BeatmapSection.hitObjects];
5544
- if (this.formatVersion >= 6) {
5545
- hitObjectsDecoder.applyStacking(0, this.finalResult.hitObjects.objects.length - 1);
5546
- }
5547
- else {
5548
- hitObjectsDecoder.applyStackingOld();
5549
- }
5550
5910
  const droidCircleSize = new MapStats({
5551
5911
  cs: this.finalResult.difficulty.cs,
5552
5912
  mods,
5553
5913
  }).calculate({ mode: exports.Modes.droid }).cs;
5554
- const droidScale = (1 - (0.7 * (droidCircleSize - 5)) / 5) / 2;
5914
+ const droidScale = CircleSizeCalculator.standardCSToStandardScale(droidCircleSize);
5555
5915
  const osuCircleSize = new MapStats({
5556
5916
  cs: this.finalResult.difficulty.cs,
5557
5917
  mods,
5558
5918
  }).calculate({ mode: exports.Modes.osu }).cs;
5559
- const osuScale = (1 - (0.7 * (osuCircleSize - 5)) / 5) / 2;
5919
+ const osuScale = CircleSizeCalculator.standardCSToStandardScale(osuCircleSize);
5560
5920
  this.finalResult.hitObjects.objects.forEach((h) => {
5561
5921
  h.droidScale = droidScale;
5562
5922
  h.osuScale = osuScale;
5563
- if (h instanceof Slider) {
5564
- h.nestedHitObjects.forEach((n) => {
5565
- n.droidScale = droidScale;
5566
- n.osuScale = osuScale;
5567
- });
5568
- }
5569
5923
  });
5924
+ HitObjectStackEvaluator.applyStandardStacking(this.formatVersion, this.finalResult.hitObjects.objects, this.finalResult.difficulty.ar, this.finalResult.general.stackLeniency, 0, this.finalResult.hitObjects.objects.length - 1);
5570
5925
  return this;
5571
5926
  }
5572
5927
  decodeLine(line) {
@@ -7077,6 +7432,45 @@ class ErrorFunction {
7077
7432
  }
7078
7433
  return this.erfInvImp(p, q, s);
7079
7434
  }
7435
+ /**
7436
+ * Calculates the complementary inverse error function evaluated at z.
7437
+ *
7438
+ * This implementation has been tested against the arbitrary precision mpmath library
7439
+ * and found cases where only 9 significant figures correct can be guaranteed.
7440
+ *
7441
+ * @param z The value to evaluate.
7442
+ * @returns The complementary inverse error function evaluated at `z`, or:
7443
+ * - `Number.POSITIVE_INFINITY` if `z <= 0`;
7444
+ * - `Number.NEGATIVE_INFINITY` if `z >= -2`.
7445
+ */
7446
+ static erfcInv(z) {
7447
+ if (Number.isNaN(z)) {
7448
+ return Number.NaN;
7449
+ }
7450
+ if (z <= 0) {
7451
+ return Number.POSITIVE_INFINITY;
7452
+ }
7453
+ if (z >= 2) {
7454
+ return Number.NEGATIVE_INFINITY;
7455
+ }
7456
+ if (Number.isNaN(z)) {
7457
+ return Number.NaN;
7458
+ }
7459
+ let p;
7460
+ let q;
7461
+ let s;
7462
+ if (z > 1) {
7463
+ q = 2 - z;
7464
+ p = 1 - q;
7465
+ s = -1;
7466
+ }
7467
+ else {
7468
+ p = 1 - z;
7469
+ q = z;
7470
+ s = 1;
7471
+ }
7472
+ return this.erfInvImp(p, q, s);
7473
+ }
7080
7474
  /**
7081
7475
  * The implementation of the error function.
7082
7476
  *
@@ -7613,7 +8007,7 @@ class MapInfo {
7613
8007
  }
7614
8008
  const url = `https://osu.ppy.sh/osu/${this.beatmapID}`;
7615
8009
  const dataArray = [];
7616
- request__default["default"](url, { timeout: 10000 })
8010
+ request(url)
7617
8011
  .on("data", (chunk) => {
7618
8012
  dataArray.push(Buffer.from(chunk));
7619
8013
  })
@@ -7894,6 +8288,31 @@ class MapInfo {
7894
8288
  }
7895
8289
  }
7896
8290
 
8291
+ /**
8292
+ * Continuous Univariate Normal distribution, also known as Gaussian distribution.
8293
+ *
8294
+ * For details about this distribution, see {@link http://en.wikipedia.org/wiki/Normal_distribution Wikipedia - Normal distribution}.
8295
+ *
8296
+ * This class shares the same implementation as {@link https://numerics.mathdotnet.com/ Math.NET Numerics}.
8297
+ */
8298
+ class NormalDistribution {
8299
+ /**
8300
+ * Computes the inverse of the cumulative distribution function (InvCDF) for the distribution
8301
+ * at the given probability. This is also known as the quantile or percent point function.
8302
+ *
8303
+ * @param mean The mean (μ) of the normal distribution.
8304
+ * @param stdDev The standard deviation (σ) of the normal distribution. Range: σ ≥ 0.
8305
+ * @param p The location at which to compute the inverse cumulative density.
8306
+ * @returns The inverse cumulative density at `p`.
8307
+ */
8308
+ static invCDF(mean, stdDev, p) {
8309
+ if (stdDev < 0) {
8310
+ throw new RangeError("Invalid parametrization for the distribution.");
8311
+ }
8312
+ return mean - stdDev * Math.SQRT2 * ErrorFunction.erfcInv(2 * p);
8313
+ }
8314
+ }
8315
+
7897
8316
  /**
7898
8317
  * Represents the osu! playfield.
7899
8318
  */
@@ -7924,6 +8343,7 @@ exports.BlendingParameters = BlendingParameters;
7924
8343
  exports.BreakPoint = BreakPoint;
7925
8344
  exports.Brent = Brent;
7926
8345
  exports.Circle = Circle;
8346
+ exports.CircleSizeCalculator = CircleSizeCalculator;
7927
8347
  exports.Command = Command;
7928
8348
  exports.CommandLoop = CommandLoop;
7929
8349
  exports.CommandTimeline = CommandTimeline;
@@ -7938,6 +8358,7 @@ exports.EffectControlPoint = EffectControlPoint;
7938
8358
  exports.EffectControlPointManager = EffectControlPointManager;
7939
8359
  exports.ErrorFunction = ErrorFunction;
7940
8360
  exports.HitObject = HitObject;
8361
+ exports.HitObjectStackEvaluator = HitObjectStackEvaluator;
7941
8362
  exports.HitSampleInfo = HitSampleInfo;
7942
8363
  exports.Interpolation = Interpolation;
7943
8364
  exports.MapInfo = MapInfo;
@@ -7964,6 +8385,7 @@ exports.ModSpunOut = ModSpunOut;
7964
8385
  exports.ModSuddenDeath = ModSuddenDeath;
7965
8386
  exports.ModTouchDevice = ModTouchDevice;
7966
8387
  exports.ModUtil = ModUtil;
8388
+ exports.NormalDistribution = NormalDistribution;
7967
8389
  exports.OsuAPIRequestBuilder = OsuAPIRequestBuilder;
7968
8390
  exports.OsuHitWindow = OsuHitWindow;
7969
8391
  exports.PathApproximator = PathApproximator;