@rian8337/osu-base 4.0.0-beta.66 → 4.0.0-beta.68
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 +1798 -467
- package/package.json +2 -2
- package/typings/index.d.ts +688 -85
package/dist/index.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var node_path = require('node:path');
|
|
4
|
-
|
|
5
3
|
/**
|
|
6
4
|
* Some math utility functions.
|
|
7
5
|
*/
|
|
@@ -788,68 +786,114 @@ class BeatmapDifficulty {
|
|
|
788
786
|
*/
|
|
789
787
|
class CircleSizeCalculator {
|
|
790
788
|
/**
|
|
791
|
-
* Converts osu!droid
|
|
789
|
+
* Converts osu!droid circle size to osu!droid scale.
|
|
790
|
+
*
|
|
791
|
+
* @param cs The circle size to convert.
|
|
792
|
+
* @returns The calculated osu!droid scale.
|
|
793
|
+
*/
|
|
794
|
+
static droidCSToDroidScale(cs) {
|
|
795
|
+
// 6.8556344386 was derived by converting the old osu!droid gameplay scale unit into osu!pixels (by dividing it
|
|
796
|
+
// with (height / 480)) and then fitting the function to the osu!standard scale function. The height in the old
|
|
797
|
+
// osu!droid gameplay scale function was set to 576, which was chosen after sampling the top 100 most used
|
|
798
|
+
// devices by players from Firebase. This is done to ensure that the new scale is as close to the old scale as
|
|
799
|
+
// possible for most players.
|
|
800
|
+
// The fitting of both functions can be found under the following graph: https://www.desmos.com/calculator/rjfxqc3yic
|
|
801
|
+
return Math.max(1e-3, this.standardCSToStandardScale(cs - 6.8556344386, true));
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Converts osu!droid scale to osu!droid circle size.
|
|
805
|
+
*
|
|
806
|
+
* @param scale The osu!droid scale to convert.
|
|
807
|
+
* @returns The calculated osu!droid circle size.
|
|
808
|
+
*/
|
|
809
|
+
static droidScaleToDroidCS(scale) {
|
|
810
|
+
return (this.standardScaleToStandardCS(Math.max(1e-3, scale), true) +
|
|
811
|
+
6.8556344386);
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Converts osu!droid CS to old osu!droid scale.
|
|
792
815
|
*
|
|
793
816
|
* @param cs The CS to convert.
|
|
794
817
|
* @param mods The mods to apply.
|
|
795
818
|
* @returns The calculated osu!droid scale.
|
|
796
819
|
*/
|
|
797
|
-
static
|
|
820
|
+
static droidCSToOldDroidScale(cs, mods) {
|
|
798
821
|
// Create a dummy beatmap difficulty for circle size calculation.
|
|
799
822
|
const difficulty = new BeatmapDifficulty();
|
|
800
823
|
difficulty.cs = cs;
|
|
801
824
|
if (mods !== undefined) {
|
|
825
|
+
const adjustmentMods = new ModMap();
|
|
826
|
+
for (const mod of mods.values()) {
|
|
827
|
+
if (mod.facilitatesAdjustment()) {
|
|
828
|
+
adjustmentMods.set(mod);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
802
831
|
for (const mod of mods.values()) {
|
|
803
832
|
if (mod.isApplicableToDifficulty()) {
|
|
804
|
-
mod.applyToDifficulty(exports.Modes.droid, difficulty);
|
|
833
|
+
mod.applyToDifficulty(exports.Modes.droid, difficulty, adjustmentMods);
|
|
805
834
|
}
|
|
806
835
|
}
|
|
807
836
|
for (const mod of mods.values()) {
|
|
808
|
-
if (mod.
|
|
809
|
-
mod.
|
|
837
|
+
if (mod.isApplicableToDifficultyWithMods()) {
|
|
838
|
+
mod.applyToDifficultyWithMods(exports.Modes.droid, difficulty, mods);
|
|
810
839
|
}
|
|
811
840
|
}
|
|
812
841
|
}
|
|
813
|
-
return Math.max(((this.
|
|
814
|
-
(54.42 - difficulty.cs * 4.48)
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
(0.5 * (11 - 5.2450170716245195)) / 5, 1e-3);
|
|
842
|
+
return Math.max(((this.oldAssumedDroidHeight / 480) *
|
|
843
|
+
(54.42 - difficulty.cs * 4.48)) /
|
|
844
|
+
HitObject.baseRadius +
|
|
845
|
+
this.oldDroidScaleMultiplier, 1e-3);
|
|
818
846
|
}
|
|
819
847
|
/**
|
|
820
|
-
* Converts osu!droid scale to osu!droid circle size.
|
|
848
|
+
* Converts old osu!droid scale to osu!droid circle size.
|
|
821
849
|
*
|
|
822
850
|
* @param scale The osu!droid scale to convert.
|
|
823
851
|
* @returns The calculated osu!droid circle size.
|
|
824
852
|
*/
|
|
825
|
-
static
|
|
853
|
+
static oldDroidScaleToDroidCS(scale) {
|
|
826
854
|
return ((54.42 -
|
|
827
|
-
((
|
|
828
|
-
|
|
829
|
-
128) /
|
|
830
|
-
2) *
|
|
855
|
+
((Math.max(1e-3, scale) - this.oldDroidScaleMultiplier) *
|
|
856
|
+
HitObject.baseRadius *
|
|
831
857
|
480) /
|
|
832
|
-
this.
|
|
858
|
+
this.oldAssumedDroidHeight) /
|
|
833
859
|
4.48);
|
|
834
860
|
}
|
|
835
861
|
/**
|
|
836
|
-
* Converts osu!droid scale to osu!
|
|
862
|
+
* Converts old osu!droid difficulty scale that is in **screen pixels** to **osu!pixels**.
|
|
863
|
+
*
|
|
864
|
+
* @param scale The osu!droid scale to convert.
|
|
865
|
+
* @returns The converted scale.
|
|
866
|
+
*/
|
|
867
|
+
static oldDroidScaleScreenPixelsToOsuPixels(scale) {
|
|
868
|
+
return (scale * 480) / this.oldAssumedDroidHeight;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Converts old osu!droid scale that is in **osu!pixels** to **screen pixels**.
|
|
872
|
+
*
|
|
873
|
+
* @param scale The osu!droid scale to convert.
|
|
874
|
+
* @returns The converted scale.
|
|
875
|
+
*/
|
|
876
|
+
static oldDroidScaleOsuPixelsToScreenPixels(scale) {
|
|
877
|
+
return (scale * this.oldAssumedDroidHeight) / 480;
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Converts old osu!droid scale to osu!standard radius.
|
|
837
881
|
*
|
|
838
882
|
* @param scale The osu!droid scale to convert.
|
|
839
883
|
* @returns The osu!standard radius of the given osu!droid scale.
|
|
840
884
|
*/
|
|
841
|
-
static
|
|
885
|
+
static oldDroidScaleToStandardRadius(scale) {
|
|
842
886
|
return ((HitObject.baseRadius * Math.max(1e-3, scale)) /
|
|
843
|
-
((this.
|
|
887
|
+
((this.oldAssumedDroidHeight * 0.85) / 384));
|
|
844
888
|
}
|
|
845
889
|
/**
|
|
846
|
-
* Converts osu!standard radius to osu!droid scale.
|
|
890
|
+
* Converts osu!standard radius to old osu!droid scale.
|
|
847
891
|
*
|
|
848
892
|
* @param radius The osu!standard radius to convert.
|
|
849
893
|
* @returns The osu!droid scale of the given osu!standard radius.
|
|
850
894
|
*/
|
|
851
|
-
static
|
|
852
|
-
return Math.max(1e-3, (radius * ((this.
|
|
895
|
+
static standardRadiusToOldDroidScale(radius) {
|
|
896
|
+
return Math.max(1e-3, (radius * ((this.oldAssumedDroidHeight * 0.85) / 384)) /
|
|
853
897
|
HitObject.baseRadius);
|
|
854
898
|
}
|
|
855
899
|
/**
|
|
@@ -892,26 +936,16 @@ class CircleSizeCalculator {
|
|
|
892
936
|
return this.standardScaleToStandardCS(radius / HitObject.baseRadius, applyFudge);
|
|
893
937
|
}
|
|
894
938
|
/**
|
|
895
|
-
* Converts osu!standard scale to osu!droid scale.
|
|
939
|
+
* Converts osu!standard scale to old osu!droid scale.
|
|
896
940
|
*
|
|
897
941
|
* @param scale The osu!standard scale to convert.
|
|
898
942
|
* @param applyFudge Whether to apply a fudge that was historically applied to osu!standard. Defaults to `false`.
|
|
899
|
-
* @returns The osu!droid scale of the given osu!standard scale.
|
|
943
|
+
* @returns The old osu!droid scale of the given osu!standard scale.
|
|
900
944
|
*/
|
|
901
|
-
static
|
|
902
|
-
return this.
|
|
945
|
+
static standardScaleToOldDroidScale(scale, applyFudge = false) {
|
|
946
|
+
return this.standardRadiusToOldDroidScale((HitObject.baseRadius * scale) /
|
|
903
947
|
(applyFudge ? this.brokenGamefieldRoundingAllowance : 1));
|
|
904
948
|
}
|
|
905
|
-
/**
|
|
906
|
-
* Converts osu!standard circle size to osu!droid scale.
|
|
907
|
-
*
|
|
908
|
-
* @param cs The osu!standard circle size to convert.
|
|
909
|
-
* @param applyFudge Whether to apply a fudge that was historically applied to osu!standard. Defaults to `false`.
|
|
910
|
-
* @returns The osu!droid scale of the given osu!droid scale.
|
|
911
|
-
*/
|
|
912
|
-
static standardCSToDroidScale(cs, applyFudge = false) {
|
|
913
|
-
return this.standardScaleToDroidScale(this.standardCSToStandardScale(cs, applyFudge));
|
|
914
|
-
}
|
|
915
949
|
}
|
|
916
950
|
/**
|
|
917
951
|
* The following comment is copied verbatim from osu!lazer and osu!stable:
|
|
@@ -925,10 +959,11 @@ class CircleSizeCalculator {
|
|
|
925
959
|
*/
|
|
926
960
|
CircleSizeCalculator.brokenGamefieldRoundingAllowance = 1.00041;
|
|
927
961
|
/**
|
|
928
|
-
*
|
|
929
|
-
*
|
|
962
|
+
* This was not the real height that is used in the game, but rather an assumption so that we can treat circle sizes
|
|
963
|
+
* similarly across all devices. This is used in difficulty calculation.
|
|
930
964
|
*/
|
|
931
|
-
CircleSizeCalculator.
|
|
965
|
+
CircleSizeCalculator.oldAssumedDroidHeight = 681;
|
|
966
|
+
CircleSizeCalculator.oldDroidScaleMultiplier = (0.5 * (11 - 5.2450170716245195)) / 5;
|
|
932
967
|
|
|
933
968
|
/**
|
|
934
969
|
* Represents a hitobject in a beatmap.
|
|
@@ -1090,13 +1125,9 @@ class HitObject {
|
|
|
1090
1125
|
this.timeFadeIn =
|
|
1091
1126
|
400 * Math.min(1, this.timePreempt / HitObject.preemptMin);
|
|
1092
1127
|
switch (mode) {
|
|
1093
|
-
case exports.Modes.droid:
|
|
1094
|
-
|
|
1095
|
-
const radius = CircleSizeCalculator.droidScaleToStandardRadius(droidScale);
|
|
1096
|
-
const cs = CircleSizeCalculator.standardRadiusToStandardCS(radius, true);
|
|
1097
|
-
this.scale = CircleSizeCalculator.standardCSToStandardScale(cs, true);
|
|
1128
|
+
case exports.Modes.droid:
|
|
1129
|
+
this.scale = CircleSizeCalculator.droidCSToDroidScale(difficulty.cs);
|
|
1098
1130
|
break;
|
|
1099
|
-
}
|
|
1100
1131
|
case exports.Modes.osu:
|
|
1101
1132
|
this.scale = CircleSizeCalculator.standardCSToStandardScale(difficulty.cs, true);
|
|
1102
1133
|
break;
|
|
@@ -1142,14 +1173,7 @@ class HitObject {
|
|
|
1142
1173
|
* @returns The stack offset with respect to the gamemode.
|
|
1143
1174
|
*/
|
|
1144
1175
|
getStackOffset(mode) {
|
|
1145
|
-
|
|
1146
|
-
case exports.Modes.droid:
|
|
1147
|
-
return new Vector2(this.stackHeight *
|
|
1148
|
-
CircleSizeCalculator.standardScaleToDroidScale(this.scale, true) *
|
|
1149
|
-
4);
|
|
1150
|
-
case exports.Modes.osu:
|
|
1151
|
-
return new Vector2(this.stackHeight * this.scale * -6.4);
|
|
1152
|
-
}
|
|
1176
|
+
return new Vector2(this.stackHeight * this.scale * (mode === exports.Modes.droid ? -4 : -6.4));
|
|
1153
1177
|
}
|
|
1154
1178
|
/**
|
|
1155
1179
|
* Evaluates the stacked position of the hitobject.
|
|
@@ -1239,16 +1263,101 @@ HitObject.preemptMin = 450;
|
|
|
1239
1263
|
*/
|
|
1240
1264
|
HitObject.controlPointLeniency = 1;
|
|
1241
1265
|
|
|
1266
|
+
/**
|
|
1267
|
+
* Represents a `Mod` specific setting.
|
|
1268
|
+
*/
|
|
1269
|
+
class ModSetting {
|
|
1270
|
+
/**
|
|
1271
|
+
* The value of this `ModSetting`.
|
|
1272
|
+
*/
|
|
1273
|
+
get value() {
|
|
1274
|
+
return this._value;
|
|
1275
|
+
}
|
|
1276
|
+
set value(value) {
|
|
1277
|
+
if (this._value !== value) {
|
|
1278
|
+
const oldValue = this._value;
|
|
1279
|
+
this._value = value;
|
|
1280
|
+
for (const listener of this.valueChangedListeners) {
|
|
1281
|
+
listener(oldValue, value);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Whether this `ModSetting` is set to its default value.
|
|
1287
|
+
*/
|
|
1288
|
+
get isDefault() {
|
|
1289
|
+
return this._value === this.defaultValue;
|
|
1290
|
+
}
|
|
1291
|
+
constructor(name, description, defaultValue) {
|
|
1292
|
+
/**
|
|
1293
|
+
* The formatter to display the value of this `ModSetting`.
|
|
1294
|
+
*/
|
|
1295
|
+
this.displayFormatter = (v) => `${v}`;
|
|
1296
|
+
this.valueChangedListeners = new Set();
|
|
1297
|
+
this.name = name;
|
|
1298
|
+
this.description = description;
|
|
1299
|
+
this.defaultValue = defaultValue;
|
|
1300
|
+
this._value = defaultValue;
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Returns a string representation of this `ModSetting`'s value.
|
|
1304
|
+
*
|
|
1305
|
+
* @returns A string representation of this `ModSetting`'s value.
|
|
1306
|
+
*/
|
|
1307
|
+
toDisplayString() {
|
|
1308
|
+
return this.displayFormatter(this.value);
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Binds an action that will be called when the value of this `ModSetting` changes.
|
|
1312
|
+
*
|
|
1313
|
+
* @param listener The action to call when the value of this `ModSetting` changes.
|
|
1314
|
+
* @param runOnceImmediately Whether to call the action immediately with the current value of this `ModSetting`.
|
|
1315
|
+
*/
|
|
1316
|
+
bindValueChanged(listener, runOnceImmediately = false) {
|
|
1317
|
+
this.valueChangedListeners.add(listener);
|
|
1318
|
+
if (runOnceImmediately) {
|
|
1319
|
+
listener(this.value, this.value);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Unbinds an action that was previously bound to this `ModSetting`.
|
|
1324
|
+
*
|
|
1325
|
+
* @param listener The action to unbind.
|
|
1326
|
+
*/
|
|
1327
|
+
unbindValueChanged(listener) {
|
|
1328
|
+
this.valueChangedListeners.delete(listener);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1242
1332
|
/**
|
|
1243
1333
|
* Represents a mod.
|
|
1244
1334
|
*/
|
|
1245
1335
|
class Mod {
|
|
1246
1336
|
constructor() {
|
|
1337
|
+
/**
|
|
1338
|
+
* Whether this `Mod` is playable by a real human user.
|
|
1339
|
+
*
|
|
1340
|
+
* Should be `false` for cases where the user is not meant to apply the `Mod` by themselves.
|
|
1341
|
+
*/
|
|
1342
|
+
this.userPlayable = true;
|
|
1247
1343
|
/**
|
|
1248
1344
|
* `Mod`s that are incompatible with this `Mod`.
|
|
1249
1345
|
*/
|
|
1250
1346
|
this.incompatibleMods = new Set();
|
|
1251
1347
|
}
|
|
1348
|
+
/**
|
|
1349
|
+
* `ModSetting`s that are specific to this `Mod`.
|
|
1350
|
+
*/
|
|
1351
|
+
get settings() {
|
|
1352
|
+
const settings = [];
|
|
1353
|
+
for (const prop in this) {
|
|
1354
|
+
const value = this[prop];
|
|
1355
|
+
if (value instanceof ModSetting) {
|
|
1356
|
+
settings.push(value);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return settings;
|
|
1360
|
+
}
|
|
1252
1361
|
/**
|
|
1253
1362
|
* Serializes this `Mod` to a `SerializedMod`.
|
|
1254
1363
|
*/
|
|
@@ -1307,8 +1416,8 @@ class Mod {
|
|
|
1307
1416
|
/**
|
|
1308
1417
|
* Whether this `Mod` can be applied to a `BeatmapDifficulty` relative to other `Mod`s and settings.
|
|
1309
1418
|
*/
|
|
1310
|
-
|
|
1311
|
-
return "
|
|
1419
|
+
isApplicableToDifficultyWithMods() {
|
|
1420
|
+
return "applyToDifficultyWithMods" in this;
|
|
1312
1421
|
}
|
|
1313
1422
|
/**
|
|
1314
1423
|
* Whether this `Mod` can be applied to a `HitObject`.
|
|
@@ -1319,8 +1428,8 @@ class Mod {
|
|
|
1319
1428
|
/**
|
|
1320
1429
|
* Whether this `Mod` can be applied to a `HitObject` relative to other `Mod`s and settings.
|
|
1321
1430
|
*/
|
|
1322
|
-
|
|
1323
|
-
return "
|
|
1431
|
+
isApplicableToHitObjectWithMods() {
|
|
1432
|
+
return "applyToHitObjectWithMods" in this;
|
|
1324
1433
|
}
|
|
1325
1434
|
/**
|
|
1326
1435
|
* Whether this `Mod` can be applied to a track's playback rate.
|
|
@@ -1334,6 +1443,12 @@ class Mod {
|
|
|
1334
1443
|
isMigratableDroidMod() {
|
|
1335
1444
|
return "migrateDroidMod" in this;
|
|
1336
1445
|
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Whether this `Mod` facilitates adjustment to a `HitObject` or `BeatmapDifficulty`.
|
|
1448
|
+
*/
|
|
1449
|
+
facilitatesAdjustment() {
|
|
1450
|
+
return "facilitateAdjustment" in this;
|
|
1451
|
+
}
|
|
1337
1452
|
/**
|
|
1338
1453
|
* Serializes the settings of this `Mod` to an object that can be converted to a JSON.
|
|
1339
1454
|
*
|
|
@@ -1342,6 +1457,15 @@ class Mod {
|
|
|
1342
1457
|
serializeSettings() {
|
|
1343
1458
|
return null;
|
|
1344
1459
|
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Compares this `Mod` to another `Mod` for equality.
|
|
1462
|
+
*
|
|
1463
|
+
* @param other The object to compare to.
|
|
1464
|
+
* @returns `true` if the object is the same `Mod`, `false` otherwise.
|
|
1465
|
+
*/
|
|
1466
|
+
equals(other) {
|
|
1467
|
+
return this === other || this.acronym === other.acronym;
|
|
1468
|
+
}
|
|
1345
1469
|
/**
|
|
1346
1470
|
* Returns the string representation of this `Mod`.
|
|
1347
1471
|
*/
|
|
@@ -1431,6 +1555,72 @@ class ModAuto extends Mod {
|
|
|
1431
1555
|
}
|
|
1432
1556
|
}
|
|
1433
1557
|
|
|
1558
|
+
/**
|
|
1559
|
+
* Represents a `Mod` specific setting that is constrained to a range of values.
|
|
1560
|
+
*/
|
|
1561
|
+
class RangeConstrainedModSetting extends ModSetting {
|
|
1562
|
+
get value() {
|
|
1563
|
+
return super.value;
|
|
1564
|
+
}
|
|
1565
|
+
set value(value) {
|
|
1566
|
+
super.value = this.processValue(value);
|
|
1567
|
+
}
|
|
1568
|
+
constructor(name, description, defaultValue, min, max, step) {
|
|
1569
|
+
super(name, description, defaultValue);
|
|
1570
|
+
this.min = min;
|
|
1571
|
+
this.max = max;
|
|
1572
|
+
this.step = step;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* Represents a `Mod` specific setting that is constrained to a number of values.
|
|
1578
|
+
*/
|
|
1579
|
+
class NumberModSetting extends RangeConstrainedModSetting {
|
|
1580
|
+
constructor(name, description, defaultValue, min, max, step) {
|
|
1581
|
+
super(name, description, defaultValue, min, max, step);
|
|
1582
|
+
this.displayFormatter = (v) => v.toString();
|
|
1583
|
+
if (min > max) {
|
|
1584
|
+
throw new RangeError(`The minimum value (${min}) must be less than or equal to the maximum value (${max}).`);
|
|
1585
|
+
}
|
|
1586
|
+
if (step < 0) {
|
|
1587
|
+
throw new RangeError(`The step size (${step}) must be greater than or equal to 0.`);
|
|
1588
|
+
}
|
|
1589
|
+
if (defaultValue < min || defaultValue > max) {
|
|
1590
|
+
throw new RangeError(`The default value (${defaultValue}) must be between the minimum (${min}) and maximum (${max}) values.`);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
processValue(value) {
|
|
1594
|
+
return MathUtils.clamp(Math.round(value / this.step) * this.step, this.min, this.max);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/**
|
|
1599
|
+
* Represents a `Mod` specific setting that is constrained to a range of decimal values.
|
|
1600
|
+
*/
|
|
1601
|
+
class DecimalModSetting extends NumberModSetting {
|
|
1602
|
+
constructor(name, description, defaultValue, min = -Number.MAX_VALUE, max = Number.MAX_VALUE, step = 0, precision = null) {
|
|
1603
|
+
super(name, description, defaultValue, min, max, step);
|
|
1604
|
+
this.displayFormatter = (v) => {
|
|
1605
|
+
if (this.precision !== null) {
|
|
1606
|
+
return v.toFixed(this.precision);
|
|
1607
|
+
}
|
|
1608
|
+
return super.toDisplayString();
|
|
1609
|
+
};
|
|
1610
|
+
if (precision !== null && precision < 0) {
|
|
1611
|
+
throw new RangeError(`The precision (${precision}) must be greater than or equal to 0.`);
|
|
1612
|
+
}
|
|
1613
|
+
this.precision = precision;
|
|
1614
|
+
}
|
|
1615
|
+
processValue(value) {
|
|
1616
|
+
const processedValue = super.processValue(value);
|
|
1617
|
+
if (this.precision !== null) {
|
|
1618
|
+
return parseFloat(processedValue.toFixed(this.precision));
|
|
1619
|
+
}
|
|
1620
|
+
return processedValue;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1434
1624
|
/**
|
|
1435
1625
|
* Represents a `Mod` that adjusts the playback rate of a track.
|
|
1436
1626
|
*/
|
|
@@ -1439,18 +1629,27 @@ class ModRateAdjust extends Mod {
|
|
|
1439
1629
|
* The generic osu!droid score multiplier of this `Mod`.
|
|
1440
1630
|
*/
|
|
1441
1631
|
get droidScoreMultiplier() {
|
|
1442
|
-
return this.trackRateMultiplier >= 1
|
|
1443
|
-
? 1 + (this.trackRateMultiplier - 1) * 0.24
|
|
1444
|
-
: Math.pow(0.3, (1 - this.trackRateMultiplier) * 4);
|
|
1632
|
+
return this.trackRateMultiplier.value >= 1
|
|
1633
|
+
? 1 + (this.trackRateMultiplier.value - 1) * 0.24
|
|
1634
|
+
: Math.pow(0.3, (1 - this.trackRateMultiplier.value) * 4);
|
|
1445
1635
|
}
|
|
1446
1636
|
/**
|
|
1447
1637
|
* Generic getter to determine if this `ModRateAdjust` is relevant.
|
|
1448
1638
|
*/
|
|
1449
1639
|
get isRelevant() {
|
|
1450
|
-
return this.trackRateMultiplier !== 1;
|
|
1640
|
+
return this.trackRateMultiplier.value !== 1;
|
|
1451
1641
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1642
|
+
constructor(trackRateMultiplier = 1) {
|
|
1643
|
+
super();
|
|
1644
|
+
this.trackRateMultiplier = new DecimalModSetting("Track rate multiplier", "The multiplier for the track's playback rate after applying this mod.", trackRateMultiplier, 0.5, 2, 0.05, 2);
|
|
1645
|
+
}
|
|
1646
|
+
applyToRate(_, rate) {
|
|
1647
|
+
return rate * this.trackRateMultiplier.value;
|
|
1648
|
+
}
|
|
1649
|
+
equals(other) {
|
|
1650
|
+
return (super.equals(other) &&
|
|
1651
|
+
other instanceof ModRateAdjust &&
|
|
1652
|
+
this.trackRateMultiplier.value === other.trackRateMultiplier.value);
|
|
1454
1653
|
}
|
|
1455
1654
|
}
|
|
1456
1655
|
|
|
@@ -1460,19 +1659,18 @@ class ModRateAdjust extends Mod {
|
|
|
1460
1659
|
* This is a replacement `Mod` for speed modify in osu!droid and custom rates in osu!lazer.
|
|
1461
1660
|
*/
|
|
1462
1661
|
class ModCustomSpeed extends ModRateAdjust {
|
|
1463
|
-
constructor(
|
|
1464
|
-
super();
|
|
1662
|
+
constructor() {
|
|
1663
|
+
super(...arguments);
|
|
1465
1664
|
this.acronym = "CS";
|
|
1466
1665
|
this.name = "Custom Speed";
|
|
1467
1666
|
this.droidRanked = true;
|
|
1468
1667
|
this.osuRanked = false;
|
|
1469
|
-
this.trackRateMultiplier = trackRateMultiplier;
|
|
1470
1668
|
}
|
|
1471
1669
|
copySettings(mod) {
|
|
1472
1670
|
var _a, _b;
|
|
1473
1671
|
super.copySettings(mod);
|
|
1474
|
-
this.trackRateMultiplier =
|
|
1475
|
-
(_b = (_a = mod.settings) === null || _a === void 0 ? void 0 : _a.rateMultiplier) !== null && _b !== void 0 ? _b : this.trackRateMultiplier;
|
|
1672
|
+
this.trackRateMultiplier.value =
|
|
1673
|
+
(_b = (_a = mod.settings) === null || _a === void 0 ? void 0 : _a.rateMultiplier) !== null && _b !== void 0 ? _b : this.trackRateMultiplier.value;
|
|
1476
1674
|
}
|
|
1477
1675
|
get isDroidRelevant() {
|
|
1478
1676
|
return this.isRelevant;
|
|
@@ -1485,16 +1683,18 @@ class ModCustomSpeed extends ModRateAdjust {
|
|
|
1485
1683
|
}
|
|
1486
1684
|
get osuScoreMultiplier() {
|
|
1487
1685
|
// Round to the nearest multiple of 0.1.
|
|
1488
|
-
let value = Math.trunc(this.trackRateMultiplier * 10) / 10;
|
|
1686
|
+
let value = Math.trunc(this.trackRateMultiplier.value * 10) / 10;
|
|
1489
1687
|
// Offset back to 0.
|
|
1490
1688
|
--value;
|
|
1491
|
-
return this.trackRateMultiplier >= 1
|
|
1689
|
+
return this.trackRateMultiplier.value >= 1
|
|
1690
|
+
? 1 + value / 5
|
|
1691
|
+
: 0.6 + value;
|
|
1492
1692
|
}
|
|
1493
1693
|
serializeSettings() {
|
|
1494
|
-
return { rateMultiplier: this.trackRateMultiplier };
|
|
1694
|
+
return { rateMultiplier: this.trackRateMultiplier.value };
|
|
1495
1695
|
}
|
|
1496
1696
|
toString() {
|
|
1497
|
-
return `${super.toString()} (${this.trackRateMultiplier.
|
|
1697
|
+
return `${super.toString()} (${this.trackRateMultiplier.toDisplayString()}x)`;
|
|
1498
1698
|
}
|
|
1499
1699
|
}
|
|
1500
1700
|
|
|
@@ -2021,6 +2221,11 @@ class Slider extends HitObject {
|
|
|
2021
2221
|
this.endPositionCache.invalidate();
|
|
2022
2222
|
this.head.position = this.position;
|
|
2023
2223
|
this.tail.position = this.endPosition;
|
|
2224
|
+
for (let i = 1; i < this.nestedHitObjects.length - 1; ++i) {
|
|
2225
|
+
const nestedObject = this.nestedHitObjects[i];
|
|
2226
|
+
const progress = (nestedObject.startTime - this.startTime) / this.duration;
|
|
2227
|
+
nestedObject.position = this.position.add(this.curvePositionAt(progress));
|
|
2228
|
+
}
|
|
2024
2229
|
}
|
|
2025
2230
|
createSlidingSamples(controlPoints) {
|
|
2026
2231
|
this.auxiliarySamples.length = 0;
|
|
@@ -2103,34 +2308,89 @@ Slider.baseWhistleSlideSample = new BankHitSampleInfo("sliderwhistle");
|
|
|
2103
2308
|
Slider.baseTickSample = new BankHitSampleInfo("slidertick");
|
|
2104
2309
|
Slider.legacyLastTickOffset = 36;
|
|
2105
2310
|
|
|
2311
|
+
/**
|
|
2312
|
+
* Represents a `Mod` specific setting that is constrained to a number of values.
|
|
2313
|
+
*
|
|
2314
|
+
* The value can be `null`, which is treated as a special case.
|
|
2315
|
+
*/
|
|
2316
|
+
class NullableDecimalModSetting extends RangeConstrainedModSetting {
|
|
2317
|
+
constructor(name, description, defaultValue, min = -Number.MAX_VALUE, max = Number.MAX_VALUE, step = 0, precision = null) {
|
|
2318
|
+
super(name, description, defaultValue, min, max, step);
|
|
2319
|
+
this.displayFormatter = (v) => {
|
|
2320
|
+
if (v === null) {
|
|
2321
|
+
return "None";
|
|
2322
|
+
}
|
|
2323
|
+
if (this.precision !== null) {
|
|
2324
|
+
return v.toFixed(this.precision);
|
|
2325
|
+
}
|
|
2326
|
+
return super.toDisplayString();
|
|
2327
|
+
};
|
|
2328
|
+
if (min > max) {
|
|
2329
|
+
throw new RangeError(`The minimum value (${min}) must be less than or equal to the maximum value (${max}).`);
|
|
2330
|
+
}
|
|
2331
|
+
if (step < 0) {
|
|
2332
|
+
throw new RangeError(`The step size (${step}) must be greater than or equal to 0.`);
|
|
2333
|
+
}
|
|
2334
|
+
if (defaultValue !== null &&
|
|
2335
|
+
(defaultValue < min || defaultValue > max)) {
|
|
2336
|
+
throw new RangeError(`The default value (${defaultValue}) must be between the minimum (${min}) and maximum (${max}) values.`);
|
|
2337
|
+
}
|
|
2338
|
+
this.precision = precision;
|
|
2339
|
+
}
|
|
2340
|
+
processValue(value) {
|
|
2341
|
+
if (value === null) {
|
|
2342
|
+
return null;
|
|
2343
|
+
}
|
|
2344
|
+
const processedValue = MathUtils.clamp(Math.round(value / this.step) * this.step, this.min, this.max);
|
|
2345
|
+
if (this.precision !== null) {
|
|
2346
|
+
return parseFloat(processedValue.toFixed(this.precision));
|
|
2347
|
+
}
|
|
2348
|
+
return processedValue;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2106
2352
|
/**
|
|
2107
2353
|
* Represents the Difficulty Adjust mod.
|
|
2108
2354
|
*/
|
|
2109
2355
|
class ModDifficultyAdjust extends Mod {
|
|
2110
2356
|
get isRelevant() {
|
|
2111
|
-
return (this.cs !==
|
|
2112
|
-
this.ar !==
|
|
2113
|
-
this.od !==
|
|
2114
|
-
this.hp !==
|
|
2357
|
+
return (this.cs.value !== null ||
|
|
2358
|
+
this.ar.value !== null ||
|
|
2359
|
+
this.od.value !== null ||
|
|
2360
|
+
this.hp.value !== null);
|
|
2115
2361
|
}
|
|
2116
2362
|
constructor(values) {
|
|
2363
|
+
var _a, _b, _c, _d;
|
|
2117
2364
|
super();
|
|
2118
2365
|
this.acronym = "DA";
|
|
2119
2366
|
this.name = "Difficulty Adjust";
|
|
2120
2367
|
this.droidRanked = false;
|
|
2121
2368
|
this.osuRanked = false;
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
this.
|
|
2369
|
+
/**
|
|
2370
|
+
* The circle size to enforce.
|
|
2371
|
+
*/
|
|
2372
|
+
this.cs = new NullableDecimalModSetting("Circle size", "The circle size to enforce.", null, 0, 15, 0.1, 1);
|
|
2373
|
+
/**
|
|
2374
|
+
* The approach rate to enforce.
|
|
2375
|
+
*/
|
|
2376
|
+
this.ar = new NullableDecimalModSetting("Approach rate", "The approach rate to enforce.", null, 0, 11, 0.1, 1);
|
|
2377
|
+
/**
|
|
2378
|
+
* The overall difficulty to enforce.
|
|
2379
|
+
*/
|
|
2380
|
+
this.od = new NullableDecimalModSetting("Overall difficulty", "The overall difficulty to enforce.", null, 0, 11, 0.1, 1);
|
|
2381
|
+
this.hp = new NullableDecimalModSetting("Health drain", "The health drain to enforce.", null, 0, 11, 0.1, 1);
|
|
2382
|
+
this.cs.value = (_a = values === null || values === void 0 ? void 0 : values.cs) !== null && _a !== void 0 ? _a : null;
|
|
2383
|
+
this.ar.value = (_b = values === null || values === void 0 ? void 0 : values.ar) !== null && _b !== void 0 ? _b : null;
|
|
2384
|
+
this.od.value = (_c = values === null || values === void 0 ? void 0 : values.od) !== null && _c !== void 0 ? _c : null;
|
|
2385
|
+
this.hp.value = (_d = values === null || values === void 0 ? void 0 : values.hp) !== null && _d !== void 0 ? _d : null;
|
|
2126
2386
|
}
|
|
2127
2387
|
copySettings(mod) {
|
|
2128
|
-
var _a, _b, _c, _d;
|
|
2388
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
2129
2389
|
super.copySettings(mod);
|
|
2130
|
-
this.cs = (_a = mod.settings) === null || _a === void 0 ? void 0 : _a.cs;
|
|
2131
|
-
this.ar = (
|
|
2132
|
-
this.od = (
|
|
2133
|
-
this.hp = (
|
|
2390
|
+
this.cs.value = ((_b = (_a = mod.settings) === null || _a === void 0 ? void 0 : _a.cs) !== null && _b !== void 0 ? _b : null);
|
|
2391
|
+
this.ar.value = ((_d = (_c = mod.settings) === null || _c === void 0 ? void 0 : _c.ar) !== null && _d !== void 0 ? _d : null);
|
|
2392
|
+
this.od.value = ((_f = (_e = mod.settings) === null || _e === void 0 ? void 0 : _e.od) !== null && _f !== void 0 ? _f : null);
|
|
2393
|
+
this.hp.value = ((_h = (_g = mod.settings) === null || _g === void 0 ? void 0 : _g.hp) !== null && _h !== void 0 ? _h : null);
|
|
2134
2394
|
}
|
|
2135
2395
|
get isDroidRelevant() {
|
|
2136
2396
|
return this.isRelevant;
|
|
@@ -2138,15 +2398,15 @@ class ModDifficultyAdjust extends Mod {
|
|
|
2138
2398
|
calculateDroidScoreMultiplier(difficulty) {
|
|
2139
2399
|
// Graph: https://www.desmos.com/calculator/yrggkhrkzz
|
|
2140
2400
|
let multiplier = 1;
|
|
2141
|
-
if (this.cs !==
|
|
2142
|
-
const diff = this.cs - difficulty.cs;
|
|
2401
|
+
if (this.cs.value !== null) {
|
|
2402
|
+
const diff = this.cs.value - difficulty.cs;
|
|
2143
2403
|
multiplier *=
|
|
2144
2404
|
diff >= 0
|
|
2145
2405
|
? 1 + 0.0075 * Math.pow(diff, 1.5)
|
|
2146
2406
|
: 2 / (1 + Math.exp(-0.5 * diff));
|
|
2147
2407
|
}
|
|
2148
|
-
if (this.od !==
|
|
2149
|
-
const diff = this.od - difficulty.od;
|
|
2408
|
+
if (this.od.value !== null) {
|
|
2409
|
+
const diff = this.od.value - difficulty.od;
|
|
2150
2410
|
multiplier *=
|
|
2151
2411
|
diff >= 0
|
|
2152
2412
|
? 1 + 0.005 * Math.pow(diff, 1.3)
|
|
@@ -2160,24 +2420,24 @@ class ModDifficultyAdjust extends Mod {
|
|
|
2160
2420
|
get osuScoreMultiplier() {
|
|
2161
2421
|
return 0.5;
|
|
2162
2422
|
}
|
|
2163
|
-
|
|
2423
|
+
applyToDifficultyWithMods(_, difficulty, mods) {
|
|
2164
2424
|
var _a, _b, _c, _d;
|
|
2165
|
-
difficulty.cs = (_a = this.cs) !== null && _a !== void 0 ? _a : difficulty.cs;
|
|
2166
|
-
difficulty.ar = (_b = this.ar) !== null && _b !== void 0 ? _b : difficulty.ar;
|
|
2167
|
-
difficulty.od = (_c = this.od) !== null && _c !== void 0 ? _c : difficulty.od;
|
|
2168
|
-
difficulty.hp = (_d = this.hp) !== null && _d !== void 0 ? _d : difficulty.hp;
|
|
2425
|
+
difficulty.cs = (_a = this.cs.value) !== null && _a !== void 0 ? _a : difficulty.cs;
|
|
2426
|
+
difficulty.ar = (_b = this.ar.value) !== null && _b !== void 0 ? _b : difficulty.ar;
|
|
2427
|
+
difficulty.od = (_c = this.od.value) !== null && _c !== void 0 ? _c : difficulty.od;
|
|
2428
|
+
difficulty.hp = (_d = this.hp.value) !== null && _d !== void 0 ? _d : difficulty.hp;
|
|
2169
2429
|
// Special case for force AR, where the AR value is kept constant with respect to game time.
|
|
2170
2430
|
// This makes the player perceive the AR as is under all speed multipliers.
|
|
2171
|
-
if (this.ar !==
|
|
2172
|
-
const preempt = BeatmapDifficulty.difficultyRange(this.ar, HitObject.preemptMax, HitObject.preemptMid, HitObject.preemptMin);
|
|
2431
|
+
if (this.ar.value !== null) {
|
|
2432
|
+
const preempt = BeatmapDifficulty.difficultyRange(this.ar.value, HitObject.preemptMax, HitObject.preemptMid, HitObject.preemptMin);
|
|
2173
2433
|
const trackRate = this.calculateTrackRate(mods.values());
|
|
2174
2434
|
difficulty.ar = BeatmapDifficulty.inverseDifficultyRange(preempt * trackRate, HitObject.preemptMax, HitObject.preemptMid, HitObject.preemptMin);
|
|
2175
2435
|
}
|
|
2176
2436
|
}
|
|
2177
|
-
|
|
2437
|
+
applyToHitObjectWithMods(_, hitObject, mods) {
|
|
2178
2438
|
// Special case for force AR, where the AR value is kept constant with respect to game time.
|
|
2179
2439
|
// This makes the player perceive the fade in animation as is under all speed multipliers.
|
|
2180
|
-
if (this.ar ===
|
|
2440
|
+
if (this.ar.value === null) {
|
|
2181
2441
|
return;
|
|
2182
2442
|
}
|
|
2183
2443
|
this.applyFadeAdjustment(hitObject, mods);
|
|
@@ -2188,24 +2448,21 @@ class ModDifficultyAdjust extends Mod {
|
|
|
2188
2448
|
}
|
|
2189
2449
|
}
|
|
2190
2450
|
serializeSettings() {
|
|
2191
|
-
if (this.
|
|
2192
|
-
this.ar === undefined &&
|
|
2193
|
-
this.od === undefined &&
|
|
2194
|
-
this.hp === undefined) {
|
|
2451
|
+
if (!this.isRelevant) {
|
|
2195
2452
|
return null;
|
|
2196
2453
|
}
|
|
2197
2454
|
const settings = {};
|
|
2198
|
-
if (this.cs !==
|
|
2199
|
-
settings.cs = this.cs;
|
|
2455
|
+
if (this.cs.value !== null) {
|
|
2456
|
+
settings.cs = this.cs.value;
|
|
2200
2457
|
}
|
|
2201
|
-
if (this.ar !==
|
|
2202
|
-
settings.ar = this.ar;
|
|
2458
|
+
if (this.ar.value !== null) {
|
|
2459
|
+
settings.ar = this.ar.value;
|
|
2203
2460
|
}
|
|
2204
|
-
if (this.od !==
|
|
2205
|
-
settings.od = this.od;
|
|
2461
|
+
if (this.od.value !== null) {
|
|
2462
|
+
settings.od = this.od.value;
|
|
2206
2463
|
}
|
|
2207
|
-
if (this.hp !==
|
|
2208
|
-
settings.hp = this.hp;
|
|
2464
|
+
if (this.hp.value !== null) {
|
|
2465
|
+
settings.hp = this.hp.value;
|
|
2209
2466
|
}
|
|
2210
2467
|
return settings;
|
|
2211
2468
|
}
|
|
@@ -2228,22 +2485,30 @@ class ModDifficultyAdjust extends Mod {
|
|
|
2228
2485
|
}
|
|
2229
2486
|
return rate;
|
|
2230
2487
|
}
|
|
2488
|
+
equals(other) {
|
|
2489
|
+
return (super.equals(other) &&
|
|
2490
|
+
other instanceof ModDifficultyAdjust &&
|
|
2491
|
+
this.cs.value === other.cs.value &&
|
|
2492
|
+
this.ar.value === other.ar.value &&
|
|
2493
|
+
this.od.value === other.od.value &&
|
|
2494
|
+
this.hp.value === other.hp.value);
|
|
2495
|
+
}
|
|
2231
2496
|
toString() {
|
|
2232
2497
|
if (!this.isRelevant) {
|
|
2233
2498
|
return super.toString();
|
|
2234
2499
|
}
|
|
2235
2500
|
const settings = [];
|
|
2236
|
-
if (this.cs !==
|
|
2237
|
-
settings.push(`CS${this.cs.
|
|
2501
|
+
if (this.cs.value !== null) {
|
|
2502
|
+
settings.push(`CS${this.cs.toDisplayString()}`);
|
|
2238
2503
|
}
|
|
2239
|
-
if (this.ar !==
|
|
2240
|
-
settings.push(`AR${this.ar.
|
|
2504
|
+
if (this.ar.value !== null) {
|
|
2505
|
+
settings.push(`AR${this.ar.toDisplayString()}`);
|
|
2241
2506
|
}
|
|
2242
|
-
if (this.od !==
|
|
2243
|
-
settings.push(`OD${this.od.
|
|
2507
|
+
if (this.od.value !== null) {
|
|
2508
|
+
settings.push(`OD${this.od.toDisplayString()}`);
|
|
2244
2509
|
}
|
|
2245
|
-
if (this.hp !==
|
|
2246
|
-
settings.push(`HP${this.hp.
|
|
2510
|
+
if (this.hp.value !== null) {
|
|
2511
|
+
settings.push(`HP${this.hp.toDisplayString()}`);
|
|
2247
2512
|
}
|
|
2248
2513
|
return `${super.toString()} (${settings.join(", ")})`;
|
|
2249
2514
|
}
|
|
@@ -2254,10 +2519,9 @@ class ModDifficultyAdjust extends Mod {
|
|
|
2254
2519
|
*/
|
|
2255
2520
|
class ModNightCore extends ModRateAdjust {
|
|
2256
2521
|
constructor() {
|
|
2257
|
-
super();
|
|
2522
|
+
super(1.5);
|
|
2258
2523
|
this.acronym = "NC";
|
|
2259
2524
|
this.name = "NightCore";
|
|
2260
|
-
this.trackRateMultiplier = 1.5;
|
|
2261
2525
|
this.droidRanked = true;
|
|
2262
2526
|
this.osuRanked = true;
|
|
2263
2527
|
this.bitwise = 1 << 9;
|
|
@@ -2282,10 +2546,9 @@ class ModNightCore extends ModRateAdjust {
|
|
|
2282
2546
|
*/
|
|
2283
2547
|
class ModHalfTime extends ModRateAdjust {
|
|
2284
2548
|
constructor() {
|
|
2285
|
-
super();
|
|
2549
|
+
super(0.75);
|
|
2286
2550
|
this.acronym = "HT";
|
|
2287
2551
|
this.name = "HalfTime";
|
|
2288
|
-
this.trackRateMultiplier = 0.75;
|
|
2289
2552
|
this.droidRanked = true;
|
|
2290
2553
|
this.osuRanked = true;
|
|
2291
2554
|
this.bitwise = 1 << 8;
|
|
@@ -2310,10 +2573,9 @@ class ModHalfTime extends ModRateAdjust {
|
|
|
2310
2573
|
*/
|
|
2311
2574
|
class ModDoubleTime extends ModRateAdjust {
|
|
2312
2575
|
constructor() {
|
|
2313
|
-
super();
|
|
2576
|
+
super(1.5);
|
|
2314
2577
|
this.acronym = "DT";
|
|
2315
2578
|
this.name = "DoubleTime";
|
|
2316
|
-
this.trackRateMultiplier = 1.5;
|
|
2317
2579
|
this.droidRanked = true;
|
|
2318
2580
|
this.osuRanked = true;
|
|
2319
2581
|
this.bitwise = 1 << 6;
|
|
@@ -2333,67 +2595,235 @@ class ModDoubleTime extends ModRateAdjust {
|
|
|
2333
2595
|
}
|
|
2334
2596
|
}
|
|
2335
2597
|
|
|
2598
|
+
/**
|
|
2599
|
+
* Represents a circle in a beatmap.
|
|
2600
|
+
*
|
|
2601
|
+
* All we need from circles is their position. All positions
|
|
2602
|
+
* stored in the objects are in playfield coordinates (512*384
|
|
2603
|
+
* rectangle).
|
|
2604
|
+
*/
|
|
2605
|
+
class Circle extends HitObject {
|
|
2606
|
+
constructor(values) {
|
|
2607
|
+
super(values);
|
|
2608
|
+
}
|
|
2609
|
+
toString() {
|
|
2610
|
+
return `Position: [${this._position.x}, ${this._position.y}]`;
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
var _a$1;
|
|
2336
2615
|
/**
|
|
2337
2616
|
* Represents the osu! playfield.
|
|
2338
2617
|
*/
|
|
2339
2618
|
class Playfield {
|
|
2340
2619
|
}
|
|
2620
|
+
_a$1 = Playfield;
|
|
2341
2621
|
/**
|
|
2342
2622
|
* The size of the playfield, which is 512x384.
|
|
2343
2623
|
*/
|
|
2344
2624
|
Playfield.baseSize = new Vector2(512, 384);
|
|
2625
|
+
/**
|
|
2626
|
+
* The center of the playfield, which is at (256, 192).
|
|
2627
|
+
*/
|
|
2628
|
+
Playfield.center = _a$1.baseSize.scale(0.5);
|
|
2345
2629
|
|
|
2346
2630
|
/**
|
|
2347
|
-
*
|
|
2631
|
+
* Represents a spinner in a beatmap.
|
|
2632
|
+
*
|
|
2633
|
+
* All we need from spinners is their duration. The
|
|
2634
|
+
* position of a spinner is always at 256x192.
|
|
2348
2635
|
*/
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2636
|
+
class Spinner extends HitObject {
|
|
2637
|
+
get endTime() {
|
|
2638
|
+
return this._endTime;
|
|
2639
|
+
}
|
|
2640
|
+
constructor(values) {
|
|
2641
|
+
super(Object.assign(Object.assign({}, values), { position: Playfield.baseSize.divide(2) }));
|
|
2642
|
+
this._endTime = values.endTime;
|
|
2643
|
+
}
|
|
2644
|
+
applySamples(controlPoints) {
|
|
2645
|
+
super.applySamples(controlPoints);
|
|
2646
|
+
const samplePoints = controlPoints.sample.between(this.startTime + HitObject.controlPointLeniency, this.endTime + HitObject.controlPointLeniency);
|
|
2647
|
+
this.auxiliarySamples.length = 0;
|
|
2648
|
+
this.auxiliarySamples.push(new SequenceHitSampleInfo(samplePoints.map((s) => new TimedHitSampleInfo(s.time, s.applyTo(Spinner.baseSpinnerSpinSample)))));
|
|
2649
|
+
this.auxiliarySamples.push(new SequenceHitSampleInfo(samplePoints.map((s) => new TimedHitSampleInfo(s.time, s.applyTo(Spinner.baseSpinnerBonusSample)))));
|
|
2650
|
+
}
|
|
2651
|
+
getStackedPosition() {
|
|
2652
|
+
return this.position;
|
|
2653
|
+
}
|
|
2654
|
+
getStackedEndPosition() {
|
|
2655
|
+
return this.position;
|
|
2656
|
+
}
|
|
2657
|
+
createHitWindow() {
|
|
2658
|
+
return new EmptyHitWindow();
|
|
2659
|
+
}
|
|
2660
|
+
toString() {
|
|
2661
|
+
return `Position: [${this._position.x}, ${this._position.y}], duration: ${this.duration}`;
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
Spinner.baseSpinnerSpinSample = new BankHitSampleInfo("spinnerspin");
|
|
2665
|
+
Spinner.baseSpinnerBonusSample = new BankHitSampleInfo("spinnerbonus");
|
|
2356
2666
|
|
|
2357
2667
|
/**
|
|
2358
|
-
*
|
|
2668
|
+
* Represents a four-dimensional vector.
|
|
2359
2669
|
*/
|
|
2360
|
-
class
|
|
2670
|
+
class Vector4 {
|
|
2671
|
+
constructor(xOrValueOrXZ, yOrYW, z, w) {
|
|
2672
|
+
if (yOrYW === undefined) {
|
|
2673
|
+
this.x = xOrValueOrXZ;
|
|
2674
|
+
this.y = xOrValueOrXZ;
|
|
2675
|
+
this.z = xOrValueOrXZ;
|
|
2676
|
+
this.w = xOrValueOrXZ;
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
if (typeof z === "undefined") {
|
|
2680
|
+
this.x = xOrValueOrXZ;
|
|
2681
|
+
this.y = yOrYW;
|
|
2682
|
+
this.z = xOrValueOrXZ;
|
|
2683
|
+
this.w = yOrYW;
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
this.x = xOrValueOrXZ;
|
|
2687
|
+
this.y = yOrYW;
|
|
2688
|
+
this.z = z;
|
|
2689
|
+
this.w = w;
|
|
2690
|
+
}
|
|
2361
2691
|
/**
|
|
2362
|
-
*
|
|
2363
|
-
*
|
|
2364
|
-
* @param value1 The first number.
|
|
2365
|
-
* @param value2 The second number.
|
|
2366
|
-
* @param acceptableDifference The acceptable difference as threshold. Default is `Precision.FLOAT_EPSILON = 1e-3`.
|
|
2692
|
+
* The X coordinate of the left edge of this vector.
|
|
2367
2693
|
*/
|
|
2368
|
-
|
|
2369
|
-
return
|
|
2694
|
+
get left() {
|
|
2695
|
+
return this.x;
|
|
2370
2696
|
}
|
|
2371
2697
|
/**
|
|
2372
|
-
*
|
|
2373
|
-
*
|
|
2374
|
-
* @param vec1 The first vector.
|
|
2375
|
-
* @param vec2 The second vector.
|
|
2376
|
-
* @param acceptableDifference The acceptable difference as threshold. Default is `Precision.FLOAT_EPSILON = 1e-3`.
|
|
2698
|
+
* The Y coordinate of the top edge of this vector.
|
|
2377
2699
|
*/
|
|
2378
|
-
|
|
2379
|
-
return
|
|
2380
|
-
this.almostEqualsNumber(vec1.y, vec2.y, acceptableDifference));
|
|
2700
|
+
get top() {
|
|
2701
|
+
return this.y;
|
|
2381
2702
|
}
|
|
2382
2703
|
/**
|
|
2383
|
-
*
|
|
2384
|
-
*
|
|
2385
|
-
* @param a The first number.
|
|
2386
|
-
* @param b The second number.
|
|
2387
|
-
* @param maximumError The accuracy required for being almost equal. Defaults to `10 * 2^(-53)`.
|
|
2388
|
-
* @returns Whether the two values differ by no more than 10 * 2^(-52).
|
|
2704
|
+
* The X coordinate of the right edge of this vector.
|
|
2389
2705
|
*/
|
|
2390
|
-
|
|
2391
|
-
return this.
|
|
2706
|
+
get right() {
|
|
2707
|
+
return this.z;
|
|
2392
2708
|
}
|
|
2393
2709
|
/**
|
|
2394
|
-
*
|
|
2395
|
-
|
|
2396
|
-
|
|
2710
|
+
* The Y coordinate of the bottom edge of this vector.
|
|
2711
|
+
*/
|
|
2712
|
+
get bottom() {
|
|
2713
|
+
return this.w;
|
|
2714
|
+
}
|
|
2715
|
+
/**
|
|
2716
|
+
* The top left corner of this vector.
|
|
2717
|
+
*/
|
|
2718
|
+
get topLeft() {
|
|
2719
|
+
return new Vector2(this.left, this.top);
|
|
2720
|
+
}
|
|
2721
|
+
/**
|
|
2722
|
+
* The top right corner of this vector.
|
|
2723
|
+
*/
|
|
2724
|
+
get topRight() {
|
|
2725
|
+
return new Vector2(this.right, this.top);
|
|
2726
|
+
}
|
|
2727
|
+
/**
|
|
2728
|
+
* The bottom left corner of this vector.
|
|
2729
|
+
*/
|
|
2730
|
+
get bottomLeft() {
|
|
2731
|
+
return new Vector2(this.left, this.bottom);
|
|
2732
|
+
}
|
|
2733
|
+
/**
|
|
2734
|
+
* The bottom right corner of this vector.
|
|
2735
|
+
*/
|
|
2736
|
+
get bottomRight() {
|
|
2737
|
+
return new Vector2(this.right, this.bottom);
|
|
2738
|
+
}
|
|
2739
|
+
/**
|
|
2740
|
+
* The width of the rectangle defined by this vector.
|
|
2741
|
+
*/
|
|
2742
|
+
get width() {
|
|
2743
|
+
return this.right - this.left;
|
|
2744
|
+
}
|
|
2745
|
+
/**
|
|
2746
|
+
* The height of the rectangle defined by this vector.
|
|
2747
|
+
*/
|
|
2748
|
+
get height() {
|
|
2749
|
+
return this.bottom - this.top;
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
/**
|
|
2754
|
+
* Contains infromation about the position of a {@link HitObject}.
|
|
2755
|
+
*/
|
|
2756
|
+
class HitObjectPositionInfo {
|
|
2757
|
+
constructor(hitObject) {
|
|
2758
|
+
/**
|
|
2759
|
+
* The jump angle from the previous {@link HitObject} to this one, relative to the previous
|
|
2760
|
+
* {@link HitObject}'s jump angle.
|
|
2761
|
+
*
|
|
2762
|
+
* The `relativeAngle` of the first {@link HitObject} in a beatmap represents the absolute angle from the
|
|
2763
|
+
* center of the playfield to the {@link HitObject}.
|
|
2764
|
+
*
|
|
2765
|
+
* If `relativeAngle` is 0, the player's cursor does not need to change its direction of movement when
|
|
2766
|
+
* passing from the previous {@link HitObject} to this one.
|
|
2767
|
+
*/
|
|
2768
|
+
this.relativeAngle = 0;
|
|
2769
|
+
/**
|
|
2770
|
+
* The jump distance from the previous {@link HitObject} to this one.
|
|
2771
|
+
*
|
|
2772
|
+
* The `distanceFromPrevious` of the first {@link HitObject} in a beatmap is relative to the center of
|
|
2773
|
+
* the playfield.
|
|
2774
|
+
*/
|
|
2775
|
+
this.distanceFromPrevious = 0;
|
|
2776
|
+
/**
|
|
2777
|
+
* The rotation of this {@link HitObject} relative to its jump angle.
|
|
2778
|
+
*
|
|
2779
|
+
* For `Slider`s, this is defined as the angle from the `Slider`'s start position to the end of its path
|
|
2780
|
+
* relative to its jump angle. For `HitCircle`s and `Spinner`s, this property is ignored.
|
|
2781
|
+
*/
|
|
2782
|
+
this.rotation = 0;
|
|
2783
|
+
this.hitObject = hitObject;
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
/**
|
|
2788
|
+
* Precision utilities.
|
|
2789
|
+
*/
|
|
2790
|
+
class Precision {
|
|
2791
|
+
/**
|
|
2792
|
+
* Checks if two numbers are equal with a given tolerance.
|
|
2793
|
+
*
|
|
2794
|
+
* @param value1 The first number.
|
|
2795
|
+
* @param value2 The second number.
|
|
2796
|
+
* @param acceptableDifference The acceptable difference as threshold. Default is `Precision.FLOAT_EPSILON = 1e-3`.
|
|
2797
|
+
*/
|
|
2798
|
+
static almostEqualsNumber(value1, value2, acceptableDifference = this.FLOAT_EPSILON) {
|
|
2799
|
+
return Math.abs(value1 - value2) <= acceptableDifference;
|
|
2800
|
+
}
|
|
2801
|
+
/**
|
|
2802
|
+
* Checks if two vectors are equal with a given tolerance.
|
|
2803
|
+
*
|
|
2804
|
+
* @param vec1 The first vector.
|
|
2805
|
+
* @param vec2 The second vector.
|
|
2806
|
+
* @param acceptableDifference The acceptable difference as threshold. Default is `Precision.FLOAT_EPSILON = 1e-3`.
|
|
2807
|
+
*/
|
|
2808
|
+
static almostEqualsVector(vec1, vec2, acceptableDifference = this.FLOAT_EPSILON) {
|
|
2809
|
+
return (this.almostEqualsNumber(vec1.x, vec2.x, acceptableDifference) &&
|
|
2810
|
+
this.almostEqualsNumber(vec1.y, vec2.y, acceptableDifference));
|
|
2811
|
+
}
|
|
2812
|
+
/**
|
|
2813
|
+
* Checks whether two real numbers are almost equal.
|
|
2814
|
+
*
|
|
2815
|
+
* @param a The first number.
|
|
2816
|
+
* @param b The second number.
|
|
2817
|
+
* @param maximumError The accuracy required for being almost equal. Defaults to `10 * 2^(-53)`.
|
|
2818
|
+
* @returns Whether the two values differ by no more than 10 * 2^(-52).
|
|
2819
|
+
*/
|
|
2820
|
+
static almostEqualRelative(a, b, maximumError = 10 * Math.pow(2, -53)) {
|
|
2821
|
+
return this.almostEqualNormRelative(a, b, a - b, maximumError);
|
|
2822
|
+
}
|
|
2823
|
+
/**
|
|
2824
|
+
* Compares two numbers and determines if they are equal within the specified maximum error.
|
|
2825
|
+
*
|
|
2826
|
+
* @param a The norm of the first value (can be negative).
|
|
2397
2827
|
* @param b The norm of the second value (can be negative).
|
|
2398
2828
|
* @param diff The norm of the difference of the two values (can be negative).
|
|
2399
2829
|
* @param maximumError The accuracy required for being almost equal.
|
|
@@ -2425,6 +2855,17 @@ class Precision {
|
|
|
2425
2855
|
}
|
|
2426
2856
|
Precision.FLOAT_EPSILON = 1e-3;
|
|
2427
2857
|
|
|
2858
|
+
/**
|
|
2859
|
+
* Types of slider paths.
|
|
2860
|
+
*/
|
|
2861
|
+
exports.PathType = void 0;
|
|
2862
|
+
(function (PathType) {
|
|
2863
|
+
PathType["Catmull"] = "C";
|
|
2864
|
+
PathType["Bezier"] = "B";
|
|
2865
|
+
PathType["Linear"] = "L";
|
|
2866
|
+
PathType["PerfectCurve"] = "P";
|
|
2867
|
+
})(exports.PathType || (exports.PathType = {}));
|
|
2868
|
+
|
|
2428
2869
|
/**
|
|
2429
2870
|
* Path approximator for sliders.
|
|
2430
2871
|
*/
|
|
@@ -2822,6 +3263,31 @@ class SliderPath {
|
|
|
2822
3263
|
const d = this.progressToDistance(progress);
|
|
2823
3264
|
return this.interpolateVerticles(this.indexOfDistance(d), d);
|
|
2824
3265
|
}
|
|
3266
|
+
/**
|
|
3267
|
+
* Computes the slider path until a given progress that ranges from 0 (beginning of the slider) to
|
|
3268
|
+
* 1 (end of the slider).
|
|
3269
|
+
*
|
|
3270
|
+
* @param p0 Start progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).
|
|
3271
|
+
* @param p1 End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).
|
|
3272
|
+
* @return The computed path between the two ranges.
|
|
3273
|
+
*/
|
|
3274
|
+
pathToProgress(p0, p1) {
|
|
3275
|
+
const path = [];
|
|
3276
|
+
const d0 = this.progressToDistance(p0);
|
|
3277
|
+
const d1 = this.progressToDistance(p1);
|
|
3278
|
+
let i = 0;
|
|
3279
|
+
while (i < this.calculatedPath.length &&
|
|
3280
|
+
this.cumulativeLength[i] < d0) {
|
|
3281
|
+
++i;
|
|
3282
|
+
}
|
|
3283
|
+
path.push(this.interpolateVerticles(i, d0));
|
|
3284
|
+
while (i < this.calculatedPath.length &&
|
|
3285
|
+
this.cumulativeLength[i] <= d1) {
|
|
3286
|
+
path.push(this.calculatedPath[i++]);
|
|
3287
|
+
}
|
|
3288
|
+
path.push(this.interpolateVerticles(i, d1));
|
|
3289
|
+
return path;
|
|
3290
|
+
}
|
|
2825
3291
|
/**
|
|
2826
3292
|
* Returns the progress of reaching expected distance.
|
|
2827
3293
|
*/
|
|
@@ -2885,31 +3351,94 @@ class SliderPath {
|
|
|
2885
3351
|
}
|
|
2886
3352
|
}
|
|
2887
3353
|
|
|
3354
|
+
var _a;
|
|
2888
3355
|
/**
|
|
2889
3356
|
* Utilities for {@link HitObject} generation.
|
|
2890
3357
|
*/
|
|
2891
3358
|
class HitObjectGenerationUtils {
|
|
3359
|
+
//#region Rotation
|
|
3360
|
+
/**
|
|
3361
|
+
* Rotates a {@link HitObject} away from the edge of the playfield while keeping a constant distance from
|
|
3362
|
+
* the previous {@link HitObject}.
|
|
3363
|
+
*
|
|
3364
|
+
* @param previousObjectPosition The position of the previous {@link HitObject}.
|
|
3365
|
+
* @param positionRelativeToPrevious The position of the {@link HitObject} to be rotated relative to the
|
|
3366
|
+
* previous {@link HitObject}.
|
|
3367
|
+
* @param rotationRatio The extent of the rotation. 0 means the {@link HitObject} is never rotated, while 1
|
|
3368
|
+
* means the {@link HitObject} will be fully rotated towards the center of the playfield when it is originally
|
|
3369
|
+
* at the edge of the playfield.
|
|
3370
|
+
* @return The new position of the {@link HitObject} relative to the previous {@link HitObject}.
|
|
3371
|
+
*/
|
|
3372
|
+
static rotateAwayFromEdge(previousObjectPosition, positionRelativeToPrevious, rotationRatio = 0.5) {
|
|
3373
|
+
const relativeRotationDistance = Math.max((previousObjectPosition.x < Playfield.center.x
|
|
3374
|
+
? this.borderDistance.x - previousObjectPosition.x
|
|
3375
|
+
: previousObjectPosition.x -
|
|
3376
|
+
(Playfield.baseSize.x - this.borderDistance.x)) /
|
|
3377
|
+
this.borderDistance.x, (previousObjectPosition.y < Playfield.center.y
|
|
3378
|
+
? this.borderDistance.y - previousObjectPosition.y
|
|
3379
|
+
: previousObjectPosition.y -
|
|
3380
|
+
(Playfield.baseSize.y - this.borderDistance.y)) /
|
|
3381
|
+
this.borderDistance.y, 0);
|
|
3382
|
+
return this.rotateVectorTowardsVector(positionRelativeToPrevious, Playfield.center.subtract(previousObjectPosition), Math.min(1, relativeRotationDistance * rotationRatio));
|
|
3383
|
+
}
|
|
3384
|
+
/**
|
|
3385
|
+
* Rotates a {@link Vector2} towards another {@link Vector2}.
|
|
3386
|
+
*
|
|
3387
|
+
* @param initial The {@link Vector2} to be rotated.
|
|
3388
|
+
* @param destination The {@link Vector2} that `initial` should be rotated towards.
|
|
3389
|
+
* @param rotationRatio How much `initial` should be rotated. 0 means no rotation. 1 mean `initial` is fully
|
|
3390
|
+
* rotated to equal `destination`.
|
|
3391
|
+
* @return The rotated {@link Vector2}.
|
|
3392
|
+
*/
|
|
3393
|
+
static rotateVectorTowardsVector(initial, destination, rotationRatio) {
|
|
3394
|
+
const initialAngle = Math.atan2(initial.y, initial.x);
|
|
3395
|
+
const destinationAngle = Math.atan2(destination.y, destination.x);
|
|
3396
|
+
let diff = destinationAngle - initialAngle;
|
|
3397
|
+
// Normalize angle
|
|
3398
|
+
while (diff < -Math.PI) {
|
|
3399
|
+
diff += 2 * Math.PI;
|
|
3400
|
+
}
|
|
3401
|
+
while (diff > Math.PI) {
|
|
3402
|
+
diff -= 2 * Math.PI;
|
|
3403
|
+
}
|
|
3404
|
+
const finalAngle = initialAngle + diff * rotationRatio;
|
|
3405
|
+
return new Vector2(initial.x * Math.cos(finalAngle), initial.y * Math.sin(finalAngle));
|
|
3406
|
+
}
|
|
3407
|
+
/**
|
|
3408
|
+
* Obtains the absolute rotation of a {@link Slider}, defined as the angle from its start position to the
|
|
3409
|
+
* end of its path.
|
|
3410
|
+
*
|
|
3411
|
+
* @param slider The {@link Slider} to obtain the rotation from.
|
|
3412
|
+
* @return The angle in radians.
|
|
3413
|
+
*/
|
|
3414
|
+
static getSliderRotation(slider) {
|
|
3415
|
+
const pathEndPosition = slider.path.positionAt(1);
|
|
3416
|
+
return Math.atan2(pathEndPosition.y, pathEndPosition.x);
|
|
3417
|
+
}
|
|
3418
|
+
/**
|
|
3419
|
+
* Rotates a {@link Vector2} by the specified angle.
|
|
3420
|
+
*
|
|
3421
|
+
* @param vec The {@link Vector2} to be rotated.
|
|
3422
|
+
* @param rotation The angle to rotate `vec` by, in radians.
|
|
3423
|
+
* @return The rotated {@link Vector2}.
|
|
3424
|
+
*/
|
|
3425
|
+
static rotateVector(vec, rotation) {
|
|
3426
|
+
const angle = Math.atan2(vec.y, vec.x) + rotation;
|
|
3427
|
+
const { length } = vec;
|
|
3428
|
+
return new Vector2(length * Math.cos(angle), length * Math.sin(angle));
|
|
3429
|
+
}
|
|
3430
|
+
//#endregion
|
|
3431
|
+
//#region Reflection
|
|
2892
3432
|
/**
|
|
2893
3433
|
* Reflects the position of a {@link HitObject} horizontally along the playfield.
|
|
2894
3434
|
*
|
|
2895
3435
|
* @param hitObject The {@link HitObject} to reflect.
|
|
2896
3436
|
*/
|
|
2897
3437
|
static reflectHorizontallyAlongPlayfield(hitObject) {
|
|
2898
|
-
// Reflect the position of the hit object.
|
|
2899
3438
|
hitObject.position = this.reflectVectorHorizontallyAlongPlayfield(hitObject.position);
|
|
2900
|
-
if (
|
|
2901
|
-
|
|
3439
|
+
if (hitObject instanceof Slider) {
|
|
3440
|
+
this.modifySlider(hitObject, (v) => new Vector2(-v.x, v.y));
|
|
2902
3441
|
}
|
|
2903
|
-
// Reflect the control points of the slider. This will reflect the positions of head and tail circles.
|
|
2904
|
-
hitObject.path = new SliderPath({
|
|
2905
|
-
pathType: hitObject.path.pathType,
|
|
2906
|
-
controlPoints: hitObject.path.controlPoints.map((v) => new Vector2(-v.x, v.y)),
|
|
2907
|
-
expectedDistance: hitObject.path.expectedDistance,
|
|
2908
|
-
});
|
|
2909
|
-
// Reflect the position of slider ticks and repeats.
|
|
2910
|
-
hitObject.nestedHitObjects.slice(1, -1).forEach((obj) => {
|
|
2911
|
-
obj.position = this.reflectVectorHorizontallyAlongPlayfield(obj.position);
|
|
2912
|
-
});
|
|
2913
3442
|
}
|
|
2914
3443
|
/**
|
|
2915
3444
|
* Reflects the position of a {@link HitObject} vertically along the playfield.
|
|
@@ -2919,18 +3448,32 @@ class HitObjectGenerationUtils {
|
|
|
2919
3448
|
static reflectVerticallyAlongPlayfield(hitObject) {
|
|
2920
3449
|
// Reflect the position of the hit object.
|
|
2921
3450
|
hitObject.position = this.reflectVectorVerticallyAlongPlayfield(hitObject.position);
|
|
2922
|
-
if (
|
|
2923
|
-
|
|
3451
|
+
if (hitObject instanceof Slider) {
|
|
3452
|
+
this.modifySlider(hitObject, (v) => new Vector2(v.x, -v.y));
|
|
2924
3453
|
}
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
3454
|
+
}
|
|
3455
|
+
/**
|
|
3456
|
+
* Flips the position of a {@link Slider} around its start position horizontally.
|
|
3457
|
+
*
|
|
3458
|
+
* @param slider The {@link Slider} to be flipped.
|
|
3459
|
+
*/
|
|
3460
|
+
static flipSliderInPlaceHorizontally(slider) {
|
|
3461
|
+
this.modifySlider(slider, (v) => new Vector2(-v.x, v.y));
|
|
3462
|
+
}
|
|
3463
|
+
/**
|
|
3464
|
+
* Rotates a {@link Slider} around its start position by the specified angle.
|
|
3465
|
+
*
|
|
3466
|
+
* @param slider The {@link Slider} to rotate.
|
|
3467
|
+
* @param rotation The angle to rotate `slider` by, in radians.
|
|
3468
|
+
*/
|
|
3469
|
+
static rotateSlider(slider, rotation) {
|
|
3470
|
+
this.modifySlider(slider, (v) => this.rotateVector(v, rotation));
|
|
3471
|
+
}
|
|
3472
|
+
static modifySlider(slider, modifyControlPoint) {
|
|
3473
|
+
slider.path = new SliderPath({
|
|
3474
|
+
pathType: slider.path.pathType,
|
|
3475
|
+
controlPoints: slider.path.controlPoints.map(modifyControlPoint),
|
|
3476
|
+
expectedDistance: slider.path.expectedDistance,
|
|
2934
3477
|
});
|
|
2935
3478
|
}
|
|
2936
3479
|
static reflectVectorHorizontallyAlongPlayfield(vector) {
|
|
@@ -2939,6 +3482,329 @@ class HitObjectGenerationUtils {
|
|
|
2939
3482
|
static reflectVectorVerticallyAlongPlayfield(vector) {
|
|
2940
3483
|
return new Vector2(vector.x, Playfield.baseSize.y - vector.y);
|
|
2941
3484
|
}
|
|
3485
|
+
//#endregion
|
|
3486
|
+
//#region Reposition
|
|
3487
|
+
/**
|
|
3488
|
+
* Generates a list of {@link HitObjectPositionInfo}s containing information for how the given list of
|
|
3489
|
+
* {@link HitObject}s are positioned.
|
|
3490
|
+
*
|
|
3491
|
+
* @param hitObjects A list of {@link HitObject}s to process.
|
|
3492
|
+
* @return A list of {@link HitObjectPositionInfo}s describing how each {@link HitObject} is positioned
|
|
3493
|
+
* relative to the previous one.
|
|
3494
|
+
*/
|
|
3495
|
+
static generatePositionInfos(hitObjects) {
|
|
3496
|
+
const positionInfos = [];
|
|
3497
|
+
let previousPosition = Playfield.center;
|
|
3498
|
+
let previousAngle = 0;
|
|
3499
|
+
for (const hitObject of hitObjects) {
|
|
3500
|
+
const relativePosition = hitObject.position.subtract(previousPosition);
|
|
3501
|
+
let absoluteAngle = Math.atan2(relativePosition.y, relativePosition.x);
|
|
3502
|
+
const relativeAngle = absoluteAngle - previousAngle;
|
|
3503
|
+
const positionInfo = new HitObjectPositionInfo(hitObject);
|
|
3504
|
+
positionInfo.relativeAngle = relativeAngle;
|
|
3505
|
+
positionInfo.distanceFromPrevious = relativePosition.length;
|
|
3506
|
+
if (hitObject instanceof Slider) {
|
|
3507
|
+
const absoluteRotation = this.getSliderRotation(hitObject);
|
|
3508
|
+
positionInfo.rotation = absoluteRotation - absoluteAngle;
|
|
3509
|
+
absoluteAngle = absoluteRotation;
|
|
3510
|
+
}
|
|
3511
|
+
previousPosition = hitObject.endPosition;
|
|
3512
|
+
previousAngle = absoluteAngle;
|
|
3513
|
+
positionInfos.push(positionInfo);
|
|
3514
|
+
}
|
|
3515
|
+
return positionInfos;
|
|
3516
|
+
}
|
|
3517
|
+
static repositionHitObjects(positionInfos) {
|
|
3518
|
+
const workingObjects = positionInfos.map((p) => new WorkingObject(p));
|
|
3519
|
+
let previous = null;
|
|
3520
|
+
for (let i = 0; i < workingObjects.length; ++i) {
|
|
3521
|
+
const current = workingObjects[i];
|
|
3522
|
+
const { hitObject } = current;
|
|
3523
|
+
if (hitObject instanceof Spinner) {
|
|
3524
|
+
previous = current;
|
|
3525
|
+
continue;
|
|
3526
|
+
}
|
|
3527
|
+
this.computeModifiedPosition(current, previous, i > 1 ? workingObjects[i - 2] : null);
|
|
3528
|
+
// Move hit objects back into the playfield if they are outside of it.
|
|
3529
|
+
let shift;
|
|
3530
|
+
if (hitObject instanceof Circle) {
|
|
3531
|
+
shift = this.clampHitCircleToPlayfield(current);
|
|
3532
|
+
}
|
|
3533
|
+
else if (hitObject instanceof Slider) {
|
|
3534
|
+
shift = this.clampSliderToPlayfield(current);
|
|
3535
|
+
}
|
|
3536
|
+
else {
|
|
3537
|
+
shift = new Vector2(0);
|
|
3538
|
+
}
|
|
3539
|
+
if (shift.x !== 0 || shift.y !== 0) {
|
|
3540
|
+
const toBeShifted = [];
|
|
3541
|
+
for (let j = i - 1; j >= Math.max(0, i - this.precedingObjectsToShift); --j) {
|
|
3542
|
+
// Only shift hit circles.
|
|
3543
|
+
if (!(workingObjects[j].hitObject instanceof Circle)) {
|
|
3544
|
+
break;
|
|
3545
|
+
}
|
|
3546
|
+
toBeShifted.push(workingObjects[j].hitObject);
|
|
3547
|
+
}
|
|
3548
|
+
this.applyDecreasingShift(toBeShifted, shift);
|
|
3549
|
+
}
|
|
3550
|
+
previous = current;
|
|
3551
|
+
}
|
|
3552
|
+
return workingObjects.map((w) => w.hitObject);
|
|
3553
|
+
}
|
|
3554
|
+
/**
|
|
3555
|
+
* Determines whether a {@link HitObject} is on a beat.
|
|
3556
|
+
*
|
|
3557
|
+
* @param beatmap The {@link Beatmap} the {@link HitObject} is a part of.
|
|
3558
|
+
* @param hitObject The {@link HitObject} to check.
|
|
3559
|
+
* @param downbeatsOnly If `true`, whether this method only returns `true` is on a downbeat.
|
|
3560
|
+
* @return `true` if the {@link HitObject} is on a (down-)beat, `false` otherwise.
|
|
3561
|
+
*/
|
|
3562
|
+
static isHitObjectOnBeat(beatmap, hitObject, downbeatsOnly = false) {
|
|
3563
|
+
const timingPoint = beatmap.controlPoints.timing.controlPointAt(hitObject.startTime);
|
|
3564
|
+
const timeSinceTimingPoint = hitObject.startTime - timingPoint.time;
|
|
3565
|
+
let beatLength = timingPoint.msPerBeat;
|
|
3566
|
+
if (downbeatsOnly) {
|
|
3567
|
+
beatLength *= timingPoint.timeSignature;
|
|
3568
|
+
}
|
|
3569
|
+
// Ensure within 1ms of expected location.
|
|
3570
|
+
return Math.abs(timeSinceTimingPoint + 1) % beatLength < 2;
|
|
3571
|
+
}
|
|
3572
|
+
/**
|
|
3573
|
+
* Generates a random number from a Normal distribution using the Box-Muller transform.
|
|
3574
|
+
*
|
|
3575
|
+
* @param random A {@link Random} to get the random number from.
|
|
3576
|
+
* @param mean The mean of the distribution.
|
|
3577
|
+
* @param stdDev The standard deviation of the distribution.
|
|
3578
|
+
* @return The random number.
|
|
3579
|
+
*/
|
|
3580
|
+
static randomGaussian(random, mean = 0, stdDev = 1) {
|
|
3581
|
+
// Generate 2 random numbers in the interval (0,1].
|
|
3582
|
+
// x1 must not be 0 since log(0) = undefined.
|
|
3583
|
+
const x1 = 1 - random.nextDouble();
|
|
3584
|
+
const x2 = 1 - random.nextDouble();
|
|
3585
|
+
const stdNormal = Math.sqrt(-2 * Math.log(x1)) * Math.sin(2 * Math.PI * x2);
|
|
3586
|
+
return mean + stdDev * stdNormal;
|
|
3587
|
+
}
|
|
3588
|
+
/**
|
|
3589
|
+
* Calculates a {@link Vector4} which contains all possible movements of a {@link Slider} (in relative
|
|
3590
|
+
* X/Y coordinates) such that the entire {@link Slider} is inside the playfield.
|
|
3591
|
+
*
|
|
3592
|
+
* If the {@link Slider} is larger than the playfield, the returned {@link Vector4} may have a Z/W component
|
|
3593
|
+
* that is smaller than its X/Y component.
|
|
3594
|
+
*
|
|
3595
|
+
* @param slider The {@link Slider} whose movement bound is to be calculated.
|
|
3596
|
+
* @return A {@link Vector4} which contains all possible movements of a {@link Slider} (in relative X/Y
|
|
3597
|
+
* coordinates) such that the entire {@link Slider} is inside the playfield.
|
|
3598
|
+
*/
|
|
3599
|
+
static calculatePossibleMovementBounds(slider) {
|
|
3600
|
+
const pathPositions = slider.path.pathToProgress(0, 1);
|
|
3601
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
3602
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
3603
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
3604
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
3605
|
+
// Compute the bounding box of the slider.
|
|
3606
|
+
for (const position of pathPositions) {
|
|
3607
|
+
minX = Math.min(minX, position.x);
|
|
3608
|
+
maxX = Math.max(maxX, position.x);
|
|
3609
|
+
minY = Math.min(minY, position.y);
|
|
3610
|
+
maxY = Math.max(maxY, position.y);
|
|
3611
|
+
}
|
|
3612
|
+
// Take the radius into account.
|
|
3613
|
+
const { radius } = slider;
|
|
3614
|
+
minX -= radius;
|
|
3615
|
+
minY -= radius;
|
|
3616
|
+
maxX += radius;
|
|
3617
|
+
maxY += radius;
|
|
3618
|
+
// Given the bounding box of the slider (via min/max X/Y), the amount that the slider can move to the left is
|
|
3619
|
+
// minX (with the sign flipped, since positive X is to the right), and the amount that it can move to the right
|
|
3620
|
+
// is WIDTH - maxX. The same calculation applies for the Y axis.
|
|
3621
|
+
const left = -minX;
|
|
3622
|
+
const right = Playfield.baseSize.x - maxX;
|
|
3623
|
+
const top = -minY;
|
|
3624
|
+
const bottom = Playfield.baseSize.y - maxY;
|
|
3625
|
+
return new Vector4(left, top, right, bottom);
|
|
3626
|
+
}
|
|
3627
|
+
/**
|
|
3628
|
+
* Computes the modified position of a {@link HitObject} while attempting to keep it inside the playfield.
|
|
3629
|
+
*
|
|
3630
|
+
* @param current The {@link WorkingObject} representing the {@link HitObject} to have the modified
|
|
3631
|
+
* position computed for.
|
|
3632
|
+
* @param previous The {@link WorkingObject} representing the {@link HitObject} immediately preceding
|
|
3633
|
+
* `current`.
|
|
3634
|
+
* @param beforePrevious The {@link WorkingObject} representing the {@link HitObject} immediately preceding
|
|
3635
|
+
* `previous`.
|
|
3636
|
+
*/
|
|
3637
|
+
static computeModifiedPosition(current, previous, beforePrevious) {
|
|
3638
|
+
var _b, _c;
|
|
3639
|
+
let previousAbsoluteAngle = 0;
|
|
3640
|
+
if (previous !== null) {
|
|
3641
|
+
if (previous.hitObject instanceof Slider) {
|
|
3642
|
+
previousAbsoluteAngle = this.getSliderRotation(previous.hitObject);
|
|
3643
|
+
}
|
|
3644
|
+
else {
|
|
3645
|
+
const earliestPosition = (_b = beforePrevious === null || beforePrevious === void 0 ? void 0 : beforePrevious.hitObject.endPosition) !== null && _b !== void 0 ? _b : Playfield.center;
|
|
3646
|
+
const relativePosition = previous.hitObject.position.subtract(earliestPosition);
|
|
3647
|
+
previousAbsoluteAngle = Math.atan2(relativePosition.y, relativePosition.x);
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
let absoluteAngle = previousAbsoluteAngle + current.positionInfo.relativeAngle;
|
|
3651
|
+
let positionRelativeToPrevious = new Vector2(current.positionInfo.distanceFromPrevious * Math.cos(absoluteAngle), current.positionInfo.distanceFromPrevious * Math.sin(absoluteAngle));
|
|
3652
|
+
const lastEndPosition = (_c = previous === null || previous === void 0 ? void 0 : previous.endPositionModified) !== null && _c !== void 0 ? _c : Playfield.center;
|
|
3653
|
+
positionRelativeToPrevious = this.rotateAwayFromEdge(lastEndPosition, positionRelativeToPrevious);
|
|
3654
|
+
current.positionModified = lastEndPosition.add(positionRelativeToPrevious);
|
|
3655
|
+
if (!(current.hitObject instanceof Slider)) {
|
|
3656
|
+
return;
|
|
3657
|
+
}
|
|
3658
|
+
absoluteAngle = Math.atan2(positionRelativeToPrevious.y, positionRelativeToPrevious.x);
|
|
3659
|
+
const centerOfMassOriginal = this.calculateCenterOfMass(current.hitObject);
|
|
3660
|
+
const centerOfMassModified = this.rotateAwayFromEdge(current.positionModified, this.rotateVector(centerOfMassOriginal, current.positionInfo.rotation +
|
|
3661
|
+
absoluteAngle -
|
|
3662
|
+
this.getSliderRotation(current.hitObject)));
|
|
3663
|
+
const relativeRotation = Math.atan2(centerOfMassModified.y, centerOfMassModified.x) -
|
|
3664
|
+
Math.atan2(centerOfMassOriginal.y, centerOfMassOriginal.x);
|
|
3665
|
+
if (!Precision.almostEqualsNumber(relativeRotation, 0)) {
|
|
3666
|
+
this.rotateSlider(current.hitObject, relativeRotation);
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
/**
|
|
3670
|
+
* Moves the modified position of a {@link Circle} so that it fits inside the playfield.
|
|
3671
|
+
*
|
|
3672
|
+
* @param workingObject The {@link WorkingObject} that represents the {@link Circle}.
|
|
3673
|
+
* @return The deviation from the original modified position in order to fit within the playfield.
|
|
3674
|
+
*/
|
|
3675
|
+
static clampHitCircleToPlayfield(workingObject) {
|
|
3676
|
+
const previousPosition = workingObject.positionModified;
|
|
3677
|
+
workingObject.positionModified = this.clampToPlayfield(workingObject.positionModified, workingObject.hitObject.radius);
|
|
3678
|
+
workingObject.endPositionModified = workingObject.positionModified;
|
|
3679
|
+
workingObject.hitObject.position = workingObject.positionModified;
|
|
3680
|
+
return workingObject.positionModified.subtract(previousPosition);
|
|
3681
|
+
}
|
|
3682
|
+
/**
|
|
3683
|
+
* Moves a {@link Slider} and all necessary `SliderHitObject`s into the playfield if they are not in
|
|
3684
|
+
* the playfield.
|
|
3685
|
+
*
|
|
3686
|
+
* @param workingObject The {@link WorkingObject} that represents the {@link Slider}.
|
|
3687
|
+
* @return The deviation from the original modified position in order to fit within the playfield.
|
|
3688
|
+
*/
|
|
3689
|
+
static clampSliderToPlayfield(workingObject) {
|
|
3690
|
+
const slider = workingObject.hitObject;
|
|
3691
|
+
let possibleMovementBounds = this.calculatePossibleMovementBounds(slider);
|
|
3692
|
+
// The slider rotation applied in computeModifiedPosition might make it impossible to fit the slider
|
|
3693
|
+
// into the playfield. For example, a long horizontal slider will be off-screen when rotated by 90
|
|
3694
|
+
// degrees. In this case, limit the rotation to either 0 or 180 degrees.
|
|
3695
|
+
if (possibleMovementBounds.width < 0 ||
|
|
3696
|
+
possibleMovementBounds.height < 0) {
|
|
3697
|
+
const currentRotation = this.getSliderRotation(slider);
|
|
3698
|
+
const diff1 = this.getAngleDifference(workingObject.rotationOriginal, currentRotation);
|
|
3699
|
+
const diff2 = this.getAngleDifference(workingObject.rotationOriginal + Math.PI, currentRotation);
|
|
3700
|
+
if (diff1 < diff2) {
|
|
3701
|
+
this.rotateSlider(slider, workingObject.rotationOriginal - currentRotation);
|
|
3702
|
+
}
|
|
3703
|
+
else {
|
|
3704
|
+
this.rotateSlider(slider, workingObject.rotationOriginal + Math.PI - currentRotation);
|
|
3705
|
+
}
|
|
3706
|
+
possibleMovementBounds =
|
|
3707
|
+
this.calculatePossibleMovementBounds(slider);
|
|
3708
|
+
}
|
|
3709
|
+
const previousPosition = workingObject.positionModified;
|
|
3710
|
+
// Clamp slider position to the placement area.
|
|
3711
|
+
// If the slider is larger than the playfield, at least make sure that the head circle is
|
|
3712
|
+
// inside the playfield.
|
|
3713
|
+
const newX = possibleMovementBounds.width < 0
|
|
3714
|
+
? MathUtils.clamp(possibleMovementBounds.left, 0, Playfield.baseSize.x)
|
|
3715
|
+
: MathUtils.clamp(previousPosition.x, possibleMovementBounds.left, possibleMovementBounds.right);
|
|
3716
|
+
const newY = possibleMovementBounds.height < 0
|
|
3717
|
+
? MathUtils.clamp(possibleMovementBounds.top, 0, Playfield.baseSize.y)
|
|
3718
|
+
: MathUtils.clamp(previousPosition.y, possibleMovementBounds.top, possibleMovementBounds.bottom);
|
|
3719
|
+
workingObject.positionModified = new Vector2(newX, newY);
|
|
3720
|
+
slider.position = workingObject.positionModified;
|
|
3721
|
+
workingObject.endPositionModified = slider.endPosition;
|
|
3722
|
+
return workingObject.positionModified.subtract(previousPosition);
|
|
3723
|
+
}
|
|
3724
|
+
/**
|
|
3725
|
+
* Clamps a {@link Vector2} into the playfield, keeping a specified distance from the edge of the playfield.
|
|
3726
|
+
*
|
|
3727
|
+
* @param vec The {@link Vector2} to clamp.
|
|
3728
|
+
* @param padding The minimum distance allowed from the edge of the playfield.
|
|
3729
|
+
* @return The clamped {@link Vector2}.
|
|
3730
|
+
*/
|
|
3731
|
+
static clampToPlayfield(vec, padding) {
|
|
3732
|
+
return new Vector2(MathUtils.clamp(vec.x, padding, Playfield.baseSize.x - padding), MathUtils.clamp(vec.y, padding, Playfield.baseSize.y - padding));
|
|
3733
|
+
}
|
|
3734
|
+
/**
|
|
3735
|
+
* Decreasingly shifts a list of {@link HitObject}s by a specified amount.
|
|
3736
|
+
*
|
|
3737
|
+
* The first item in the list is shifted by the largest amount, while the last item is shifted by the
|
|
3738
|
+
* smallest amount.
|
|
3739
|
+
*
|
|
3740
|
+
* @param hitObjects The list of {@link HitObject}s to be shifted.
|
|
3741
|
+
* @param shift The amount to shift the {@link HitObject}s by.
|
|
3742
|
+
*/
|
|
3743
|
+
static applyDecreasingShift(hitObjects, shift) {
|
|
3744
|
+
for (let i = 0; i < hitObjects.length; ++i) {
|
|
3745
|
+
const hitObject = hitObjects[i];
|
|
3746
|
+
// The first object is shifted by a vector slightly smaller than shift.
|
|
3747
|
+
// The last object is shifted by a vector slightly larger than zero.
|
|
3748
|
+
const position = hitObject.position.add(shift.scale((hitObjects.length - i) / (hitObjects.length + 1)));
|
|
3749
|
+
hitObject.position = this.clampToPlayfield(position, hitObject.radius);
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
/**
|
|
3753
|
+
* Estimates the center of mass of a {@link Slider} relative to its start position.
|
|
3754
|
+
*
|
|
3755
|
+
* @param slider The {@link Slider} whose center mass is to be estimated.
|
|
3756
|
+
* @return The estimated center of mass of `slider`.
|
|
3757
|
+
*/
|
|
3758
|
+
static calculateCenterOfMass(slider) {
|
|
3759
|
+
const sampleStep = 50;
|
|
3760
|
+
// Only sample the start and end positions if the slider is too short.
|
|
3761
|
+
if (slider.distance <= sampleStep) {
|
|
3762
|
+
return slider.path.positionAt(1).divide(2);
|
|
3763
|
+
}
|
|
3764
|
+
let count = 0;
|
|
3765
|
+
let sum = new Vector2(0);
|
|
3766
|
+
for (let i = 0; i < slider.distance; i += sampleStep) {
|
|
3767
|
+
sum = sum.add(slider.path.positionAt(i / slider.distance));
|
|
3768
|
+
++count;
|
|
3769
|
+
}
|
|
3770
|
+
return sum.divide(count);
|
|
3771
|
+
}
|
|
3772
|
+
/**
|
|
3773
|
+
* Calculates the absolute difference between two angles in radians.
|
|
3774
|
+
*
|
|
3775
|
+
* @param angle1 The first angle.
|
|
3776
|
+
* @param angle2 The second angle.
|
|
3777
|
+
* @return THe absolute difference within interval `[0, Math.PI]`.
|
|
3778
|
+
*/
|
|
3779
|
+
static getAngleDifference(angle1, angle2) {
|
|
3780
|
+
const diff = Math.abs(angle1 - angle2) % (2 * Math.PI);
|
|
3781
|
+
return Math.min(diff, 2 * Math.PI - diff);
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
_a = HitObjectGenerationUtils;
|
|
3785
|
+
/**
|
|
3786
|
+
* The relative distance to the edge of the playfield before {@link HitObject} positions should start
|
|
3787
|
+
* to "turn around" and curve towards the middle. The closer the {@link HitObject}s draw to the border,
|
|
3788
|
+
* the sharper the turn.
|
|
3789
|
+
*/
|
|
3790
|
+
HitObjectGenerationUtils.playfieldEdgeRatio = 0.375;
|
|
3791
|
+
/**
|
|
3792
|
+
* The amount of previous {@link HitObject}s to be shifted together when a {@link HitObject} is being moved.
|
|
3793
|
+
*/
|
|
3794
|
+
HitObjectGenerationUtils.precedingObjectsToShift = 10;
|
|
3795
|
+
HitObjectGenerationUtils.borderDistance = Playfield.baseSize.scale(_a.playfieldEdgeRatio);
|
|
3796
|
+
class WorkingObject {
|
|
3797
|
+
get hitObject() {
|
|
3798
|
+
return this.positionInfo.hitObject;
|
|
3799
|
+
}
|
|
3800
|
+
constructor(positionInfo) {
|
|
3801
|
+
this.rotationOriginal = this.hitObject instanceof Slider
|
|
3802
|
+
? HitObjectGenerationUtils.getSliderRotation(this.hitObject)
|
|
3803
|
+
: 0;
|
|
3804
|
+
this.positionModified = this.hitObject.position;
|
|
3805
|
+
this.endPositionModified = this.hitObject.endPosition;
|
|
3806
|
+
this.positionInfo = positionInfo;
|
|
3807
|
+
}
|
|
2942
3808
|
}
|
|
2943
3809
|
|
|
2944
3810
|
/**
|
|
@@ -2954,7 +3820,7 @@ class ModMirror extends Mod {
|
|
|
2954
3820
|
/**
|
|
2955
3821
|
* The axes to reflect the `HitObject`s along.
|
|
2956
3822
|
*/
|
|
2957
|
-
this.flippedAxes = exports.Axes.x;
|
|
3823
|
+
this.flippedAxes = new ModSetting("Flipped axes", "The axes to reflect the hit objects along.", exports.Axes.x);
|
|
2958
3824
|
this.incompatibleMods.add(ModHardRock);
|
|
2959
3825
|
}
|
|
2960
3826
|
get isDroidRelevant() {
|
|
@@ -2974,18 +3840,18 @@ class ModMirror extends Mod {
|
|
|
2974
3840
|
super.copySettings(mod);
|
|
2975
3841
|
switch ((_a = mod.settings) === null || _a === void 0 ? void 0 : _a.flippedAxes) {
|
|
2976
3842
|
case 0:
|
|
2977
|
-
this.flippedAxes = exports.Axes.x;
|
|
3843
|
+
this.flippedAxes.value = exports.Axes.x;
|
|
2978
3844
|
break;
|
|
2979
3845
|
case 1:
|
|
2980
|
-
this.flippedAxes = exports.Axes.y;
|
|
3846
|
+
this.flippedAxes.value = exports.Axes.y;
|
|
2981
3847
|
break;
|
|
2982
3848
|
case 2:
|
|
2983
|
-
this.flippedAxes = exports.Axes.both;
|
|
3849
|
+
this.flippedAxes.value = exports.Axes.both;
|
|
2984
3850
|
break;
|
|
2985
3851
|
}
|
|
2986
3852
|
}
|
|
2987
3853
|
applyToHitObject(_, hitObject) {
|
|
2988
|
-
switch (this.flippedAxes) {
|
|
3854
|
+
switch (this.flippedAxes.value) {
|
|
2989
3855
|
case exports.Axes.x:
|
|
2990
3856
|
HitObjectGenerationUtils.reflectHorizontallyAlongPlayfield(hitObject);
|
|
2991
3857
|
break;
|
|
@@ -2999,20 +3865,83 @@ class ModMirror extends Mod {
|
|
|
2999
3865
|
}
|
|
3000
3866
|
}
|
|
3001
3867
|
serializeSettings() {
|
|
3002
|
-
return { flippedAxes: this.flippedAxes - 1 };
|
|
3868
|
+
return { flippedAxes: this.flippedAxes.value - 1 };
|
|
3869
|
+
}
|
|
3870
|
+
equals(other) {
|
|
3871
|
+
return (super.equals(other) &&
|
|
3872
|
+
other instanceof ModMirror &&
|
|
3873
|
+
other.flippedAxes.value === this.flippedAxes.value);
|
|
3003
3874
|
}
|
|
3004
3875
|
toString() {
|
|
3005
3876
|
const settings = [];
|
|
3006
|
-
if (this.flippedAxes === exports.Axes.x ||
|
|
3877
|
+
if (this.flippedAxes.value === exports.Axes.x ||
|
|
3878
|
+
this.flippedAxes.value === exports.Axes.both) {
|
|
3007
3879
|
settings.push("↔");
|
|
3008
3880
|
}
|
|
3009
|
-
if (this.flippedAxes === exports.Axes.y ||
|
|
3881
|
+
if (this.flippedAxes.value === exports.Axes.y ||
|
|
3882
|
+
this.flippedAxes.value === exports.Axes.both) {
|
|
3010
3883
|
settings.push("↕");
|
|
3011
3884
|
}
|
|
3012
3885
|
return `${super.toString()} (${settings.join(", ")})`;
|
|
3013
3886
|
}
|
|
3014
3887
|
}
|
|
3015
3888
|
|
|
3889
|
+
/**
|
|
3890
|
+
* Represents the Replay V6 mod.
|
|
3891
|
+
*
|
|
3892
|
+
* Some behavior of beatmap parsing was changed in replay version 7. More specifically, object stacking
|
|
3893
|
+
* behavior now matches osu!stable and osu!lazer.
|
|
3894
|
+
*
|
|
3895
|
+
* This `Mod` is meant to reapply the stacking behavior prior to replay version 7 to a `Beatmap` that
|
|
3896
|
+
* was played in replays recorded in version 6 and older for replayability and difficulty calculation.
|
|
3897
|
+
*/
|
|
3898
|
+
class ModReplayV6 extends Mod {
|
|
3899
|
+
constructor() {
|
|
3900
|
+
super(...arguments);
|
|
3901
|
+
this.name = "Replay V6";
|
|
3902
|
+
this.acronym = "RV6";
|
|
3903
|
+
this.userPlayable = false;
|
|
3904
|
+
this.droidRanked = false;
|
|
3905
|
+
this.facilitateAdjustment = true;
|
|
3906
|
+
}
|
|
3907
|
+
get isDroidRelevant() {
|
|
3908
|
+
return true;
|
|
3909
|
+
}
|
|
3910
|
+
calculateDroidScoreMultiplier() {
|
|
3911
|
+
return 1;
|
|
3912
|
+
}
|
|
3913
|
+
applyToBeatmap(beatmap) {
|
|
3914
|
+
const { objects } = beatmap.hitObjects;
|
|
3915
|
+
if (objects.length === 0) {
|
|
3916
|
+
return;
|
|
3917
|
+
}
|
|
3918
|
+
// Reset stacking
|
|
3919
|
+
objects.forEach((h) => {
|
|
3920
|
+
h.stackHeight = 0;
|
|
3921
|
+
});
|
|
3922
|
+
for (let i = 0; i < objects.length - 1; ++i) {
|
|
3923
|
+
const current = objects[i];
|
|
3924
|
+
const next = objects[i + 1];
|
|
3925
|
+
this.revertObjectScale(current, beatmap.difficulty);
|
|
3926
|
+
this.revertObjectScale(next, beatmap.difficulty);
|
|
3927
|
+
const convertedScale = CircleSizeCalculator.standardScaleToOldDroidScale(objects[0].scale);
|
|
3928
|
+
if (current instanceof Circle &&
|
|
3929
|
+
next.startTime - current.startTime <
|
|
3930
|
+
2000 * beatmap.general.stackLeniency &&
|
|
3931
|
+
next.position.getDistance(current.position) <
|
|
3932
|
+
Math.sqrt(convertedScale)) {
|
|
3933
|
+
next.stackHeight = current.stackHeight + 1;
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
revertObjectScale(hitObject, difficulty) {
|
|
3938
|
+
const droidScale = CircleSizeCalculator.droidCSToOldDroidScale(difficulty.cs);
|
|
3939
|
+
const radius = CircleSizeCalculator.oldDroidScaleToStandardRadius(droidScale);
|
|
3940
|
+
const standardCS = CircleSizeCalculator.standardRadiusToStandardCS(radius, true);
|
|
3941
|
+
hitObject.scale = CircleSizeCalculator.standardCSToStandardScale(standardCS, true);
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3016
3945
|
/**
|
|
3017
3946
|
* Represents the HardRock mod.
|
|
3018
3947
|
*/
|
|
@@ -3039,17 +3968,15 @@ class ModHardRock extends Mod {
|
|
|
3039
3968
|
get osuScoreMultiplier() {
|
|
3040
3969
|
return 1.06;
|
|
3041
3970
|
}
|
|
3042
|
-
applyToDifficulty(mode, difficulty) {
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
difficulty.cs = this.applySetting(difficulty.cs, 1.3);
|
|
3052
|
-
break;
|
|
3971
|
+
applyToDifficulty(mode, difficulty, adjustmentMods) {
|
|
3972
|
+
if (mode === exports.Modes.osu || !adjustmentMods.has(ModReplayV6)) {
|
|
3973
|
+
difficulty.cs = this.applySetting(difficulty.cs, 1.3);
|
|
3974
|
+
}
|
|
3975
|
+
else {
|
|
3976
|
+
const scale = CircleSizeCalculator.droidCSToOldDroidScale(difficulty.cs);
|
|
3977
|
+
// The 0.125 scale that was added before replay version 7 was in screen pixels. We need it in osu! pixels.
|
|
3978
|
+
difficulty.cs = CircleSizeCalculator.oldDroidScaleToDroidCS(scale -
|
|
3979
|
+
CircleSizeCalculator.oldDroidScaleScreenPixelsToOsuPixels(0.125));
|
|
3053
3980
|
}
|
|
3054
3981
|
difficulty.ar = this.applySetting(difficulty.ar);
|
|
3055
3982
|
difficulty.od = this.applySetting(difficulty.od);
|
|
@@ -3058,12 +3985,6 @@ class ModHardRock extends Mod {
|
|
|
3058
3985
|
applyToHitObject(_, hitObject) {
|
|
3059
3986
|
HitObjectGenerationUtils.reflectVerticallyAlongPlayfield(hitObject);
|
|
3060
3987
|
}
|
|
3061
|
-
reflectVector(vector) {
|
|
3062
|
-
return new Vector2(vector.x, Playfield.baseSize.y - vector.y);
|
|
3063
|
-
}
|
|
3064
|
-
reflectControlPoint(vector) {
|
|
3065
|
-
return new Vector2(vector.x, -vector.y);
|
|
3066
|
-
}
|
|
3067
3988
|
applySetting(value, ratio = 1.4) {
|
|
3068
3989
|
return Math.min(value * ratio, 10);
|
|
3069
3990
|
}
|
|
@@ -3094,15 +4015,15 @@ class ModEasy extends Mod {
|
|
|
3094
4015
|
get osuScoreMultiplier() {
|
|
3095
4016
|
return 0.5;
|
|
3096
4017
|
}
|
|
3097
|
-
applyToDifficulty(mode, difficulty) {
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
4018
|
+
applyToDifficulty(mode, difficulty, adjustmentMods) {
|
|
4019
|
+
if (mode === exports.Modes.osu || !adjustmentMods.has(ModReplayV6)) {
|
|
4020
|
+
difficulty.cs /= 2;
|
|
4021
|
+
}
|
|
4022
|
+
else {
|
|
4023
|
+
const scale = CircleSizeCalculator.droidCSToOldDroidScale(difficulty.cs);
|
|
4024
|
+
// The 0.125 scale that was added before replay version 7 was in screen pixels. We need it in osu! pixels.
|
|
4025
|
+
difficulty.cs = CircleSizeCalculator.oldDroidScaleToDroidCS(scale +
|
|
4026
|
+
CircleSizeCalculator.oldDroidScaleScreenPixelsToOsuPixels(0.125));
|
|
3106
4027
|
}
|
|
3107
4028
|
difficulty.ar /= 2;
|
|
3108
4029
|
difficulty.od /= 2;
|
|
@@ -3124,13 +4045,13 @@ class ModFlashlight extends Mod {
|
|
|
3124
4045
|
/**
|
|
3125
4046
|
* The amount of seconds until the Flashlight follow area reaches the cursor.
|
|
3126
4047
|
*/
|
|
3127
|
-
this.followDelay = ModFlashlight.defaultFollowDelay;
|
|
4048
|
+
this.followDelay = new DecimalModSetting("Flashlight follow delay", "The amount of seconds until the Flashlight follow area reaches the cursor.", ModFlashlight.defaultFollowDelay, ModFlashlight.defaultFollowDelay, ModFlashlight.defaultFollowDelay * 10, ModFlashlight.defaultFollowDelay, 2);
|
|
3128
4049
|
}
|
|
3129
4050
|
copySettings(mod) {
|
|
3130
4051
|
var _a, _b;
|
|
3131
4052
|
super.copySettings(mod);
|
|
3132
|
-
this.followDelay =
|
|
3133
|
-
(_b = (_a = mod.settings) === null || _a === void 0 ? void 0 : _a.areaFollowDelay) !== null && _b !== void 0 ? _b : this.followDelay;
|
|
4053
|
+
this.followDelay.value =
|
|
4054
|
+
(_b = (_a = mod.settings) === null || _a === void 0 ? void 0 : _a.areaFollowDelay) !== null && _b !== void 0 ? _b : this.followDelay.value;
|
|
3134
4055
|
}
|
|
3135
4056
|
get isDroidRelevant() {
|
|
3136
4057
|
return true;
|
|
@@ -3145,13 +4066,18 @@ class ModFlashlight extends Mod {
|
|
|
3145
4066
|
return 1.12;
|
|
3146
4067
|
}
|
|
3147
4068
|
serializeSettings() {
|
|
3148
|
-
return { areaFollowDelay: this.followDelay };
|
|
4069
|
+
return { areaFollowDelay: this.followDelay.value };
|
|
4070
|
+
}
|
|
4071
|
+
equals(other) {
|
|
4072
|
+
return (super.equals(other) &&
|
|
4073
|
+
other instanceof ModFlashlight &&
|
|
4074
|
+
other.followDelay.value === this.followDelay.value);
|
|
3149
4075
|
}
|
|
3150
4076
|
toString() {
|
|
3151
|
-
if (this.followDelay === ModFlashlight.defaultFollowDelay) {
|
|
4077
|
+
if (this.followDelay.value === ModFlashlight.defaultFollowDelay) {
|
|
3152
4078
|
return super.toString();
|
|
3153
4079
|
}
|
|
3154
|
-
return `${super.toString()} (${this.followDelay.
|
|
4080
|
+
return `${super.toString()} (${this.followDelay.toDisplayString()}s follow delay)`;
|
|
3155
4081
|
}
|
|
3156
4082
|
}
|
|
3157
4083
|
/**
|
|
@@ -3185,6 +4111,16 @@ class ModTraceable extends Mod {
|
|
|
3185
4111
|
}
|
|
3186
4112
|
}
|
|
3187
4113
|
|
|
4114
|
+
/**
|
|
4115
|
+
* Represents a `Mod` specific setting that is constrained to a boolean value.
|
|
4116
|
+
*/
|
|
4117
|
+
class BooleanModSetting extends ModSetting {
|
|
4118
|
+
constructor() {
|
|
4119
|
+
super(...arguments);
|
|
4120
|
+
this.displayFormatter = (v) => v ? "Enabled" : "Disabled";
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
|
|
3188
4124
|
/**
|
|
3189
4125
|
* Represents the Hidden mod.
|
|
3190
4126
|
*/
|
|
@@ -3196,6 +4132,12 @@ class ModHidden extends Mod {
|
|
|
3196
4132
|
this.droidRanked = true;
|
|
3197
4133
|
this.osuRanked = true;
|
|
3198
4134
|
this.bitwise = 1 << 3;
|
|
4135
|
+
/**
|
|
4136
|
+
* Whether to only fade approach circles.
|
|
4137
|
+
*
|
|
4138
|
+
* The main object body will not fade when enabled.
|
|
4139
|
+
*/
|
|
4140
|
+
this.onlyFadeApproachCircles = new BooleanModSetting("Only fade approach circles", "The main object body will not fade when enabled.", false);
|
|
3199
4141
|
this.incompatibleMods.add(ModTraceable);
|
|
3200
4142
|
}
|
|
3201
4143
|
get isDroidRelevant() {
|
|
@@ -3210,6 +4152,12 @@ class ModHidden extends Mod {
|
|
|
3210
4152
|
get osuScoreMultiplier() {
|
|
3211
4153
|
return 1.06;
|
|
3212
4154
|
}
|
|
4155
|
+
copySettings(mod) {
|
|
4156
|
+
var _a, _b;
|
|
4157
|
+
super.copySettings(mod);
|
|
4158
|
+
this.onlyFadeApproachCircles.value =
|
|
4159
|
+
(_b = (_a = mod.settings) === null || _a === void 0 ? void 0 : _a.onlyFadeApproachCircles) !== null && _b !== void 0 ? _b : this.onlyFadeApproachCircles.value;
|
|
4160
|
+
}
|
|
3213
4161
|
applyToBeatmap(beatmap) {
|
|
3214
4162
|
const applyFadeInAdjustment = (hitObject) => {
|
|
3215
4163
|
hitObject.timeFadeIn =
|
|
@@ -3220,6 +4168,23 @@ class ModHidden extends Mod {
|
|
|
3220
4168
|
};
|
|
3221
4169
|
beatmap.hitObjects.objects.forEach(applyFadeInAdjustment);
|
|
3222
4170
|
}
|
|
4171
|
+
serializeSettings() {
|
|
4172
|
+
return this.onlyFadeApproachCircles.value
|
|
4173
|
+
? { onlyFadeApproachCircles: this.onlyFadeApproachCircles.value }
|
|
4174
|
+
: null;
|
|
4175
|
+
}
|
|
4176
|
+
equals(other) {
|
|
4177
|
+
return (super.equals(other) &&
|
|
4178
|
+
other instanceof ModHidden &&
|
|
4179
|
+
other.onlyFadeApproachCircles.value ===
|
|
4180
|
+
this.onlyFadeApproachCircles.value);
|
|
4181
|
+
}
|
|
4182
|
+
toString() {
|
|
4183
|
+
if (!this.onlyFadeApproachCircles.value) {
|
|
4184
|
+
return super.toString();
|
|
4185
|
+
}
|
|
4186
|
+
return `${super.toString()} (approach circles only)`;
|
|
4187
|
+
}
|
|
3223
4188
|
}
|
|
3224
4189
|
ModHidden.fadeInDurationMultiplier = 0.4;
|
|
3225
4190
|
ModHidden.fadeOutDurationMultiplier = 0.3;
|
|
@@ -3306,72 +4271,347 @@ class ModNoFail extends Mod {
|
|
|
3306
4271
|
}
|
|
3307
4272
|
|
|
3308
4273
|
/**
|
|
3309
|
-
* Represents
|
|
3310
|
-
*
|
|
3311
|
-
* All we need from spinners is their duration. The
|
|
3312
|
-
* position of a spinner is always at 256x192.
|
|
4274
|
+
* Represents the Precise mod.
|
|
3313
4275
|
*/
|
|
3314
|
-
class
|
|
3315
|
-
|
|
3316
|
-
|
|
4276
|
+
class ModPrecise extends Mod {
|
|
4277
|
+
constructor() {
|
|
4278
|
+
super(...arguments);
|
|
4279
|
+
this.acronym = "PR";
|
|
4280
|
+
this.name = "Precise";
|
|
4281
|
+
this.droidRanked = true;
|
|
3317
4282
|
}
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
this._endTime = values.endTime;
|
|
4283
|
+
get isDroidRelevant() {
|
|
4284
|
+
return true;
|
|
3321
4285
|
}
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
4286
|
+
calculateDroidScoreMultiplier() {
|
|
4287
|
+
return 1.06;
|
|
4288
|
+
}
|
|
4289
|
+
applyToHitObject(mode, hitObject) {
|
|
4290
|
+
var _a, _b;
|
|
4291
|
+
if (mode !== exports.Modes.droid || hitObject instanceof Spinner) {
|
|
4292
|
+
return;
|
|
4293
|
+
}
|
|
4294
|
+
if (hitObject instanceof Slider) {
|
|
4295
|
+
// For sliders, the hit window is enforced in the head - everything else is an instant hit or miss.
|
|
4296
|
+
hitObject.head.hitWindow = new PreciseDroidHitWindow((_a = hitObject.head.hitWindow) === null || _a === void 0 ? void 0 : _a.overallDifficulty);
|
|
4297
|
+
}
|
|
4298
|
+
else {
|
|
4299
|
+
hitObject.hitWindow = new PreciseDroidHitWindow((_b = hitObject.hitWindow) === null || _b === void 0 ? void 0 : _b.overallDifficulty);
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
/**
|
|
4305
|
+
* Some utilities, no biggie.
|
|
4306
|
+
*/
|
|
4307
|
+
class Utils {
|
|
4308
|
+
/**
|
|
4309
|
+
* Returns a random element of an array.
|
|
4310
|
+
*
|
|
4311
|
+
* @param array The array to get the element from.
|
|
4312
|
+
*/
|
|
4313
|
+
static getRandomArrayElement(array) {
|
|
4314
|
+
return array[Math.floor(Math.random() * array.length)];
|
|
4315
|
+
}
|
|
4316
|
+
/**
|
|
4317
|
+
* Creates an array with specific length that's prefilled with an initial value.
|
|
4318
|
+
*
|
|
4319
|
+
* @param length The length of the array.
|
|
4320
|
+
* @param initialValue The initial value of each element, or a function that returns the initial value of each element.
|
|
4321
|
+
* @returns The array.
|
|
4322
|
+
*/
|
|
4323
|
+
static initializeArray(length, initialValue) {
|
|
4324
|
+
const array = new Array(length);
|
|
4325
|
+
if (initialValue !== undefined) {
|
|
4326
|
+
for (let i = 0; i < length; ++i) {
|
|
4327
|
+
array[i] =
|
|
4328
|
+
typeof initialValue === "function"
|
|
4329
|
+
? initialValue(i)
|
|
4330
|
+
: initialValue;
|
|
4331
|
+
}
|
|
4332
|
+
}
|
|
4333
|
+
return array;
|
|
4334
|
+
}
|
|
4335
|
+
/**
|
|
4336
|
+
* Pauses the execution of a function for
|
|
4337
|
+
* the specified duration.
|
|
4338
|
+
*
|
|
4339
|
+
* @param duration The duration to pause for, in seconds.
|
|
4340
|
+
*/
|
|
4341
|
+
static sleep(duration) {
|
|
4342
|
+
return new Promise((resolve) => setTimeout(resolve, duration * 1000));
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
|
|
4346
|
+
/**
|
|
4347
|
+
* A pseudo-random number generator that shares the same implementation with
|
|
4348
|
+
* {@link https://github.com/dotnet/runtime/blob/v9.0.4/src/libraries/System.Private.CoreLib/src/System/Random.Net5CompatImpl.cs .NET 5's `System.Random`}.
|
|
4349
|
+
*
|
|
4350
|
+
* Used in the Random mod to ensure that a seed generates the result that users expect.
|
|
4351
|
+
*/
|
|
4352
|
+
class Random {
|
|
4353
|
+
/**
|
|
4354
|
+
* Constructs a new instance of the `Random` class using the specified seed.
|
|
4355
|
+
*
|
|
4356
|
+
* @param seed The seed to use for the random number generator. This value is clamped to the range [-2147483648, 2147483647] and must be an integer.
|
|
4357
|
+
*/
|
|
4358
|
+
constructor(seed) {
|
|
4359
|
+
this.seedArray = Utils.initializeArray(56, 0);
|
|
4360
|
+
this.iNext = 0;
|
|
4361
|
+
this.iNextP = 21;
|
|
4362
|
+
seed = Math.trunc(seed);
|
|
4363
|
+
const subtraction = seed <= Random.MIN_VALUE ? Random.MAX_VALUE : Math.abs(seed);
|
|
4364
|
+
// Magic number based on Phi (golden ratio).
|
|
4365
|
+
let mj = 161803398 - subtraction;
|
|
4366
|
+
this.seedArray[55] = mj;
|
|
4367
|
+
let mk = 1;
|
|
4368
|
+
let ii = 0;
|
|
4369
|
+
for (let i = 1; i < 55; ++i) {
|
|
4370
|
+
// The range [1..55] is special (Knuth) and so we're wasting the 0'th position.
|
|
4371
|
+
ii = (21 * i) % 55;
|
|
4372
|
+
this.seedArray[ii] = mk;
|
|
4373
|
+
mk = mj - mk;
|
|
4374
|
+
if (mk < 0) {
|
|
4375
|
+
mk += Random.MAX_VALUE;
|
|
4376
|
+
}
|
|
4377
|
+
mj = this.seedArray[ii];
|
|
4378
|
+
}
|
|
4379
|
+
for (let k = 1; k < 5; ++k) {
|
|
4380
|
+
for (let i = 1; i < 56; ++i) {
|
|
4381
|
+
const n = (i + 30) % 55;
|
|
4382
|
+
this.seedArray[i] -= this.seedArray[n + 1];
|
|
4383
|
+
if (this.seedArray[i] < 0) {
|
|
4384
|
+
this.seedArray[i] += Random.MAX_VALUE;
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
nextDouble() {
|
|
4390
|
+
return this.sample();
|
|
3328
4391
|
}
|
|
3329
|
-
|
|
3330
|
-
return this.
|
|
4392
|
+
sample() {
|
|
4393
|
+
return this.internalSample() / 2147483647;
|
|
3331
4394
|
}
|
|
3332
|
-
|
|
3333
|
-
|
|
4395
|
+
internalSample() {
|
|
4396
|
+
let locINext = this.iNext;
|
|
4397
|
+
if (++locINext >= 56) {
|
|
4398
|
+
locINext = 1;
|
|
4399
|
+
}
|
|
4400
|
+
let locINextP = this.iNextP;
|
|
4401
|
+
if (++locINextP >= 56) {
|
|
4402
|
+
locINextP = 1;
|
|
4403
|
+
}
|
|
4404
|
+
let retVal = this.seedArray[locINext] - this.seedArray[locINextP];
|
|
4405
|
+
if (retVal === Random.MAX_VALUE) {
|
|
4406
|
+
--retVal;
|
|
4407
|
+
}
|
|
4408
|
+
if (retVal < 0) {
|
|
4409
|
+
retVal += Random.MAX_VALUE;
|
|
4410
|
+
}
|
|
4411
|
+
this.seedArray[locINext] = retVal;
|
|
4412
|
+
this.iNext = locINext;
|
|
4413
|
+
this.iNextP = locINextP;
|
|
4414
|
+
return retVal;
|
|
3334
4415
|
}
|
|
3335
|
-
|
|
3336
|
-
|
|
4416
|
+
}
|
|
4417
|
+
Random.MIN_VALUE = -2147483648;
|
|
4418
|
+
Random.MAX_VALUE = 2147483647;
|
|
4419
|
+
|
|
4420
|
+
/**
|
|
4421
|
+
* Represents a `Mod` specific setting that is constrained to a number of values.
|
|
4422
|
+
*
|
|
4423
|
+
* The value can be `null`, which is treated as a special case.
|
|
4424
|
+
*/
|
|
4425
|
+
class NullableIntegerModSetting extends RangeConstrainedModSetting {
|
|
4426
|
+
constructor(name, description, defaultValue, min = -2147483648, max = 2147483647) {
|
|
4427
|
+
super(name, description, defaultValue, min, max, 1);
|
|
4428
|
+
this.displayFormatter = (v) => { var _a; return (_a = v === null || v === void 0 ? void 0 : v.toString()) !== null && _a !== void 0 ? _a : "None"; };
|
|
4429
|
+
if (min > max) {
|
|
4430
|
+
throw new RangeError(`The minimum value (${min}) must be less than or equal to the maximum value (${max}).`);
|
|
4431
|
+
}
|
|
4432
|
+
if (defaultValue !== null &&
|
|
4433
|
+
(defaultValue < min || defaultValue > max)) {
|
|
4434
|
+
throw new RangeError(`The default value (${defaultValue}) must be between the minimum (${min}) and maximum (${max}) values.`);
|
|
4435
|
+
}
|
|
3337
4436
|
}
|
|
3338
|
-
|
|
3339
|
-
|
|
4437
|
+
processValue(value) {
|
|
4438
|
+
if (value === null) {
|
|
4439
|
+
return null;
|
|
4440
|
+
}
|
|
4441
|
+
return Math.trunc(MathUtils.clamp(value, this.min, this.max));
|
|
3340
4442
|
}
|
|
3341
4443
|
}
|
|
3342
|
-
Spinner.baseSpinnerSpinSample = new BankHitSampleInfo("spinnerspin");
|
|
3343
|
-
Spinner.baseSpinnerBonusSample = new BankHitSampleInfo("spinnerbonus");
|
|
3344
4444
|
|
|
3345
4445
|
/**
|
|
3346
|
-
* Represents the
|
|
4446
|
+
* Represents the Random mod.
|
|
3347
4447
|
*/
|
|
3348
|
-
class
|
|
4448
|
+
class ModRandom extends Mod {
|
|
3349
4449
|
constructor() {
|
|
3350
4450
|
super(...arguments);
|
|
3351
|
-
this.
|
|
3352
|
-
this.
|
|
3353
|
-
this.droidRanked =
|
|
4451
|
+
this.name = "Random";
|
|
4452
|
+
this.acronym = "RD";
|
|
4453
|
+
this.droidRanked = false;
|
|
4454
|
+
this.osuRanked = false;
|
|
4455
|
+
/**
|
|
4456
|
+
* The seed to use.
|
|
4457
|
+
*/
|
|
4458
|
+
this.seed = new NullableIntegerModSetting("Seed", "Use a custom seed instead of a random one.", null, 0);
|
|
4459
|
+
/**
|
|
4460
|
+
* Defines how sharp the angles of `HitObject`s should be.
|
|
4461
|
+
*/
|
|
4462
|
+
this.angleSharpness = new DecimalModSetting("Angle sharpness", "Defines how sharp the angles of hit objects should be.", 7, 1, 10, 0.1, 1);
|
|
4463
|
+
this.random = null;
|
|
3354
4464
|
}
|
|
3355
4465
|
get isDroidRelevant() {
|
|
3356
4466
|
return true;
|
|
3357
4467
|
}
|
|
3358
4468
|
calculateDroidScoreMultiplier() {
|
|
3359
|
-
return 1
|
|
4469
|
+
return 1;
|
|
3360
4470
|
}
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
4471
|
+
get isOsuRelevant() {
|
|
4472
|
+
return true;
|
|
4473
|
+
}
|
|
4474
|
+
get osuScoreMultiplier() {
|
|
4475
|
+
return 1;
|
|
4476
|
+
}
|
|
4477
|
+
copySettings(mod) {
|
|
4478
|
+
super.copySettings(mod);
|
|
4479
|
+
const { settings } = mod;
|
|
4480
|
+
if (typeof (settings === null || settings === void 0 ? void 0 : settings.seed) === "number") {
|
|
4481
|
+
this.seed.value = settings.seed;
|
|
3365
4482
|
}
|
|
3366
|
-
if (
|
|
3367
|
-
|
|
3368
|
-
hitObject.head.hitWindow = new PreciseDroidHitWindow((_a = hitObject.head.hitWindow) === null || _a === void 0 ? void 0 : _a.overallDifficulty);
|
|
4483
|
+
if (typeof (settings === null || settings === void 0 ? void 0 : settings.angleSharpness) === "number") {
|
|
4484
|
+
this.angleSharpness.value = settings.angleSharpness;
|
|
3369
4485
|
}
|
|
3370
|
-
|
|
3371
|
-
|
|
4486
|
+
}
|
|
4487
|
+
applyToBeatmap(beatmap) {
|
|
4488
|
+
var _a;
|
|
4489
|
+
var _b;
|
|
4490
|
+
(_a = (_b = this.seed).value) !== null && _a !== void 0 ? _a : (_b.value = Math.floor(Math.random() * 2147483647));
|
|
4491
|
+
this.random = new Random(this.seed.value);
|
|
4492
|
+
const positionInfos = HitObjectGenerationUtils.generatePositionInfos(beatmap.hitObjects.objects);
|
|
4493
|
+
// Offsets the angles of all hit objects in a "section" by the same amount.
|
|
4494
|
+
let sectionOffset = 0;
|
|
4495
|
+
// Whether the angles are positive or negative (clockwise or counter-clockwise flow).
|
|
4496
|
+
let flowDirection = false;
|
|
4497
|
+
for (let i = 0; i < positionInfos.length; ++i) {
|
|
4498
|
+
const positionInfo = positionInfos[i];
|
|
4499
|
+
if (this.shouldStartNewSection(beatmap, positionInfos, i)) {
|
|
4500
|
+
sectionOffset = this.getRandomOffset(0.0008);
|
|
4501
|
+
flowDirection = !flowDirection;
|
|
4502
|
+
}
|
|
4503
|
+
if (positionInfo.hitObject instanceof Slider &&
|
|
4504
|
+
this.random.nextDouble() < 0.5) {
|
|
4505
|
+
HitObjectGenerationUtils.flipSliderInPlaceHorizontally(positionInfo.hitObject);
|
|
4506
|
+
}
|
|
4507
|
+
if (i === 0) {
|
|
4508
|
+
positionInfo.distanceFromPrevious =
|
|
4509
|
+
this.random.nextDouble() * Playfield.center.y;
|
|
4510
|
+
positionInfo.relativeAngle =
|
|
4511
|
+
this.random.nextDouble() * 2 * Math.PI - Math.PI;
|
|
4512
|
+
}
|
|
4513
|
+
else {
|
|
4514
|
+
// Offsets only the angle of the current hit object if a flow change occurs.
|
|
4515
|
+
let flowChangeOffset = 0;
|
|
4516
|
+
// Offsets only the angle of the current hit object.
|
|
4517
|
+
const oneTimeOffset = this.getRandomOffset(0.002);
|
|
4518
|
+
if (this.shouldApplyFlowChange(positionInfos, i)) {
|
|
4519
|
+
flowChangeOffset = this.getRandomOffset(0.002);
|
|
4520
|
+
flowDirection = !flowDirection;
|
|
4521
|
+
}
|
|
4522
|
+
const totalOffset =
|
|
4523
|
+
// sectionOffset and oneTimeOffset should mainly affect patterns with large spacing.
|
|
4524
|
+
(sectionOffset + oneTimeOffset) *
|
|
4525
|
+
positionInfo.distanceFromPrevious +
|
|
4526
|
+
// flowChangeOffset should mainly affect streams.
|
|
4527
|
+
flowChangeOffset *
|
|
4528
|
+
(ModRandom.playfieldDiagonal -
|
|
4529
|
+
positionInfo.distanceFromPrevious);
|
|
4530
|
+
positionInfo.relativeAngle = this.getRelativeTargetAngle(positionInfo.distanceFromPrevious, totalOffset, flowDirection);
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
const repositionedHitObjects = HitObjectGenerationUtils.repositionHitObjects(positionInfos);
|
|
4534
|
+
for (let i = 0; i < repositionedHitObjects.length; ++i) {
|
|
4535
|
+
beatmap.hitObjects.objects[i] = repositionedHitObjects[i];
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
serializeSettings() {
|
|
4539
|
+
const settings = {};
|
|
4540
|
+
if (this.seed.value !== null) {
|
|
4541
|
+
settings.seed = this.seed.value;
|
|
4542
|
+
}
|
|
4543
|
+
settings.angleSharpness = this.angleSharpness.value;
|
|
4544
|
+
return settings;
|
|
4545
|
+
}
|
|
4546
|
+
getRandomOffset(stdDev) {
|
|
4547
|
+
// Range: [0.5, 2]
|
|
4548
|
+
// Higher angle sharpness -> lower multiplier
|
|
4549
|
+
const customMultiplier = (1.5 * this.angleSharpness.max - this.angleSharpness.value) /
|
|
4550
|
+
(1.5 * this.angleSharpness.max - this.angleSharpness.defaultValue);
|
|
4551
|
+
return HitObjectGenerationUtils.randomGaussian(this.random, 0, stdDev * customMultiplier);
|
|
4552
|
+
}
|
|
4553
|
+
/**
|
|
4554
|
+
* @param targetDistance The target distance between the previous and the current `HitObject`.
|
|
4555
|
+
* @param offset The angle (in radians) by which the target angle should be offset.
|
|
4556
|
+
* @param flowDirection Whether the relative angle should be positive (`false`) or negative (`true`).
|
|
4557
|
+
*/
|
|
4558
|
+
getRelativeTargetAngle(targetDistance, offset, flowDirection) {
|
|
4559
|
+
// Range: [0.1, 1]
|
|
4560
|
+
const angleSharpness = this.angleSharpness.value / this.angleSharpness.max;
|
|
4561
|
+
// Range: [0, 0.9]
|
|
4562
|
+
const angleWideness = 1 - angleSharpness;
|
|
4563
|
+
// Range: [-60, 30]
|
|
4564
|
+
const customOffsetX = angleSharpness * 100 - 70;
|
|
4565
|
+
// Range: [-0.075, 0.15]
|
|
4566
|
+
const customOffsetY = angleWideness * 0.25 - 0.075;
|
|
4567
|
+
const angle = 2.16 /
|
|
4568
|
+
(1 +
|
|
4569
|
+
200 *
|
|
4570
|
+
Math.exp(0.036 * (targetDistance + customOffsetX * 2 - 310))) +
|
|
4571
|
+
0.5 +
|
|
4572
|
+
offset +
|
|
4573
|
+
customOffsetY;
|
|
4574
|
+
const relativeAngle = Math.PI - angle;
|
|
4575
|
+
return flowDirection ? -relativeAngle : relativeAngle;
|
|
4576
|
+
}
|
|
4577
|
+
/**
|
|
4578
|
+
* Determines whether a new section should be started at the current [HitObject].
|
|
4579
|
+
*/
|
|
4580
|
+
shouldStartNewSection(beatmap, positionInfos, i) {
|
|
4581
|
+
if (i === 0) {
|
|
4582
|
+
return true;
|
|
4583
|
+
}
|
|
4584
|
+
// Exclude new-combo-spam and 1-2-combos.
|
|
4585
|
+
const previousObjectStartedCombo = positionInfos[Math.max(0, i - 2)].hitObject.indexInCurrentCombo >
|
|
4586
|
+
1 && positionInfos[i - 1].hitObject.isNewCombo;
|
|
4587
|
+
const previousObjectWasOnDownBeat = HitObjectGenerationUtils.isHitObjectOnBeat(beatmap, positionInfos[i - 1].hitObject, true);
|
|
4588
|
+
const previousObjectWasOnBeat = HitObjectGenerationUtils.isHitObjectOnBeat(beatmap, positionInfos[i - 1].hitObject);
|
|
4589
|
+
return ((previousObjectStartedCombo && this.random.nextDouble() < 0.6) ||
|
|
4590
|
+
previousObjectWasOnDownBeat ||
|
|
4591
|
+
(previousObjectWasOnBeat && this.random.nextDouble() < 0.4));
|
|
4592
|
+
}
|
|
4593
|
+
shouldApplyFlowChange(positionInfos, i) {
|
|
4594
|
+
// Exclude new-combo-spam and 1-2-combos.
|
|
4595
|
+
const previousObjectStartedCombo = positionInfos[Math.max(0, i - 2)].hitObject.indexInCurrentCombo >
|
|
4596
|
+
1 && positionInfos[i - 1].hitObject.isNewCombo;
|
|
4597
|
+
return previousObjectStartedCombo && this.random.nextDouble() < 0.6;
|
|
4598
|
+
}
|
|
4599
|
+
equals(other) {
|
|
4600
|
+
return (super.equals(other) &&
|
|
4601
|
+
other instanceof ModRandom &&
|
|
4602
|
+
other.seed.value === this.seed.value &&
|
|
4603
|
+
other.angleSharpness.value === this.angleSharpness.value);
|
|
4604
|
+
}
|
|
4605
|
+
toString() {
|
|
4606
|
+
const settings = [];
|
|
4607
|
+
if (this.seed.value !== null) {
|
|
4608
|
+
settings.push(`seed: ${this.seed.value}`);
|
|
3372
4609
|
}
|
|
4610
|
+
settings.push(`angle sharpness: ${this.angleSharpness.toDisplayString()}`);
|
|
4611
|
+
return `${super.toString()} (${settings.join(", ")})`;
|
|
3373
4612
|
}
|
|
3374
4613
|
}
|
|
4614
|
+
ModRandom.playfieldDiagonal = Playfield.baseSize.length;
|
|
3375
4615
|
|
|
3376
4616
|
/**
|
|
3377
4617
|
* Represents the ReallyEasy mod.
|
|
@@ -3389,29 +4629,36 @@ class ModReallyEasy extends Mod {
|
|
|
3389
4629
|
calculateDroidScoreMultiplier() {
|
|
3390
4630
|
return 0.4;
|
|
3391
4631
|
}
|
|
3392
|
-
|
|
4632
|
+
applyToDifficultyWithMods(mode, difficulty, mods) {
|
|
3393
4633
|
var _a;
|
|
3394
4634
|
if (mode !== exports.Modes.droid) {
|
|
3395
4635
|
return;
|
|
3396
4636
|
}
|
|
3397
|
-
const
|
|
3398
|
-
if ((
|
|
4637
|
+
const difficultyAdjust = mods.get(ModDifficultyAdjust);
|
|
4638
|
+
if (typeof (difficultyAdjust === null || difficultyAdjust === void 0 ? void 0 : difficultyAdjust.ar.value) !== "number") {
|
|
3399
4639
|
if (mods.has(ModEasy)) {
|
|
3400
4640
|
difficulty.ar *= 2;
|
|
3401
4641
|
difficulty.ar -= 0.5;
|
|
3402
4642
|
}
|
|
3403
4643
|
const customSpeed = mods.get(ModCustomSpeed);
|
|
3404
4644
|
difficulty.ar -= 0.5;
|
|
3405
|
-
difficulty.ar -= ((_a = customSpeed === null || customSpeed === void 0 ? void 0 : customSpeed.trackRateMultiplier) !== null && _a !== void 0 ? _a : 1) - 1;
|
|
4645
|
+
difficulty.ar -= ((_a = customSpeed === null || customSpeed === void 0 ? void 0 : customSpeed.trackRateMultiplier.value) !== null && _a !== void 0 ? _a : 1) - 1;
|
|
3406
4646
|
}
|
|
3407
|
-
if ((
|
|
3408
|
-
|
|
3409
|
-
|
|
4647
|
+
if (typeof (difficultyAdjust === null || difficultyAdjust === void 0 ? void 0 : difficultyAdjust.cs.value) !== "number") {
|
|
4648
|
+
if (!mods.has(ModReplayV6)) {
|
|
4649
|
+
difficulty.cs /= 2;
|
|
4650
|
+
}
|
|
4651
|
+
else {
|
|
4652
|
+
const scale = CircleSizeCalculator.droidCSToOldDroidScale(difficulty.cs);
|
|
4653
|
+
// The 0.125 scale that was added before replay version 7 was in screen pixels. We need it in osu! pixels.
|
|
4654
|
+
difficulty.cs = CircleSizeCalculator.oldDroidScaleToDroidCS(scale +
|
|
4655
|
+
CircleSizeCalculator.oldDroidScaleScreenPixelsToOsuPixels(0.125));
|
|
4656
|
+
}
|
|
3410
4657
|
}
|
|
3411
|
-
if ((
|
|
4658
|
+
if (typeof (difficultyAdjust === null || difficultyAdjust === void 0 ? void 0 : difficultyAdjust.od.value) !== "number") {
|
|
3412
4659
|
difficulty.od /= 2;
|
|
3413
4660
|
}
|
|
3414
|
-
if ((
|
|
4661
|
+
if (typeof (difficultyAdjust === null || difficultyAdjust === void 0 ? void 0 : difficultyAdjust.hp.value) !== "number") {
|
|
3415
4662
|
difficulty.hp /= 2;
|
|
3416
4663
|
}
|
|
3417
4664
|
}
|
|
@@ -3464,19 +4711,13 @@ class ModSmallCircle extends Mod {
|
|
|
3464
4711
|
migrateDroidMod(difficulty) {
|
|
3465
4712
|
return new ModDifficultyAdjust({ cs: difficulty.cs + 4 });
|
|
3466
4713
|
}
|
|
3467
|
-
applyToDifficulty(mode, difficulty) {
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
2) /
|
|
3475
|
-
128);
|
|
3476
|
-
break;
|
|
3477
|
-
}
|
|
3478
|
-
case exports.Modes.osu:
|
|
3479
|
-
difficulty.cs += 4;
|
|
4714
|
+
applyToDifficulty(mode, difficulty, adjustmentMods) {
|
|
4715
|
+
if (mode === exports.Modes.osu || !adjustmentMods.has(ModDifficultyAdjust)) {
|
|
4716
|
+
difficulty.cs += 4;
|
|
4717
|
+
}
|
|
4718
|
+
else {
|
|
4719
|
+
const scale = CircleSizeCalculator.droidCSToOldDroidScale(difficulty.cs);
|
|
4720
|
+
difficulty.cs = CircleSizeCalculator.oldDroidScaleToDroidCS(scale + CircleSizeCalculator.droidCSToOldDroidScale(4));
|
|
3480
4721
|
}
|
|
3481
4722
|
}
|
|
3482
4723
|
}
|
|
@@ -3778,8 +5019,10 @@ class ModTimeRamp extends Mod {
|
|
|
3778
5019
|
var _a, _b;
|
|
3779
5020
|
super.copySettings(mod);
|
|
3780
5021
|
const { settings } = mod;
|
|
3781
|
-
this.initialRate =
|
|
3782
|
-
|
|
5022
|
+
this.initialRate.value =
|
|
5023
|
+
(_a = settings === null || settings === void 0 ? void 0 : settings.initialRate) !== null && _a !== void 0 ? _a : this.initialRate.value;
|
|
5024
|
+
this.finalRate.value =
|
|
5025
|
+
(_b = settings === null || settings === void 0 ? void 0 : settings.finalRate) !== null && _b !== void 0 ? _b : this.finalRate.value;
|
|
3783
5026
|
}
|
|
3784
5027
|
applyToBeatmap(beatmap) {
|
|
3785
5028
|
var _a, _b, _c, _d;
|
|
@@ -3790,13 +5033,22 @@ class ModTimeRamp extends Mod {
|
|
|
3790
5033
|
const amount = (time - this.initialRateTime) /
|
|
3791
5034
|
(this.finalRateTime - this.initialRateTime);
|
|
3792
5035
|
return (rate *
|
|
3793
|
-
Interpolation.lerp(this.initialRate, this.finalRate, MathUtils.clamp(amount, 0, 1)));
|
|
5036
|
+
Interpolation.lerp(this.initialRate.value, this.finalRate.value, MathUtils.clamp(amount, 0, 1)));
|
|
3794
5037
|
}
|
|
3795
5038
|
serializeSettings() {
|
|
3796
|
-
return {
|
|
5039
|
+
return {
|
|
5040
|
+
initialRate: this.initialRate.value,
|
|
5041
|
+
finalRate: this.finalRate.value,
|
|
5042
|
+
};
|
|
5043
|
+
}
|
|
5044
|
+
equals(other) {
|
|
5045
|
+
return (super.equals(other) &&
|
|
5046
|
+
other instanceof ModTimeRamp &&
|
|
5047
|
+
other.initialRate.value === this.initialRate.value &&
|
|
5048
|
+
other.finalRate.value === this.finalRate.value);
|
|
3797
5049
|
}
|
|
3798
5050
|
toString() {
|
|
3799
|
-
return `${super.toString()} (${this.initialRate.
|
|
5051
|
+
return `${super.toString()} (${this.initialRate.toDisplayString()}x - ${this.finalRate.toDisplayString()}x)`;
|
|
3800
5052
|
}
|
|
3801
5053
|
}
|
|
3802
5054
|
/**
|
|
@@ -3809,31 +5061,23 @@ ModTimeRamp.finalRateProgress = 0.75;
|
|
|
3809
5061
|
*/
|
|
3810
5062
|
class ModWindDown extends ModTimeRamp {
|
|
3811
5063
|
constructor() {
|
|
3812
|
-
super(
|
|
5064
|
+
super();
|
|
3813
5065
|
this.name = "Wind Down";
|
|
3814
5066
|
this.acronym = "WD";
|
|
3815
|
-
this._initialRate = 1;
|
|
3816
|
-
this._finalRate = 0.75;
|
|
3817
5067
|
this.droidRanked = false;
|
|
3818
5068
|
this.osuRanked = false;
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
}
|
|
3832
|
-
set finalRate(value) {
|
|
3833
|
-
this._finalRate = value;
|
|
3834
|
-
if (value >= this.initialRate) {
|
|
3835
|
-
this.initialRate = value + 0.01;
|
|
3836
|
-
}
|
|
5069
|
+
this.initialRate = new DecimalModSetting("Initial rate", "The starting speed of the track.", 1, 0.51, 2, 0.01, 2);
|
|
5070
|
+
this.finalRate = new DecimalModSetting("Final rate", "The final speed to ramp to.", 0.75, 0.5, 1.99, 0.01, 2);
|
|
5071
|
+
this.initialRate.bindValueChanged((_, value) => {
|
|
5072
|
+
if (value <= this.finalRate.value) {
|
|
5073
|
+
this.finalRate.value = value - this.finalRate.step;
|
|
5074
|
+
}
|
|
5075
|
+
});
|
|
5076
|
+
this.finalRate.bindValueChanged((_, value) => {
|
|
5077
|
+
if (value >= this.initialRate.value) {
|
|
5078
|
+
this.initialRate.value = value + this.initialRate.step;
|
|
5079
|
+
}
|
|
5080
|
+
});
|
|
3837
5081
|
}
|
|
3838
5082
|
get isDroidRelevant() {
|
|
3839
5083
|
return true;
|
|
@@ -3854,31 +5098,23 @@ class ModWindDown extends ModTimeRamp {
|
|
|
3854
5098
|
*/
|
|
3855
5099
|
class ModWindUp extends ModTimeRamp {
|
|
3856
5100
|
constructor() {
|
|
3857
|
-
super(
|
|
5101
|
+
super();
|
|
3858
5102
|
this.name = "Wind Up";
|
|
3859
5103
|
this.acronym = "WU";
|
|
3860
|
-
this._initialRate = 1;
|
|
3861
|
-
this._finalRate = 1;
|
|
3862
5104
|
this.droidRanked = false;
|
|
3863
5105
|
this.osuRanked = false;
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
}
|
|
3877
|
-
set finalRate(value) {
|
|
3878
|
-
this._finalRate = value;
|
|
3879
|
-
if (value <= this.initialRate) {
|
|
3880
|
-
this.initialRate = value - 0.01;
|
|
3881
|
-
}
|
|
5106
|
+
this.initialRate = new DecimalModSetting("Initial rate", "The starting speed of the track.", 1, 0.5, 1.99, 0.01, 2);
|
|
5107
|
+
this.finalRate = new DecimalModSetting("Final rate", "The final speed to ramp to.", 1.5, 0.51, 2, 0.01, 2);
|
|
5108
|
+
this.initialRate.bindValueChanged((_, value) => {
|
|
5109
|
+
if (value >= this.finalRate.value) {
|
|
5110
|
+
this.finalRate.value = value + this.finalRate.step;
|
|
5111
|
+
}
|
|
5112
|
+
});
|
|
5113
|
+
this.finalRate.bindValueChanged((_, value) => {
|
|
5114
|
+
if (value <= this.initialRate.value) {
|
|
5115
|
+
this.initialRate.value = value - this.initialRate.step;
|
|
5116
|
+
}
|
|
5117
|
+
});
|
|
3882
5118
|
}
|
|
3883
5119
|
get isDroidRelevant() {
|
|
3884
5120
|
return true;
|
|
@@ -3921,11 +5157,15 @@ class ModUtil {
|
|
|
3921
5157
|
* Serializes a list of `Mod`s.
|
|
3922
5158
|
*
|
|
3923
5159
|
* @param mods The list of `Mod`s to serialize.
|
|
5160
|
+
* @param includeNonUserPlayable Whether to include non-user-playable mods. Defaults to `true`.
|
|
3924
5161
|
* @returns The serialized list of `Mod`s.
|
|
3925
5162
|
*/
|
|
3926
|
-
static serializeMods(mods) {
|
|
5163
|
+
static serializeMods(mods, includeNonUserPlayable = true) {
|
|
3927
5164
|
const serializedMods = [];
|
|
3928
5165
|
for (const mod of mods) {
|
|
5166
|
+
if (!includeNonUserPlayable && !mod.userPlayable) {
|
|
5167
|
+
continue;
|
|
5168
|
+
}
|
|
3929
5169
|
serializedMods.push(mod.serialize());
|
|
3930
5170
|
}
|
|
3931
5171
|
return serializedMods;
|
|
@@ -3962,9 +5202,8 @@ class ModUtil {
|
|
|
3962
5202
|
str = str.toLowerCase();
|
|
3963
5203
|
while (str) {
|
|
3964
5204
|
let nchars = 1;
|
|
3965
|
-
for (const acronym of this.allMods
|
|
5205
|
+
for (const [acronym, modType] of this.allMods) {
|
|
3966
5206
|
if (str.startsWith(acronym.toLowerCase())) {
|
|
3967
|
-
const modType = this.allMods.get(acronym);
|
|
3968
5207
|
map.set(modType);
|
|
3969
5208
|
nchars = acronym.length;
|
|
3970
5209
|
break;
|
|
@@ -3994,15 +5233,16 @@ class ModUtil {
|
|
|
3994
5233
|
* Converts a list of `Mod`s into an ordered string based on {@link allMods}.
|
|
3995
5234
|
*
|
|
3996
5235
|
* @param mods The list of `Mod`s to convert.
|
|
5236
|
+
* @param includeNonUserPlayable Whether to include non-user-playable mods. Defaults to `true`.
|
|
3997
5237
|
* @returns The string representing the `Mod`s in ordered form.
|
|
3998
5238
|
*/
|
|
3999
|
-
static modsToOrderedString(mods) {
|
|
5239
|
+
static modsToOrderedString(mods, includeNonUserPlayable = true) {
|
|
4000
5240
|
const strs = [];
|
|
4001
5241
|
for (const modType of this.allMods.values()) {
|
|
4002
5242
|
const mod = mods instanceof ModMap
|
|
4003
5243
|
? mods.get(modType)
|
|
4004
5244
|
: mods.find((m) => m instanceof modType);
|
|
4005
|
-
if (mod) {
|
|
5245
|
+
if (mod && (includeNonUserPlayable || mod.userPlayable)) {
|
|
4006
5246
|
strs.push(mod.toString());
|
|
4007
5247
|
continue;
|
|
4008
5248
|
}
|
|
@@ -4028,17 +5268,23 @@ class ModUtil {
|
|
|
4028
5268
|
*/
|
|
4029
5269
|
static applyModsToBeatmapDifficulty(difficulty, mode, mods, withRateChange = false) {
|
|
4030
5270
|
if (mods !== undefined) {
|
|
5271
|
+
const adjustmentMods = new ModMap();
|
|
5272
|
+
for (const mod of mods.values()) {
|
|
5273
|
+
if (mod.facilitatesAdjustment()) {
|
|
5274
|
+
adjustmentMods.set(mod);
|
|
5275
|
+
}
|
|
5276
|
+
}
|
|
4031
5277
|
for (const mod of mods.values()) {
|
|
4032
5278
|
if (mod.isApplicableToDifficulty()) {
|
|
4033
|
-
mod.applyToDifficulty(mode, difficulty);
|
|
5279
|
+
mod.applyToDifficulty(mode, difficulty, adjustmentMods);
|
|
4034
5280
|
}
|
|
4035
5281
|
}
|
|
4036
5282
|
}
|
|
4037
5283
|
let rate = 1;
|
|
4038
5284
|
if (mods !== undefined) {
|
|
4039
5285
|
for (const mod of mods.values()) {
|
|
4040
|
-
if (mod.
|
|
4041
|
-
mod.
|
|
5286
|
+
if (mod.isApplicableToDifficultyWithMods()) {
|
|
5287
|
+
mod.applyToDifficultyWithMods(mode, difficulty, mods);
|
|
4042
5288
|
}
|
|
4043
5289
|
if (mod.isApplicableToTrackRate()) {
|
|
4044
5290
|
rate = mod.applyToRate(0, rate);
|
|
@@ -4112,8 +5358,10 @@ ModUtil.allMods = (() => {
|
|
|
4112
5358
|
ModSuddenDeath,
|
|
4113
5359
|
ModPerfect,
|
|
4114
5360
|
ModPrecise,
|
|
5361
|
+
ModRandom,
|
|
4115
5362
|
ModReallyEasy,
|
|
4116
5363
|
ModSynesthesia,
|
|
5364
|
+
ModReplayV6,
|
|
4117
5365
|
ModScoreV2,
|
|
4118
5366
|
ModSmallCircle,
|
|
4119
5367
|
ModSpunOut,
|
|
@@ -4194,25 +5442,41 @@ class ModMap extends Map {
|
|
|
4194
5442
|
}
|
|
4195
5443
|
/**
|
|
4196
5444
|
* Serializes all `Mod`s that are in this map.
|
|
5445
|
+
*
|
|
5446
|
+
* @param includeNonUserPlayable Whether to include non-user-playable mods. Defaults to `true`.
|
|
4197
5447
|
*/
|
|
4198
|
-
serializeMods() {
|
|
4199
|
-
return ModUtil.serializeMods(this.values());
|
|
5448
|
+
serializeMods(includeNonUserPlayable = true) {
|
|
5449
|
+
return ModUtil.serializeMods(this.values(), includeNonUserPlayable);
|
|
4200
5450
|
}
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
5451
|
+
/**
|
|
5452
|
+
* Determines whether this `ModMap` is equal to another `ModMap`.
|
|
5453
|
+
*
|
|
5454
|
+
* This equality check succeeds if and only if the two `ModMap`s have the same size and
|
|
5455
|
+
* all `Mod`s in this `ModMap` are equal to the corresponding `Mod`s in the other `ModMap`.
|
|
5456
|
+
*
|
|
5457
|
+
* @param other The other `ModMap` to compare to.
|
|
5458
|
+
* @returns Whether the two `ModMap`s are equal.
|
|
5459
|
+
*/
|
|
5460
|
+
equals(other) {
|
|
5461
|
+
if (this.size !== other.size) {
|
|
5462
|
+
return false;
|
|
5463
|
+
}
|
|
5464
|
+
for (const [key, value] of this) {
|
|
5465
|
+
const otherValue = other.get(key);
|
|
5466
|
+
if (!(otherValue === null || otherValue === void 0 ? void 0 : otherValue.equals(value))) {
|
|
5467
|
+
return false;
|
|
5468
|
+
}
|
|
5469
|
+
}
|
|
5470
|
+
return true;
|
|
4213
5471
|
}
|
|
4214
|
-
|
|
4215
|
-
|
|
5472
|
+
/**
|
|
5473
|
+
* Returns a string representation of this `ModMap`.
|
|
5474
|
+
*
|
|
5475
|
+
* @param includeNonUserPlayable Whether to include non-user-playable mods. Defaults to `true`.
|
|
5476
|
+
* @returns A string representation of this `ModMap`.
|
|
5477
|
+
*/
|
|
5478
|
+
toString(includeNonUserPlayable = true) {
|
|
5479
|
+
return ModUtil.modsToOrderedString(this, includeNonUserPlayable);
|
|
4216
5480
|
}
|
|
4217
5481
|
}
|
|
4218
5482
|
|
|
@@ -4221,17 +5485,14 @@ class Circle extends HitObject {
|
|
|
4221
5485
|
*/
|
|
4222
5486
|
class BeatmapHitObjects {
|
|
4223
5487
|
constructor() {
|
|
4224
|
-
|
|
5488
|
+
/**
|
|
5489
|
+
* The objects of the beatmap.
|
|
5490
|
+
*/
|
|
5491
|
+
this.objects = [];
|
|
4225
5492
|
this._circles = 0;
|
|
4226
5493
|
this._sliders = 0;
|
|
4227
5494
|
this._spinners = 0;
|
|
4228
5495
|
}
|
|
4229
|
-
/**
|
|
4230
|
-
* The objects of the beatmap.
|
|
4231
|
-
*/
|
|
4232
|
-
get objects() {
|
|
4233
|
-
return this._objects;
|
|
4234
|
-
}
|
|
4235
5496
|
/**
|
|
4236
5497
|
* The amount of circles in the beatmap.
|
|
4237
5498
|
*/
|
|
@@ -4284,7 +5545,7 @@ class BeatmapHitObjects {
|
|
|
4284
5545
|
// Objects may be out of order *only* if a user has manually edited an .osu file.
|
|
4285
5546
|
// Unfortunately there are "ranked" maps in this state (example: https://osu.ppy.sh/s/594828).
|
|
4286
5547
|
// Finding index is used to guarantee that the parsing order of hitobjects with equal start times is maintained (stably-sorted).
|
|
4287
|
-
this.
|
|
5548
|
+
this.objects.splice(this.findInsertionIndex(object.startTime), 0, object);
|
|
4288
5549
|
if (object instanceof Circle) {
|
|
4289
5550
|
++this._circles;
|
|
4290
5551
|
}
|
|
@@ -4304,7 +5565,7 @@ class BeatmapHitObjects {
|
|
|
4304
5565
|
*/
|
|
4305
5566
|
removeAt(index) {
|
|
4306
5567
|
var _a;
|
|
4307
|
-
const object = (_a = this.
|
|
5568
|
+
const object = (_a = this.objects.splice(index, 1)[0]) !== null && _a !== void 0 ? _a : null;
|
|
4308
5569
|
if (object instanceof Circle) {
|
|
4309
5570
|
--this._circles;
|
|
4310
5571
|
}
|
|
@@ -4320,7 +5581,7 @@ class BeatmapHitObjects {
|
|
|
4320
5581
|
* Clears all hitobjects.
|
|
4321
5582
|
*/
|
|
4322
5583
|
clear() {
|
|
4323
|
-
this.
|
|
5584
|
+
this.objects.length = 0;
|
|
4324
5585
|
this._circles = 0;
|
|
4325
5586
|
this._sliders = 0;
|
|
4326
5587
|
this._spinners = 0;
|
|
@@ -4331,21 +5592,21 @@ class BeatmapHitObjects {
|
|
|
4331
5592
|
* @param startTime The start time of the hitobject.
|
|
4332
5593
|
*/
|
|
4333
5594
|
findInsertionIndex(startTime) {
|
|
4334
|
-
if (this.
|
|
4335
|
-
startTime < this.
|
|
5595
|
+
if (this.objects.length === 0 ||
|
|
5596
|
+
startTime < this.objects[0].startTime) {
|
|
4336
5597
|
return 0;
|
|
4337
5598
|
}
|
|
4338
|
-
if (startTime >= this.
|
|
4339
|
-
return this.
|
|
5599
|
+
if (startTime >= this.objects.at(-1).startTime) {
|
|
5600
|
+
return this.objects.length;
|
|
4340
5601
|
}
|
|
4341
5602
|
let l = 0;
|
|
4342
|
-
let r = this.
|
|
5603
|
+
let r = this.objects.length - 2;
|
|
4343
5604
|
while (l <= r) {
|
|
4344
5605
|
const pivot = l + ((r - l) >> 1);
|
|
4345
|
-
if (this.
|
|
5606
|
+
if (this.objects[pivot].startTime < startTime) {
|
|
4346
5607
|
l = pivot + 1;
|
|
4347
5608
|
}
|
|
4348
|
-
else if (this.
|
|
5609
|
+
else if (this.objects[pivot].startTime > startTime) {
|
|
4349
5610
|
r = pivot - 1;
|
|
4350
5611
|
}
|
|
4351
5612
|
else {
|
|
@@ -4458,10 +5719,8 @@ class BeatmapProcessor {
|
|
|
4458
5719
|
* and mods will have been applied to all hitobjects.
|
|
4459
5720
|
*
|
|
4460
5721
|
* This should be used to add alterations to hitobjects while they are in their most playable state.
|
|
4461
|
-
*
|
|
4462
|
-
* @param mode The mode to add alterations for.
|
|
4463
5722
|
*/
|
|
4464
|
-
postProcess(
|
|
5723
|
+
postProcess() {
|
|
4465
5724
|
const objects = this.beatmap.hitObjects.objects;
|
|
4466
5725
|
if (objects.length === 0) {
|
|
4467
5726
|
return;
|
|
@@ -4470,39 +5729,14 @@ class BeatmapProcessor {
|
|
|
4470
5729
|
objects.forEach((h) => {
|
|
4471
5730
|
h.stackHeight = 0;
|
|
4472
5731
|
});
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
this.applyDroidStacking();
|
|
4476
|
-
break;
|
|
4477
|
-
case exports.Modes.osu:
|
|
4478
|
-
if (this.beatmap.formatVersion >= 6) {
|
|
4479
|
-
this.applyStandardStacking();
|
|
4480
|
-
}
|
|
4481
|
-
else {
|
|
4482
|
-
this.applyStandardOldStacking();
|
|
4483
|
-
}
|
|
4484
|
-
break;
|
|
4485
|
-
}
|
|
4486
|
-
}
|
|
4487
|
-
applyDroidStacking() {
|
|
4488
|
-
const objects = this.beatmap.hitObjects.objects;
|
|
4489
|
-
if (objects.length === 0) {
|
|
4490
|
-
return;
|
|
5732
|
+
if (this.beatmap.formatVersion >= 6) {
|
|
5733
|
+
this.applyStacking();
|
|
4491
5734
|
}
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
const current = objects[i];
|
|
4495
|
-
const next = objects[i + 1];
|
|
4496
|
-
if (current instanceof Circle &&
|
|
4497
|
-
next.startTime - current.startTime <
|
|
4498
|
-
2000 * this.beatmap.general.stackLeniency &&
|
|
4499
|
-
next.position.getDistance(current.position) <
|
|
4500
|
-
Math.sqrt(convertedScale)) {
|
|
4501
|
-
next.stackHeight = current.stackHeight + 1;
|
|
4502
|
-
}
|
|
5735
|
+
else {
|
|
5736
|
+
this.applyOldStacking();
|
|
4503
5737
|
}
|
|
4504
5738
|
}
|
|
4505
|
-
|
|
5739
|
+
applyStacking() {
|
|
4506
5740
|
const objects = this.beatmap.hitObjects.objects;
|
|
4507
5741
|
const startIndex = 0;
|
|
4508
5742
|
const endIndex = objects.length - 1;
|
|
@@ -4631,7 +5865,7 @@ class BeatmapProcessor {
|
|
|
4631
5865
|
}
|
|
4632
5866
|
}
|
|
4633
5867
|
}
|
|
4634
|
-
|
|
5868
|
+
applyOldStacking() {
|
|
4635
5869
|
const objects = this.beatmap.hitObjects.objects;
|
|
4636
5870
|
for (let i = 0; i < objects.length; ++i) {
|
|
4637
5871
|
const currentObject = objects[i];
|
|
@@ -5598,15 +6832,21 @@ class Beatmap {
|
|
|
5598
6832
|
}
|
|
5599
6833
|
// Convert
|
|
5600
6834
|
const converted = new BeatmapConverter(this).convert();
|
|
6835
|
+
const adjustmentMods = new ModMap();
|
|
6836
|
+
for (const mod of mods.values()) {
|
|
6837
|
+
if (mod.facilitatesAdjustment()) {
|
|
6838
|
+
adjustmentMods.set(mod);
|
|
6839
|
+
}
|
|
6840
|
+
}
|
|
5601
6841
|
// Apply difficulty mods
|
|
5602
6842
|
mods.forEach((mod) => {
|
|
5603
6843
|
if (mod.isApplicableToDifficulty()) {
|
|
5604
|
-
mod.applyToDifficulty(mode, converted.difficulty);
|
|
6844
|
+
mod.applyToDifficulty(mode, converted.difficulty, adjustmentMods);
|
|
5605
6845
|
}
|
|
5606
6846
|
});
|
|
5607
6847
|
mods.forEach((mod) => {
|
|
5608
|
-
if (mod.
|
|
5609
|
-
mod.
|
|
6848
|
+
if (mod.isApplicableToDifficultyWithMods()) {
|
|
6849
|
+
mod.applyToDifficultyWithMods(mode, converted.difficulty, mods);
|
|
5610
6850
|
}
|
|
5611
6851
|
});
|
|
5612
6852
|
const processor = new BeatmapProcessor(converted);
|
|
@@ -5616,18 +6856,18 @@ class Beatmap {
|
|
|
5616
6856
|
mods.forEach((mod) => {
|
|
5617
6857
|
if (mod.isApplicableToHitObject()) {
|
|
5618
6858
|
for (const hitObject of converted.hitObjects.objects) {
|
|
5619
|
-
mod.applyToHitObject(mode, hitObject);
|
|
6859
|
+
mod.applyToHitObject(mode, hitObject, adjustmentMods);
|
|
5620
6860
|
}
|
|
5621
6861
|
}
|
|
5622
6862
|
});
|
|
5623
6863
|
mods.forEach((mod) => {
|
|
5624
|
-
if (mod.
|
|
6864
|
+
if (mod.isApplicableToHitObjectWithMods()) {
|
|
5625
6865
|
for (const hitObject of converted.hitObjects.objects) {
|
|
5626
|
-
mod.
|
|
6866
|
+
mod.applyToHitObjectWithMods(mode, hitObject, mods);
|
|
5627
6867
|
}
|
|
5628
6868
|
}
|
|
5629
6869
|
});
|
|
5630
|
-
processor.postProcess(
|
|
6870
|
+
processor.postProcess();
|
|
5631
6871
|
mods.forEach((mod) => {
|
|
5632
6872
|
if (mod.isApplicableToBeatmap()) {
|
|
5633
6873
|
mod.applyToBeatmap(converted);
|
|
@@ -5718,48 +6958,6 @@ var ParserConstants;
|
|
|
5718
6958
|
ParserConstants[ParserConstants["MAX_MSPERBEAT_VALUE"] = 60000] = "MAX_MSPERBEAT_VALUE";
|
|
5719
6959
|
})(ParserConstants || (ParserConstants = {}));
|
|
5720
6960
|
|
|
5721
|
-
/**
|
|
5722
|
-
* Some utilities, no biggie.
|
|
5723
|
-
*/
|
|
5724
|
-
class Utils {
|
|
5725
|
-
/**
|
|
5726
|
-
* Returns a random element of an array.
|
|
5727
|
-
*
|
|
5728
|
-
* @param array The array to get the element from.
|
|
5729
|
-
*/
|
|
5730
|
-
static getRandomArrayElement(array) {
|
|
5731
|
-
return array[Math.floor(Math.random() * array.length)];
|
|
5732
|
-
}
|
|
5733
|
-
/**
|
|
5734
|
-
* Creates an array with specific length that's prefilled with an initial value.
|
|
5735
|
-
*
|
|
5736
|
-
* @param length The length of the array.
|
|
5737
|
-
* @param initialValue The initial value of each element, or a function that returns the initial value of each element.
|
|
5738
|
-
* @returns The array.
|
|
5739
|
-
*/
|
|
5740
|
-
static initializeArray(length, initialValue) {
|
|
5741
|
-
const array = new Array(length);
|
|
5742
|
-
if (initialValue !== undefined) {
|
|
5743
|
-
for (let i = 0; i < length; ++i) {
|
|
5744
|
-
array[i] =
|
|
5745
|
-
typeof initialValue === "function"
|
|
5746
|
-
? initialValue(i)
|
|
5747
|
-
: initialValue;
|
|
5748
|
-
}
|
|
5749
|
-
}
|
|
5750
|
-
return array;
|
|
5751
|
-
}
|
|
5752
|
-
/**
|
|
5753
|
-
* Pauses the execution of a function for
|
|
5754
|
-
* the specified duration.
|
|
5755
|
-
*
|
|
5756
|
-
* @param duration The duration to pause for, in seconds.
|
|
5757
|
-
*/
|
|
5758
|
-
static sleep(duration) {
|
|
5759
|
-
return new Promise((resolve) => setTimeout(resolve, duration * 1000));
|
|
5760
|
-
}
|
|
5761
|
-
}
|
|
5762
|
-
|
|
5763
6961
|
/**
|
|
5764
6962
|
* Represents an information about a hitobject-specific sample bank.
|
|
5765
6963
|
*/
|
|
@@ -6646,6 +7844,118 @@ class BeatmapColorDecoder extends SectionDecoder {
|
|
|
6646
7844
|
}
|
|
6647
7845
|
}
|
|
6648
7846
|
|
|
7847
|
+
/**
|
|
7848
|
+
* Normalize a string path, reducing '..' and '.' parts.
|
|
7849
|
+
* When multiple slashes are found, they're replaced by a single one; when the path contains a trailing slash,
|
|
7850
|
+
* it is preserved.
|
|
7851
|
+
*
|
|
7852
|
+
* This function's implementation matches Node.js v10.3 API.
|
|
7853
|
+
*
|
|
7854
|
+
* @param path string path to normalize.
|
|
7855
|
+
* @returns The normalized path.
|
|
7856
|
+
* @throws {TypeError} if `path` is not a string.
|
|
7857
|
+
*/
|
|
7858
|
+
function normalize(path) {
|
|
7859
|
+
if (typeof path !== "string") {
|
|
7860
|
+
throw new TypeError(`Expected a string, got ${typeof path}`);
|
|
7861
|
+
}
|
|
7862
|
+
if (path.length === 0) {
|
|
7863
|
+
return ".";
|
|
7864
|
+
}
|
|
7865
|
+
const isAbsolute = path.charCodeAt(0) === 47; /*/*/
|
|
7866
|
+
const trailingSeparator = path.charCodeAt(path.length - 1) === 47; /*/*/
|
|
7867
|
+
// Normalize the path
|
|
7868
|
+
path = normalizeStringPosix(path, !isAbsolute);
|
|
7869
|
+
if (path.length === 0 && !isAbsolute) {
|
|
7870
|
+
path = ".";
|
|
7871
|
+
}
|
|
7872
|
+
if (path.length > 0 && trailingSeparator) {
|
|
7873
|
+
path += "/";
|
|
7874
|
+
}
|
|
7875
|
+
if (isAbsolute) {
|
|
7876
|
+
return "/" + path;
|
|
7877
|
+
}
|
|
7878
|
+
return path;
|
|
7879
|
+
}
|
|
7880
|
+
// Resolves . and .. elements in a path with directory names
|
|
7881
|
+
function normalizeStringPosix(path, allowAboveRoot) {
|
|
7882
|
+
let res = "";
|
|
7883
|
+
let lastSegmentLength = 0;
|
|
7884
|
+
let lastSlash = -1;
|
|
7885
|
+
let dots = 0;
|
|
7886
|
+
let code;
|
|
7887
|
+
for (let i = 0; i <= path.length; ++i) {
|
|
7888
|
+
if (i < path.length) {
|
|
7889
|
+
code = path.charCodeAt(i);
|
|
7890
|
+
}
|
|
7891
|
+
else if (code === 47 /*/*/) {
|
|
7892
|
+
break;
|
|
7893
|
+
}
|
|
7894
|
+
else {
|
|
7895
|
+
code = 47 /*/*/;
|
|
7896
|
+
}
|
|
7897
|
+
if (code === 47 /*/*/) {
|
|
7898
|
+
if (lastSlash === i - 1 || dots === 1) ;
|
|
7899
|
+
else if (lastSlash !== i - 1 && dots === 2) {
|
|
7900
|
+
if (res.length < 2 ||
|
|
7901
|
+
lastSegmentLength !== 2 ||
|
|
7902
|
+
res.charCodeAt(res.length - 1) !== 46 /*.*/ ||
|
|
7903
|
+
res.charCodeAt(res.length - 2) !== 46 /*.*/) {
|
|
7904
|
+
if (res.length > 2) {
|
|
7905
|
+
const lastSlashIndex = res.lastIndexOf("/");
|
|
7906
|
+
if (lastSlashIndex !== res.length - 1) {
|
|
7907
|
+
if (lastSlashIndex === -1) {
|
|
7908
|
+
res = "";
|
|
7909
|
+
lastSegmentLength = 0;
|
|
7910
|
+
}
|
|
7911
|
+
else {
|
|
7912
|
+
res = res.slice(0, lastSlashIndex);
|
|
7913
|
+
lastSegmentLength =
|
|
7914
|
+
res.length - 1 - res.lastIndexOf("/");
|
|
7915
|
+
}
|
|
7916
|
+
lastSlash = i;
|
|
7917
|
+
dots = 0;
|
|
7918
|
+
continue;
|
|
7919
|
+
}
|
|
7920
|
+
}
|
|
7921
|
+
else if (res.length === 2 || res.length === 1) {
|
|
7922
|
+
res = "";
|
|
7923
|
+
lastSegmentLength = 0;
|
|
7924
|
+
lastSlash = i;
|
|
7925
|
+
dots = 0;
|
|
7926
|
+
continue;
|
|
7927
|
+
}
|
|
7928
|
+
}
|
|
7929
|
+
if (allowAboveRoot) {
|
|
7930
|
+
if (res.length > 0)
|
|
7931
|
+
res += "/..";
|
|
7932
|
+
else
|
|
7933
|
+
res = "..";
|
|
7934
|
+
lastSegmentLength = 2;
|
|
7935
|
+
}
|
|
7936
|
+
}
|
|
7937
|
+
else {
|
|
7938
|
+
if (res.length > 0) {
|
|
7939
|
+
res += "/" + path.slice(lastSlash + 1, i);
|
|
7940
|
+
}
|
|
7941
|
+
else {
|
|
7942
|
+
res = path.slice(lastSlash + 1, i);
|
|
7943
|
+
}
|
|
7944
|
+
lastSegmentLength = i - lastSlash - 1;
|
|
7945
|
+
}
|
|
7946
|
+
lastSlash = i;
|
|
7947
|
+
dots = 0;
|
|
7948
|
+
}
|
|
7949
|
+
else if (code === 46 /*.*/ && dots !== -1) {
|
|
7950
|
+
++dots;
|
|
7951
|
+
}
|
|
7952
|
+
else {
|
|
7953
|
+
dots = -1;
|
|
7954
|
+
}
|
|
7955
|
+
}
|
|
7956
|
+
return res;
|
|
7957
|
+
}
|
|
7958
|
+
|
|
6649
7959
|
/**
|
|
6650
7960
|
* Determines how color blending should be done.
|
|
6651
7961
|
*/
|
|
@@ -7389,7 +8699,7 @@ class StoryboardEventsDecoder extends SectionDecoder {
|
|
|
7389
8699
|
while (end > start && name.charAt(end - 1) === '"') {
|
|
7390
8700
|
--end;
|
|
7391
8701
|
}
|
|
7392
|
-
return
|
|
8702
|
+
return normalize(start > 0 || end < name.length ? name.substring(start, end) : name);
|
|
7393
8703
|
}
|
|
7394
8704
|
}
|
|
7395
8705
|
|
|
@@ -7548,7 +8858,7 @@ class BeatmapDecoder extends Decoder {
|
|
|
7548
8858
|
h.applyDefaults(this.finalResult.controlPoints, this.finalResult.difficulty, mode);
|
|
7549
8859
|
h.applySamples(this.finalResult.controlPoints);
|
|
7550
8860
|
});
|
|
7551
|
-
processor.postProcess(
|
|
8861
|
+
processor.postProcess();
|
|
7552
8862
|
return this;
|
|
7553
8863
|
}
|
|
7554
8864
|
decodeLine(line) {
|
|
@@ -8810,7 +10120,7 @@ class DroidLegacyModConverter {
|
|
|
8810
10120
|
flashlight = new ModFlashlight();
|
|
8811
10121
|
map.set(flashlight);
|
|
8812
10122
|
}
|
|
8813
|
-
flashlight.followDelay = parseFloat(s.slice(3));
|
|
10123
|
+
flashlight.followDelay.value = parseFloat(s.slice(3));
|
|
8814
10124
|
break;
|
|
8815
10125
|
}
|
|
8816
10126
|
// Speed multiplier
|
|
@@ -8820,7 +10130,7 @@ class DroidLegacyModConverter {
|
|
|
8820
10130
|
customSpeed = new ModCustomSpeed();
|
|
8821
10131
|
map.set(customSpeed);
|
|
8822
10132
|
}
|
|
8823
|
-
customSpeed.trackRateMultiplier = parseFloat(s.slice(1));
|
|
10133
|
+
customSpeed.trackRateMultiplier.value = parseFloat(s.slice(1));
|
|
8824
10134
|
break;
|
|
8825
10135
|
}
|
|
8826
10136
|
}
|
|
@@ -9639,6 +10949,15 @@ ErrorFunction.ervInvImpGd = [
|
|
|
9639
10949
|
0.3999688121938621e-6, 0.1618092908879045e-8, 0.2315586083102596e-11,
|
|
9640
10950
|
];
|
|
9641
10951
|
|
|
10952
|
+
/**
|
|
10953
|
+
* Represents a `Mod` specific setting that is constrained to a range of integer values.
|
|
10954
|
+
*/
|
|
10955
|
+
class IntegerModSetting extends NumberModSetting {
|
|
10956
|
+
constructor(name, description, defaultValue, min = -2147483648, max = 2147483647) {
|
|
10957
|
+
super(name, description, defaultValue, min, max, 1);
|
|
10958
|
+
}
|
|
10959
|
+
}
|
|
10960
|
+
|
|
9642
10961
|
/**
|
|
9643
10962
|
* Ranking status of a beatmap.
|
|
9644
10963
|
*/
|
|
@@ -10073,8 +11392,8 @@ class MapInfo {
|
|
|
10073
11392
|
*/
|
|
10074
11393
|
class ModOldNightCore extends ModNightCore {
|
|
10075
11394
|
constructor() {
|
|
10076
|
-
super(
|
|
10077
|
-
this.trackRateMultiplier = 1.39;
|
|
11395
|
+
super();
|
|
11396
|
+
this.trackRateMultiplier.value = 1.39;
|
|
10078
11397
|
}
|
|
10079
11398
|
calculateDroidScoreMultiplier() {
|
|
10080
11399
|
return 1.12;
|
|
@@ -10124,6 +11443,7 @@ exports.BeatmapMetadata = BeatmapMetadata;
|
|
|
10124
11443
|
exports.BeatmapProcessor = BeatmapProcessor;
|
|
10125
11444
|
exports.BeatmapVideo = BeatmapVideo;
|
|
10126
11445
|
exports.BlendingParameters = BlendingParameters;
|
|
11446
|
+
exports.BooleanModSetting = BooleanModSetting;
|
|
10127
11447
|
exports.BreakPoint = BreakPoint;
|
|
10128
11448
|
exports.Brent = Brent;
|
|
10129
11449
|
exports.Circle = Circle;
|
|
@@ -10134,6 +11454,7 @@ exports.CommandTimeline = CommandTimeline;
|
|
|
10134
11454
|
exports.CommandTimelineGroup = CommandTimelineGroup;
|
|
10135
11455
|
exports.CommandTrigger = CommandTrigger;
|
|
10136
11456
|
exports.ControlPointManager = ControlPointManager;
|
|
11457
|
+
exports.DecimalModSetting = DecimalModSetting;
|
|
10137
11458
|
exports.DifficultyControlPoint = DifficultyControlPoint;
|
|
10138
11459
|
exports.DifficultyControlPointManager = DifficultyControlPointManager;
|
|
10139
11460
|
exports.DroidAPIRequestBuilder = DroidAPIRequestBuilder;
|
|
@@ -10149,6 +11470,7 @@ exports.HitObject = HitObject;
|
|
|
10149
11470
|
exports.HitObjectGenerationUtils = HitObjectGenerationUtils;
|
|
10150
11471
|
exports.HitSampleInfo = HitSampleInfo;
|
|
10151
11472
|
exports.HitWindow = HitWindow;
|
|
11473
|
+
exports.IntegerModSetting = IntegerModSetting;
|
|
10152
11474
|
exports.Interpolation = Interpolation;
|
|
10153
11475
|
exports.MapInfo = MapInfo;
|
|
10154
11476
|
exports.MathUtils = MathUtils;
|
|
@@ -10170,10 +11492,13 @@ exports.ModNoFail = ModNoFail;
|
|
|
10170
11492
|
exports.ModOldNightCore = ModOldNightCore;
|
|
10171
11493
|
exports.ModPerfect = ModPerfect;
|
|
10172
11494
|
exports.ModPrecise = ModPrecise;
|
|
11495
|
+
exports.ModRandom = ModRandom;
|
|
10173
11496
|
exports.ModRateAdjust = ModRateAdjust;
|
|
10174
11497
|
exports.ModReallyEasy = ModReallyEasy;
|
|
10175
11498
|
exports.ModRelax = ModRelax;
|
|
11499
|
+
exports.ModReplayV6 = ModReplayV6;
|
|
10176
11500
|
exports.ModScoreV2 = ModScoreV2;
|
|
11501
|
+
exports.ModSetting = ModSetting;
|
|
10177
11502
|
exports.ModSmallCircle = ModSmallCircle;
|
|
10178
11503
|
exports.ModSpunOut = ModSpunOut;
|
|
10179
11504
|
exports.ModSuddenDeath = ModSuddenDeath;
|
|
@@ -10184,6 +11509,8 @@ exports.ModUtil = ModUtil;
|
|
|
10184
11509
|
exports.ModWindDown = ModWindDown;
|
|
10185
11510
|
exports.ModWindUp = ModWindUp;
|
|
10186
11511
|
exports.NormalDistribution = NormalDistribution;
|
|
11512
|
+
exports.NullableDecimalModSetting = NullableDecimalModSetting;
|
|
11513
|
+
exports.NumberModSetting = NumberModSetting;
|
|
10187
11514
|
exports.OsuAPIRequestBuilder = OsuAPIRequestBuilder;
|
|
10188
11515
|
exports.OsuHitWindow = OsuHitWindow;
|
|
10189
11516
|
exports.OsuPlayableBeatmap = OsuPlayableBeatmap;
|
|
@@ -10194,6 +11521,8 @@ exports.Polynomial = Polynomial;
|
|
|
10194
11521
|
exports.PreciseDroidHitWindow = PreciseDroidHitWindow;
|
|
10195
11522
|
exports.Precision = Precision;
|
|
10196
11523
|
exports.RGBColor = RGBColor;
|
|
11524
|
+
exports.Random = Random;
|
|
11525
|
+
exports.RangeConstrainedModSetting = RangeConstrainedModSetting;
|
|
10197
11526
|
exports.SampleBankInfo = SampleBankInfo;
|
|
10198
11527
|
exports.SampleControlPoint = SampleControlPoint;
|
|
10199
11528
|
exports.SampleControlPointManager = SampleControlPointManager;
|
|
@@ -10219,5 +11548,7 @@ exports.TimingControlPoint = TimingControlPoint;
|
|
|
10219
11548
|
exports.TimingControlPointManager = TimingControlPointManager;
|
|
10220
11549
|
exports.Utils = Utils;
|
|
10221
11550
|
exports.Vector2 = Vector2;
|
|
11551
|
+
exports.Vector4 = Vector4;
|
|
10222
11552
|
exports.ZeroCrossingBracketing = ZeroCrossingBracketing;
|
|
11553
|
+
exports.normalize = normalize;
|
|
10223
11554
|
//# sourceMappingURL=index.js.map
|