@rian8337/osu-base 4.0.0-beta.69 → 4.0.0-beta.71

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.
Files changed (3) hide show
  1. package/dist/index.js +1646 -1585
  2. package/package.json +2 -2
  3. package/typings/index.d.ts +60 -13
package/dist/index.js CHANGED
@@ -1318,7 +1318,7 @@ class ModSetting {
1318
1318
  * The default value of this `ModSetting`.
1319
1319
  */
1320
1320
  get defaultValue() {
1321
- return this._value;
1321
+ return this._defaultValue;
1322
1322
  }
1323
1323
  set defaultValue(value) {
1324
1324
  this._defaultValue = value;
@@ -1334,7 +1334,7 @@ class ModSetting {
1334
1334
  const oldValue = this._value;
1335
1335
  this._value = value;
1336
1336
  for (const listener of this.valueChangedListeners) {
1337
- listener(oldValue, value);
1337
+ listener({ oldValue, newValue: value });
1338
1338
  }
1339
1339
  }
1340
1340
  }
@@ -1372,7 +1372,7 @@ class ModSetting {
1372
1372
  bindValueChanged(listener, runOnceImmediately = false) {
1373
1373
  this.valueChangedListeners.add(listener);
1374
1374
  if (runOnceImmediately) {
1375
- listener(this.value, this.value);
1375
+ listener({ oldValue: this.value, newValue: this.value });
1376
1376
  }
1377
1377
  }
1378
1378
  /**
@@ -1397,7 +1397,14 @@ class Mod {
1397
1397
  */
1398
1398
  this.userPlayable = true;
1399
1399
  /**
1400
- * `Mod`s that are incompatible with this `Mod`.
1400
+ * The {@link Mod}s this {@link Mod} cannot be enabled with.
1401
+ *
1402
+ * This is merely a static list of {@link Mod} constructors that this {@link Mod} is incompatible with,
1403
+ * regardless of the actual instance of the {@link Mod}.
1404
+ *
1405
+ * Some {@link Mod}s may have additional compatibility requirements that are captured in
1406
+ * {@link isCompatibleWith}. When checking for {@link Mod} compatibility, always use
1407
+ * {@link isCompatibleWith}.
1401
1408
  */
1402
1409
  this.incompatibleMods = new Set();
1403
1410
  this.settingsBacking = null;
@@ -1424,6 +1431,19 @@ class Mod {
1424
1431
  get usesDefaultSettings() {
1425
1432
  return this.settings.every((s) => s.isDefault);
1426
1433
  }
1434
+ /**
1435
+ * Determines whether this {@link Mod} is compatible with another {@link Mod}.
1436
+ *
1437
+ * This extends {@link incompatibleMods} by allowing for dynamic checks against
1438
+ * the actual instance of the {@link Mod} (i.e., its specific settings).
1439
+ *
1440
+ * @param other The {@link Mod} to check compatibility with.
1441
+ * @return `true` if this {@link Mod} is compatible with {@link other}, `false` otherwise.
1442
+ */
1443
+ isCompatibleWith(other) {
1444
+ return (!this.incompatibleMods.has(other.constructor) &&
1445
+ !other.incompatibleMods.has(this.constructor));
1446
+ }
1427
1447
  /**
1428
1448
  * Serializes this `Mod` to a `SerializedMod`.
1429
1449
  */
@@ -1884,7 +1904,7 @@ class Slider extends HitObject {
1884
1904
  .length;
1885
1905
  }
1886
1906
  get stackHeight() {
1887
- return this._stackHeight;
1907
+ return super.stackHeight;
1888
1908
  }
1889
1909
  set stackHeight(value) {
1890
1910
  super.stackHeight = value;
@@ -1893,7 +1913,7 @@ class Slider extends HitObject {
1893
1913
  }
1894
1914
  }
1895
1915
  get scale() {
1896
- return this._scale;
1916
+ return super.scale;
1897
1917
  }
1898
1918
  set scale(value) {
1899
1919
  super.scale = value;
@@ -1901,6 +1921,15 @@ class Slider extends HitObject {
1901
1921
  nestedObject.scale = value;
1902
1922
  }
1903
1923
  }
1924
+ get stackOffsetMultiplier() {
1925
+ return super.stackOffsetMultiplier;
1926
+ }
1927
+ set stackOffsetMultiplier(value) {
1928
+ super.stackOffsetMultiplier = value;
1929
+ for (const nestedObject of this.nestedHitObjects) {
1930
+ nestedObject.stackOffsetMultiplier = value;
1931
+ }
1932
+ }
1904
1933
  constructor(values) {
1905
1934
  super(values);
1906
1935
  /**
@@ -2751,1716 +2780,1849 @@ class Circle extends HitObject {
2751
2780
  }
2752
2781
  }
2753
2782
 
2783
+ var _a$1;
2754
2784
  /**
2755
- * Represents the Replay V6 mod.
2756
- *
2757
- * Some behavior of beatmap parsing was changed in replay version 7. More specifically, object stacking
2758
- * behavior now matches osu!stable and osu!lazer.
2785
+ * Represents the osu! playfield.
2786
+ */
2787
+ class Playfield {
2788
+ }
2789
+ _a$1 = Playfield;
2790
+ /**
2791
+ * The size of the playfield, which is 512x384.
2792
+ */
2793
+ Playfield.baseSize = new Vector2(512, 384);
2794
+ /**
2795
+ * The center of the playfield, which is at (256, 192).
2796
+ */
2797
+ Playfield.center = _a$1.baseSize.scale(0.5);
2798
+
2799
+ /**
2800
+ * Represents a spinner in a beatmap.
2759
2801
  *
2760
- * This `Mod` is meant to reapply the stacking behavior prior to replay version 7 to a `Beatmap` that
2761
- * was played in replays recorded in version 6 and older for replayability and difficulty calculation.
2802
+ * All we need from spinners is their duration. The
2803
+ * position of a spinner is always at 256x192.
2762
2804
  */
2763
- class ModReplayV6 extends Mod {
2764
- constructor() {
2765
- super(...arguments);
2766
- this.name = "Replay V6";
2767
- this.acronym = "RV6";
2768
- this.userPlayable = false;
2769
- this.droidRanked = false;
2770
- this.facilitateAdjustment = true;
2805
+ class Spinner extends HitObject {
2806
+ get endTime() {
2807
+ return this._endTime;
2771
2808
  }
2772
- get isDroidRelevant() {
2773
- return true;
2809
+ constructor(values) {
2810
+ super(Object.assign(Object.assign({}, values), { position: Playfield.baseSize.divide(2) }));
2811
+ this._endTime = values.endTime;
2774
2812
  }
2775
- calculateDroidScoreMultiplier() {
2776
- return 1;
2813
+ applySamples(controlPoints) {
2814
+ super.applySamples(controlPoints);
2815
+ const samplePoints = controlPoints.sample.between(this.startTime + HitObject.controlPointLeniency, this.endTime + HitObject.controlPointLeniency);
2816
+ this.auxiliarySamples.length = 0;
2817
+ this.auxiliarySamples.push(new SequenceHitSampleInfo(samplePoints.map((s) => new TimedHitSampleInfo(s.time, s.applyTo(Spinner.baseSpinnerSpinSample)))));
2818
+ this.auxiliarySamples.push(new SequenceHitSampleInfo(samplePoints.map((s) => new TimedHitSampleInfo(s.time, s.applyTo(Spinner.baseSpinnerBonusSample)))));
2777
2819
  }
2778
- applyToBeatmap(beatmap) {
2779
- const { objects } = beatmap.hitObjects;
2780
- if (objects.length === 0) {
2781
- return;
2782
- }
2783
- // Reset stacking
2784
- objects.forEach((h) => {
2785
- h.stackHeight = 0;
2786
- });
2787
- for (let i = 0; i < objects.length - 1; ++i) {
2788
- const current = objects[i];
2789
- const next = objects[i + 1];
2790
- this.revertObjectScale(current, beatmap.difficulty);
2791
- this.revertObjectScale(next, beatmap.difficulty);
2792
- const convertedScale = CircleSizeCalculator.standardScaleToOldDroidScale(objects[0].scale);
2793
- if (current instanceof Circle &&
2794
- next.startTime - current.startTime <
2795
- 2000 * beatmap.general.stackLeniency &&
2796
- next.position.getDistance(current.position) <
2797
- Math.sqrt(convertedScale)) {
2798
- next.stackHeight = current.stackHeight + 1;
2799
- }
2800
- }
2820
+ get stackedPosition() {
2821
+ return this.position;
2801
2822
  }
2802
- revertObjectScale(hitObject, difficulty) {
2803
- const droidScale = CircleSizeCalculator.droidCSToOldDroidScale(difficulty.cs);
2804
- const radius = CircleSizeCalculator.oldDroidScaleToStandardRadius(droidScale);
2805
- const standardCS = CircleSizeCalculator.standardRadiusToStandardCS(radius, true);
2806
- hitObject.scale = CircleSizeCalculator.standardCSToStandardScale(standardCS, true);
2807
- hitObject.stackOffsetMultiplier = 4;
2823
+ get stackedEndPosition() {
2824
+ return this.position;
2825
+ }
2826
+ createHitWindow() {
2827
+ return new EmptyHitWindow();
2828
+ }
2829
+ toString() {
2830
+ return `Position: [${this._position.x}, ${this._position.y}], duration: ${this.duration}`;
2808
2831
  }
2809
2832
  }
2833
+ Spinner.baseSpinnerSpinSample = new BankHitSampleInfo("spinnerspin");
2834
+ Spinner.baseSpinnerBonusSample = new BankHitSampleInfo("spinnerbonus");
2810
2835
 
2811
2836
  /**
2812
- * Represents a `Mod` specific setting that is constrained to a number of values.
2813
- *
2814
- * The value can be `null`, which is treated as a special case.
2837
+ * Represents a four-dimensional vector.
2815
2838
  */
2816
- class NullableDecimalModSetting extends RangeConstrainedModSetting {
2839
+ class Vector4 {
2840
+ constructor(xOrValueOrXZ, yOrYW, z, w) {
2841
+ if (yOrYW === undefined) {
2842
+ this.x = xOrValueOrXZ;
2843
+ this.y = xOrValueOrXZ;
2844
+ this.z = xOrValueOrXZ;
2845
+ this.w = xOrValueOrXZ;
2846
+ return;
2847
+ }
2848
+ if (typeof z === "undefined") {
2849
+ this.x = xOrValueOrXZ;
2850
+ this.y = yOrYW;
2851
+ this.z = xOrValueOrXZ;
2852
+ this.w = yOrYW;
2853
+ return;
2854
+ }
2855
+ this.x = xOrValueOrXZ;
2856
+ this.y = yOrYW;
2857
+ this.z = z;
2858
+ this.w = w;
2859
+ }
2817
2860
  /**
2818
- * The number of decimal places to round the value to.
2819
- *
2820
- * When set to `null`, the value will not be rounded.
2861
+ * The X coordinate of the left edge of this vector.
2821
2862
  */
2822
- get precision() {
2823
- return this._precision;
2863
+ get left() {
2864
+ return this.x;
2824
2865
  }
2825
- set precision(value) {
2826
- if (value !== null && value < 0) {
2827
- throw new RangeError(`The precision (${value}) must be greater than or equal to 0.`);
2828
- }
2829
- this._precision = value;
2830
- if (value !== null) {
2831
- this.value = this.processValue(this.value);
2832
- }
2866
+ /**
2867
+ * The Y coordinate of the top edge of this vector.
2868
+ */
2869
+ get top() {
2870
+ return this.y;
2833
2871
  }
2834
- constructor(name, description, defaultValue, min = -Number.MAX_VALUE, max = Number.MAX_VALUE, step = 0, precision = null) {
2835
- super(name, description, defaultValue, min, max, step);
2836
- this.displayFormatter = (v) => {
2837
- if (v === null) {
2838
- return "None";
2839
- }
2840
- if (this.precision !== null) {
2841
- return v.toFixed(this.precision);
2842
- }
2843
- return super.toDisplayString();
2844
- };
2845
- if (min > max) {
2846
- throw new RangeError(`The minimum value (${min}) must be less than or equal to the maximum value (${max}).`);
2847
- }
2848
- if (step < 0) {
2849
- throw new RangeError(`The step size (${step}) must be greater than or equal to 0.`);
2850
- }
2851
- if (defaultValue !== null &&
2852
- (defaultValue < min || defaultValue > max)) {
2853
- throw new RangeError(`The default value (${defaultValue}) must be between the minimum (${min}) and maximum (${max}) values.`);
2854
- }
2855
- this._precision = precision;
2872
+ /**
2873
+ * The X coordinate of the right edge of this vector.
2874
+ */
2875
+ get right() {
2876
+ return this.z;
2856
2877
  }
2857
- processValue(value) {
2858
- if (value === null) {
2859
- return null;
2860
- }
2861
- const processedValue = MathUtils.clamp(Math.round(value / this.step) * this.step, this.min, this.max);
2862
- if (this.precision !== null) {
2863
- return parseFloat(processedValue.toFixed(this.precision));
2864
- }
2865
- return processedValue;
2878
+ /**
2879
+ * The Y coordinate of the bottom edge of this vector.
2880
+ */
2881
+ get bottom() {
2882
+ return this.w;
2883
+ }
2884
+ /**
2885
+ * The top left corner of this vector.
2886
+ */
2887
+ get topLeft() {
2888
+ return new Vector2(this.left, this.top);
2889
+ }
2890
+ /**
2891
+ * The top right corner of this vector.
2892
+ */
2893
+ get topRight() {
2894
+ return new Vector2(this.right, this.top);
2895
+ }
2896
+ /**
2897
+ * The bottom left corner of this vector.
2898
+ */
2899
+ get bottomLeft() {
2900
+ return new Vector2(this.left, this.bottom);
2901
+ }
2902
+ /**
2903
+ * The bottom right corner of this vector.
2904
+ */
2905
+ get bottomRight() {
2906
+ return new Vector2(this.right, this.bottom);
2907
+ }
2908
+ /**
2909
+ * The width of the rectangle defined by this vector.
2910
+ */
2911
+ get width() {
2912
+ return this.right - this.left;
2913
+ }
2914
+ /**
2915
+ * The height of the rectangle defined by this vector.
2916
+ */
2917
+ get height() {
2918
+ return this.bottom - this.top;
2866
2919
  }
2867
2920
  }
2868
2921
 
2869
2922
  /**
2870
- * Represents the Difficulty Adjust mod.
2923
+ * Contains infromation about the position of a {@link HitObject}.
2871
2924
  */
2872
- class ModDifficultyAdjust extends Mod {
2873
- get isRelevant() {
2874
- return (this.cs.value !== null ||
2875
- this.ar.value !== null ||
2876
- this.od.value !== null ||
2877
- this.hp.value !== null);
2878
- }
2879
- constructor(values) {
2880
- var _a, _b, _c, _d;
2881
- super();
2882
- this.acronym = "DA";
2883
- this.name = "Difficulty Adjust";
2884
- this.droidRanked = false;
2885
- this.osuRanked = false;
2886
- /**
2887
- * The circle size to enforce.
2888
- */
2889
- this.cs = new NullableDecimalModSetting("Circle size", "The circle size to enforce.", null, 0, 15, 0.1, 1);
2890
- /**
2891
- * The approach rate to enforce.
2892
- */
2893
- this.ar = new NullableDecimalModSetting("Approach rate", "The approach rate to enforce.", null, 0, 11, 0.1, 1);
2925
+ class HitObjectPositionInfo {
2926
+ constructor(hitObject) {
2894
2927
  /**
2895
- * The overall difficulty to enforce.
2928
+ * The jump angle from the previous {@link HitObject} to this one, relative to the previous
2929
+ * {@link HitObject}'s jump angle.
2930
+ *
2931
+ * The `relativeAngle` of the first {@link HitObject} in a beatmap represents the absolute angle from the
2932
+ * center of the playfield to the {@link HitObject}.
2933
+ *
2934
+ * If `relativeAngle` is 0, the player's cursor does not need to change its direction of movement when
2935
+ * passing from the previous {@link HitObject} to this one.
2896
2936
  */
2897
- this.od = new NullableDecimalModSetting("Overall difficulty", "The overall difficulty to enforce.", null, 0, 11, 0.1, 1);
2898
- this.hp = new NullableDecimalModSetting("Health drain", "The health drain to enforce.", null, 0, 11, 0.1, 1);
2899
- this.cs.value = (_a = values === null || values === void 0 ? void 0 : values.cs) !== null && _a !== void 0 ? _a : null;
2900
- this.ar.value = (_b = values === null || values === void 0 ? void 0 : values.ar) !== null && _b !== void 0 ? _b : null;
2901
- this.od.value = (_c = values === null || values === void 0 ? void 0 : values.od) !== null && _c !== void 0 ? _c : null;
2902
- this.hp.value = (_d = values === null || values === void 0 ? void 0 : values.hp) !== null && _d !== void 0 ? _d : null;
2903
- }
2904
- copySettings(mod) {
2905
- var _a, _b, _c, _d, _e, _f, _g, _h;
2906
- super.copySettings(mod);
2907
- this.cs.value = ((_b = (_a = mod.settings) === null || _a === void 0 ? void 0 : _a.cs) !== null && _b !== void 0 ? _b : null);
2908
- this.ar.value = ((_d = (_c = mod.settings) === null || _c === void 0 ? void 0 : _c.ar) !== null && _d !== void 0 ? _d : null);
2909
- this.od.value = ((_f = (_e = mod.settings) === null || _e === void 0 ? void 0 : _e.od) !== null && _f !== void 0 ? _f : null);
2910
- this.hp.value = ((_h = (_g = mod.settings) === null || _g === void 0 ? void 0 : _g.hp) !== null && _h !== void 0 ? _h : null);
2911
- }
2912
- get isDroidRelevant() {
2913
- return this.isRelevant;
2914
- }
2915
- calculateDroidScoreMultiplier(difficulty) {
2916
- // Graph: https://www.desmos.com/calculator/yrggkhrkzz
2917
- let multiplier = 1;
2918
- if (this.cs.value !== null) {
2919
- const diff = this.cs.value - difficulty.cs;
2920
- multiplier *=
2921
- diff >= 0
2922
- ? 1 + 0.0075 * Math.pow(diff, 1.5)
2923
- : 2 / (1 + Math.exp(-0.5 * diff));
2924
- }
2925
- if (this.od.value !== null) {
2926
- const diff = this.od.value - difficulty.od;
2927
- multiplier *=
2928
- diff >= 0
2929
- ? 1 + 0.005 * Math.pow(diff, 1.3)
2930
- : 2 / (1 + Math.exp(-0.25 * diff));
2931
- }
2932
- return multiplier;
2933
- }
2934
- get isOsuRelevant() {
2935
- return this.isRelevant;
2936
- }
2937
- get osuScoreMultiplier() {
2938
- return 0.5;
2939
- }
2940
- applyToDifficultyWithMods(_, difficulty, mods) {
2941
- var _a, _b, _c, _d;
2942
- difficulty.cs = (_a = this.cs.value) !== null && _a !== void 0 ? _a : difficulty.cs;
2943
- difficulty.ar = (_b = this.ar.value) !== null && _b !== void 0 ? _b : difficulty.ar;
2944
- difficulty.od = (_c = this.od.value) !== null && _c !== void 0 ? _c : difficulty.od;
2945
- difficulty.hp = (_d = this.hp.value) !== null && _d !== void 0 ? _d : difficulty.hp;
2946
- // Special case for force AR in replay version 6 and older, where the AR value is kept constant with
2947
- // respect to game time. This makes the player perceive the AR as is under all speed multipliers.
2948
- if (this.ar.value !== null && mods.has(ModReplayV6)) {
2949
- const preempt = BeatmapDifficulty.difficultyRange(this.ar.value, HitObject.preemptMax, HitObject.preemptMid, HitObject.preemptMin);
2950
- const trackRate = this.calculateTrackRate(mods.values());
2951
- difficulty.ar = BeatmapDifficulty.inverseDifficultyRange(preempt * trackRate, HitObject.preemptMax, HitObject.preemptMid, HitObject.preemptMin);
2952
- }
2953
- }
2954
- applyToHitObjectWithMods(_, hitObject, mods) {
2955
- // Special case for force AR in replay version 6 and older, where the AR value is kept constant with
2956
- // respect to game time. This makes the player perceive the fade in animation as is under all speed
2957
- // multipliers.
2958
- if (this.ar.value === null || !mods.has(ModReplayV6)) {
2959
- return;
2960
- }
2961
- this.applyFadeAdjustment(hitObject, mods);
2962
- if (hitObject instanceof Slider) {
2963
- for (const nested of hitObject.nestedHitObjects) {
2964
- this.applyFadeAdjustment(nested, mods);
2965
- }
2966
- }
2937
+ this.relativeAngle = 0;
2938
+ /**
2939
+ * The jump distance from the previous {@link HitObject} to this one.
2940
+ *
2941
+ * The `distanceFromPrevious` of the first {@link HitObject} in a beatmap is relative to the center of
2942
+ * the playfield.
2943
+ */
2944
+ this.distanceFromPrevious = 0;
2945
+ /**
2946
+ * The rotation of this {@link HitObject} relative to its jump angle.
2947
+ *
2948
+ * For `Slider`s, this is defined as the angle from the `Slider`'s start position to the end of its path
2949
+ * relative to its jump angle. For `HitCircle`s and `Spinner`s, this property is ignored.
2950
+ */
2951
+ this.rotation = 0;
2952
+ this.hitObject = hitObject;
2967
2953
  }
2968
- serializeSettings() {
2969
- if (!this.isRelevant) {
2970
- return null;
2971
- }
2972
- const settings = {};
2973
- if (this.cs.value !== null) {
2974
- settings.cs = this.cs.value;
2975
- }
2976
- if (this.ar.value !== null) {
2977
- settings.ar = this.ar.value;
2978
- }
2979
- if (this.od.value !== null) {
2980
- settings.od = this.od.value;
2981
- }
2982
- if (this.hp.value !== null) {
2983
- settings.hp = this.hp.value;
2984
- }
2985
- return settings;
2954
+ }
2955
+
2956
+ /**
2957
+ * Precision utilities.
2958
+ */
2959
+ class Precision {
2960
+ /**
2961
+ * Checks if two numbers are equal with a given tolerance.
2962
+ *
2963
+ * @param value1 The first number.
2964
+ * @param value2 The second number.
2965
+ * @param acceptableDifference The acceptable difference as threshold. Default is `Precision.FLOAT_EPSILON = 1e-3`.
2966
+ */
2967
+ static almostEqualsNumber(value1, value2, acceptableDifference = this.FLOAT_EPSILON) {
2968
+ return Math.abs(value1 - value2) <= acceptableDifference;
2986
2969
  }
2987
- applyFadeAdjustment(hitObject, mods) {
2988
- // IMPORTANT: These do not use `ModUtil.calculateRateWithMods` to avoid circular dependency.
2989
- const initialTrackRate = this.calculateTrackRate(mods.values());
2990
- const currentTrackRate = this.calculateTrackRate(mods.values(), hitObject.startTime);
2991
- // Cancel the rate that was initially applied to timePreempt (via applyToDifficulty above and
2992
- // HitObject.applyDefaults) and apply the current one.
2993
- hitObject.timePreempt *= currentTrackRate / initialTrackRate;
2994
- hitObject.timeFadeIn *= currentTrackRate;
2970
+ /**
2971
+ * Checks if two vectors are equal with a given tolerance.
2972
+ *
2973
+ * @param vec1 The first vector.
2974
+ * @param vec2 The second vector.
2975
+ * @param acceptableDifference The acceptable difference as threshold. Default is `Precision.FLOAT_EPSILON = 1e-3`.
2976
+ */
2977
+ static almostEqualsVector(vec1, vec2, acceptableDifference = this.FLOAT_EPSILON) {
2978
+ return (this.almostEqualsNumber(vec1.x, vec2.x, acceptableDifference) &&
2979
+ this.almostEqualsNumber(vec1.y, vec2.y, acceptableDifference));
2995
2980
  }
2996
- calculateTrackRate(mods, time = 0) {
2997
- // IMPORTANT: This does not use `ModUtil.calculateRateWithMods` to avoid circular dependency.
2998
- let rate = 1;
2999
- for (const mod of mods) {
3000
- if (mod.isApplicableToTrackRate()) {
3001
- rate = mod.applyToRate(time, rate);
3002
- }
3003
- }
3004
- return rate;
2981
+ /**
2982
+ * Checks whether two real numbers are almost equal.
2983
+ *
2984
+ * @param a The first number.
2985
+ * @param b The second number.
2986
+ * @param maximumError The accuracy required for being almost equal. Defaults to `10 * 2^(-53)`.
2987
+ * @returns Whether the two values differ by no more than 10 * 2^(-52).
2988
+ */
2989
+ static almostEqualRelative(a, b, maximumError = 10 * Math.pow(2, -53)) {
2990
+ return this.almostEqualNormRelative(a, b, a - b, maximumError);
3005
2991
  }
3006
- toString() {
3007
- if (!this.isRelevant) {
3008
- return super.toString();
3009
- }
3010
- const settings = [];
3011
- if (this.cs.value !== null) {
3012
- settings.push(`CS${this.cs.toDisplayString()}`);
2992
+ /**
2993
+ * Compares two numbers and determines if they are equal within the specified maximum error.
2994
+ *
2995
+ * @param a The norm of the first value (can be negative).
2996
+ * @param b The norm of the second value (can be negative).
2997
+ * @param diff The norm of the difference of the two values (can be negative).
2998
+ * @param maximumError The accuracy required for being almost equal.
2999
+ * @returns Whether both numbers are almost equal up to the specified maximum error.
3000
+ */
3001
+ static almostEqualNormRelative(a, b, diff, maximumError) {
3002
+ // If A or B are infinity (positive or negative) then
3003
+ // only return true if they are exactly equal to each other -
3004
+ // that is, if they are both infinities of the same sign.
3005
+ if (!Number.isFinite(a) || !Number.isFinite(b)) {
3006
+ return a === b;
3013
3007
  }
3014
- if (this.ar.value !== null) {
3015
- settings.push(`AR${this.ar.toDisplayString()}`);
3008
+ // If A or B are a NAN, return false. NANs are equal to nothing,
3009
+ // not even themselves.
3010
+ if (Number.isNaN(a) || Number.isNaN(b)) {
3011
+ return false;
3016
3012
  }
3017
- if (this.od.value !== null) {
3018
- settings.push(`OD${this.od.toDisplayString()}`);
3013
+ // If one is almost zero, fall back to absolute equality.
3014
+ const doublePrecision = Math.pow(2, -53);
3015
+ if (Math.abs(a) < doublePrecision || Math.abs(b) < doublePrecision) {
3016
+ return Math.abs(diff) < maximumError;
3019
3017
  }
3020
- if (this.hp.value !== null) {
3021
- settings.push(`HP${this.hp.toDisplayString()}`);
3018
+ if ((a === 0 && Math.abs(b) < maximumError) ||
3019
+ (b === 0 && Math.abs(a) < maximumError)) {
3020
+ return true;
3022
3021
  }
3023
- return `${super.toString()} (${settings.join(", ")})`;
3022
+ return (Math.abs(diff) < maximumError * Math.max(Math.abs(a), Math.abs(b)));
3024
3023
  }
3025
3024
  }
3025
+ Precision.FLOAT_EPSILON = 1e-3;
3026
3026
 
3027
3027
  /**
3028
- * Represents the NightCore mod.
3028
+ * Types of slider paths.
3029
3029
  */
3030
- class ModNightCore extends ModRateAdjust {
3031
- constructor() {
3032
- super(1.5);
3033
- this.acronym = "NC";
3034
- this.name = "NightCore";
3035
- this.droidRanked = true;
3036
- this.osuRanked = true;
3037
- this.bitwise = 1 << 9;
3038
- this.incompatibleMods.add(ModDoubleTime).add(ModHalfTime);
3039
- }
3040
- get isDroidRelevant() {
3041
- return this.isRelevant;
3042
- }
3043
- calculateDroidScoreMultiplier() {
3044
- return this.droidScoreMultiplier;
3045
- }
3046
- get isOsuRelevant() {
3047
- return this.isRelevant;
3048
- }
3049
- get osuScoreMultiplier() {
3050
- return 1.12;
3051
- }
3052
- }
3030
+ exports.PathType = void 0;
3031
+ (function (PathType) {
3032
+ PathType["Catmull"] = "C";
3033
+ PathType["Bezier"] = "B";
3034
+ PathType["Linear"] = "L";
3035
+ PathType["PerfectCurve"] = "P";
3036
+ })(exports.PathType || (exports.PathType = {}));
3053
3037
 
3054
3038
  /**
3055
- * Represents the HalfTime mod.
3039
+ * Path approximator for sliders.
3056
3040
  */
3057
- class ModHalfTime extends ModRateAdjust {
3058
- constructor() {
3059
- super(0.75);
3060
- this.acronym = "HT";
3061
- this.name = "HalfTime";
3062
- this.droidRanked = true;
3063
- this.osuRanked = true;
3064
- this.bitwise = 1 << 8;
3065
- this.incompatibleMods.add(ModDoubleTime).add(ModNightCore);
3066
- }
3067
- get isDroidRelevant() {
3068
- return this.isRelevant;
3069
- }
3070
- calculateDroidScoreMultiplier() {
3071
- return this.droidScoreMultiplier;
3041
+ class PathApproximator {
3042
+ /**
3043
+ * Approximates a bezier slider's path.
3044
+ *
3045
+ * Creates a piecewise-linear approximation of a bezier curve by adaptively repeatedly subdividing
3046
+ * the control points until their approximation error vanishes below a given threshold.
3047
+ *
3048
+ * @param controlPoints The anchor points of the slider.
3049
+ */
3050
+ static approximateBezier(controlPoints) {
3051
+ const output = [];
3052
+ const count = controlPoints.length - 1;
3053
+ if (count < 0) {
3054
+ return output;
3055
+ }
3056
+ const subdivisionBuffer1 = new Array(count + 1);
3057
+ const subdivisionBuffer2 = new Array(count * 2 + 1);
3058
+ // "toFlatten" contains all the curves which are not yet approximated well enough.
3059
+ // We use a stack to emulate recursion without the risk of running into a stack overflow.
3060
+ // (More specifically, we iteratively and adaptively refine our curve with a
3061
+ // depth-first search (https://en.wikipedia.org/wiki/Depth-first_search)
3062
+ // over the tree resulting from the subdivisions we make.)
3063
+ const toFlatten = [controlPoints.slice()];
3064
+ const freeBuffers = [];
3065
+ const leftChild = subdivisionBuffer2;
3066
+ while (toFlatten.length > 0) {
3067
+ const parent = toFlatten.pop();
3068
+ if (this.bezierIsFlatEnough(parent)) {
3069
+ // If the control points we currently operate on are sufficiently "flat", we use
3070
+ // an extension to De Casteljau's algorithm to obtain a piecewise-linear approximation
3071
+ // of the bezier curve represented by our control points, consisting of the same amount
3072
+ // of points as there are control points.
3073
+ this.bezierApproximate(parent, output, subdivisionBuffer1, subdivisionBuffer2, count + 1);
3074
+ freeBuffers.push(parent);
3075
+ continue;
3076
+ }
3077
+ // If we do not yet have a sufficiently "flat" (in other words, detailed) approximation we keep
3078
+ // subdividing the curve we are currently operating on.
3079
+ const rightChild = freeBuffers.length > 0
3080
+ ? freeBuffers.pop()
3081
+ : new Array(count + 1);
3082
+ this.bezierSubdivide(parent, leftChild, rightChild, subdivisionBuffer1, count + 1);
3083
+ // We re-use the buffer of the parent for one of the children, so that we save one allocation per iteration.
3084
+ for (let i = 0; i < count + 1; ++i) {
3085
+ parent[i] = leftChild[i];
3086
+ }
3087
+ toFlatten.push(rightChild);
3088
+ toFlatten.push(parent);
3089
+ }
3090
+ output.push(controlPoints[count]);
3091
+ return output;
3072
3092
  }
3073
- get isOsuRelevant() {
3074
- return this.isRelevant;
3093
+ /**
3094
+ * Approximates a catmull slider's path.
3095
+ *
3096
+ * Creates a piecewise-linear approximation of a Catmull-Rom spline.
3097
+ *
3098
+ * @param controlPoints The anchor points of the slider.
3099
+ */
3100
+ static approximateCatmull(controlPoints) {
3101
+ const result = [];
3102
+ for (let i = 0; i < controlPoints.length - 1; ++i) {
3103
+ const v1 = i > 0 ? controlPoints[i - 1] : controlPoints[i];
3104
+ const v2 = controlPoints[i];
3105
+ const v3 = controlPoints[i + 1];
3106
+ const v4 = i < controlPoints.length - 2
3107
+ ? controlPoints[i + 2]
3108
+ : v3.add(v3).subtract(v2);
3109
+ for (let c = 0; c < this.catmullDetail; ++c) {
3110
+ result.push(this.catmullFindPoint(v1, v2, v3, v4, c / this.catmullDetail));
3111
+ result.push(this.catmullFindPoint(v1, v2, v3, v4, (c + 1) / this.catmullDetail));
3112
+ }
3113
+ }
3114
+ return result;
3075
3115
  }
3076
- get osuScoreMultiplier() {
3077
- return 0.3;
3116
+ /**
3117
+ * Approximates a slider's circular arc.
3118
+ *
3119
+ * Creates a piecewise-linear approximation of a circular arc curve.
3120
+ *
3121
+ * @param controlPoints The anchor points of the slider.
3122
+ */
3123
+ static approximateCircularArc(controlPoints) {
3124
+ const a = controlPoints[0];
3125
+ const b = controlPoints[1];
3126
+ const c = controlPoints[2];
3127
+ // If we have a degenerate triangle where a side-length is almost zero, then give up and fall
3128
+ // back to a more numerically stable method.
3129
+ if (Precision.almostEqualsNumber(0, (b.y - a.y) * (c.x - a.x) - (b.x - a.x) * (c.y - a.y))) {
3130
+ return this.approximateBezier(controlPoints);
3131
+ }
3132
+ // See: https://en.wikipedia.org/wiki/Circumscribed_circle#Cartesian_coordinates_2
3133
+ const d = 2 *
3134
+ (a.x * b.subtract(c).y +
3135
+ b.x * c.subtract(a).y +
3136
+ c.x * a.subtract(b).y);
3137
+ const aSq = Math.pow(a.length, 2);
3138
+ const bSq = Math.pow(b.length, 2);
3139
+ const cSq = Math.pow(c.length, 2);
3140
+ const center = new Vector2(aSq * b.subtract(c).y +
3141
+ bSq * c.subtract(a).y +
3142
+ cSq * a.subtract(b).y, aSq * c.subtract(b).x +
3143
+ bSq * a.subtract(c).x +
3144
+ cSq * b.subtract(a).x).divide(d);
3145
+ const dA = a.subtract(center);
3146
+ const dC = c.subtract(center);
3147
+ const r = dA.length;
3148
+ const thetaStart = Math.atan2(dA.y, dA.x);
3149
+ let thetaEnd = Math.atan2(dC.y, dC.x);
3150
+ while (thetaEnd < thetaStart) {
3151
+ thetaEnd += 2 * Math.PI;
3152
+ }
3153
+ let dir = 1;
3154
+ let thetaRange = thetaEnd - thetaStart;
3155
+ // Decide in which direction to draw the circle, depending on which side of
3156
+ // AC B lies.
3157
+ let orthoAtoC = c.subtract(a);
3158
+ orthoAtoC = new Vector2(orthoAtoC.y, -orthoAtoC.x);
3159
+ if (orthoAtoC.dot(b.subtract(a)) < 0) {
3160
+ dir = -dir;
3161
+ thetaRange = 2 * Math.PI - thetaRange;
3162
+ }
3163
+ // We select the amount of points for the approximation by requiring the discrete curvature
3164
+ // to be smaller than the provided tolerance. The exact angle required to meet the tolerance
3165
+ // is: 2 * Math.Acos(1 - TOLERANCE / r)
3166
+ // The special case is required for extremely short sliders where the radius is smaller than
3167
+ // the tolerance. This is a pathological rather than a realistic case.
3168
+ const amountPoints = 2 * r <= this.circularArcTolerance
3169
+ ? 2
3170
+ : Math.max(2, Math.ceil(thetaRange /
3171
+ (2 *
3172
+ Math.acos(1 - this.circularArcTolerance / r))));
3173
+ const output = [];
3174
+ for (let i = 0; i < amountPoints; ++i) {
3175
+ const fract = i / (amountPoints - 1);
3176
+ const theta = thetaStart + dir * fract * thetaRange;
3177
+ const o = new Vector2(Math.cos(theta), Math.sin(theta)).scale(r);
3178
+ output.push(center.add(o));
3179
+ }
3180
+ return output;
3078
3181
  }
3079
- }
3080
-
3081
- /**
3082
- * Represents the DoubleTime mod.
3083
- */
3084
- class ModDoubleTime extends ModRateAdjust {
3085
- constructor() {
3086
- super(1.5);
3087
- this.acronym = "DT";
3088
- this.name = "DoubleTime";
3089
- this.droidRanked = true;
3090
- this.osuRanked = true;
3091
- this.bitwise = 1 << 6;
3092
- this.incompatibleMods.add(ModHalfTime).add(ModNightCore);
3182
+ /**
3183
+ * Approximates a linear slider's path.
3184
+ *
3185
+ * Creates a piecewise-linear approximation of a linear curve.
3186
+ * Basically, returns the input.
3187
+ *
3188
+ * @param controlPoints The anchor points of the slider.
3189
+ */
3190
+ static approximateLinear(controlPoints) {
3191
+ return controlPoints;
3093
3192
  }
3094
- get isDroidRelevant() {
3095
- return this.isRelevant;
3193
+ /**
3194
+ * Checks if a bezier slider is flat enough to be approximated.
3195
+ *
3196
+ * Make sure the 2nd order derivative (approximated using finite elements) is within tolerable bounds.
3197
+ *
3198
+ * NOTE: The 2nd order derivative of a 2D curve represents its curvature, so intuitively this function
3199
+ * checks (as the name suggests) whether our approximation is _locally_ "flat". More curvy parts
3200
+ * need to have a denser approximation to be more "flat".
3201
+ *
3202
+ * @param controlPoints The anchor points of the slider.
3203
+ */
3204
+ static bezierIsFlatEnough(controlPoints) {
3205
+ for (let i = 1; i < controlPoints.length - 1; ++i) {
3206
+ const prev = controlPoints[i - 1];
3207
+ const current = controlPoints[i];
3208
+ const next = controlPoints[i + 1];
3209
+ const final = prev.subtract(current.scale(2)).add(next);
3210
+ if (Math.pow(final.length, 2) >
3211
+ Math.pow(this.bezierTolerance, 2) * 4) {
3212
+ return false;
3213
+ }
3214
+ }
3215
+ return true;
3096
3216
  }
3097
- calculateDroidScoreMultiplier() {
3098
- return this.droidScoreMultiplier;
3217
+ /**
3218
+ * Approximates a bezier slider's path.
3219
+ *
3220
+ * This uses {@link https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm De Casteljau's algorithm} to obtain an optimal
3221
+ * piecewise-linear approximation of the bezier curve with the same amount of points as there are control points.
3222
+ *
3223
+ * @param controlPoints The control points describing the bezier curve to be approximated.
3224
+ * @param output The points representing the resulting piecewise-linear approximation.
3225
+ * @param subdivisionBuffer1 The first buffer containing the current subdivision state.
3226
+ * @param subdivisionBuffer2 The second buffer containing the current subdivision state.
3227
+ * @param count The number of control points in the original array.
3228
+ */
3229
+ static bezierApproximate(controlPoints, output, subdivisionBuffer1, subdivisionBuffer2, count) {
3230
+ const l = subdivisionBuffer2;
3231
+ const r = subdivisionBuffer1;
3232
+ this.bezierSubdivide(controlPoints, l, r, subdivisionBuffer1, count);
3233
+ for (let i = 0; i < count - 1; ++i) {
3234
+ l[count + i] = r[i + 1];
3235
+ }
3236
+ output.push(controlPoints[0]);
3237
+ for (let i = 1; i < count - 1; ++i) {
3238
+ const index = 2 * i;
3239
+ const p = l[index - 1]
3240
+ .add(l[index].scale(2))
3241
+ .add(l[index + 1])
3242
+ .scale(0.25);
3243
+ output.push(p);
3244
+ }
3099
3245
  }
3100
- get isOsuRelevant() {
3101
- return this.isRelevant;
3246
+ /**
3247
+ * Subdivides `n` control points representing a bezier curve into 2 sets of `n` control points, each
3248
+ * describing a bezier curve equivalent to a half of the original curve. Effectively this splits
3249
+ * the original curve into 2 curves which result in the original curve when pieced back together.
3250
+ *
3251
+ * @param controlPoints The anchor points of the slider.
3252
+ * @param l Parts of the slider for approximation.
3253
+ * @param r Parts of the slider for approximation.
3254
+ * @param subdivisionBuffer Parts of the slider for approximation.
3255
+ * @param count The amount of anchor points in the slider.
3256
+ */
3257
+ static bezierSubdivide(controlPoints, l, r, subdivisionBuffer, count) {
3258
+ const midpoints = subdivisionBuffer;
3259
+ for (let i = 0; i < count; ++i) {
3260
+ midpoints[i] = controlPoints[i];
3261
+ }
3262
+ for (let i = 0; i < count; ++i) {
3263
+ l[i] = midpoints[0];
3264
+ r[count - i - 1] = midpoints[count - i - 1];
3265
+ for (let j = 0; j < count - i - 1; ++j) {
3266
+ midpoints[j] = midpoints[j].add(midpoints[j + 1]).divide(2);
3267
+ }
3268
+ }
3102
3269
  }
3103
- get osuScoreMultiplier() {
3104
- return 1.12;
3270
+ /**
3271
+ * Finds a point on the spline at the position of a parameter.
3272
+ *
3273
+ * @param vec1 The first vector.
3274
+ * @param vec2 The second vector.
3275
+ * @param vec3 The third vector.
3276
+ * @param vec4 The fourth vector.
3277
+ * @param t The parameter at which to find the point on the spline, in the range [0, 1].
3278
+ */
3279
+ static catmullFindPoint(vec1, vec2, vec3, vec4, t) {
3280
+ const t2 = Math.pow(t, 2);
3281
+ const t3 = Math.pow(t, 3);
3282
+ return new Vector2(0.5 *
3283
+ (2 * vec2.x +
3284
+ (-vec1.x + vec3.x) * t +
3285
+ (2 * vec1.x - 5 * vec2.x + 4 * vec3.x - vec4.x) * t2 +
3286
+ (-vec1.x + 3 * vec2.x - 3 * vec3.x + vec4.x) * t3), 0.5 *
3287
+ (2 * vec2.y +
3288
+ (-vec1.y + vec3.y) * t +
3289
+ (2 * vec1.y - 5 * vec2.y + 4 * vec3.y - vec4.y) * t2 +
3290
+ (-vec1.y + 3 * vec2.y - 3 * vec3.y + vec4.y) * t3));
3105
3291
  }
3106
3292
  }
3107
-
3108
- var _a$1;
3109
- /**
3110
- * Represents the osu! playfield.
3111
- */
3112
- class Playfield {
3113
- }
3114
- _a$1 = Playfield;
3115
- /**
3116
- * The size of the playfield, which is 512x384.
3117
- */
3118
- Playfield.baseSize = new Vector2(512, 384);
3293
+ PathApproximator.bezierTolerance = 0.25;
3119
3294
  /**
3120
- * The center of the playfield, which is at (256, 192).
3295
+ * The amount of pieces to calculate for each control point quadruplet.
3121
3296
  */
3122
- Playfield.center = _a$1.baseSize.scale(0.5);
3297
+ PathApproximator.catmullDetail = 50;
3298
+ PathApproximator.circularArcTolerance = 0.1;
3123
3299
 
3124
3300
  /**
3125
- * Represents a spinner in a beatmap.
3126
- *
3127
- * All we need from spinners is their duration. The
3128
- * position of a spinner is always at 256x192.
3129
- */
3130
- class Spinner extends HitObject {
3131
- get endTime() {
3132
- return this._endTime;
3133
- }
3301
+ * Represents a slider's path.
3302
+ */
3303
+ class SliderPath {
3134
3304
  constructor(values) {
3135
- super(Object.assign(Object.assign({}, values), { position: Playfield.baseSize.divide(2) }));
3136
- this._endTime = values.endTime;
3305
+ /**
3306
+ * Whether or not the instance has been initialized.
3307
+ */
3308
+ this.isInitialized = false;
3309
+ /**
3310
+ * The calculated path of the slider.
3311
+ */
3312
+ this.calculatedPath = [];
3313
+ /**
3314
+ * The cumulative length of the slider.
3315
+ */
3316
+ this.cumulativeLength = [];
3317
+ this.pathType = values.pathType;
3318
+ this.controlPoints = values.controlPoints;
3319
+ this.expectedDistance = values.expectedDistance;
3320
+ this.ensureInitialized();
3137
3321
  }
3138
- applySamples(controlPoints) {
3139
- super.applySamples(controlPoints);
3140
- const samplePoints = controlPoints.sample.between(this.startTime + HitObject.controlPointLeniency, this.endTime + HitObject.controlPointLeniency);
3141
- this.auxiliarySamples.length = 0;
3142
- this.auxiliarySamples.push(new SequenceHitSampleInfo(samplePoints.map((s) => new TimedHitSampleInfo(s.time, s.applyTo(Spinner.baseSpinnerSpinSample)))));
3143
- this.auxiliarySamples.push(new SequenceHitSampleInfo(samplePoints.map((s) => new TimedHitSampleInfo(s.time, s.applyTo(Spinner.baseSpinnerBonusSample)))));
3322
+ /**
3323
+ * Initializes the instance.
3324
+ */
3325
+ ensureInitialized() {
3326
+ if (this.isInitialized) {
3327
+ return;
3328
+ }
3329
+ this.isInitialized = true;
3330
+ this.calculatedPath.length = 0;
3331
+ this.cumulativeLength.length = 0;
3332
+ this.calculatePath();
3333
+ this.calculateCumulativeLength();
3144
3334
  }
3145
- get stackedPosition() {
3146
- return this.position;
3335
+ /**
3336
+ * Calculates the slider's path.
3337
+ */
3338
+ calculatePath() {
3339
+ this.calculatedPath.length = 0;
3340
+ let spanStart = 0;
3341
+ for (let i = 0; i < this.controlPoints.length; i++) {
3342
+ if (i === this.controlPoints.length - 1 ||
3343
+ this.controlPoints[i].equals(this.controlPoints[i + 1])) {
3344
+ const spanEnd = i + 1;
3345
+ const cpSpan = this.controlPoints.slice(spanStart, spanEnd);
3346
+ this.calculateSubPath(cpSpan).forEach((t) => {
3347
+ if (this.calculatedPath.length === 0 ||
3348
+ !this.calculatedPath.at(-1).equals(t)) {
3349
+ this.calculatedPath.push(t);
3350
+ }
3351
+ });
3352
+ spanStart = spanEnd;
3353
+ }
3354
+ }
3147
3355
  }
3148
- get stackedEndPosition() {
3149
- return this.position;
3356
+ /**
3357
+ * Calculates the slider's subpath.
3358
+ */
3359
+ calculateSubPath(subControlPoints) {
3360
+ switch (this.pathType) {
3361
+ case exports.PathType.Linear:
3362
+ return PathApproximator.approximateLinear(subControlPoints);
3363
+ case exports.PathType.PerfectCurve: {
3364
+ if (subControlPoints.length !== 3) {
3365
+ break;
3366
+ }
3367
+ const subPath = PathApproximator.approximateCircularArc(subControlPoints);
3368
+ // If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable Bézier approximation.
3369
+ if (subPath.length === 0) {
3370
+ break;
3371
+ }
3372
+ return subPath;
3373
+ }
3374
+ case exports.PathType.Catmull:
3375
+ return PathApproximator.approximateCatmull(subControlPoints);
3376
+ }
3377
+ return PathApproximator.approximateBezier(subControlPoints);
3150
3378
  }
3151
- createHitWindow() {
3152
- return new EmptyHitWindow();
3379
+ /**
3380
+ * Calculates the slider's cumulative length.
3381
+ */
3382
+ calculateCumulativeLength() {
3383
+ let calculatedLength = 0;
3384
+ this.cumulativeLength.length = 0;
3385
+ this.cumulativeLength.push(0);
3386
+ for (let i = 0; i < this.calculatedPath.length - 1; ++i) {
3387
+ const diff = this.calculatedPath[i + 1].subtract(this.calculatedPath[i]);
3388
+ calculatedLength += diff.length;
3389
+ this.cumulativeLength.push(calculatedLength);
3390
+ }
3391
+ if (calculatedLength !== this.expectedDistance) {
3392
+ // In osu-stable, if the last two path points of a slider are equal, extension is not performed.
3393
+ if (this.calculatedPath.length >= 2 &&
3394
+ this.calculatedPath
3395
+ .at(-1)
3396
+ .equals(this.calculatedPath.at(-2)) &&
3397
+ this.expectedDistance > calculatedLength) {
3398
+ this.cumulativeLength.push(calculatedLength);
3399
+ return;
3400
+ }
3401
+ // The last length is always incorrect
3402
+ this.cumulativeLength.pop();
3403
+ let pathEndIndex = this.calculatedPath.length - 1;
3404
+ if (calculatedLength > this.expectedDistance) {
3405
+ // The path will be shortened further, in which case we should trim any more unnecessary lengths and their associated path segments
3406
+ while (this.cumulativeLength.length > 0 &&
3407
+ this.cumulativeLength.at(-1) >= this.expectedDistance) {
3408
+ this.cumulativeLength.pop();
3409
+ this.calculatedPath.splice(pathEndIndex--, 1);
3410
+ }
3411
+ }
3412
+ if (pathEndIndex <= 0) {
3413
+ // The expected distance is negative or zero
3414
+ this.cumulativeLength.push(0);
3415
+ return;
3416
+ }
3417
+ // The direction of the segment to shorten or lengthen
3418
+ const dir = this.calculatedPath[pathEndIndex].subtract(this.calculatedPath[pathEndIndex - 1]);
3419
+ dir.normalize();
3420
+ this.calculatedPath[pathEndIndex] = this.calculatedPath[pathEndIndex - 1].add(dir.scale(this.expectedDistance - this.cumulativeLength.at(-1)));
3421
+ this.cumulativeLength.push(this.expectedDistance);
3422
+ }
3153
3423
  }
3154
- toString() {
3155
- return `Position: [${this._position.x}, ${this._position.y}], duration: ${this.duration}`;
3424
+ /**
3425
+ * Computes the position on the slider at a given progress that ranges from 0 (beginning of the path)
3426
+ * to 1 (end of the path).
3427
+ *
3428
+ * @param progress Ranges from 0 (beginning of the path) to 1 (end of the path).
3429
+ */
3430
+ positionAt(progress) {
3431
+ this.ensureInitialized();
3432
+ const d = this.progressToDistance(progress);
3433
+ return this.interpolateVerticles(this.indexOfDistance(d), d);
3156
3434
  }
3157
- }
3158
- Spinner.baseSpinnerSpinSample = new BankHitSampleInfo("spinnerspin");
3159
- Spinner.baseSpinnerBonusSample = new BankHitSampleInfo("spinnerbonus");
3160
-
3161
- /**
3162
- * Represents a four-dimensional vector.
3163
- */
3164
- class Vector4 {
3165
- constructor(xOrValueOrXZ, yOrYW, z, w) {
3166
- if (yOrYW === undefined) {
3167
- this.x = xOrValueOrXZ;
3168
- this.y = xOrValueOrXZ;
3169
- this.z = xOrValueOrXZ;
3170
- this.w = xOrValueOrXZ;
3171
- return;
3435
+ /**
3436
+ * Computes the slider path until a given progress that ranges from 0 (beginning of the slider) to
3437
+ * 1 (end of the slider).
3438
+ *
3439
+ * @param p0 Start progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).
3440
+ * @param p1 End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).
3441
+ * @return The computed path between the two ranges.
3442
+ */
3443
+ pathToProgress(p0, p1) {
3444
+ const path = [];
3445
+ const d0 = this.progressToDistance(p0);
3446
+ const d1 = this.progressToDistance(p1);
3447
+ let i = 0;
3448
+ while (i < this.calculatedPath.length &&
3449
+ this.cumulativeLength[i] < d0) {
3450
+ ++i;
3172
3451
  }
3173
- if (typeof z === "undefined") {
3174
- this.x = xOrValueOrXZ;
3175
- this.y = yOrYW;
3176
- this.z = xOrValueOrXZ;
3177
- this.w = yOrYW;
3178
- return;
3452
+ path.push(this.interpolateVerticles(i, d0));
3453
+ while (i < this.calculatedPath.length &&
3454
+ this.cumulativeLength[i] <= d1) {
3455
+ path.push(this.calculatedPath[i++]);
3179
3456
  }
3180
- this.x = xOrValueOrXZ;
3181
- this.y = yOrYW;
3182
- this.z = z;
3183
- this.w = w;
3457
+ path.push(this.interpolateVerticles(i, d1));
3458
+ return path;
3184
3459
  }
3185
3460
  /**
3186
- * The X coordinate of the left edge of this vector.
3461
+ * Returns the progress of reaching expected distance.
3187
3462
  */
3188
- get left() {
3189
- return this.x;
3463
+ progressToDistance(progress) {
3464
+ return Math.min(Math.max(progress, 0), 1) * this.expectedDistance;
3465
+ }
3466
+ /**
3467
+ * Interpolates verticles of the slider.
3468
+ */
3469
+ interpolateVerticles(i, d) {
3470
+ if (this.calculatedPath.length === 0) {
3471
+ return new Vector2(0);
3472
+ }
3473
+ if (i <= 0) {
3474
+ return this.calculatedPath[0];
3475
+ }
3476
+ if (i >= this.calculatedPath.length) {
3477
+ return this.calculatedPath.at(-1);
3478
+ }
3479
+ const p0 = this.calculatedPath[i - 1];
3480
+ const p1 = this.calculatedPath[i];
3481
+ const d0 = this.cumulativeLength[i - 1];
3482
+ const d1 = this.cumulativeLength[i];
3483
+ // Avoid division by and almost-zero number in case two points are extremely close to each other.
3484
+ if (Precision.almostEqualsNumber(d0, d1)) {
3485
+ return p0;
3486
+ }
3487
+ const w = (d - d0) / (d1 - d0);
3488
+ return p0.add(p1.subtract(p0).scale(w));
3489
+ }
3490
+ /**
3491
+ * Binary searches the cumulative length array and returns the
3492
+ * index at which `arr[index] >= d`.
3493
+ *
3494
+ * @param d The distance to search.
3495
+ * @returns The index.
3496
+ */
3497
+ indexOfDistance(d) {
3498
+ if (this.cumulativeLength.length === 0 ||
3499
+ d < this.cumulativeLength[0]) {
3500
+ return 0;
3501
+ }
3502
+ if (d >= this.cumulativeLength.at(-1)) {
3503
+ return this.cumulativeLength.length;
3504
+ }
3505
+ let l = 0;
3506
+ let r = this.cumulativeLength.length - 2;
3507
+ while (l <= r) {
3508
+ const pivot = l + ((r - l) >> 1);
3509
+ if (this.cumulativeLength[pivot] < d) {
3510
+ l = pivot + 1;
3511
+ }
3512
+ else if (this.cumulativeLength[pivot] > d) {
3513
+ r = pivot - 1;
3514
+ }
3515
+ else {
3516
+ return pivot;
3517
+ }
3518
+ }
3519
+ return l;
3190
3520
  }
3521
+ }
3522
+
3523
+ var _a;
3524
+ /**
3525
+ * Utilities for {@link HitObject} generation.
3526
+ */
3527
+ class HitObjectGenerationUtils {
3528
+ //#region Rotation
3191
3529
  /**
3192
- * The Y coordinate of the top edge of this vector.
3530
+ * Rotates a {@link HitObject} away from the edge of the playfield while keeping a constant distance from
3531
+ * the previous {@link HitObject}.
3532
+ *
3533
+ * @param previousObjectPosition The position of the previous {@link HitObject}.
3534
+ * @param positionRelativeToPrevious The position of the {@link HitObject} to be rotated relative to the
3535
+ * previous {@link HitObject}.
3536
+ * @param rotationRatio The extent of the rotation. 0 means the {@link HitObject} is never rotated, while 1
3537
+ * means the {@link HitObject} will be fully rotated towards the center of the playfield when it is originally
3538
+ * at the edge of the playfield.
3539
+ * @return The new position of the {@link HitObject} relative to the previous {@link HitObject}.
3193
3540
  */
3194
- get top() {
3195
- return this.y;
3541
+ static rotateAwayFromEdge(previousObjectPosition, positionRelativeToPrevious, rotationRatio = 0.5) {
3542
+ const relativeRotationDistance = Math.max((previousObjectPosition.x < Playfield.center.x
3543
+ ? this.borderDistance.x - previousObjectPosition.x
3544
+ : previousObjectPosition.x -
3545
+ (Playfield.baseSize.x - this.borderDistance.x)) /
3546
+ this.borderDistance.x, (previousObjectPosition.y < Playfield.center.y
3547
+ ? this.borderDistance.y - previousObjectPosition.y
3548
+ : previousObjectPosition.y -
3549
+ (Playfield.baseSize.y - this.borderDistance.y)) /
3550
+ this.borderDistance.y, 0);
3551
+ return this.rotateVectorTowardsVector(positionRelativeToPrevious, Playfield.center.subtract(previousObjectPosition), Math.min(1, relativeRotationDistance * rotationRatio));
3196
3552
  }
3197
3553
  /**
3198
- * The X coordinate of the right edge of this vector.
3554
+ * Rotates a {@link Vector2} towards another {@link Vector2}.
3555
+ *
3556
+ * @param initial The {@link Vector2} to be rotated.
3557
+ * @param destination The {@link Vector2} that `initial` should be rotated towards.
3558
+ * @param rotationRatio How much `initial` should be rotated. 0 means no rotation. 1 mean `initial` is fully
3559
+ * rotated to equal `destination`.
3560
+ * @return The rotated {@link Vector2}.
3199
3561
  */
3200
- get right() {
3201
- return this.z;
3562
+ static rotateVectorTowardsVector(initial, destination, rotationRatio) {
3563
+ const initialAngle = Math.atan2(initial.y, initial.x);
3564
+ const destinationAngle = Math.atan2(destination.y, destination.x);
3565
+ let diff = destinationAngle - initialAngle;
3566
+ // Normalize angle
3567
+ while (diff < -Math.PI) {
3568
+ diff += 2 * Math.PI;
3569
+ }
3570
+ while (diff > Math.PI) {
3571
+ diff -= 2 * Math.PI;
3572
+ }
3573
+ const finalAngle = initialAngle + diff * rotationRatio;
3574
+ return new Vector2(initial.x * Math.cos(finalAngle), initial.y * Math.sin(finalAngle));
3202
3575
  }
3203
3576
  /**
3204
- * The Y coordinate of the bottom edge of this vector.
3577
+ * Obtains the absolute rotation of a {@link Slider}, defined as the angle from its start position to the
3578
+ * end of its path.
3579
+ *
3580
+ * @param slider The {@link Slider} to obtain the rotation from.
3581
+ * @return The angle in radians.
3205
3582
  */
3206
- get bottom() {
3207
- return this.w;
3583
+ static getSliderRotation(slider) {
3584
+ const pathEndPosition = slider.path.positionAt(1);
3585
+ return Math.atan2(pathEndPosition.y, pathEndPosition.x);
3208
3586
  }
3209
3587
  /**
3210
- * The top left corner of this vector.
3588
+ * Rotates a {@link Vector2} by the specified angle.
3589
+ *
3590
+ * @param vec The {@link Vector2} to be rotated.
3591
+ * @param rotation The angle to rotate `vec` by, in radians.
3592
+ * @return The rotated {@link Vector2}.
3211
3593
  */
3212
- get topLeft() {
3213
- return new Vector2(this.left, this.top);
3594
+ static rotateVector(vec, rotation) {
3595
+ const angle = Math.atan2(vec.y, vec.x) + rotation;
3596
+ const { length } = vec;
3597
+ return new Vector2(length * Math.cos(angle), length * Math.sin(angle));
3214
3598
  }
3599
+ //#endregion
3600
+ //#region Reflection
3215
3601
  /**
3216
- * The top right corner of this vector.
3602
+ * Reflects the position of a {@link HitObject} horizontally along the playfield.
3603
+ *
3604
+ * @param hitObject The {@link HitObject} to reflect.
3217
3605
  */
3218
- get topRight() {
3219
- return new Vector2(this.right, this.top);
3606
+ static reflectHorizontallyAlongPlayfield(hitObject) {
3607
+ hitObject.position = this.reflectVectorHorizontallyAlongPlayfield(hitObject.position);
3608
+ if (hitObject instanceof Slider) {
3609
+ this.modifySlider(hitObject, (v) => new Vector2(-v.x, v.y));
3610
+ }
3220
3611
  }
3221
3612
  /**
3222
- * The bottom left corner of this vector.
3613
+ * Reflects the position of a {@link HitObject} vertically along the playfield.
3614
+ *
3615
+ * @param hitObject The {@link HitObject} to reflect.
3223
3616
  */
3224
- get bottomLeft() {
3225
- return new Vector2(this.left, this.bottom);
3617
+ static reflectVerticallyAlongPlayfield(hitObject) {
3618
+ // Reflect the position of the hit object.
3619
+ hitObject.position = this.reflectVectorVerticallyAlongPlayfield(hitObject.position);
3620
+ if (hitObject instanceof Slider) {
3621
+ this.modifySlider(hitObject, (v) => new Vector2(v.x, -v.y));
3622
+ }
3226
3623
  }
3227
3624
  /**
3228
- * The bottom right corner of this vector.
3625
+ * Flips the position of a {@link Slider} around its start position horizontally.
3626
+ *
3627
+ * @param slider The {@link Slider} to be flipped.
3229
3628
  */
3230
- get bottomRight() {
3231
- return new Vector2(this.right, this.bottom);
3629
+ static flipSliderInPlaceHorizontally(slider) {
3630
+ this.modifySlider(slider, (v) => new Vector2(-v.x, v.y));
3232
3631
  }
3233
3632
  /**
3234
- * The width of the rectangle defined by this vector.
3633
+ * Rotates a {@link Slider} around its start position by the specified angle.
3634
+ *
3635
+ * @param slider The {@link Slider} to rotate.
3636
+ * @param rotation The angle to rotate `slider` by, in radians.
3235
3637
  */
3236
- get width() {
3237
- return this.right - this.left;
3638
+ static rotateSlider(slider, rotation) {
3639
+ this.modifySlider(slider, (v) => this.rotateVector(v, rotation));
3238
3640
  }
3239
- /**
3240
- * The height of the rectangle defined by this vector.
3241
- */
3242
- get height() {
3243
- return this.bottom - this.top;
3641
+ static modifySlider(slider, modifyControlPoint) {
3642
+ slider.path = new SliderPath({
3643
+ pathType: slider.path.pathType,
3644
+ controlPoints: slider.path.controlPoints.map(modifyControlPoint),
3645
+ expectedDistance: slider.path.expectedDistance,
3646
+ });
3244
3647
  }
3245
- }
3246
-
3247
- /**
3248
- * Contains infromation about the position of a {@link HitObject}.
3249
- */
3250
- class HitObjectPositionInfo {
3251
- constructor(hitObject) {
3252
- /**
3253
- * The jump angle from the previous {@link HitObject} to this one, relative to the previous
3254
- * {@link HitObject}'s jump angle.
3255
- *
3256
- * The `relativeAngle` of the first {@link HitObject} in a beatmap represents the absolute angle from the
3257
- * center of the playfield to the {@link HitObject}.
3258
- *
3259
- * If `relativeAngle` is 0, the player's cursor does not need to change its direction of movement when
3260
- * passing from the previous {@link HitObject} to this one.
3261
- */
3262
- this.relativeAngle = 0;
3263
- /**
3264
- * The jump distance from the previous {@link HitObject} to this one.
3265
- *
3266
- * The `distanceFromPrevious` of the first {@link HitObject} in a beatmap is relative to the center of
3267
- * the playfield.
3268
- */
3269
- this.distanceFromPrevious = 0;
3270
- /**
3271
- * The rotation of this {@link HitObject} relative to its jump angle.
3272
- *
3273
- * For `Slider`s, this is defined as the angle from the `Slider`'s start position to the end of its path
3274
- * relative to its jump angle. For `HitCircle`s and `Spinner`s, this property is ignored.
3275
- */
3276
- this.rotation = 0;
3277
- this.hitObject = hitObject;
3648
+ static reflectVectorHorizontallyAlongPlayfield(vector) {
3649
+ return new Vector2(Playfield.baseSize.x - vector.x, vector.y);
3278
3650
  }
3279
- }
3280
-
3281
- /**
3282
- * Precision utilities.
3283
- */
3284
- class Precision {
3285
- /**
3286
- * Checks if two numbers are equal with a given tolerance.
3287
- *
3288
- * @param value1 The first number.
3289
- * @param value2 The second number.
3290
- * @param acceptableDifference The acceptable difference as threshold. Default is `Precision.FLOAT_EPSILON = 1e-3`.
3291
- */
3292
- static almostEqualsNumber(value1, value2, acceptableDifference = this.FLOAT_EPSILON) {
3293
- return Math.abs(value1 - value2) <= acceptableDifference;
3651
+ static reflectVectorVerticallyAlongPlayfield(vector) {
3652
+ return new Vector2(vector.x, Playfield.baseSize.y - vector.y);
3294
3653
  }
3654
+ //#endregion
3655
+ //#region Reposition
3295
3656
  /**
3296
- * Checks if two vectors are equal with a given tolerance.
3657
+ * Generates a list of {@link HitObjectPositionInfo}s containing information for how the given list of
3658
+ * {@link HitObject}s are positioned.
3297
3659
  *
3298
- * @param vec1 The first vector.
3299
- * @param vec2 The second vector.
3300
- * @param acceptableDifference The acceptable difference as threshold. Default is `Precision.FLOAT_EPSILON = 1e-3`.
3660
+ * @param hitObjects A list of {@link HitObject}s to process.
3661
+ * @return A list of {@link HitObjectPositionInfo}s describing how each {@link HitObject} is positioned
3662
+ * relative to the previous one.
3301
3663
  */
3302
- static almostEqualsVector(vec1, vec2, acceptableDifference = this.FLOAT_EPSILON) {
3303
- return (this.almostEqualsNumber(vec1.x, vec2.x, acceptableDifference) &&
3304
- this.almostEqualsNumber(vec1.y, vec2.y, acceptableDifference));
3664
+ static generatePositionInfos(hitObjects) {
3665
+ const positionInfos = [];
3666
+ let previousPosition = Playfield.center;
3667
+ let previousAngle = 0;
3668
+ for (const hitObject of hitObjects) {
3669
+ const relativePosition = hitObject.position.subtract(previousPosition);
3670
+ let absoluteAngle = Math.atan2(relativePosition.y, relativePosition.x);
3671
+ const relativeAngle = absoluteAngle - previousAngle;
3672
+ const positionInfo = new HitObjectPositionInfo(hitObject);
3673
+ positionInfo.relativeAngle = relativeAngle;
3674
+ positionInfo.distanceFromPrevious = relativePosition.length;
3675
+ if (hitObject instanceof Slider) {
3676
+ const absoluteRotation = this.getSliderRotation(hitObject);
3677
+ positionInfo.rotation = absoluteRotation - absoluteAngle;
3678
+ absoluteAngle = absoluteRotation;
3679
+ }
3680
+ previousPosition = hitObject.endPosition;
3681
+ previousAngle = absoluteAngle;
3682
+ positionInfos.push(positionInfo);
3683
+ }
3684
+ return positionInfos;
3305
3685
  }
3306
- /**
3307
- * Checks whether two real numbers are almost equal.
3308
- *
3309
- * @param a The first number.
3310
- * @param b The second number.
3311
- * @param maximumError The accuracy required for being almost equal. Defaults to `10 * 2^(-53)`.
3312
- * @returns Whether the two values differ by no more than 10 * 2^(-52).
3313
- */
3314
- static almostEqualRelative(a, b, maximumError = 10 * Math.pow(2, -53)) {
3315
- return this.almostEqualNormRelative(a, b, a - b, maximumError);
3686
+ static repositionHitObjects(positionInfos) {
3687
+ const workingObjects = positionInfos.map((p) => new WorkingObject(p));
3688
+ let previous = null;
3689
+ for (let i = 0; i < workingObjects.length; ++i) {
3690
+ const current = workingObjects[i];
3691
+ const { hitObject } = current;
3692
+ if (hitObject instanceof Spinner) {
3693
+ previous = current;
3694
+ continue;
3695
+ }
3696
+ this.computeModifiedPosition(current, previous, i > 1 ? workingObjects[i - 2] : null);
3697
+ // Move hit objects back into the playfield if they are outside of it.
3698
+ let shift;
3699
+ if (hitObject instanceof Circle) {
3700
+ shift = this.clampHitCircleToPlayfield(current);
3701
+ }
3702
+ else if (hitObject instanceof Slider) {
3703
+ shift = this.clampSliderToPlayfield(current);
3704
+ }
3705
+ else {
3706
+ shift = new Vector2(0);
3707
+ }
3708
+ if (shift.x !== 0 || shift.y !== 0) {
3709
+ const toBeShifted = [];
3710
+ for (let j = i - 1; j >= Math.max(0, i - this.precedingObjectsToShift); --j) {
3711
+ // Only shift hit circles.
3712
+ if (!(workingObjects[j].hitObject instanceof Circle)) {
3713
+ break;
3714
+ }
3715
+ toBeShifted.push(workingObjects[j].hitObject);
3716
+ }
3717
+ this.applyDecreasingShift(toBeShifted, shift);
3718
+ }
3719
+ previous = current;
3720
+ }
3721
+ return workingObjects.map((w) => w.hitObject);
3316
3722
  }
3317
3723
  /**
3318
- * Compares two numbers and determines if they are equal within the specified maximum error.
3724
+ * Determines whether a {@link HitObject} is on a beat.
3319
3725
  *
3320
- * @param a The norm of the first value (can be negative).
3321
- * @param b The norm of the second value (can be negative).
3322
- * @param diff The norm of the difference of the two values (can be negative).
3323
- * @param maximumError The accuracy required for being almost equal.
3324
- * @returns Whether both numbers are almost equal up to the specified maximum error.
3726
+ * @param beatmap The {@link Beatmap} the {@link HitObject} is a part of.
3727
+ * @param hitObject The {@link HitObject} to check.
3728
+ * @param downbeatsOnly If `true`, whether this method only returns `true` is on a downbeat.
3729
+ * @return `true` if the {@link HitObject} is on a (down-)beat, `false` otherwise.
3325
3730
  */
3326
- static almostEqualNormRelative(a, b, diff, maximumError) {
3327
- // If A or B are infinity (positive or negative) then
3328
- // only return true if they are exactly equal to each other -
3329
- // that is, if they are both infinities of the same sign.
3330
- if (!Number.isFinite(a) || !Number.isFinite(b)) {
3331
- return a === b;
3332
- }
3333
- // If A or B are a NAN, return false. NANs are equal to nothing,
3334
- // not even themselves.
3335
- if (Number.isNaN(a) || Number.isNaN(b)) {
3336
- return false;
3337
- }
3338
- // If one is almost zero, fall back to absolute equality.
3339
- const doublePrecision = Math.pow(2, -53);
3340
- if (Math.abs(a) < doublePrecision || Math.abs(b) < doublePrecision) {
3341
- return Math.abs(diff) < maximumError;
3342
- }
3343
- if ((a === 0 && Math.abs(b) < maximumError) ||
3344
- (b === 0 && Math.abs(a) < maximumError)) {
3345
- return true;
3731
+ static isHitObjectOnBeat(beatmap, hitObject, downbeatsOnly = false) {
3732
+ const timingPoint = beatmap.controlPoints.timing.controlPointAt(hitObject.startTime);
3733
+ const timeSinceTimingPoint = hitObject.startTime - timingPoint.time;
3734
+ let beatLength = timingPoint.msPerBeat;
3735
+ if (downbeatsOnly) {
3736
+ beatLength *= timingPoint.timeSignature;
3346
3737
  }
3347
- return (Math.abs(diff) < maximumError * Math.max(Math.abs(a), Math.abs(b)));
3738
+ // Ensure within 1ms of expected location.
3739
+ return Math.abs(timeSinceTimingPoint + 1) % beatLength < 2;
3348
3740
  }
3349
- }
3350
- Precision.FLOAT_EPSILON = 1e-3;
3351
-
3352
- /**
3353
- * Types of slider paths.
3354
- */
3355
- exports.PathType = void 0;
3356
- (function (PathType) {
3357
- PathType["Catmull"] = "C";
3358
- PathType["Bezier"] = "B";
3359
- PathType["Linear"] = "L";
3360
- PathType["PerfectCurve"] = "P";
3361
- })(exports.PathType || (exports.PathType = {}));
3362
-
3363
- /**
3364
- * Path approximator for sliders.
3365
- */
3366
- class PathApproximator {
3367
3741
  /**
3368
- * Approximates a bezier slider's path.
3369
- *
3370
- * Creates a piecewise-linear approximation of a bezier curve by adaptively repeatedly subdividing
3371
- * the control points until their approximation error vanishes below a given threshold.
3742
+ * Generates a random number from a Normal distribution using the Box-Muller transform.
3372
3743
  *
3373
- * @param controlPoints The anchor points of the slider.
3744
+ * @param random A {@link Random} to get the random number from.
3745
+ * @param mean The mean of the distribution.
3746
+ * @param stdDev The standard deviation of the distribution.
3747
+ * @return The random number.
3374
3748
  */
3375
- static approximateBezier(controlPoints) {
3376
- const output = [];
3377
- const count = controlPoints.length - 1;
3378
- if (count < 0) {
3379
- return output;
3380
- }
3381
- const subdivisionBuffer1 = new Array(count + 1);
3382
- const subdivisionBuffer2 = new Array(count * 2 + 1);
3383
- // "toFlatten" contains all the curves which are not yet approximated well enough.
3384
- // We use a stack to emulate recursion without the risk of running into a stack overflow.
3385
- // (More specifically, we iteratively and adaptively refine our curve with a
3386
- // depth-first search (https://en.wikipedia.org/wiki/Depth-first_search)
3387
- // over the tree resulting from the subdivisions we make.)
3388
- const toFlatten = [controlPoints.slice()];
3389
- const freeBuffers = [];
3390
- const leftChild = subdivisionBuffer2;
3391
- while (toFlatten.length > 0) {
3392
- const parent = toFlatten.pop();
3393
- if (this.bezierIsFlatEnough(parent)) {
3394
- // If the control points we currently operate on are sufficiently "flat", we use
3395
- // an extension to De Casteljau's algorithm to obtain a piecewise-linear approximation
3396
- // of the bezier curve represented by our control points, consisting of the same amount
3397
- // of points as there are control points.
3398
- this.bezierApproximate(parent, output, subdivisionBuffer1, subdivisionBuffer2, count + 1);
3399
- freeBuffers.push(parent);
3400
- continue;
3401
- }
3402
- // If we do not yet have a sufficiently "flat" (in other words, detailed) approximation we keep
3403
- // subdividing the curve we are currently operating on.
3404
- const rightChild = freeBuffers.length > 0
3405
- ? freeBuffers.pop()
3406
- : new Array(count + 1);
3407
- this.bezierSubdivide(parent, leftChild, rightChild, subdivisionBuffer1, count + 1);
3408
- // We re-use the buffer of the parent for one of the children, so that we save one allocation per iteration.
3409
- for (let i = 0; i < count + 1; ++i) {
3410
- parent[i] = leftChild[i];
3411
- }
3412
- toFlatten.push(rightChild);
3413
- toFlatten.push(parent);
3414
- }
3415
- output.push(controlPoints[count]);
3416
- return output;
3749
+ static randomGaussian(random, mean = 0, stdDev = 1) {
3750
+ // Generate 2 random numbers in the interval (0,1].
3751
+ // x1 must not be 0 since log(0) = undefined.
3752
+ const x1 = 1 - random.nextDouble();
3753
+ const x2 = 1 - random.nextDouble();
3754
+ const stdNormal = Math.sqrt(-2 * Math.log(x1)) * Math.sin(2 * Math.PI * x2);
3755
+ return mean + stdDev * stdNormal;
3417
3756
  }
3418
3757
  /**
3419
- * Approximates a catmull slider's path.
3758
+ * Calculates a {@link Vector4} which contains all possible movements of a {@link Slider} (in relative
3759
+ * X/Y coordinates) such that the entire {@link Slider} is inside the playfield.
3420
3760
  *
3421
- * Creates a piecewise-linear approximation of a Catmull-Rom spline.
3761
+ * If the {@link Slider} is larger than the playfield, the returned {@link Vector4} may have a Z/W component
3762
+ * that is smaller than its X/Y component.
3422
3763
  *
3423
- * @param controlPoints The anchor points of the slider.
3764
+ * @param slider The {@link Slider} whose movement bound is to be calculated.
3765
+ * @return A {@link Vector4} which contains all possible movements of a {@link Slider} (in relative X/Y
3766
+ * coordinates) such that the entire {@link Slider} is inside the playfield.
3424
3767
  */
3425
- static approximateCatmull(controlPoints) {
3426
- const result = [];
3427
- for (let i = 0; i < controlPoints.length - 1; ++i) {
3428
- const v1 = i > 0 ? controlPoints[i - 1] : controlPoints[i];
3429
- const v2 = controlPoints[i];
3430
- const v3 = controlPoints[i + 1];
3431
- const v4 = i < controlPoints.length - 2
3432
- ? controlPoints[i + 2]
3433
- : v3.add(v3).subtract(v2);
3434
- for (let c = 0; c < this.catmullDetail; ++c) {
3435
- result.push(this.catmullFindPoint(v1, v2, v3, v4, c / this.catmullDetail));
3436
- result.push(this.catmullFindPoint(v1, v2, v3, v4, (c + 1) / this.catmullDetail));
3437
- }
3768
+ static calculatePossibleMovementBounds(slider) {
3769
+ const pathPositions = slider.path.pathToProgress(0, 1);
3770
+ let minX = Number.POSITIVE_INFINITY;
3771
+ let maxX = Number.NEGATIVE_INFINITY;
3772
+ let minY = Number.POSITIVE_INFINITY;
3773
+ let maxY = Number.NEGATIVE_INFINITY;
3774
+ // Compute the bounding box of the slider.
3775
+ for (const position of pathPositions) {
3776
+ minX = Math.min(minX, position.x);
3777
+ maxX = Math.max(maxX, position.x);
3778
+ minY = Math.min(minY, position.y);
3779
+ maxY = Math.max(maxY, position.y);
3438
3780
  }
3439
- return result;
3781
+ // Take the radius into account.
3782
+ const { radius } = slider;
3783
+ minX -= radius;
3784
+ minY -= radius;
3785
+ maxX += radius;
3786
+ maxY += radius;
3787
+ // Given the bounding box of the slider (via min/max X/Y), the amount that the slider can move to the left is
3788
+ // minX (with the sign flipped, since positive X is to the right), and the amount that it can move to the right
3789
+ // is WIDTH - maxX. The same calculation applies for the Y axis.
3790
+ const left = -minX;
3791
+ const right = Playfield.baseSize.x - maxX;
3792
+ const top = -minY;
3793
+ const bottom = Playfield.baseSize.y - maxY;
3794
+ return new Vector4(left, top, right, bottom);
3440
3795
  }
3441
3796
  /**
3442
- * Approximates a slider's circular arc.
3443
- *
3444
- * Creates a piecewise-linear approximation of a circular arc curve.
3797
+ * Computes the modified position of a {@link HitObject} while attempting to keep it inside the playfield.
3445
3798
  *
3446
- * @param controlPoints The anchor points of the slider.
3799
+ * @param current The {@link WorkingObject} representing the {@link HitObject} to have the modified
3800
+ * position computed for.
3801
+ * @param previous The {@link WorkingObject} representing the {@link HitObject} immediately preceding
3802
+ * `current`.
3803
+ * @param beforePrevious The {@link WorkingObject} representing the {@link HitObject} immediately preceding
3804
+ * `previous`.
3447
3805
  */
3448
- static approximateCircularArc(controlPoints) {
3449
- const a = controlPoints[0];
3450
- const b = controlPoints[1];
3451
- const c = controlPoints[2];
3452
- // If we have a degenerate triangle where a side-length is almost zero, then give up and fall
3453
- // back to a more numerically stable method.
3454
- if (Precision.almostEqualsNumber(0, (b.y - a.y) * (c.x - a.x) - (b.x - a.x) * (c.y - a.y))) {
3455
- return this.approximateBezier(controlPoints);
3456
- }
3457
- // See: https://en.wikipedia.org/wiki/Circumscribed_circle#Cartesian_coordinates_2
3458
- const d = 2 *
3459
- (a.x * b.subtract(c).y +
3460
- b.x * c.subtract(a).y +
3461
- c.x * a.subtract(b).y);
3462
- const aSq = Math.pow(a.length, 2);
3463
- const bSq = Math.pow(b.length, 2);
3464
- const cSq = Math.pow(c.length, 2);
3465
- const center = new Vector2(aSq * b.subtract(c).y +
3466
- bSq * c.subtract(a).y +
3467
- cSq * a.subtract(b).y, aSq * c.subtract(b).x +
3468
- bSq * a.subtract(c).x +
3469
- cSq * b.subtract(a).x).divide(d);
3470
- const dA = a.subtract(center);
3471
- const dC = c.subtract(center);
3472
- const r = dA.length;
3473
- const thetaStart = Math.atan2(dA.y, dA.x);
3474
- let thetaEnd = Math.atan2(dC.y, dC.x);
3475
- while (thetaEnd < thetaStart) {
3476
- thetaEnd += 2 * Math.PI;
3806
+ static computeModifiedPosition(current, previous, beforePrevious) {
3807
+ var _b, _c;
3808
+ let previousAbsoluteAngle = 0;
3809
+ if (previous !== null) {
3810
+ if (previous.hitObject instanceof Slider) {
3811
+ previousAbsoluteAngle = this.getSliderRotation(previous.hitObject);
3812
+ }
3813
+ else {
3814
+ const earliestPosition = (_b = beforePrevious === null || beforePrevious === void 0 ? void 0 : beforePrevious.hitObject.endPosition) !== null && _b !== void 0 ? _b : Playfield.center;
3815
+ const relativePosition = previous.hitObject.position.subtract(earliestPosition);
3816
+ previousAbsoluteAngle = Math.atan2(relativePosition.y, relativePosition.x);
3817
+ }
3477
3818
  }
3478
- let dir = 1;
3479
- let thetaRange = thetaEnd - thetaStart;
3480
- // Decide in which direction to draw the circle, depending on which side of
3481
- // AC B lies.
3482
- let orthoAtoC = c.subtract(a);
3483
- orthoAtoC = new Vector2(orthoAtoC.y, -orthoAtoC.x);
3484
- if (orthoAtoC.dot(b.subtract(a)) < 0) {
3485
- dir = -dir;
3486
- thetaRange = 2 * Math.PI - thetaRange;
3819
+ let absoluteAngle = previousAbsoluteAngle + current.positionInfo.relativeAngle;
3820
+ let positionRelativeToPrevious = new Vector2(current.positionInfo.distanceFromPrevious * Math.cos(absoluteAngle), current.positionInfo.distanceFromPrevious * Math.sin(absoluteAngle));
3821
+ const lastEndPosition = (_c = previous === null || previous === void 0 ? void 0 : previous.endPositionModified) !== null && _c !== void 0 ? _c : Playfield.center;
3822
+ positionRelativeToPrevious = this.rotateAwayFromEdge(lastEndPosition, positionRelativeToPrevious);
3823
+ current.positionModified = lastEndPosition.add(positionRelativeToPrevious);
3824
+ if (!(current.hitObject instanceof Slider)) {
3825
+ return;
3487
3826
  }
3488
- // We select the amount of points for the approximation by requiring the discrete curvature
3489
- // to be smaller than the provided tolerance. The exact angle required to meet the tolerance
3490
- // is: 2 * Math.Acos(1 - TOLERANCE / r)
3491
- // The special case is required for extremely short sliders where the radius is smaller than
3492
- // the tolerance. This is a pathological rather than a realistic case.
3493
- const amountPoints = 2 * r <= this.circularArcTolerance
3494
- ? 2
3495
- : Math.max(2, Math.ceil(thetaRange /
3496
- (2 *
3497
- Math.acos(1 - this.circularArcTolerance / r))));
3498
- const output = [];
3499
- for (let i = 0; i < amountPoints; ++i) {
3500
- const fract = i / (amountPoints - 1);
3501
- const theta = thetaStart + dir * fract * thetaRange;
3502
- const o = new Vector2(Math.cos(theta), Math.sin(theta)).scale(r);
3503
- output.push(center.add(o));
3827
+ absoluteAngle = Math.atan2(positionRelativeToPrevious.y, positionRelativeToPrevious.x);
3828
+ const centerOfMassOriginal = this.calculateCenterOfMass(current.hitObject);
3829
+ const centerOfMassModified = this.rotateAwayFromEdge(current.positionModified, this.rotateVector(centerOfMassOriginal, current.positionInfo.rotation +
3830
+ absoluteAngle -
3831
+ this.getSliderRotation(current.hitObject)));
3832
+ const relativeRotation = Math.atan2(centerOfMassModified.y, centerOfMassModified.x) -
3833
+ Math.atan2(centerOfMassOriginal.y, centerOfMassOriginal.x);
3834
+ if (!Precision.almostEqualsNumber(relativeRotation, 0)) {
3835
+ this.rotateSlider(current.hitObject, relativeRotation);
3504
3836
  }
3505
- return output;
3506
3837
  }
3507
3838
  /**
3508
- * Approximates a linear slider's path.
3839
+ * Moves the modified position of a {@link Circle} so that it fits inside the playfield.
3509
3840
  *
3510
- * Creates a piecewise-linear approximation of a linear curve.
3511
- * Basically, returns the input.
3841
+ * @param workingObject The {@link WorkingObject} that represents the {@link Circle}.
3842
+ * @return The deviation from the original modified position in order to fit within the playfield.
3843
+ */
3844
+ static clampHitCircleToPlayfield(workingObject) {
3845
+ const previousPosition = workingObject.positionModified;
3846
+ workingObject.positionModified = this.clampToPlayfield(workingObject.positionModified, workingObject.hitObject.radius);
3847
+ workingObject.endPositionModified = workingObject.positionModified;
3848
+ workingObject.hitObject.position = workingObject.positionModified;
3849
+ return workingObject.positionModified.subtract(previousPosition);
3850
+ }
3851
+ /**
3852
+ * Moves a {@link Slider} and all necessary `SliderHitObject`s into the playfield if they are not in
3853
+ * the playfield.
3512
3854
  *
3513
- * @param controlPoints The anchor points of the slider.
3855
+ * @param workingObject The {@link WorkingObject} that represents the {@link Slider}.
3856
+ * @return The deviation from the original modified position in order to fit within the playfield.
3514
3857
  */
3515
- static approximateLinear(controlPoints) {
3516
- return controlPoints;
3858
+ static clampSliderToPlayfield(workingObject) {
3859
+ const slider = workingObject.hitObject;
3860
+ let possibleMovementBounds = this.calculatePossibleMovementBounds(slider);
3861
+ // The slider rotation applied in computeModifiedPosition might make it impossible to fit the slider
3862
+ // into the playfield. For example, a long horizontal slider will be off-screen when rotated by 90
3863
+ // degrees. In this case, limit the rotation to either 0 or 180 degrees.
3864
+ if (possibleMovementBounds.width < 0 ||
3865
+ possibleMovementBounds.height < 0) {
3866
+ const currentRotation = this.getSliderRotation(slider);
3867
+ const diff1 = this.getAngleDifference(workingObject.rotationOriginal, currentRotation);
3868
+ const diff2 = this.getAngleDifference(workingObject.rotationOriginal + Math.PI, currentRotation);
3869
+ if (diff1 < diff2) {
3870
+ this.rotateSlider(slider, workingObject.rotationOriginal - currentRotation);
3871
+ }
3872
+ else {
3873
+ this.rotateSlider(slider, workingObject.rotationOriginal + Math.PI - currentRotation);
3874
+ }
3875
+ possibleMovementBounds =
3876
+ this.calculatePossibleMovementBounds(slider);
3877
+ }
3878
+ const previousPosition = workingObject.positionModified;
3879
+ // Clamp slider position to the placement area.
3880
+ // If the slider is larger than the playfield, at least make sure that the head circle is
3881
+ // inside the playfield.
3882
+ const newX = possibleMovementBounds.width < 0
3883
+ ? MathUtils.clamp(possibleMovementBounds.left, 0, Playfield.baseSize.x)
3884
+ : MathUtils.clamp(previousPosition.x, possibleMovementBounds.left, possibleMovementBounds.right);
3885
+ const newY = possibleMovementBounds.height < 0
3886
+ ? MathUtils.clamp(possibleMovementBounds.top, 0, Playfield.baseSize.y)
3887
+ : MathUtils.clamp(previousPosition.y, possibleMovementBounds.top, possibleMovementBounds.bottom);
3888
+ workingObject.positionModified = new Vector2(newX, newY);
3889
+ slider.position = workingObject.positionModified;
3890
+ workingObject.endPositionModified = slider.endPosition;
3891
+ return workingObject.positionModified.subtract(previousPosition);
3517
3892
  }
3518
3893
  /**
3519
- * Checks if a bezier slider is flat enough to be approximated.
3894
+ * Clamps a {@link Vector2} into the playfield, keeping a specified distance from the edge of the playfield.
3520
3895
  *
3521
- * Make sure the 2nd order derivative (approximated using finite elements) is within tolerable bounds.
3896
+ * @param vec The {@link Vector2} to clamp.
3897
+ * @param padding The minimum distance allowed from the edge of the playfield.
3898
+ * @return The clamped {@link Vector2}.
3899
+ */
3900
+ static clampToPlayfield(vec, padding) {
3901
+ return new Vector2(MathUtils.clamp(vec.x, padding, Playfield.baseSize.x - padding), MathUtils.clamp(vec.y, padding, Playfield.baseSize.y - padding));
3902
+ }
3903
+ /**
3904
+ * Decreasingly shifts a list of {@link HitObject}s by a specified amount.
3522
3905
  *
3523
- * NOTE: The 2nd order derivative of a 2D curve represents its curvature, so intuitively this function
3524
- * checks (as the name suggests) whether our approximation is _locally_ "flat". More curvy parts
3525
- * need to have a denser approximation to be more "flat".
3906
+ * The first item in the list is shifted by the largest amount, while the last item is shifted by the
3907
+ * smallest amount.
3526
3908
  *
3527
- * @param controlPoints The anchor points of the slider.
3909
+ * @param hitObjects The list of {@link HitObject}s to be shifted.
3910
+ * @param shift The amount to shift the {@link HitObject}s by.
3528
3911
  */
3529
- static bezierIsFlatEnough(controlPoints) {
3530
- for (let i = 1; i < controlPoints.length - 1; ++i) {
3531
- const prev = controlPoints[i - 1];
3532
- const current = controlPoints[i];
3533
- const next = controlPoints[i + 1];
3534
- const final = prev.subtract(current.scale(2)).add(next);
3535
- if (Math.pow(final.length, 2) >
3536
- Math.pow(this.bezierTolerance, 2) * 4) {
3537
- return false;
3538
- }
3912
+ static applyDecreasingShift(hitObjects, shift) {
3913
+ for (let i = 0; i < hitObjects.length; ++i) {
3914
+ const hitObject = hitObjects[i];
3915
+ // The first object is shifted by a vector slightly smaller than shift.
3916
+ // The last object is shifted by a vector slightly larger than zero.
3917
+ const position = hitObject.position.add(shift.scale((hitObjects.length - i) / (hitObjects.length + 1)));
3918
+ hitObject.position = this.clampToPlayfield(position, hitObject.radius);
3539
3919
  }
3540
- return true;
3541
3920
  }
3542
3921
  /**
3543
- * Approximates a bezier slider's path.
3544
- *
3545
- * This uses {@link https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm De Casteljau's algorithm} to obtain an optimal
3546
- * piecewise-linear approximation of the bezier curve with the same amount of points as there are control points.
3922
+ * Estimates the center of mass of a {@link Slider} relative to its start position.
3547
3923
  *
3548
- * @param controlPoints The control points describing the bezier curve to be approximated.
3549
- * @param output The points representing the resulting piecewise-linear approximation.
3550
- * @param subdivisionBuffer1 The first buffer containing the current subdivision state.
3551
- * @param subdivisionBuffer2 The second buffer containing the current subdivision state.
3552
- * @param count The number of control points in the original array.
3924
+ * @param slider The {@link Slider} whose center mass is to be estimated.
3925
+ * @return The estimated center of mass of `slider`.
3553
3926
  */
3554
- static bezierApproximate(controlPoints, output, subdivisionBuffer1, subdivisionBuffer2, count) {
3555
- const l = subdivisionBuffer2;
3556
- const r = subdivisionBuffer1;
3557
- this.bezierSubdivide(controlPoints, l, r, subdivisionBuffer1, count);
3558
- for (let i = 0; i < count - 1; ++i) {
3559
- l[count + i] = r[i + 1];
3927
+ static calculateCenterOfMass(slider) {
3928
+ const sampleStep = 50;
3929
+ // Only sample the start and end positions if the slider is too short.
3930
+ if (slider.distance <= sampleStep) {
3931
+ return slider.path.positionAt(1).divide(2);
3560
3932
  }
3561
- output.push(controlPoints[0]);
3562
- for (let i = 1; i < count - 1; ++i) {
3563
- const index = 2 * i;
3564
- const p = l[index - 1]
3565
- .add(l[index].scale(2))
3566
- .add(l[index + 1])
3567
- .scale(0.25);
3568
- output.push(p);
3933
+ let count = 0;
3934
+ let sum = new Vector2(0);
3935
+ for (let i = 0; i < slider.distance; i += sampleStep) {
3936
+ sum = sum.add(slider.path.positionAt(i / slider.distance));
3937
+ ++count;
3569
3938
  }
3939
+ return sum.divide(count);
3570
3940
  }
3571
3941
  /**
3572
- * Subdivides `n` control points representing a bezier curve into 2 sets of `n` control points, each
3573
- * describing a bezier curve equivalent to a half of the original curve. Effectively this splits
3574
- * the original curve into 2 curves which result in the original curve when pieced back together.
3942
+ * Calculates the absolute difference between two angles in radians.
3575
3943
  *
3576
- * @param controlPoints The anchor points of the slider.
3577
- * @param l Parts of the slider for approximation.
3578
- * @param r Parts of the slider for approximation.
3579
- * @param subdivisionBuffer Parts of the slider for approximation.
3580
- * @param count The amount of anchor points in the slider.
3944
+ * @param angle1 The first angle.
3945
+ * @param angle2 The second angle.
3946
+ * @return THe absolute difference within interval `[0, Math.PI]`.
3581
3947
  */
3582
- static bezierSubdivide(controlPoints, l, r, subdivisionBuffer, count) {
3583
- const midpoints = subdivisionBuffer;
3584
- for (let i = 0; i < count; ++i) {
3585
- midpoints[i] = controlPoints[i];
3948
+ static getAngleDifference(angle1, angle2) {
3949
+ const diff = Math.abs(angle1 - angle2) % (2 * Math.PI);
3950
+ return Math.min(diff, 2 * Math.PI - diff);
3951
+ }
3952
+ }
3953
+ _a = HitObjectGenerationUtils;
3954
+ /**
3955
+ * The relative distance to the edge of the playfield before {@link HitObject} positions should start
3956
+ * to "turn around" and curve towards the middle. The closer the {@link HitObject}s draw to the border,
3957
+ * the sharper the turn.
3958
+ */
3959
+ HitObjectGenerationUtils.playfieldEdgeRatio = 0.375;
3960
+ /**
3961
+ * The amount of previous {@link HitObject}s to be shifted together when a {@link HitObject} is being moved.
3962
+ */
3963
+ HitObjectGenerationUtils.precedingObjectsToShift = 10;
3964
+ HitObjectGenerationUtils.borderDistance = Playfield.baseSize.scale(_a.playfieldEdgeRatio);
3965
+ class WorkingObject {
3966
+ get hitObject() {
3967
+ return this.positionInfo.hitObject;
3968
+ }
3969
+ constructor(positionInfo) {
3970
+ this.rotationOriginal = this.hitObject instanceof Slider
3971
+ ? HitObjectGenerationUtils.getSliderRotation(this.hitObject)
3972
+ : 0;
3973
+ this.positionModified = this.hitObject.position;
3974
+ this.endPositionModified = this.hitObject.endPosition;
3975
+ this.positionInfo = positionInfo;
3976
+ }
3977
+ }
3978
+
3979
+ /**
3980
+ * Represents the Mirror mod.
3981
+ */
3982
+ class ModMirror extends Mod {
3983
+ constructor() {
3984
+ super();
3985
+ this.name = "Mirror";
3986
+ this.acronym = "MR";
3987
+ this.droidRanked = false;
3988
+ this.osuRanked = false;
3989
+ /**
3990
+ * The axes to reflect the `HitObject`s along.
3991
+ */
3992
+ this.flippedAxes = new ModSetting("Flipped axes", "The axes to reflect the hit objects along.", exports.Axes.x);
3993
+ this.incompatibleMods.add(ModHardRock);
3994
+ }
3995
+ get isDroidRelevant() {
3996
+ return true;
3997
+ }
3998
+ calculateDroidScoreMultiplier() {
3999
+ return 1;
4000
+ }
4001
+ get isOsuRelevant() {
4002
+ return true;
4003
+ }
4004
+ get osuScoreMultiplier() {
4005
+ return 1;
4006
+ }
4007
+ copySettings(mod) {
4008
+ var _a;
4009
+ super.copySettings(mod);
4010
+ switch ((_a = mod.settings) === null || _a === void 0 ? void 0 : _a.flippedAxes) {
4011
+ case 0:
4012
+ this.flippedAxes.value = exports.Axes.x;
4013
+ break;
4014
+ case 1:
4015
+ this.flippedAxes.value = exports.Axes.y;
4016
+ break;
4017
+ case 2:
4018
+ this.flippedAxes.value = exports.Axes.both;
4019
+ break;
3586
4020
  }
3587
- for (let i = 0; i < count; ++i) {
3588
- l[i] = midpoints[0];
3589
- r[count - i - 1] = midpoints[count - i - 1];
3590
- for (let j = 0; j < count - i - 1; ++j) {
3591
- midpoints[j] = midpoints[j].add(midpoints[j + 1]).divide(2);
3592
- }
4021
+ }
4022
+ applyToHitObject(_, hitObject) {
4023
+ switch (this.flippedAxes.value) {
4024
+ case exports.Axes.x:
4025
+ HitObjectGenerationUtils.reflectHorizontallyAlongPlayfield(hitObject);
4026
+ break;
4027
+ case exports.Axes.y:
4028
+ HitObjectGenerationUtils.reflectVerticallyAlongPlayfield(hitObject);
4029
+ break;
4030
+ case exports.Axes.both:
4031
+ HitObjectGenerationUtils.reflectHorizontallyAlongPlayfield(hitObject);
4032
+ HitObjectGenerationUtils.reflectVerticallyAlongPlayfield(hitObject);
4033
+ break;
4034
+ }
4035
+ }
4036
+ serializeSettings() {
4037
+ return { flippedAxes: this.flippedAxes.value - 1 };
4038
+ }
4039
+ toString() {
4040
+ const settings = [];
4041
+ if (this.flippedAxes.value === exports.Axes.x ||
4042
+ this.flippedAxes.value === exports.Axes.both) {
4043
+ settings.push("↔");
3593
4044
  }
3594
- }
3595
- /**
3596
- * Finds a point on the spline at the position of a parameter.
3597
- *
3598
- * @param vec1 The first vector.
3599
- * @param vec2 The second vector.
3600
- * @param vec3 The third vector.
3601
- * @param vec4 The fourth vector.
3602
- * @param t The parameter at which to find the point on the spline, in the range [0, 1].
3603
- */
3604
- static catmullFindPoint(vec1, vec2, vec3, vec4, t) {
3605
- const t2 = Math.pow(t, 2);
3606
- const t3 = Math.pow(t, 3);
3607
- return new Vector2(0.5 *
3608
- (2 * vec2.x +
3609
- (-vec1.x + vec3.x) * t +
3610
- (2 * vec1.x - 5 * vec2.x + 4 * vec3.x - vec4.x) * t2 +
3611
- (-vec1.x + 3 * vec2.x - 3 * vec3.x + vec4.x) * t3), 0.5 *
3612
- (2 * vec2.y +
3613
- (-vec1.y + vec3.y) * t +
3614
- (2 * vec1.y - 5 * vec2.y + 4 * vec3.y - vec4.y) * t2 +
3615
- (-vec1.y + 3 * vec2.y - 3 * vec3.y + vec4.y) * t3));
4045
+ if (this.flippedAxes.value === exports.Axes.y ||
4046
+ this.flippedAxes.value === exports.Axes.both) {
4047
+ settings.push("↕");
4048
+ }
4049
+ return `${super.toString()} (${settings.join(", ")})`;
3616
4050
  }
3617
4051
  }
3618
- PathApproximator.bezierTolerance = 0.25;
3619
- /**
3620
- * The amount of pieces to calculate for each control point quadruplet.
3621
- */
3622
- PathApproximator.catmullDetail = 50;
3623
- PathApproximator.circularArcTolerance = 0.1;
3624
4052
 
3625
4053
  /**
3626
- * Represents a slider's path.
4054
+ * Represents the Replay V6 mod.
4055
+ *
4056
+ * Some behavior of beatmap parsing was changed in replay version 7. More specifically, object stacking
4057
+ * behavior now matches osu!stable and osu!lazer.
4058
+ *
4059
+ * This `Mod` is meant to reapply the stacking behavior prior to replay version 7 to a `Beatmap` that
4060
+ * was played in replays recorded in version 6 and older for replayability and difficulty calculation.
3627
4061
  */
3628
- class SliderPath {
3629
- constructor(values) {
3630
- /**
3631
- * Whether or not the instance has been initialized.
3632
- */
3633
- this.isInitialized = false;
3634
- /**
3635
- * The calculated path of the slider.
3636
- */
3637
- this.calculatedPath = [];
3638
- /**
3639
- * The cumulative length of the slider.
3640
- */
3641
- this.cumulativeLength = [];
3642
- this.pathType = values.pathType;
3643
- this.controlPoints = values.controlPoints;
3644
- this.expectedDistance = values.expectedDistance;
3645
- this.ensureInitialized();
4062
+ class ModReplayV6 extends Mod {
4063
+ constructor() {
4064
+ super(...arguments);
4065
+ this.name = "Replay V6";
4066
+ this.acronym = "RV6";
4067
+ this.userPlayable = false;
4068
+ this.droidRanked = false;
4069
+ this.facilitateAdjustment = true;
3646
4070
  }
3647
- /**
3648
- * Initializes the instance.
3649
- */
3650
- ensureInitialized() {
3651
- if (this.isInitialized) {
4071
+ get isDroidRelevant() {
4072
+ return true;
4073
+ }
4074
+ calculateDroidScoreMultiplier() {
4075
+ return 1;
4076
+ }
4077
+ applyToBeatmap(beatmap) {
4078
+ const { objects } = beatmap.hitObjects;
4079
+ if (objects.length === 0) {
3652
4080
  return;
3653
4081
  }
3654
- this.isInitialized = true;
3655
- this.calculatedPath.length = 0;
3656
- this.cumulativeLength.length = 0;
3657
- this.calculatePath();
3658
- this.calculateCumulativeLength();
3659
- }
3660
- /**
3661
- * Calculates the slider's path.
3662
- */
3663
- calculatePath() {
3664
- this.calculatedPath.length = 0;
3665
- let spanStart = 0;
3666
- for (let i = 0; i < this.controlPoints.length; i++) {
3667
- if (i === this.controlPoints.length - 1 ||
3668
- this.controlPoints[i].equals(this.controlPoints[i + 1])) {
3669
- const spanEnd = i + 1;
3670
- const cpSpan = this.controlPoints.slice(spanStart, spanEnd);
3671
- this.calculateSubPath(cpSpan).forEach((t) => {
3672
- if (this.calculatedPath.length === 0 ||
3673
- !this.calculatedPath.at(-1).equals(t)) {
3674
- this.calculatedPath.push(t);
3675
- }
3676
- });
3677
- spanStart = spanEnd;
4082
+ // Reset stacking
4083
+ objects.forEach((h) => {
4084
+ h.stackHeight = 0;
4085
+ });
4086
+ for (let i = 0; i < objects.length - 1; ++i) {
4087
+ const current = objects[i];
4088
+ const next = objects[i + 1];
4089
+ this.revertObjectScale(current, beatmap.difficulty);
4090
+ this.revertObjectScale(next, beatmap.difficulty);
4091
+ const convertedScale = CircleSizeCalculator.standardScaleToOldDroidScale(objects[0].scale);
4092
+ if (current instanceof Circle &&
4093
+ next.startTime - current.startTime <
4094
+ 2000 * beatmap.general.stackLeniency &&
4095
+ next.position.getDistance(current.position) <
4096
+ Math.sqrt(convertedScale)) {
4097
+ next.stackHeight = current.stackHeight + 1;
3678
4098
  }
3679
4099
  }
3680
4100
  }
3681
- /**
3682
- * Calculates the slider's subpath.
3683
- */
3684
- calculateSubPath(subControlPoints) {
3685
- switch (this.pathType) {
3686
- case exports.PathType.Linear:
3687
- return PathApproximator.approximateLinear(subControlPoints);
3688
- case exports.PathType.PerfectCurve: {
3689
- if (subControlPoints.length !== 3) {
3690
- break;
3691
- }
3692
- const subPath = PathApproximator.approximateCircularArc(subControlPoints);
3693
- // If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable Bézier approximation.
3694
- if (subPath.length === 0) {
3695
- break;
3696
- }
3697
- return subPath;
3698
- }
3699
- case exports.PathType.Catmull:
3700
- return PathApproximator.approximateCatmull(subControlPoints);
3701
- }
3702
- return PathApproximator.approximateBezier(subControlPoints);
4101
+ revertObjectScale(hitObject, difficulty) {
4102
+ const droidScale = CircleSizeCalculator.droidCSToOldDroidScale(difficulty.cs);
4103
+ const radius = CircleSizeCalculator.oldDroidScaleToStandardRadius(droidScale);
4104
+ const standardCS = CircleSizeCalculator.standardRadiusToStandardCS(radius, true);
4105
+ hitObject.scale = CircleSizeCalculator.standardCSToStandardScale(standardCS, true);
4106
+ hitObject.stackOffsetMultiplier = 4;
3703
4107
  }
3704
- /**
3705
- * Calculates the slider's cumulative length.
3706
- */
3707
- calculateCumulativeLength() {
3708
- let calculatedLength = 0;
3709
- this.cumulativeLength.length = 0;
3710
- this.cumulativeLength.push(0);
3711
- for (let i = 0; i < this.calculatedPath.length - 1; ++i) {
3712
- const diff = this.calculatedPath[i + 1].subtract(this.calculatedPath[i]);
3713
- calculatedLength += diff.length;
3714
- this.cumulativeLength.push(calculatedLength);
3715
- }
3716
- if (calculatedLength !== this.expectedDistance) {
3717
- // In osu-stable, if the last two path points of a slider are equal, extension is not performed.
3718
- if (this.calculatedPath.length >= 2 &&
3719
- this.calculatedPath
3720
- .at(-1)
3721
- .equals(this.calculatedPath.at(-2)) &&
3722
- this.expectedDistance > calculatedLength) {
3723
- this.cumulativeLength.push(calculatedLength);
3724
- return;
3725
- }
3726
- // The last length is always incorrect
3727
- this.cumulativeLength.pop();
3728
- let pathEndIndex = this.calculatedPath.length - 1;
3729
- if (calculatedLength > this.expectedDistance) {
3730
- // The path will be shortened further, in which case we should trim any more unnecessary lengths and their associated path segments
3731
- while (this.cumulativeLength.length > 0 &&
3732
- this.cumulativeLength.at(-1) >= this.expectedDistance) {
3733
- this.cumulativeLength.pop();
3734
- this.calculatedPath.splice(pathEndIndex--, 1);
3735
- }
3736
- }
3737
- if (pathEndIndex <= 0) {
3738
- // The expected distance is negative or zero
3739
- this.cumulativeLength.push(0);
3740
- return;
3741
- }
3742
- // The direction of the segment to shorten or lengthen
3743
- const dir = this.calculatedPath[pathEndIndex].subtract(this.calculatedPath[pathEndIndex - 1]);
3744
- dir.normalize();
3745
- this.calculatedPath[pathEndIndex] = this.calculatedPath[pathEndIndex - 1].add(dir.scale(this.expectedDistance - this.cumulativeLength.at(-1)));
3746
- this.cumulativeLength.push(this.expectedDistance);
3747
- }
4108
+ }
4109
+
4110
+ /**
4111
+ * Represents the HardRock mod.
4112
+ */
4113
+ class ModHardRock extends Mod {
4114
+ constructor() {
4115
+ super();
4116
+ this.acronym = "HR";
4117
+ this.name = "HardRock";
4118
+ this.droidRanked = true;
4119
+ this.osuRanked = true;
4120
+ this.bitwise = 1 << 4;
4121
+ this.incompatibleMods.add(ModEasy);
4122
+ this.incompatibleMods.add(ModMirror);
3748
4123
  }
3749
- /**
3750
- * Computes the position on the slider at a given progress that ranges from 0 (beginning of the path)
3751
- * to 1 (end of the path).
3752
- *
3753
- * @param progress Ranges from 0 (beginning of the path) to 1 (end of the path).
3754
- */
3755
- positionAt(progress) {
3756
- this.ensureInitialized();
3757
- const d = this.progressToDistance(progress);
3758
- return this.interpolateVerticles(this.indexOfDistance(d), d);
4124
+ get isDroidRelevant() {
4125
+ return true;
3759
4126
  }
3760
- /**
3761
- * Computes the slider path until a given progress that ranges from 0 (beginning of the slider) to
3762
- * 1 (end of the slider).
3763
- *
3764
- * @param p0 Start progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).
3765
- * @param p1 End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).
3766
- * @return The computed path between the two ranges.
3767
- */
3768
- pathToProgress(p0, p1) {
3769
- const path = [];
3770
- const d0 = this.progressToDistance(p0);
3771
- const d1 = this.progressToDistance(p1);
3772
- let i = 0;
3773
- while (i < this.calculatedPath.length &&
3774
- this.cumulativeLength[i] < d0) {
3775
- ++i;
3776
- }
3777
- path.push(this.interpolateVerticles(i, d0));
3778
- while (i < this.calculatedPath.length &&
3779
- this.cumulativeLength[i] <= d1) {
3780
- path.push(this.calculatedPath[i++]);
3781
- }
3782
- path.push(this.interpolateVerticles(i, d1));
3783
- return path;
4127
+ calculateDroidScoreMultiplier() {
4128
+ return 1.06;
3784
4129
  }
3785
- /**
3786
- * Returns the progress of reaching expected distance.
3787
- */
3788
- progressToDistance(progress) {
3789
- return Math.min(Math.max(progress, 0), 1) * this.expectedDistance;
4130
+ get isOsuRelevant() {
4131
+ return true;
4132
+ }
4133
+ get osuScoreMultiplier() {
4134
+ return 1.06;
3790
4135
  }
3791
- /**
3792
- * Interpolates verticles of the slider.
3793
- */
3794
- interpolateVerticles(i, d) {
3795
- if (this.calculatedPath.length === 0) {
3796
- return new Vector2(0);
3797
- }
3798
- if (i <= 0) {
3799
- return this.calculatedPath[0];
3800
- }
3801
- if (i >= this.calculatedPath.length) {
3802
- return this.calculatedPath.at(-1);
4136
+ applyToDifficulty(mode, difficulty, adjustmentMods) {
4137
+ if (mode === exports.Modes.osu || !adjustmentMods.has(ModReplayV6)) {
4138
+ difficulty.cs = this.applySetting(difficulty.cs, 1.3);
3803
4139
  }
3804
- const p0 = this.calculatedPath[i - 1];
3805
- const p1 = this.calculatedPath[i];
3806
- const d0 = this.cumulativeLength[i - 1];
3807
- const d1 = this.cumulativeLength[i];
3808
- // Avoid division by and almost-zero number in case two points are extremely close to each other.
3809
- if (Precision.almostEqualsNumber(d0, d1)) {
3810
- return p0;
4140
+ else {
4141
+ const scale = CircleSizeCalculator.droidCSToOldDroidScale(difficulty.cs);
4142
+ // The 0.125 scale that was added before replay version 7 was in screen pixels. We need it in osu! pixels.
4143
+ difficulty.cs = CircleSizeCalculator.oldDroidScaleToDroidCS(scale -
4144
+ CircleSizeCalculator.oldDroidScaleScreenPixelsToOsuPixels(0.125));
3811
4145
  }
3812
- const w = (d - d0) / (d1 - d0);
3813
- return p0.add(p1.subtract(p0).scale(w));
4146
+ difficulty.ar = this.applySetting(difficulty.ar);
4147
+ difficulty.od = this.applySetting(difficulty.od);
4148
+ difficulty.hp = this.applySetting(difficulty.hp);
3814
4149
  }
3815
- /**
3816
- * Binary searches the cumulative length array and returns the
3817
- * index at which `arr[index] >= d`.
3818
- *
3819
- * @param d The distance to search.
3820
- * @returns The index.
3821
- */
3822
- indexOfDistance(d) {
3823
- if (this.cumulativeLength.length === 0 ||
3824
- d < this.cumulativeLength[0]) {
3825
- return 0;
3826
- }
3827
- if (d >= this.cumulativeLength.at(-1)) {
3828
- return this.cumulativeLength.length;
3829
- }
3830
- let l = 0;
3831
- let r = this.cumulativeLength.length - 2;
3832
- while (l <= r) {
3833
- const pivot = l + ((r - l) >> 1);
3834
- if (this.cumulativeLength[pivot] < d) {
3835
- l = pivot + 1;
3836
- }
3837
- else if (this.cumulativeLength[pivot] > d) {
3838
- r = pivot - 1;
3839
- }
3840
- else {
3841
- return pivot;
3842
- }
4150
+ applyToHitObject(_, hitObject) {
4151
+ HitObjectGenerationUtils.reflectVerticallyAlongPlayfield(hitObject);
4152
+ }
4153
+ isCompatibleWith(other) {
4154
+ if (other instanceof ModDifficultyAdjust) {
4155
+ return (other.cs.value === null ||
4156
+ other.ar.value === null ||
4157
+ other.od.value === null ||
4158
+ other.hp.value === null);
3843
4159
  }
3844
- return l;
4160
+ return super.isCompatibleWith(other);
4161
+ }
4162
+ applySetting(value, ratio = 1.4) {
4163
+ return Math.min(value * ratio, 10);
3845
4164
  }
3846
4165
  }
3847
4166
 
3848
- var _a;
3849
4167
  /**
3850
- * Utilities for {@link HitObject} generation.
4168
+ * Represents the Easy mod.
3851
4169
  */
3852
- class HitObjectGenerationUtils {
3853
- //#region Rotation
3854
- /**
3855
- * Rotates a {@link HitObject} away from the edge of the playfield while keeping a constant distance from
3856
- * the previous {@link HitObject}.
3857
- *
3858
- * @param previousObjectPosition The position of the previous {@link HitObject}.
3859
- * @param positionRelativeToPrevious The position of the {@link HitObject} to be rotated relative to the
3860
- * previous {@link HitObject}.
3861
- * @param rotationRatio The extent of the rotation. 0 means the {@link HitObject} is never rotated, while 1
3862
- * means the {@link HitObject} will be fully rotated towards the center of the playfield when it is originally
3863
- * at the edge of the playfield.
3864
- * @return The new position of the {@link HitObject} relative to the previous {@link HitObject}.
3865
- */
3866
- static rotateAwayFromEdge(previousObjectPosition, positionRelativeToPrevious, rotationRatio = 0.5) {
3867
- const relativeRotationDistance = Math.max((previousObjectPosition.x < Playfield.center.x
3868
- ? this.borderDistance.x - previousObjectPosition.x
3869
- : previousObjectPosition.x -
3870
- (Playfield.baseSize.x - this.borderDistance.x)) /
3871
- this.borderDistance.x, (previousObjectPosition.y < Playfield.center.y
3872
- ? this.borderDistance.y - previousObjectPosition.y
3873
- : previousObjectPosition.y -
3874
- (Playfield.baseSize.y - this.borderDistance.y)) /
3875
- this.borderDistance.y, 0);
3876
- return this.rotateVectorTowardsVector(positionRelativeToPrevious, Playfield.center.subtract(previousObjectPosition), Math.min(1, relativeRotationDistance * rotationRatio));
4170
+ class ModEasy extends Mod {
4171
+ constructor() {
4172
+ super();
4173
+ this.acronym = "EZ";
4174
+ this.name = "Easy";
4175
+ this.droidRanked = true;
4176
+ this.osuRanked = true;
4177
+ this.bitwise = 1 << 1;
4178
+ this.incompatibleMods.add(ModHardRock);
3877
4179
  }
3878
- /**
3879
- * Rotates a {@link Vector2} towards another {@link Vector2}.
3880
- *
3881
- * @param initial The {@link Vector2} to be rotated.
3882
- * @param destination The {@link Vector2} that `initial` should be rotated towards.
3883
- * @param rotationRatio How much `initial` should be rotated. 0 means no rotation. 1 mean `initial` is fully
3884
- * rotated to equal `destination`.
3885
- * @return The rotated {@link Vector2}.
3886
- */
3887
- static rotateVectorTowardsVector(initial, destination, rotationRatio) {
3888
- const initialAngle = Math.atan2(initial.y, initial.x);
3889
- const destinationAngle = Math.atan2(destination.y, destination.x);
3890
- let diff = destinationAngle - initialAngle;
3891
- // Normalize angle
3892
- while (diff < -Math.PI) {
3893
- diff += 2 * Math.PI;
4180
+ get isDroidRelevant() {
4181
+ return true;
4182
+ }
4183
+ calculateDroidScoreMultiplier() {
4184
+ return 0.5;
4185
+ }
4186
+ get isOsuRelevant() {
4187
+ return true;
4188
+ }
4189
+ get osuScoreMultiplier() {
4190
+ return 0.5;
4191
+ }
4192
+ applyToDifficulty(mode, difficulty, adjustmentMods) {
4193
+ if (mode === exports.Modes.osu || !adjustmentMods.has(ModReplayV6)) {
4194
+ difficulty.cs /= 2;
3894
4195
  }
3895
- while (diff > Math.PI) {
3896
- diff -= 2 * Math.PI;
4196
+ else {
4197
+ const scale = CircleSizeCalculator.droidCSToOldDroidScale(difficulty.cs);
4198
+ // The 0.125 scale that was added before replay version 7 was in screen pixels. We need it in osu! pixels.
4199
+ difficulty.cs = CircleSizeCalculator.oldDroidScaleToDroidCS(scale +
4200
+ CircleSizeCalculator.oldDroidScaleScreenPixelsToOsuPixels(0.125));
3897
4201
  }
3898
- const finalAngle = initialAngle + diff * rotationRatio;
3899
- return new Vector2(initial.x * Math.cos(finalAngle), initial.y * Math.sin(finalAngle));
4202
+ difficulty.ar /= 2;
4203
+ difficulty.od /= 2;
4204
+ difficulty.hp /= 2;
3900
4205
  }
3901
- /**
3902
- * Obtains the absolute rotation of a {@link Slider}, defined as the angle from its start position to the
3903
- * end of its path.
3904
- *
3905
- * @param slider The {@link Slider} to obtain the rotation from.
3906
- * @return The angle in radians.
3907
- */
3908
- static getSliderRotation(slider) {
3909
- const pathEndPosition = slider.path.positionAt(1);
3910
- return Math.atan2(pathEndPosition.y, pathEndPosition.x);
4206
+ isCompatibleWith(other) {
4207
+ if (other instanceof ModDifficultyAdjust) {
4208
+ return (other.cs.value === null ||
4209
+ other.ar.value === null ||
4210
+ other.od.value === null ||
4211
+ other.hp.value === null);
4212
+ }
4213
+ return super.isCompatibleWith(other);
3911
4214
  }
3912
- /**
3913
- * Rotates a {@link Vector2} by the specified angle.
3914
- *
3915
- * @param vec The {@link Vector2} to be rotated.
3916
- * @param rotation The angle to rotate `vec` by, in radians.
3917
- * @return The rotated {@link Vector2}.
3918
- */
3919
- static rotateVector(vec, rotation) {
3920
- const angle = Math.atan2(vec.y, vec.x) + rotation;
3921
- const { length } = vec;
3922
- return new Vector2(length * Math.cos(angle), length * Math.sin(angle));
4215
+ }
4216
+
4217
+ /**
4218
+ * Represents the ReallyEasy mod.
4219
+ */
4220
+ class ModReallyEasy extends Mod {
4221
+ constructor() {
4222
+ super(...arguments);
4223
+ this.acronym = "RE";
4224
+ this.name = "Really Easy";
4225
+ this.droidRanked = false;
3923
4226
  }
3924
- //#endregion
3925
- //#region Reflection
3926
- /**
3927
- * Reflects the position of a {@link HitObject} horizontally along the playfield.
3928
- *
3929
- * @param hitObject The {@link HitObject} to reflect.
3930
- */
3931
- static reflectHorizontallyAlongPlayfield(hitObject) {
3932
- hitObject.position = this.reflectVectorHorizontallyAlongPlayfield(hitObject.position);
3933
- if (hitObject instanceof Slider) {
3934
- this.modifySlider(hitObject, (v) => new Vector2(-v.x, v.y));
4227
+ get isDroidRelevant() {
4228
+ return true;
4229
+ }
4230
+ calculateDroidScoreMultiplier() {
4231
+ return 0.4;
4232
+ }
4233
+ applyToDifficultyWithMods(mode, difficulty, mods) {
4234
+ var _a;
4235
+ if (mode !== exports.Modes.droid) {
4236
+ return;
4237
+ }
4238
+ const difficultyAdjust = mods.get(ModDifficultyAdjust);
4239
+ if (typeof (difficultyAdjust === null || difficultyAdjust === void 0 ? void 0 : difficultyAdjust.ar.value) !== "number") {
4240
+ if (mods.has(ModEasy)) {
4241
+ difficulty.ar *= 2;
4242
+ difficulty.ar -= 0.5;
4243
+ }
4244
+ const customSpeed = mods.get(ModCustomSpeed);
4245
+ difficulty.ar -= 0.5;
4246
+ difficulty.ar -= ((_a = customSpeed === null || customSpeed === void 0 ? void 0 : customSpeed.trackRateMultiplier.value) !== null && _a !== void 0 ? _a : 1) - 1;
4247
+ }
4248
+ if (typeof (difficultyAdjust === null || difficultyAdjust === void 0 ? void 0 : difficultyAdjust.cs.value) !== "number") {
4249
+ if (!mods.has(ModReplayV6)) {
4250
+ difficulty.cs /= 2;
4251
+ }
4252
+ else {
4253
+ const scale = CircleSizeCalculator.droidCSToOldDroidScale(difficulty.cs);
4254
+ // The 0.125 scale that was added before replay version 7 was in screen pixels. We need it in osu! pixels.
4255
+ difficulty.cs = CircleSizeCalculator.oldDroidScaleToDroidCS(scale +
4256
+ CircleSizeCalculator.oldDroidScaleScreenPixelsToOsuPixels(0.125));
4257
+ }
4258
+ }
4259
+ if (typeof (difficultyAdjust === null || difficultyAdjust === void 0 ? void 0 : difficultyAdjust.od.value) !== "number") {
4260
+ difficulty.od /= 2;
4261
+ }
4262
+ if (typeof (difficultyAdjust === null || difficultyAdjust === void 0 ? void 0 : difficultyAdjust.hp.value) !== "number") {
4263
+ difficulty.hp /= 2;
3935
4264
  }
3936
4265
  }
3937
- /**
3938
- * Reflects the position of a {@link HitObject} vertically along the playfield.
3939
- *
3940
- * @param hitObject The {@link HitObject} to reflect.
3941
- */
3942
- static reflectVerticallyAlongPlayfield(hitObject) {
3943
- // Reflect the position of the hit object.
3944
- hitObject.position = this.reflectVectorVerticallyAlongPlayfield(hitObject.position);
3945
- if (hitObject instanceof Slider) {
3946
- this.modifySlider(hitObject, (v) => new Vector2(v.x, -v.y));
4266
+ isCompatibleWith(other) {
4267
+ if (other instanceof ModDifficultyAdjust) {
4268
+ return (other.cs.value === null ||
4269
+ other.ar.value === null ||
4270
+ other.od.value === null ||
4271
+ other.hp.value === null);
3947
4272
  }
4273
+ return super.isCompatibleWith(other);
3948
4274
  }
3949
- /**
3950
- * Flips the position of a {@link Slider} around its start position horizontally.
3951
- *
3952
- * @param slider The {@link Slider} to be flipped.
3953
- */
3954
- static flipSliderInPlaceHorizontally(slider) {
3955
- this.modifySlider(slider, (v) => new Vector2(-v.x, v.y));
4275
+ }
4276
+
4277
+ /**
4278
+ * Represents the SmallCircle mod.
4279
+ *
4280
+ * This is a legacy osu!droid mod that may still be exist when parsing replays.
4281
+ */
4282
+ class ModSmallCircle extends Mod {
4283
+ constructor() {
4284
+ super(...arguments);
4285
+ this.acronym = "SC";
4286
+ this.name = "SmallCircle";
4287
+ this.droidRanked = false;
3956
4288
  }
3957
- /**
3958
- * Rotates a {@link Slider} around its start position by the specified angle.
3959
- *
3960
- * @param slider The {@link Slider} to rotate.
3961
- * @param rotation The angle to rotate `slider` by, in radians.
3962
- */
3963
- static rotateSlider(slider, rotation) {
3964
- this.modifySlider(slider, (v) => this.rotateVector(v, rotation));
4289
+ get isDroidRelevant() {
4290
+ return true;
3965
4291
  }
3966
- static modifySlider(slider, modifyControlPoint) {
3967
- slider.path = new SliderPath({
3968
- pathType: slider.path.pathType,
3969
- controlPoints: slider.path.controlPoints.map(modifyControlPoint),
3970
- expectedDistance: slider.path.expectedDistance,
3971
- });
4292
+ calculateDroidScoreMultiplier() {
4293
+ return 1.06;
3972
4294
  }
3973
- static reflectVectorHorizontallyAlongPlayfield(vector) {
3974
- return new Vector2(Playfield.baseSize.x - vector.x, vector.y);
4295
+ migrateDroidMod(difficulty) {
4296
+ return new ModDifficultyAdjust({ cs: difficulty.cs + 4 });
3975
4297
  }
3976
- static reflectVectorVerticallyAlongPlayfield(vector) {
3977
- return new Vector2(vector.x, Playfield.baseSize.y - vector.y);
4298
+ applyToDifficulty(mode, difficulty, adjustmentMods) {
4299
+ if (mode === exports.Modes.osu || !adjustmentMods.has(ModDifficultyAdjust)) {
4300
+ difficulty.cs += 4;
4301
+ }
4302
+ else {
4303
+ const scale = CircleSizeCalculator.droidCSToOldDroidScale(difficulty.cs);
4304
+ difficulty.cs = CircleSizeCalculator.oldDroidScaleToDroidCS(scale + CircleSizeCalculator.droidCSToOldDroidScale(4));
4305
+ }
3978
4306
  }
3979
- //#endregion
3980
- //#region Reposition
4307
+ isCompatibleWith(other) {
4308
+ if (other instanceof ModDifficultyAdjust) {
4309
+ return other.cs.value === null;
4310
+ }
4311
+ return super.isCompatibleWith(other);
4312
+ }
4313
+ }
4314
+
4315
+ /**
4316
+ * Represents a `Mod` specific setting that is constrained to a number of values.
4317
+ *
4318
+ * The value can be `null`, which is treated as a special case.
4319
+ */
4320
+ class NullableDecimalModSetting extends RangeConstrainedModSetting {
3981
4321
  /**
3982
- * Generates a list of {@link HitObjectPositionInfo}s containing information for how the given list of
3983
- * {@link HitObject}s are positioned.
4322
+ * The number of decimal places to round the value to.
3984
4323
  *
3985
- * @param hitObjects A list of {@link HitObject}s to process.
3986
- * @return A list of {@link HitObjectPositionInfo}s describing how each {@link HitObject} is positioned
3987
- * relative to the previous one.
4324
+ * When set to `null`, the value will not be rounded.
3988
4325
  */
3989
- static generatePositionInfos(hitObjects) {
3990
- const positionInfos = [];
3991
- let previousPosition = Playfield.center;
3992
- let previousAngle = 0;
3993
- for (const hitObject of hitObjects) {
3994
- const relativePosition = hitObject.position.subtract(previousPosition);
3995
- let absoluteAngle = Math.atan2(relativePosition.y, relativePosition.x);
3996
- const relativeAngle = absoluteAngle - previousAngle;
3997
- const positionInfo = new HitObjectPositionInfo(hitObject);
3998
- positionInfo.relativeAngle = relativeAngle;
3999
- positionInfo.distanceFromPrevious = relativePosition.length;
4000
- if (hitObject instanceof Slider) {
4001
- const absoluteRotation = this.getSliderRotation(hitObject);
4002
- positionInfo.rotation = absoluteRotation - absoluteAngle;
4003
- absoluteAngle = absoluteRotation;
4004
- }
4005
- previousPosition = hitObject.endPosition;
4006
- previousAngle = absoluteAngle;
4007
- positionInfos.push(positionInfo);
4326
+ get precision() {
4327
+ return this._precision;
4328
+ }
4329
+ set precision(value) {
4330
+ if (value !== null && value < 0) {
4331
+ throw new RangeError(`The precision (${value}) must be greater than or equal to 0.`);
4332
+ }
4333
+ this._precision = value;
4334
+ if (value !== null) {
4335
+ this.value = this.processValue(this.value);
4008
4336
  }
4009
- return positionInfos;
4010
4337
  }
4011
- static repositionHitObjects(positionInfos) {
4012
- const workingObjects = positionInfos.map((p) => new WorkingObject(p));
4013
- let previous = null;
4014
- for (let i = 0; i < workingObjects.length; ++i) {
4015
- const current = workingObjects[i];
4016
- const { hitObject } = current;
4017
- if (hitObject instanceof Spinner) {
4018
- previous = current;
4019
- continue;
4020
- }
4021
- this.computeModifiedPosition(current, previous, i > 1 ? workingObjects[i - 2] : null);
4022
- // Move hit objects back into the playfield if they are outside of it.
4023
- let shift;
4024
- if (hitObject instanceof Circle) {
4025
- shift = this.clampHitCircleToPlayfield(current);
4026
- }
4027
- else if (hitObject instanceof Slider) {
4028
- shift = this.clampSliderToPlayfield(current);
4029
- }
4030
- else {
4031
- shift = new Vector2(0);
4338
+ constructor(name, description, defaultValue, min = -Number.MAX_VALUE, max = Number.MAX_VALUE, step = 0, precision = null) {
4339
+ super(name, description, defaultValue, min, max, step);
4340
+ this.displayFormatter = (v) => {
4341
+ if (v === null) {
4342
+ return "None";
4032
4343
  }
4033
- if (shift.x !== 0 || shift.y !== 0) {
4034
- const toBeShifted = [];
4035
- for (let j = i - 1; j >= Math.max(0, i - this.precedingObjectsToShift); --j) {
4036
- // Only shift hit circles.
4037
- if (!(workingObjects[j].hitObject instanceof Circle)) {
4038
- break;
4039
- }
4040
- toBeShifted.push(workingObjects[j].hitObject);
4041
- }
4042
- this.applyDecreasingShift(toBeShifted, shift);
4344
+ if (this.precision !== null) {
4345
+ return v.toFixed(this.precision);
4043
4346
  }
4044
- previous = current;
4347
+ return super.toDisplayString();
4348
+ };
4349
+ if (min > max) {
4350
+ throw new RangeError(`The minimum value (${min}) must be less than or equal to the maximum value (${max}).`);
4045
4351
  }
4046
- return workingObjects.map((w) => w.hitObject);
4352
+ if (step < 0) {
4353
+ throw new RangeError(`The step size (${step}) must be greater than or equal to 0.`);
4354
+ }
4355
+ if (defaultValue !== null &&
4356
+ (defaultValue < min || defaultValue > max)) {
4357
+ throw new RangeError(`The default value (${defaultValue}) must be between the minimum (${min}) and maximum (${max}) values.`);
4358
+ }
4359
+ this._precision = precision;
4047
4360
  }
4048
- /**
4049
- * Determines whether a {@link HitObject} is on a beat.
4050
- *
4051
- * @param beatmap The {@link Beatmap} the {@link HitObject} is a part of.
4052
- * @param hitObject The {@link HitObject} to check.
4053
- * @param downbeatsOnly If `true`, whether this method only returns `true` is on a downbeat.
4054
- * @return `true` if the {@link HitObject} is on a (down-)beat, `false` otherwise.
4055
- */
4056
- static isHitObjectOnBeat(beatmap, hitObject, downbeatsOnly = false) {
4057
- const timingPoint = beatmap.controlPoints.timing.controlPointAt(hitObject.startTime);
4058
- const timeSinceTimingPoint = hitObject.startTime - timingPoint.time;
4059
- let beatLength = timingPoint.msPerBeat;
4060
- if (downbeatsOnly) {
4061
- beatLength *= timingPoint.timeSignature;
4361
+ processValue(value) {
4362
+ if (value === null) {
4363
+ return null;
4062
4364
  }
4063
- // Ensure within 1ms of expected location.
4064
- return Math.abs(timeSinceTimingPoint + 1) % beatLength < 2;
4365
+ const processedValue = MathUtils.clamp(Math.round(value / this.step) * this.step, this.min, this.max);
4366
+ if (this.precision !== null) {
4367
+ return parseFloat(processedValue.toFixed(this.precision));
4368
+ }
4369
+ return processedValue;
4065
4370
  }
4066
- /**
4067
- * Generates a random number from a Normal distribution using the Box-Muller transform.
4068
- *
4069
- * @param random A {@link Random} to get the random number from.
4070
- * @param mean The mean of the distribution.
4071
- * @param stdDev The standard deviation of the distribution.
4072
- * @return The random number.
4073
- */
4074
- static randomGaussian(random, mean = 0, stdDev = 1) {
4075
- // Generate 2 random numbers in the interval (0,1].
4076
- // x1 must not be 0 since log(0) = undefined.
4077
- const x1 = 1 - random.nextDouble();
4078
- const x2 = 1 - random.nextDouble();
4079
- const stdNormal = Math.sqrt(-2 * Math.log(x1)) * Math.sin(2 * Math.PI * x2);
4080
- return mean + stdDev * stdNormal;
4371
+ }
4372
+
4373
+ /**
4374
+ * Represents the Difficulty Adjust mod.
4375
+ */
4376
+ class ModDifficultyAdjust extends Mod {
4377
+ get isRelevant() {
4378
+ return (this.cs.value !== null ||
4379
+ this.ar.value !== null ||
4380
+ this.od.value !== null ||
4381
+ this.hp.value !== null);
4081
4382
  }
4082
- /**
4083
- * Calculates a {@link Vector4} which contains all possible movements of a {@link Slider} (in relative
4084
- * X/Y coordinates) such that the entire {@link Slider} is inside the playfield.
4085
- *
4086
- * If the {@link Slider} is larger than the playfield, the returned {@link Vector4} may have a Z/W component
4087
- * that is smaller than its X/Y component.
4088
- *
4089
- * @param slider The {@link Slider} whose movement bound is to be calculated.
4090
- * @return A {@link Vector4} which contains all possible movements of a {@link Slider} (in relative X/Y
4091
- * coordinates) such that the entire {@link Slider} is inside the playfield.
4092
- */
4093
- static calculatePossibleMovementBounds(slider) {
4094
- const pathPositions = slider.path.pathToProgress(0, 1);
4095
- let minX = Number.POSITIVE_INFINITY;
4096
- let maxX = Number.NEGATIVE_INFINITY;
4097
- let minY = Number.POSITIVE_INFINITY;
4098
- let maxY = Number.NEGATIVE_INFINITY;
4099
- // Compute the bounding box of the slider.
4100
- for (const position of pathPositions) {
4101
- minX = Math.min(minX, position.x);
4102
- maxX = Math.max(maxX, position.x);
4103
- minY = Math.min(minY, position.y);
4104
- maxY = Math.max(maxY, position.y);
4105
- }
4106
- // Take the radius into account.
4107
- const { radius } = slider;
4108
- minX -= radius;
4109
- minY -= radius;
4110
- maxX += radius;
4111
- maxY += radius;
4112
- // Given the bounding box of the slider (via min/max X/Y), the amount that the slider can move to the left is
4113
- // minX (with the sign flipped, since positive X is to the right), and the amount that it can move to the right
4114
- // is WIDTH - maxX. The same calculation applies for the Y axis.
4115
- const left = -minX;
4116
- const right = Playfield.baseSize.x - maxX;
4117
- const top = -minY;
4118
- const bottom = Playfield.baseSize.y - maxY;
4119
- return new Vector4(left, top, right, bottom);
4383
+ constructor(values) {
4384
+ var _a, _b, _c, _d;
4385
+ super();
4386
+ this.acronym = "DA";
4387
+ this.name = "Difficulty Adjust";
4388
+ this.droidRanked = false;
4389
+ this.osuRanked = false;
4390
+ /**
4391
+ * The circle size to enforce.
4392
+ */
4393
+ this.cs = new NullableDecimalModSetting("Circle size", "The circle size to enforce.", null, 0, 15, 0.1, 1);
4394
+ /**
4395
+ * The approach rate to enforce.
4396
+ */
4397
+ this.ar = new NullableDecimalModSetting("Approach rate", "The approach rate to enforce.", null, 0, 11, 0.1, 1);
4398
+ /**
4399
+ * The overall difficulty to enforce.
4400
+ */
4401
+ this.od = new NullableDecimalModSetting("Overall difficulty", "The overall difficulty to enforce.", null, 0, 11, 0.1, 1);
4402
+ /**
4403
+ * The health drain rate to enforce.
4404
+ */
4405
+ this.hp = new NullableDecimalModSetting("Health drain", "The health drain to enforce.", null, 0, 11, 0.1, 1);
4406
+ this.cs.value = (_a = values === null || values === void 0 ? void 0 : values.cs) !== null && _a !== void 0 ? _a : null;
4407
+ this.ar.value = (_b = values === null || values === void 0 ? void 0 : values.ar) !== null && _b !== void 0 ? _b : null;
4408
+ this.od.value = (_c = values === null || values === void 0 ? void 0 : values.od) !== null && _c !== void 0 ? _c : null;
4409
+ this.hp.value = (_d = values === null || values === void 0 ? void 0 : values.hp) !== null && _d !== void 0 ? _d : null;
4410
+ }
4411
+ copySettings(mod) {
4412
+ var _a, _b, _c, _d, _e, _f, _g, _h;
4413
+ super.copySettings(mod);
4414
+ this.cs.value = ((_b = (_a = mod.settings) === null || _a === void 0 ? void 0 : _a.cs) !== null && _b !== void 0 ? _b : null);
4415
+ this.ar.value = ((_d = (_c = mod.settings) === null || _c === void 0 ? void 0 : _c.ar) !== null && _d !== void 0 ? _d : null);
4416
+ this.od.value = ((_f = (_e = mod.settings) === null || _e === void 0 ? void 0 : _e.od) !== null && _f !== void 0 ? _f : null);
4417
+ this.hp.value = ((_h = (_g = mod.settings) === null || _g === void 0 ? void 0 : _g.hp) !== null && _h !== void 0 ? _h : null);
4120
4418
  }
4121
- /**
4122
- * Computes the modified position of a {@link HitObject} while attempting to keep it inside the playfield.
4123
- *
4124
- * @param current The {@link WorkingObject} representing the {@link HitObject} to have the modified
4125
- * position computed for.
4126
- * @param previous The {@link WorkingObject} representing the {@link HitObject} immediately preceding
4127
- * `current`.
4128
- * @param beforePrevious The {@link WorkingObject} representing the {@link HitObject} immediately preceding
4129
- * `previous`.
4130
- */
4131
- static computeModifiedPosition(current, previous, beforePrevious) {
4132
- var _b, _c;
4133
- let previousAbsoluteAngle = 0;
4134
- if (previous !== null) {
4135
- if (previous.hitObject instanceof Slider) {
4136
- previousAbsoluteAngle = this.getSliderRotation(previous.hitObject);
4137
- }
4138
- else {
4139
- const earliestPosition = (_b = beforePrevious === null || beforePrevious === void 0 ? void 0 : beforePrevious.hitObject.endPosition) !== null && _b !== void 0 ? _b : Playfield.center;
4140
- const relativePosition = previous.hitObject.position.subtract(earliestPosition);
4141
- previousAbsoluteAngle = Math.atan2(relativePosition.y, relativePosition.x);
4142
- }
4143
- }
4144
- let absoluteAngle = previousAbsoluteAngle + current.positionInfo.relativeAngle;
4145
- let positionRelativeToPrevious = new Vector2(current.positionInfo.distanceFromPrevious * Math.cos(absoluteAngle), current.positionInfo.distanceFromPrevious * Math.sin(absoluteAngle));
4146
- const lastEndPosition = (_c = previous === null || previous === void 0 ? void 0 : previous.endPositionModified) !== null && _c !== void 0 ? _c : Playfield.center;
4147
- positionRelativeToPrevious = this.rotateAwayFromEdge(lastEndPosition, positionRelativeToPrevious);
4148
- current.positionModified = lastEndPosition.add(positionRelativeToPrevious);
4149
- if (!(current.hitObject instanceof Slider)) {
4150
- return;
4419
+ get isDroidRelevant() {
4420
+ return this.isRelevant;
4421
+ }
4422
+ calculateDroidScoreMultiplier(difficulty) {
4423
+ // Graph: https://www.desmos.com/calculator/yrggkhrkzz
4424
+ let multiplier = 1;
4425
+ if (this.cs.value !== null) {
4426
+ const diff = this.cs.value - difficulty.cs;
4427
+ multiplier *=
4428
+ diff >= 0
4429
+ ? 1 + 0.0075 * Math.pow(diff, 1.5)
4430
+ : 2 / (1 + Math.exp(-0.5 * diff));
4151
4431
  }
4152
- absoluteAngle = Math.atan2(positionRelativeToPrevious.y, positionRelativeToPrevious.x);
4153
- const centerOfMassOriginal = this.calculateCenterOfMass(current.hitObject);
4154
- const centerOfMassModified = this.rotateAwayFromEdge(current.positionModified, this.rotateVector(centerOfMassOriginal, current.positionInfo.rotation +
4155
- absoluteAngle -
4156
- this.getSliderRotation(current.hitObject)));
4157
- const relativeRotation = Math.atan2(centerOfMassModified.y, centerOfMassModified.x) -
4158
- Math.atan2(centerOfMassOriginal.y, centerOfMassOriginal.x);
4159
- if (!Precision.almostEqualsNumber(relativeRotation, 0)) {
4160
- this.rotateSlider(current.hitObject, relativeRotation);
4432
+ if (this.od.value !== null) {
4433
+ const diff = this.od.value - difficulty.od;
4434
+ multiplier *=
4435
+ diff >= 0
4436
+ ? 1 + 0.005 * Math.pow(diff, 1.3)
4437
+ : 2 / (1 + Math.exp(-0.25 * diff));
4161
4438
  }
4439
+ return multiplier;
4162
4440
  }
4163
- /**
4164
- * Moves the modified position of a {@link Circle} so that it fits inside the playfield.
4165
- *
4166
- * @param workingObject The {@link WorkingObject} that represents the {@link Circle}.
4167
- * @return The deviation from the original modified position in order to fit within the playfield.
4168
- */
4169
- static clampHitCircleToPlayfield(workingObject) {
4170
- const previousPosition = workingObject.positionModified;
4171
- workingObject.positionModified = this.clampToPlayfield(workingObject.positionModified, workingObject.hitObject.radius);
4172
- workingObject.endPositionModified = workingObject.positionModified;
4173
- workingObject.hitObject.position = workingObject.positionModified;
4174
- return workingObject.positionModified.subtract(previousPosition);
4441
+ get isOsuRelevant() {
4442
+ return this.isRelevant;
4175
4443
  }
4176
- /**
4177
- * Moves a {@link Slider} and all necessary `SliderHitObject`s into the playfield if they are not in
4178
- * the playfield.
4179
- *
4180
- * @param workingObject The {@link WorkingObject} that represents the {@link Slider}.
4181
- * @return The deviation from the original modified position in order to fit within the playfield.
4182
- */
4183
- static clampSliderToPlayfield(workingObject) {
4184
- const slider = workingObject.hitObject;
4185
- let possibleMovementBounds = this.calculatePossibleMovementBounds(slider);
4186
- // The slider rotation applied in computeModifiedPosition might make it impossible to fit the slider
4187
- // into the playfield. For example, a long horizontal slider will be off-screen when rotated by 90
4188
- // degrees. In this case, limit the rotation to either 0 or 180 degrees.
4189
- if (possibleMovementBounds.width < 0 ||
4190
- possibleMovementBounds.height < 0) {
4191
- const currentRotation = this.getSliderRotation(slider);
4192
- const diff1 = this.getAngleDifference(workingObject.rotationOriginal, currentRotation);
4193
- const diff2 = this.getAngleDifference(workingObject.rotationOriginal + Math.PI, currentRotation);
4194
- if (diff1 < diff2) {
4195
- this.rotateSlider(slider, workingObject.rotationOriginal - currentRotation);
4196
- }
4197
- else {
4198
- this.rotateSlider(slider, workingObject.rotationOriginal + Math.PI - currentRotation);
4199
- }
4200
- possibleMovementBounds =
4201
- this.calculatePossibleMovementBounds(slider);
4444
+ get osuScoreMultiplier() {
4445
+ return 0.5;
4446
+ }
4447
+ applyToDifficultyWithMods(_, difficulty, mods) {
4448
+ var _a, _b, _c, _d;
4449
+ difficulty.cs = (_a = this.cs.value) !== null && _a !== void 0 ? _a : difficulty.cs;
4450
+ difficulty.ar = (_b = this.ar.value) !== null && _b !== void 0 ? _b : difficulty.ar;
4451
+ difficulty.od = (_c = this.od.value) !== null && _c !== void 0 ? _c : difficulty.od;
4452
+ difficulty.hp = (_d = this.hp.value) !== null && _d !== void 0 ? _d : difficulty.hp;
4453
+ // Special case for force AR in replay version 6 and older, where the AR value is kept constant with
4454
+ // respect to game time. This makes the player perceive the AR as is under all speed multipliers.
4455
+ if (this.ar.value !== null && mods.has(ModReplayV6)) {
4456
+ const preempt = BeatmapDifficulty.difficultyRange(this.ar.value, HitObject.preemptMax, HitObject.preemptMid, HitObject.preemptMin);
4457
+ const trackRate = this.calculateTrackRate(mods.values());
4458
+ difficulty.ar = BeatmapDifficulty.inverseDifficultyRange(preempt * trackRate, HitObject.preemptMax, HitObject.preemptMid, HitObject.preemptMin);
4202
4459
  }
4203
- const previousPosition = workingObject.positionModified;
4204
- // Clamp slider position to the placement area.
4205
- // If the slider is larger than the playfield, at least make sure that the head circle is
4206
- // inside the playfield.
4207
- const newX = possibleMovementBounds.width < 0
4208
- ? MathUtils.clamp(possibleMovementBounds.left, 0, Playfield.baseSize.x)
4209
- : MathUtils.clamp(previousPosition.x, possibleMovementBounds.left, possibleMovementBounds.right);
4210
- const newY = possibleMovementBounds.height < 0
4211
- ? MathUtils.clamp(possibleMovementBounds.top, 0, Playfield.baseSize.y)
4212
- : MathUtils.clamp(previousPosition.y, possibleMovementBounds.top, possibleMovementBounds.bottom);
4213
- workingObject.positionModified = new Vector2(newX, newY);
4214
- slider.position = workingObject.positionModified;
4215
- workingObject.endPositionModified = slider.endPosition;
4216
- return workingObject.positionModified.subtract(previousPosition);
4217
4460
  }
4218
- /**
4219
- * Clamps a {@link Vector2} into the playfield, keeping a specified distance from the edge of the playfield.
4220
- *
4221
- * @param vec The {@link Vector2} to clamp.
4222
- * @param padding The minimum distance allowed from the edge of the playfield.
4223
- * @return The clamped {@link Vector2}.
4224
- */
4225
- static clampToPlayfield(vec, padding) {
4226
- return new Vector2(MathUtils.clamp(vec.x, padding, Playfield.baseSize.x - padding), MathUtils.clamp(vec.y, padding, Playfield.baseSize.y - padding));
4461
+ applyToHitObjectWithMods(_, hitObject, mods) {
4462
+ // Special case for force AR in replay version 6 and older, where the AR value is kept constant with
4463
+ // respect to game time. This makes the player perceive the fade in animation as is under all speed
4464
+ // multipliers.
4465
+ if (this.ar.value === null || !mods.has(ModReplayV6)) {
4466
+ return;
4467
+ }
4468
+ this.applyFadeAdjustment(hitObject, mods);
4469
+ if (hitObject instanceof Slider) {
4470
+ for (const nested of hitObject.nestedHitObjects) {
4471
+ this.applyFadeAdjustment(nested, mods);
4472
+ }
4473
+ }
4227
4474
  }
4228
- /**
4229
- * Decreasingly shifts a list of {@link HitObject}s by a specified amount.
4230
- *
4231
- * The first item in the list is shifted by the largest amount, while the last item is shifted by the
4232
- * smallest amount.
4233
- *
4234
- * @param hitObjects The list of {@link HitObject}s to be shifted.
4235
- * @param shift The amount to shift the {@link HitObject}s by.
4236
- */
4237
- static applyDecreasingShift(hitObjects, shift) {
4238
- for (let i = 0; i < hitObjects.length; ++i) {
4239
- const hitObject = hitObjects[i];
4240
- // The first object is shifted by a vector slightly smaller than shift.
4241
- // The last object is shifted by a vector slightly larger than zero.
4242
- const position = hitObject.position.add(shift.scale((hitObjects.length - i) / (hitObjects.length + 1)));
4243
- hitObject.position = this.clampToPlayfield(position, hitObject.radius);
4475
+ isCompatibleWith(other) {
4476
+ if (this.cs.value !== null && other instanceof ModSmallCircle) {
4477
+ return false;
4244
4478
  }
4479
+ if (this.cs.value !== null &&
4480
+ this.ar.value !== null &&
4481
+ this.od.value !== null &&
4482
+ this.hp.value !== null) {
4483
+ return !(other instanceof ModEasy ||
4484
+ other instanceof ModHardRock ||
4485
+ other instanceof ModReallyEasy);
4486
+ }
4487
+ return super.isCompatibleWith(other);
4245
4488
  }
4246
- /**
4247
- * Estimates the center of mass of a {@link Slider} relative to its start position.
4248
- *
4249
- * @param slider The {@link Slider} whose center mass is to be estimated.
4250
- * @return The estimated center of mass of `slider`.
4251
- */
4252
- static calculateCenterOfMass(slider) {
4253
- const sampleStep = 50;
4254
- // Only sample the start and end positions if the slider is too short.
4255
- if (slider.distance <= sampleStep) {
4256
- return slider.path.positionAt(1).divide(2);
4489
+ serializeSettings() {
4490
+ if (!this.isRelevant) {
4491
+ return null;
4257
4492
  }
4258
- let count = 0;
4259
- let sum = new Vector2(0);
4260
- for (let i = 0; i < slider.distance; i += sampleStep) {
4261
- sum = sum.add(slider.path.positionAt(i / slider.distance));
4262
- ++count;
4493
+ const settings = {};
4494
+ if (this.cs.value !== null) {
4495
+ settings.cs = this.cs.value;
4263
4496
  }
4264
- return sum.divide(count);
4497
+ if (this.ar.value !== null) {
4498
+ settings.ar = this.ar.value;
4499
+ }
4500
+ if (this.od.value !== null) {
4501
+ settings.od = this.od.value;
4502
+ }
4503
+ if (this.hp.value !== null) {
4504
+ settings.hp = this.hp.value;
4505
+ }
4506
+ return settings;
4265
4507
  }
4266
- /**
4267
- * Calculates the absolute difference between two angles in radians.
4268
- *
4269
- * @param angle1 The first angle.
4270
- * @param angle2 The second angle.
4271
- * @return THe absolute difference within interval `[0, Math.PI]`.
4272
- */
4273
- static getAngleDifference(angle1, angle2) {
4274
- const diff = Math.abs(angle1 - angle2) % (2 * Math.PI);
4275
- return Math.min(diff, 2 * Math.PI - diff);
4508
+ applyFadeAdjustment(hitObject, mods) {
4509
+ // IMPORTANT: These do not use `ModUtil.calculateRateWithMods` to avoid circular dependency.
4510
+ const initialTrackRate = this.calculateTrackRate(mods.values());
4511
+ const currentTrackRate = this.calculateTrackRate(mods.values(), hitObject.startTime);
4512
+ // Cancel the rate that was initially applied to timePreempt (via applyToDifficulty above and
4513
+ // HitObject.applyDefaults) and apply the current one.
4514
+ hitObject.timePreempt *= currentTrackRate / initialTrackRate;
4515
+ hitObject.timeFadeIn *= currentTrackRate;
4276
4516
  }
4277
- }
4278
- _a = HitObjectGenerationUtils;
4279
- /**
4280
- * The relative distance to the edge of the playfield before {@link HitObject} positions should start
4281
- * to "turn around" and curve towards the middle. The closer the {@link HitObject}s draw to the border,
4282
- * the sharper the turn.
4283
- */
4284
- HitObjectGenerationUtils.playfieldEdgeRatio = 0.375;
4285
- /**
4286
- * The amount of previous {@link HitObject}s to be shifted together when a {@link HitObject} is being moved.
4287
- */
4288
- HitObjectGenerationUtils.precedingObjectsToShift = 10;
4289
- HitObjectGenerationUtils.borderDistance = Playfield.baseSize.scale(_a.playfieldEdgeRatio);
4290
- class WorkingObject {
4291
- get hitObject() {
4292
- return this.positionInfo.hitObject;
4517
+ calculateTrackRate(mods, time = 0) {
4518
+ // IMPORTANT: This does not use `ModUtil.calculateRateWithMods` to avoid circular dependency.
4519
+ let rate = 1;
4520
+ for (const mod of mods) {
4521
+ if (mod.isApplicableToTrackRate()) {
4522
+ rate = mod.applyToRate(time, rate);
4523
+ }
4524
+ }
4525
+ return rate;
4293
4526
  }
4294
- constructor(positionInfo) {
4295
- this.rotationOriginal = this.hitObject instanceof Slider
4296
- ? HitObjectGenerationUtils.getSliderRotation(this.hitObject)
4297
- : 0;
4298
- this.positionModified = this.hitObject.position;
4299
- this.endPositionModified = this.hitObject.endPosition;
4300
- this.positionInfo = positionInfo;
4527
+ toString() {
4528
+ if (!this.isRelevant) {
4529
+ return super.toString();
4530
+ }
4531
+ const settings = [];
4532
+ if (this.cs.value !== null) {
4533
+ settings.push(`CS${this.cs.toDisplayString()}`);
4534
+ }
4535
+ if (this.ar.value !== null) {
4536
+ settings.push(`AR${this.ar.toDisplayString()}`);
4537
+ }
4538
+ if (this.od.value !== null) {
4539
+ settings.push(`OD${this.od.toDisplayString()}`);
4540
+ }
4541
+ if (this.hp.value !== null) {
4542
+ settings.push(`HP${this.hp.toDisplayString()}`);
4543
+ }
4544
+ return `${super.toString()} (${settings.join(", ")})`;
4301
4545
  }
4302
4546
  }
4303
4547
 
4304
4548
  /**
4305
- * Represents the Mirror mod.
4549
+ * Represents the NightCore mod.
4306
4550
  */
4307
- class ModMirror extends Mod {
4551
+ class ModNightCore extends ModRateAdjust {
4308
4552
  constructor() {
4309
- super();
4310
- this.name = "Mirror";
4311
- this.acronym = "MR";
4312
- this.droidRanked = false;
4313
- this.osuRanked = false;
4314
- /**
4315
- * The axes to reflect the `HitObject`s along.
4316
- */
4317
- this.flippedAxes = new ModSetting("Flipped axes", "The axes to reflect the hit objects along.", exports.Axes.x);
4318
- this.incompatibleMods.add(ModHardRock);
4553
+ super(1.5);
4554
+ this.acronym = "NC";
4555
+ this.name = "NightCore";
4556
+ this.droidRanked = true;
4557
+ this.osuRanked = true;
4558
+ this.bitwise = 1 << 9;
4559
+ this.incompatibleMods.add(ModDoubleTime).add(ModHalfTime);
4319
4560
  }
4320
4561
  get isDroidRelevant() {
4321
- return true;
4562
+ return this.isRelevant;
4322
4563
  }
4323
4564
  calculateDroidScoreMultiplier() {
4324
- return 1;
4565
+ return this.droidScoreMultiplier;
4325
4566
  }
4326
4567
  get isOsuRelevant() {
4327
- return true;
4568
+ return this.isRelevant;
4328
4569
  }
4329
4570
  get osuScoreMultiplier() {
4330
- return 1;
4331
- }
4332
- copySettings(mod) {
4333
- var _a;
4334
- super.copySettings(mod);
4335
- switch ((_a = mod.settings) === null || _a === void 0 ? void 0 : _a.flippedAxes) {
4336
- case 0:
4337
- this.flippedAxes.value = exports.Axes.x;
4338
- break;
4339
- case 1:
4340
- this.flippedAxes.value = exports.Axes.y;
4341
- break;
4342
- case 2:
4343
- this.flippedAxes.value = exports.Axes.both;
4344
- break;
4345
- }
4346
- }
4347
- applyToHitObject(_, hitObject) {
4348
- switch (this.flippedAxes.value) {
4349
- case exports.Axes.x:
4350
- HitObjectGenerationUtils.reflectHorizontallyAlongPlayfield(hitObject);
4351
- break;
4352
- case exports.Axes.y:
4353
- HitObjectGenerationUtils.reflectVerticallyAlongPlayfield(hitObject);
4354
- break;
4355
- case exports.Axes.both:
4356
- HitObjectGenerationUtils.reflectHorizontallyAlongPlayfield(hitObject);
4357
- HitObjectGenerationUtils.reflectVerticallyAlongPlayfield(hitObject);
4358
- break;
4359
- }
4360
- }
4361
- serializeSettings() {
4362
- return { flippedAxes: this.flippedAxes.value - 1 };
4363
- }
4364
- toString() {
4365
- const settings = [];
4366
- if (this.flippedAxes.value === exports.Axes.x ||
4367
- this.flippedAxes.value === exports.Axes.both) {
4368
- settings.push("↔");
4369
- }
4370
- if (this.flippedAxes.value === exports.Axes.y ||
4371
- this.flippedAxes.value === exports.Axes.both) {
4372
- settings.push("↕");
4373
- }
4374
- return `${super.toString()} (${settings.join(", ")})`;
4571
+ return 1.12;
4375
4572
  }
4376
4573
  }
4377
4574
 
4378
4575
  /**
4379
- * Represents the HardRock mod.
4576
+ * Represents the HalfTime mod.
4380
4577
  */
4381
- class ModHardRock extends Mod {
4578
+ class ModHalfTime extends ModRateAdjust {
4382
4579
  constructor() {
4383
- super();
4384
- this.acronym = "HR";
4385
- this.name = "HardRock";
4580
+ super(0.75);
4581
+ this.acronym = "HT";
4582
+ this.name = "HalfTime";
4386
4583
  this.droidRanked = true;
4387
4584
  this.osuRanked = true;
4388
- this.bitwise = 1 << 4;
4389
- this.incompatibleMods.add(ModEasy);
4390
- this.incompatibleMods.add(ModMirror);
4585
+ this.bitwise = 1 << 8;
4586
+ this.incompatibleMods.add(ModDoubleTime).add(ModNightCore);
4391
4587
  }
4392
4588
  get isDroidRelevant() {
4393
- return true;
4589
+ return this.isRelevant;
4394
4590
  }
4395
4591
  calculateDroidScoreMultiplier() {
4396
- return 1.06;
4592
+ return this.droidScoreMultiplier;
4397
4593
  }
4398
4594
  get isOsuRelevant() {
4399
- return true;
4595
+ return this.isRelevant;
4400
4596
  }
4401
4597
  get osuScoreMultiplier() {
4402
- return 1.06;
4403
- }
4404
- applyToDifficulty(mode, difficulty, adjustmentMods) {
4405
- if (mode === exports.Modes.osu || !adjustmentMods.has(ModReplayV6)) {
4406
- difficulty.cs = this.applySetting(difficulty.cs, 1.3);
4407
- }
4408
- else {
4409
- const scale = CircleSizeCalculator.droidCSToOldDroidScale(difficulty.cs);
4410
- // The 0.125 scale that was added before replay version 7 was in screen pixels. We need it in osu! pixels.
4411
- difficulty.cs = CircleSizeCalculator.oldDroidScaleToDroidCS(scale -
4412
- CircleSizeCalculator.oldDroidScaleScreenPixelsToOsuPixels(0.125));
4413
- }
4414
- difficulty.ar = this.applySetting(difficulty.ar);
4415
- difficulty.od = this.applySetting(difficulty.od);
4416
- difficulty.hp = this.applySetting(difficulty.hp);
4417
- }
4418
- applyToHitObject(_, hitObject) {
4419
- HitObjectGenerationUtils.reflectVerticallyAlongPlayfield(hitObject);
4420
- }
4421
- applySetting(value, ratio = 1.4) {
4422
- return Math.min(value * ratio, 10);
4598
+ return 0.3;
4423
4599
  }
4424
4600
  }
4425
4601
 
4426
4602
  /**
4427
- * Represents the Easy mod.
4603
+ * Represents the DoubleTime mod.
4428
4604
  */
4429
- class ModEasy extends Mod {
4605
+ class ModDoubleTime extends ModRateAdjust {
4430
4606
  constructor() {
4431
- super();
4432
- this.acronym = "EZ";
4433
- this.name = "Easy";
4607
+ super(1.5);
4608
+ this.acronym = "DT";
4609
+ this.name = "DoubleTime";
4434
4610
  this.droidRanked = true;
4435
4611
  this.osuRanked = true;
4436
- this.bitwise = 1 << 1;
4437
- this.incompatibleMods.add(ModHardRock);
4612
+ this.bitwise = 1 << 6;
4613
+ this.incompatibleMods.add(ModHalfTime).add(ModNightCore);
4438
4614
  }
4439
4615
  get isDroidRelevant() {
4440
- return true;
4616
+ return this.isRelevant;
4441
4617
  }
4442
4618
  calculateDroidScoreMultiplier() {
4443
- return 0.5;
4619
+ return this.droidScoreMultiplier;
4444
4620
  }
4445
4621
  get isOsuRelevant() {
4446
- return true;
4622
+ return this.isRelevant;
4447
4623
  }
4448
4624
  get osuScoreMultiplier() {
4449
- return 0.5;
4450
- }
4451
- applyToDifficulty(mode, difficulty, adjustmentMods) {
4452
- if (mode === exports.Modes.osu || !adjustmentMods.has(ModReplayV6)) {
4453
- difficulty.cs /= 2;
4454
- }
4455
- else {
4456
- const scale = CircleSizeCalculator.droidCSToOldDroidScale(difficulty.cs);
4457
- // The 0.125 scale that was added before replay version 7 was in screen pixels. We need it in osu! pixels.
4458
- difficulty.cs = CircleSizeCalculator.oldDroidScaleToDroidCS(scale +
4459
- CircleSizeCalculator.oldDroidScaleScreenPixelsToOsuPixels(0.125));
4460
- }
4461
- difficulty.ar /= 2;
4462
- difficulty.od /= 2;
4463
- difficulty.hp /= 2;
4625
+ return 1.12;
4464
4626
  }
4465
4627
  }
4466
4628
 
@@ -4594,8 +4756,8 @@ class ModMuted extends Mod {
4594
4756
  * Hit sounds are also muted alongside the track.
4595
4757
  */
4596
4758
  this.affectsHitSounds = new BooleanModSetting("Mute hit sounds", "Hit sounds are also muted alongside the track.", true);
4597
- this.inverseMuting.bindValueChanged((_, newValue) => {
4598
- this.muteComboCount.min = newValue ? 1 : 0;
4759
+ this.inverseMuting.bindValueChanged((value) => {
4760
+ this.muteComboCount.min = value.newValue ? 1 : 0;
4599
4761
  }, true);
4600
4762
  }
4601
4763
  /**
@@ -5049,57 +5211,6 @@ class ModRandom extends Mod {
5049
5211
  }
5050
5212
  ModRandom.playfieldDiagonal = Playfield.baseSize.length;
5051
5213
 
5052
- /**
5053
- * Represents the ReallyEasy mod.
5054
- */
5055
- class ModReallyEasy extends Mod {
5056
- constructor() {
5057
- super(...arguments);
5058
- this.acronym = "RE";
5059
- this.name = "ReallyEasy";
5060
- this.droidRanked = false;
5061
- }
5062
- get isDroidRelevant() {
5063
- return true;
5064
- }
5065
- calculateDroidScoreMultiplier() {
5066
- return 0.4;
5067
- }
5068
- applyToDifficultyWithMods(mode, difficulty, mods) {
5069
- var _a;
5070
- if (mode !== exports.Modes.droid) {
5071
- return;
5072
- }
5073
- const difficultyAdjust = mods.get(ModDifficultyAdjust);
5074
- if (typeof (difficultyAdjust === null || difficultyAdjust === void 0 ? void 0 : difficultyAdjust.ar.value) !== "number") {
5075
- if (mods.has(ModEasy)) {
5076
- difficulty.ar *= 2;
5077
- difficulty.ar -= 0.5;
5078
- }
5079
- const customSpeed = mods.get(ModCustomSpeed);
5080
- difficulty.ar -= 0.5;
5081
- difficulty.ar -= ((_a = customSpeed === null || customSpeed === void 0 ? void 0 : customSpeed.trackRateMultiplier.value) !== null && _a !== void 0 ? _a : 1) - 1;
5082
- }
5083
- if (typeof (difficultyAdjust === null || difficultyAdjust === void 0 ? void 0 : difficultyAdjust.cs.value) !== "number") {
5084
- if (!mods.has(ModReplayV6)) {
5085
- difficulty.cs /= 2;
5086
- }
5087
- else {
5088
- const scale = CircleSizeCalculator.droidCSToOldDroidScale(difficulty.cs);
5089
- // The 0.125 scale that was added before replay version 7 was in screen pixels. We need it in osu! pixels.
5090
- difficulty.cs = CircleSizeCalculator.oldDroidScaleToDroidCS(scale +
5091
- CircleSizeCalculator.oldDroidScaleScreenPixelsToOsuPixels(0.125));
5092
- }
5093
- }
5094
- if (typeof (difficultyAdjust === null || difficultyAdjust === void 0 ? void 0 : difficultyAdjust.od.value) !== "number") {
5095
- difficulty.od /= 2;
5096
- }
5097
- if (typeof (difficultyAdjust === null || difficultyAdjust === void 0 ? void 0 : difficultyAdjust.hp.value) !== "number") {
5098
- difficulty.hp /= 2;
5099
- }
5100
- }
5101
- }
5102
-
5103
5214
  /**
5104
5215
  * Represents the ScoreV2 mod.
5105
5216
  */
@@ -5126,38 +5237,6 @@ class ModScoreV2 extends Mod {
5126
5237
  }
5127
5238
  }
5128
5239
 
5129
- /**
5130
- * Represents the SmallCircle mod.
5131
- *
5132
- * This is a legacy osu!droid mod that may still be exist when parsing replays.
5133
- */
5134
- class ModSmallCircle extends Mod {
5135
- constructor() {
5136
- super(...arguments);
5137
- this.acronym = "SC";
5138
- this.name = "SmallCircle";
5139
- this.droidRanked = false;
5140
- }
5141
- get isDroidRelevant() {
5142
- return true;
5143
- }
5144
- calculateDroidScoreMultiplier() {
5145
- return 1.06;
5146
- }
5147
- migrateDroidMod(difficulty) {
5148
- return new ModDifficultyAdjust({ cs: difficulty.cs + 4 });
5149
- }
5150
- applyToDifficulty(mode, difficulty, adjustmentMods) {
5151
- if (mode === exports.Modes.osu || !adjustmentMods.has(ModDifficultyAdjust)) {
5152
- difficulty.cs += 4;
5153
- }
5154
- else {
5155
- const scale = CircleSizeCalculator.droidCSToOldDroidScale(difficulty.cs);
5156
- difficulty.cs = CircleSizeCalculator.oldDroidScaleToDroidCS(scale + CircleSizeCalculator.droidCSToOldDroidScale(4));
5157
- }
5158
- }
5159
- }
5160
-
5161
5240
  /**
5162
5241
  * Represents the SpunOut mod.
5163
5242
  */
@@ -5453,14 +5532,14 @@ class ModWindDown extends ModTimeRamp {
5453
5532
  this.osuRanked = false;
5454
5533
  this.initialRate = new DecimalModSetting("Initial rate", "The starting speed of the track.", 1, 0.51, 2, 0.01, 2);
5455
5534
  this.finalRate = new DecimalModSetting("Final rate", "The final speed to ramp to.", 0.75, 0.5, 1.99, 0.01, 2);
5456
- this.initialRate.bindValueChanged((_, value) => {
5457
- if (value <= this.finalRate.value) {
5458
- this.finalRate.value = value - this.finalRate.step;
5535
+ this.initialRate.bindValueChanged((value) => {
5536
+ if (value.newValue <= this.finalRate.value) {
5537
+ this.finalRate.value = value.newValue - this.finalRate.step;
5459
5538
  }
5460
5539
  });
5461
- this.finalRate.bindValueChanged((_, value) => {
5462
- if (value >= this.initialRate.value) {
5463
- this.initialRate.value = value + this.initialRate.step;
5540
+ this.finalRate.bindValueChanged((value) => {
5541
+ if (value.newValue >= this.initialRate.value) {
5542
+ this.initialRate.value = value.newValue + this.initialRate.step;
5464
5543
  }
5465
5544
  });
5466
5545
  }
@@ -5490,14 +5569,14 @@ class ModWindUp extends ModTimeRamp {
5490
5569
  this.osuRanked = false;
5491
5570
  this.initialRate = new DecimalModSetting("Initial rate", "The starting speed of the track.", 1, 0.5, 1.99, 0.01, 2);
5492
5571
  this.finalRate = new DecimalModSetting("Final rate", "The final speed to ramp to.", 1.5, 0.51, 2, 0.01, 2);
5493
- this.initialRate.bindValueChanged((_, value) => {
5494
- if (value >= this.finalRate.value) {
5495
- this.finalRate.value = value + this.finalRate.step;
5572
+ this.initialRate.bindValueChanged((value) => {
5573
+ if (value.newValue >= this.finalRate.value) {
5574
+ this.finalRate.value = value.newValue + this.finalRate.step;
5496
5575
  }
5497
5576
  });
5498
- this.finalRate.bindValueChanged((_, value) => {
5499
- if (value <= this.initialRate.value) {
5500
- this.initialRate.value = value - this.initialRate.step;
5577
+ this.finalRate.bindValueChanged((value) => {
5578
+ if (value.newValue <= this.initialRate.value) {
5579
+ this.initialRate.value = value.newValue - this.initialRate.step;
5501
5580
  }
5502
5581
  });
5503
5582
  }
@@ -5777,15 +5856,17 @@ class ModMap extends Map {
5777
5856
  return this.size === 0;
5778
5857
  }
5779
5858
  constructor(iterable) {
5859
+ // We are not passing `iterable` here to preserve mod-specific settings.
5860
+ super();
5780
5861
  if (Array.isArray(iterable)) {
5781
5862
  for (const [key, value] of iterable) {
5782
5863
  // Ensure the mod type corresponds to the mod instance.
5783
5864
  if (key !== value.constructor) {
5784
5865
  throw new TypeError(`Key ${key.name} does not match value ${value.constructor.name}`);
5785
5866
  }
5867
+ this.set(value);
5786
5868
  }
5787
5869
  }
5788
- super(iterable);
5789
5870
  }
5790
5871
  has(keyOrValue) {
5791
5872
  const key = keyOrValue instanceof Mod
@@ -5804,27 +5885,11 @@ class ModMap extends Map {
5804
5885
  throw new TypeError(`Key ${key.name} does not match value ${value.constructor.name}`);
5805
5886
  }
5806
5887
  const existing = this.get(key);
5807
- // If all difficulty statistics are set, all other difficulty adjusting mods are irrelevant, so we remove them.
5808
- // This prevents potential abuse cases where score multipliers from non-affecting mods stack (i.e., forcing
5809
- // all difficulty statistics while using the Hard Rock mod).
5810
- const removeDifficultyAdjustmentMods = value instanceof ModDifficultyAdjust &&
5811
- value.cs !== undefined &&
5812
- value.ar !== undefined &&
5813
- value.od !== undefined &&
5814
- value.hp !== undefined;
5815
- if (removeDifficultyAdjustmentMods) {
5816
- this.delete(ModEasy);
5817
- this.delete(ModHardRock);
5818
- this.delete(ModReallyEasy);
5819
- this.delete(ModSmallCircle);
5820
- }
5821
5888
  // Check if there are any mods that are incompatible with the new mod.
5822
5889
  // If so, remove them.
5823
- for (const incompatibleMod of value.incompatibleMods) {
5824
- for (const mod of this.values()) {
5825
- if (mod instanceof incompatibleMod) {
5826
- this.delete(mod.constructor);
5827
- }
5890
+ for (const mod of this.values()) {
5891
+ if (!value.isCompatibleWith(mod)) {
5892
+ this.delete(mod.constructor);
5828
5893
  }
5829
5894
  }
5830
5895
  super.set(key, value);
@@ -6329,7 +6394,10 @@ class PlayableBeatmap {
6329
6394
  this.hitObjects = baseBeatmap.hitObjects;
6330
6395
  this.maxCombo = baseBeatmap.maxCombo;
6331
6396
  this.mods = mods;
6332
- this.speedMultiplier = ModUtil.calculateRateWithMods(this.mods.values());
6397
+ this.speedMultiplier = ModUtil.calculateRateWithMods(this.mods.values(), Number.POSITIVE_INFINITY);
6398
+ }
6399
+ getOffsetTime(time) {
6400
+ return time + (this.formatVersion < 5 ? 24 : 0);
6333
6401
  }
6334
6402
  }
6335
6403
 
@@ -7079,13 +7147,6 @@ class Beatmap {
7079
7147
  .sort((a, b) => b.duration - a.duration)[0];
7080
7148
  return (_e = mostCommon === null || mostCommon === void 0 ? void 0 : mostCommon.beatLength) !== null && _e !== void 0 ? _e : 0;
7081
7149
  }
7082
- /**
7083
- * Returns a time combined with beatmap-wide time offset.
7084
- *
7085
- * BeatmapVersion 4 and lower had an incorrect offset. Stable has this set as 24ms off.
7086
- *
7087
- * @param time The time.
7088
- */
7089
7150
  getOffsetTime(time) {
7090
7151
  return time + (this.formatVersion < 5 ? 24 : 0);
7091
7152
  }