@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.
- package/dist/index.js +1646 -1585
- package/package.json +2 -2
- 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.
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
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
|
-
*
|
|
2761
|
-
*
|
|
2802
|
+
* All we need from spinners is their duration. The
|
|
2803
|
+
* position of a spinner is always at 256x192.
|
|
2762
2804
|
*/
|
|
2763
|
-
class
|
|
2764
|
-
|
|
2765
|
-
|
|
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
|
-
|
|
2773
|
-
|
|
2809
|
+
constructor(values) {
|
|
2810
|
+
super(Object.assign(Object.assign({}, values), { position: Playfield.baseSize.divide(2) }));
|
|
2811
|
+
this._endTime = values.endTime;
|
|
2774
2812
|
}
|
|
2775
|
-
|
|
2776
|
-
|
|
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
|
-
|
|
2779
|
-
|
|
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
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
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
|
|
2813
|
-
*
|
|
2814
|
-
* The value can be `null`, which is treated as a special case.
|
|
2837
|
+
* Represents a four-dimensional vector.
|
|
2815
2838
|
*/
|
|
2816
|
-
class
|
|
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
|
|
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
|
|
2823
|
-
return this.
|
|
2863
|
+
get left() {
|
|
2864
|
+
return this.x;
|
|
2824
2865
|
}
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
this.
|
|
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
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
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
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
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
|
-
*
|
|
2923
|
+
* Contains infromation about the position of a {@link HitObject}.
|
|
2871
2924
|
*/
|
|
2872
|
-
class
|
|
2873
|
-
|
|
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
|
|
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.
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
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
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
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
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
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
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
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
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
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
|
-
|
|
3015
|
-
|
|
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
|
-
|
|
3018
|
-
|
|
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 (
|
|
3021
|
-
|
|
3018
|
+
if ((a === 0 && Math.abs(b) < maximumError) ||
|
|
3019
|
+
(b === 0 && Math.abs(a) < maximumError)) {
|
|
3020
|
+
return true;
|
|
3022
3021
|
}
|
|
3023
|
-
return
|
|
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
|
-
*
|
|
3028
|
+
* Types of slider paths.
|
|
3029
3029
|
*/
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
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
|
-
*
|
|
3039
|
+
* Path approximator for sliders.
|
|
3056
3040
|
*/
|
|
3057
|
-
class
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
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
|
-
|
|
3074
|
-
|
|
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
|
-
|
|
3077
|
-
|
|
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
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
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
|
-
|
|
3095
|
-
|
|
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
|
-
|
|
3098
|
-
|
|
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
|
-
|
|
3101
|
-
|
|
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
|
-
|
|
3104
|
-
|
|
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
|
|
3295
|
+
* The amount of pieces to calculate for each control point quadruplet.
|
|
3121
3296
|
*/
|
|
3122
|
-
|
|
3297
|
+
PathApproximator.catmullDetail = 50;
|
|
3298
|
+
PathApproximator.circularArcTolerance = 0.1;
|
|
3123
3299
|
|
|
3124
3300
|
/**
|
|
3125
|
-
* Represents a
|
|
3126
|
-
|
|
3127
|
-
|
|
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
|
-
|
|
3136
|
-
|
|
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
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
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
|
-
|
|
3146
|
-
|
|
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
|
-
|
|
3149
|
-
|
|
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
|
-
|
|
3152
|
-
|
|
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
|
-
|
|
3155
|
-
|
|
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
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
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
|
-
|
|
3174
|
-
|
|
3175
|
-
this.
|
|
3176
|
-
this.
|
|
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.
|
|
3181
|
-
|
|
3182
|
-
this.z = z;
|
|
3183
|
-
this.w = w;
|
|
3457
|
+
path.push(this.interpolateVerticles(i, d1));
|
|
3458
|
+
return path;
|
|
3184
3459
|
}
|
|
3185
3460
|
/**
|
|
3186
|
-
*
|
|
3461
|
+
* Returns the progress of reaching expected distance.
|
|
3187
3462
|
*/
|
|
3188
|
-
|
|
3189
|
-
return this.
|
|
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
|
-
*
|
|
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
|
-
|
|
3195
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
3201
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
3207
|
-
|
|
3583
|
+
static getSliderRotation(slider) {
|
|
3584
|
+
const pathEndPosition = slider.path.positionAt(1);
|
|
3585
|
+
return Math.atan2(pathEndPosition.y, pathEndPosition.x);
|
|
3208
3586
|
}
|
|
3209
3587
|
/**
|
|
3210
|
-
*
|
|
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
|
-
|
|
3213
|
-
|
|
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
|
-
*
|
|
3602
|
+
* Reflects the position of a {@link HitObject} horizontally along the playfield.
|
|
3603
|
+
*
|
|
3604
|
+
* @param hitObject The {@link HitObject} to reflect.
|
|
3217
3605
|
*/
|
|
3218
|
-
|
|
3219
|
-
|
|
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
|
-
*
|
|
3613
|
+
* Reflects the position of a {@link HitObject} vertically along the playfield.
|
|
3614
|
+
*
|
|
3615
|
+
* @param hitObject The {@link HitObject} to reflect.
|
|
3223
3616
|
*/
|
|
3224
|
-
|
|
3225
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
3231
|
-
|
|
3629
|
+
static flipSliderInPlaceHorizontally(slider) {
|
|
3630
|
+
this.modifySlider(slider, (v) => new Vector2(-v.x, v.y));
|
|
3232
3631
|
}
|
|
3233
3632
|
/**
|
|
3234
|
-
*
|
|
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
|
-
|
|
3237
|
-
|
|
3638
|
+
static rotateSlider(slider, rotation) {
|
|
3639
|
+
this.modifySlider(slider, (v) => this.rotateVector(v, rotation));
|
|
3238
3640
|
}
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
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
|
-
*
|
|
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
|
|
3299
|
-
* @
|
|
3300
|
-
*
|
|
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
|
|
3303
|
-
|
|
3304
|
-
|
|
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
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
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
|
-
*
|
|
3724
|
+
* Determines whether a {@link HitObject} is on a beat.
|
|
3319
3725
|
*
|
|
3320
|
-
* @param
|
|
3321
|
-
* @param
|
|
3322
|
-
* @param
|
|
3323
|
-
* @
|
|
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
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
if (
|
|
3331
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
3426
|
-
const
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
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
|
|
3479
|
-
let
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
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
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
const
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
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
|
-
*
|
|
3839
|
+
* Moves the modified position of a {@link Circle} so that it fits inside the playfield.
|
|
3509
3840
|
*
|
|
3510
|
-
*
|
|
3511
|
-
*
|
|
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
|
|
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
|
|
3516
|
-
|
|
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
|
-
*
|
|
3894
|
+
* Clamps a {@link Vector2} into the playfield, keeping a specified distance from the edge of the playfield.
|
|
3520
3895
|
*
|
|
3521
|
-
*
|
|
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
|
-
*
|
|
3524
|
-
*
|
|
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
|
|
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
|
|
3530
|
-
for (let i =
|
|
3531
|
-
const
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
const
|
|
3535
|
-
|
|
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
|
-
*
|
|
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
|
|
3549
|
-
* @
|
|
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
|
|
3555
|
-
const
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
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
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
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
|
-
*
|
|
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
|
|
3577
|
-
* @param
|
|
3578
|
-
* @
|
|
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
|
|
3583
|
-
const
|
|
3584
|
-
|
|
3585
|
-
|
|
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
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
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
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
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
|
|
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
|
|
3629
|
-
constructor(
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
this.
|
|
3634
|
-
|
|
3635
|
-
|
|
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
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
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
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
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
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
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
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
return
|
|
4130
|
+
get isOsuRelevant() {
|
|
4131
|
+
return true;
|
|
4132
|
+
}
|
|
4133
|
+
get osuScoreMultiplier() {
|
|
4134
|
+
return 1.06;
|
|
3790
4135
|
}
|
|
3791
|
-
|
|
3792
|
-
|
|
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
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
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
|
-
|
|
3813
|
-
|
|
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
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
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
|
|
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
|
-
*
|
|
4168
|
+
* Represents the Easy mod.
|
|
3851
4169
|
*/
|
|
3852
|
-
class
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
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
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
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
|
-
|
|
3896
|
-
|
|
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
|
-
|
|
3899
|
-
|
|
4202
|
+
difficulty.ar /= 2;
|
|
4203
|
+
difficulty.od /= 2;
|
|
4204
|
+
difficulty.hp /= 2;
|
|
3900
4205
|
}
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
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
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
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
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
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
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
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
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3967
|
-
|
|
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
|
-
|
|
3974
|
-
return new
|
|
4295
|
+
migrateDroidMod(difficulty) {
|
|
4296
|
+
return new ModDifficultyAdjust({ cs: difficulty.cs + 4 });
|
|
3975
4297
|
}
|
|
3976
|
-
|
|
3977
|
-
|
|
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
|
-
|
|
3980
|
-
|
|
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
|
-
*
|
|
3983
|
-
* {@link HitObject}s are positioned.
|
|
4322
|
+
* The number of decimal places to round the value to.
|
|
3984
4323
|
*
|
|
3985
|
-
*
|
|
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
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
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
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
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 (
|
|
4034
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4064
|
-
|
|
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
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
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
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
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
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
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
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
//
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
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
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
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
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
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
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
this.
|
|
4300
|
-
|
|
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
|
|
4549
|
+
* Represents the NightCore mod.
|
|
4306
4550
|
*/
|
|
4307
|
-
class
|
|
4551
|
+
class ModNightCore extends ModRateAdjust {
|
|
4308
4552
|
constructor() {
|
|
4309
|
-
super();
|
|
4310
|
-
this.
|
|
4311
|
-
this.
|
|
4312
|
-
this.droidRanked =
|
|
4313
|
-
this.osuRanked =
|
|
4314
|
-
|
|
4315
|
-
|
|
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
|
|
4562
|
+
return this.isRelevant;
|
|
4322
4563
|
}
|
|
4323
4564
|
calculateDroidScoreMultiplier() {
|
|
4324
|
-
return
|
|
4565
|
+
return this.droidScoreMultiplier;
|
|
4325
4566
|
}
|
|
4326
4567
|
get isOsuRelevant() {
|
|
4327
|
-
return
|
|
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
|
|
4576
|
+
* Represents the HalfTime mod.
|
|
4380
4577
|
*/
|
|
4381
|
-
class
|
|
4578
|
+
class ModHalfTime extends ModRateAdjust {
|
|
4382
4579
|
constructor() {
|
|
4383
|
-
super();
|
|
4384
|
-
this.acronym = "
|
|
4385
|
-
this.name = "
|
|
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 <<
|
|
4389
|
-
this.incompatibleMods.add(
|
|
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
|
|
4589
|
+
return this.isRelevant;
|
|
4394
4590
|
}
|
|
4395
4591
|
calculateDroidScoreMultiplier() {
|
|
4396
|
-
return
|
|
4592
|
+
return this.droidScoreMultiplier;
|
|
4397
4593
|
}
|
|
4398
4594
|
get isOsuRelevant() {
|
|
4399
|
-
return
|
|
4595
|
+
return this.isRelevant;
|
|
4400
4596
|
}
|
|
4401
4597
|
get osuScoreMultiplier() {
|
|
4402
|
-
return
|
|
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
|
|
4603
|
+
* Represents the DoubleTime mod.
|
|
4428
4604
|
*/
|
|
4429
|
-
class
|
|
4605
|
+
class ModDoubleTime extends ModRateAdjust {
|
|
4430
4606
|
constructor() {
|
|
4431
|
-
super();
|
|
4432
|
-
this.acronym = "
|
|
4433
|
-
this.name = "
|
|
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 <<
|
|
4437
|
-
this.incompatibleMods.add(
|
|
4612
|
+
this.bitwise = 1 << 6;
|
|
4613
|
+
this.incompatibleMods.add(ModHalfTime).add(ModNightCore);
|
|
4438
4614
|
}
|
|
4439
4615
|
get isDroidRelevant() {
|
|
4440
|
-
return
|
|
4616
|
+
return this.isRelevant;
|
|
4441
4617
|
}
|
|
4442
4618
|
calculateDroidScoreMultiplier() {
|
|
4443
|
-
return
|
|
4619
|
+
return this.droidScoreMultiplier;
|
|
4444
4620
|
}
|
|
4445
4621
|
get isOsuRelevant() {
|
|
4446
|
-
return
|
|
4622
|
+
return this.isRelevant;
|
|
4447
4623
|
}
|
|
4448
4624
|
get osuScoreMultiplier() {
|
|
4449
|
-
return
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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
|
|
5824
|
-
|
|
5825
|
-
|
|
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
|
}
|