@rian8337/osu-base 4.0.0-beta.56 → 4.0.0-beta.59
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 +1657 -1611
- package/package.json +2 -2
- package/typings/index.d.ts +73 -63
package/dist/index.js
CHANGED
|
@@ -389,139 +389,144 @@ exports.Modes = void 0;
|
|
|
389
389
|
})(exports.Modes || (exports.Modes = {}));
|
|
390
390
|
|
|
391
391
|
/**
|
|
392
|
-
* Represents a
|
|
392
|
+
* Represents a hit window.
|
|
393
393
|
*/
|
|
394
|
-
class
|
|
395
|
-
constructor() {
|
|
396
|
-
/**
|
|
397
|
-
* `Mod`s that are incompatible with this `Mod`.
|
|
398
|
-
*/
|
|
399
|
-
this.incompatibleMods = new Set();
|
|
400
|
-
}
|
|
394
|
+
class HitWindow {
|
|
401
395
|
/**
|
|
402
|
-
*
|
|
396
|
+
* @param overallDifficulty The overall difficulty of this `HitWindow`. Defaults to 5.
|
|
403
397
|
*/
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const serialized = {
|
|
407
|
-
acronym: this.acronym,
|
|
408
|
-
settings: (_a = this.serializeSettings()) !== null && _a !== void 0 ? _a : undefined,
|
|
409
|
-
};
|
|
410
|
-
if (!serialized.settings) {
|
|
411
|
-
delete serialized.settings;
|
|
412
|
-
}
|
|
413
|
-
return serialized;
|
|
398
|
+
constructor(overallDifficulty = 5) {
|
|
399
|
+
this.overallDifficulty = overallDifficulty;
|
|
414
400
|
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* A fixed miss hit window regardless of difficulty settings.
|
|
404
|
+
*/
|
|
405
|
+
HitWindow.missWindow = 400;
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Represents the hit window of osu!droid _without_ the Precise mod.
|
|
409
|
+
*/
|
|
410
|
+
class DroidHitWindow extends HitWindow {
|
|
415
411
|
/**
|
|
416
|
-
*
|
|
412
|
+
* Calculates the overall difficulty value of a great (300) hit window.
|
|
417
413
|
*
|
|
418
|
-
* @param
|
|
419
|
-
* @
|
|
414
|
+
* @param value The value of the hit window, in milliseconds.
|
|
415
|
+
* @returns The overall difficulty value.
|
|
420
416
|
*/
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
throw new TypeError(`Cannot copy settings from ${mod.acronym} to ${this.acronym}`);
|
|
424
|
-
}
|
|
417
|
+
static greatWindowToOD(value) {
|
|
418
|
+
return 5 - (value - 75) / 5;
|
|
425
419
|
}
|
|
426
420
|
/**
|
|
427
|
-
*
|
|
421
|
+
* Calculates the overall difficulty value of a good (100) hit window.
|
|
422
|
+
*
|
|
423
|
+
* @param value The value of the hit window, in milliseconds.
|
|
424
|
+
* @returns The overall difficulty value.
|
|
428
425
|
*/
|
|
429
|
-
|
|
430
|
-
return
|
|
426
|
+
static okWindowToOD(value) {
|
|
427
|
+
return 5 - (value - 150) / 10;
|
|
431
428
|
}
|
|
432
429
|
/**
|
|
433
|
-
*
|
|
430
|
+
* Calculates the overall difficulty value of a meh (50) hit window.
|
|
431
|
+
*
|
|
432
|
+
* @param value The value of the hit window, in milliseconds.
|
|
433
|
+
* @returns The overall difficulty value.
|
|
434
434
|
*/
|
|
435
|
-
|
|
436
|
-
return
|
|
435
|
+
static mehWindowToOD(value) {
|
|
436
|
+
return 5 - (value - 250) / 10;
|
|
437
437
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
*/
|
|
441
|
-
isApplicableToOsuStable() {
|
|
442
|
-
return "bitwise" in this;
|
|
438
|
+
get greatWindow() {
|
|
439
|
+
return 75 + 5 * (5 - this.overallDifficulty);
|
|
443
440
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
*/
|
|
447
|
-
isApplicableToBeatmap() {
|
|
448
|
-
return "applyToBeatmap" in this;
|
|
441
|
+
get okWindow() {
|
|
442
|
+
return 150 + 10 * (5 - this.overallDifficulty);
|
|
449
443
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
*/
|
|
453
|
-
isApplicableToDifficulty() {
|
|
454
|
-
return "applyToDifficulty" in this;
|
|
444
|
+
get mehWindow() {
|
|
445
|
+
return 250 + 10 * (5 - this.overallDifficulty);
|
|
455
446
|
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Represents the hit window of osu!standard.
|
|
451
|
+
*/
|
|
452
|
+
class OsuHitWindow extends HitWindow {
|
|
456
453
|
/**
|
|
457
|
-
*
|
|
454
|
+
* Calculates the overall difficulty value of a great (300) hit window.
|
|
455
|
+
*
|
|
456
|
+
* @param value The value of the hit window, in milliseconds.
|
|
457
|
+
* @returns The overall difficulty value.
|
|
458
458
|
*/
|
|
459
|
-
|
|
460
|
-
return
|
|
459
|
+
static greatWindowToOD(value) {
|
|
460
|
+
return (80 - value) / 6;
|
|
461
461
|
}
|
|
462
462
|
/**
|
|
463
|
-
*
|
|
463
|
+
* Calculates the overall difficulty value of a good (100) hit window.
|
|
464
|
+
*
|
|
465
|
+
* @param value The value of the hit window, in milliseconds.
|
|
466
|
+
* @returns The overall difficulty value.
|
|
464
467
|
*/
|
|
465
|
-
|
|
466
|
-
return
|
|
468
|
+
static okWindowToOD(value) {
|
|
469
|
+
return (140 - value) / 8;
|
|
467
470
|
}
|
|
468
471
|
/**
|
|
469
|
-
*
|
|
472
|
+
* Calculates the overall difficulty value of a meh hit window.
|
|
473
|
+
*
|
|
474
|
+
* @param value The value of the hit window, in milliseconds.
|
|
475
|
+
* @returns The overall difficulty value.
|
|
470
476
|
*/
|
|
471
|
-
|
|
472
|
-
return
|
|
477
|
+
static mehWindowToOD(value) {
|
|
478
|
+
return (200 - value) / 10;
|
|
473
479
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
return
|
|
480
|
+
get greatWindow() {
|
|
481
|
+
return 80 - 6 * this.overallDifficulty;
|
|
482
|
+
}
|
|
483
|
+
get okWindow() {
|
|
484
|
+
return 140 - 8 * this.overallDifficulty;
|
|
479
485
|
}
|
|
486
|
+
get mehWindow() {
|
|
487
|
+
return 200 - 10 * this.overallDifficulty;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Represents the hit window of osu!droid _with_ the Precise mod.
|
|
493
|
+
*/
|
|
494
|
+
class PreciseDroidHitWindow extends HitWindow {
|
|
480
495
|
/**
|
|
481
|
-
*
|
|
496
|
+
* Calculates the overall difficulty value of a great (300) hit window.
|
|
497
|
+
*
|
|
498
|
+
* @param value The value of the hit window, in milliseconds.
|
|
499
|
+
* @returns The overall difficulty value.
|
|
482
500
|
*/
|
|
483
|
-
|
|
484
|
-
return
|
|
501
|
+
static greatWindowToOD(value) {
|
|
502
|
+
return 5 - (value - 55) / 6;
|
|
485
503
|
}
|
|
486
504
|
/**
|
|
487
|
-
*
|
|
505
|
+
* Calculates the overall difficulty value of a good (100) hit window.
|
|
488
506
|
*
|
|
489
|
-
* @
|
|
507
|
+
* @param value The value of the hit window, in milliseconds.
|
|
508
|
+
* @returns The overall difficulty value.
|
|
490
509
|
*/
|
|
491
|
-
|
|
492
|
-
return
|
|
510
|
+
static okWindowToOD(value) {
|
|
511
|
+
return 5 - (value - 120) / 8;
|
|
493
512
|
}
|
|
494
513
|
/**
|
|
495
|
-
*
|
|
514
|
+
* Calculates the overall difficulty value of a meh (50) hit window.
|
|
515
|
+
*
|
|
516
|
+
* @param value The value of the hit window, in milliseconds.
|
|
517
|
+
* @returns The overall difficulty value.
|
|
496
518
|
*/
|
|
497
|
-
|
|
498
|
-
return
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* Represents the ScoreV2 mod.
|
|
504
|
-
*/
|
|
505
|
-
class ModScoreV2 extends Mod {
|
|
506
|
-
constructor() {
|
|
507
|
-
super(...arguments);
|
|
508
|
-
this.acronym = "V2";
|
|
509
|
-
this.name = "ScoreV2";
|
|
510
|
-
this.droidRanked = false;
|
|
511
|
-
this.osuRanked = false;
|
|
512
|
-
this.bitwise = 1 << 29;
|
|
513
|
-
}
|
|
514
|
-
get isDroidRelevant() {
|
|
515
|
-
return true;
|
|
519
|
+
static mehWindowToOD(value) {
|
|
520
|
+
return 5 - (value - 180) / 10;
|
|
516
521
|
}
|
|
517
|
-
|
|
518
|
-
return
|
|
522
|
+
get greatWindow() {
|
|
523
|
+
return 55 + 6 * (5 - this.overallDifficulty);
|
|
519
524
|
}
|
|
520
|
-
get
|
|
521
|
-
return
|
|
525
|
+
get okWindow() {
|
|
526
|
+
return 120 + 8 * (5 - this.overallDifficulty);
|
|
522
527
|
}
|
|
523
|
-
get
|
|
524
|
-
return
|
|
528
|
+
get mehWindow() {
|
|
529
|
+
return 180 + 10 * (5 - this.overallDifficulty);
|
|
525
530
|
}
|
|
526
531
|
}
|
|
527
532
|
|
|
@@ -766,18 +771,20 @@ class CircleSizeCalculator {
|
|
|
766
771
|
* @param mods The mods to apply.
|
|
767
772
|
* @returns The calculated osu!droid scale.
|
|
768
773
|
*/
|
|
769
|
-
static droidCSToDroidScale(cs, mods
|
|
774
|
+
static droidCSToDroidScale(cs, mods) {
|
|
770
775
|
// Create a dummy beatmap difficulty for circle size calculation.
|
|
771
776
|
const difficulty = new BeatmapDifficulty();
|
|
772
777
|
difficulty.cs = cs;
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
mod.
|
|
778
|
+
if (mods !== undefined) {
|
|
779
|
+
for (const mod of mods.values()) {
|
|
780
|
+
if (mod.isApplicableToDifficulty()) {
|
|
781
|
+
mod.applyToDifficulty(exports.Modes.droid, difficulty);
|
|
782
|
+
}
|
|
776
783
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
784
|
+
for (const mod of mods.values()) {
|
|
785
|
+
if (mod.isApplicableToDifficultyWithSettings()) {
|
|
786
|
+
mod.applyToDifficultyWithSettings(exports.Modes.droid, difficulty, mods);
|
|
787
|
+
}
|
|
781
788
|
}
|
|
782
789
|
}
|
|
783
790
|
return Math.max(((this.assumedDroidHeight / 480) *
|
|
@@ -901,135 +908,35 @@ CircleSizeCalculator.brokenGamefieldRoundingAllowance = 1.00041;
|
|
|
901
908
|
CircleSizeCalculator.assumedDroidHeight = 681;
|
|
902
909
|
|
|
903
910
|
/**
|
|
904
|
-
* Represents a
|
|
911
|
+
* Represents a hitobject in a beatmap.
|
|
905
912
|
*/
|
|
906
|
-
class
|
|
913
|
+
class HitObject {
|
|
907
914
|
/**
|
|
908
|
-
*
|
|
915
|
+
* The position of the hitobject in osu!pixels.
|
|
909
916
|
*/
|
|
910
|
-
|
|
911
|
-
this.
|
|
917
|
+
get position() {
|
|
918
|
+
return this._position;
|
|
919
|
+
}
|
|
920
|
+
set position(value) {
|
|
921
|
+
this._position = value;
|
|
912
922
|
}
|
|
913
|
-
}
|
|
914
|
-
/**
|
|
915
|
-
* A fixed miss hit window regardless of difficulty settings.
|
|
916
|
-
*/
|
|
917
|
-
HitWindow.missWindow = 400;
|
|
918
|
-
|
|
919
|
-
/**
|
|
920
|
-
* Represents the hit window of osu!droid _without_ the Precise mod.
|
|
921
|
-
*/
|
|
922
|
-
class DroidHitWindow extends HitWindow {
|
|
923
923
|
/**
|
|
924
|
-
*
|
|
925
|
-
*
|
|
926
|
-
* @param value The value of the hit window, in milliseconds.
|
|
927
|
-
* @returns The overall difficulty value.
|
|
924
|
+
* The end position of the hitobject in osu!pixels.
|
|
928
925
|
*/
|
|
929
|
-
|
|
930
|
-
return
|
|
926
|
+
get endPosition() {
|
|
927
|
+
return this.position;
|
|
931
928
|
}
|
|
932
929
|
/**
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
* @param value The value of the hit window, in milliseconds.
|
|
936
|
-
* @returns The overall difficulty value.
|
|
930
|
+
* The end time of the hitobject.
|
|
937
931
|
*/
|
|
938
|
-
|
|
939
|
-
return
|
|
932
|
+
get endTime() {
|
|
933
|
+
return this.startTime;
|
|
940
934
|
}
|
|
941
935
|
/**
|
|
942
|
-
*
|
|
943
|
-
*
|
|
944
|
-
* @param value The value of the hit window, in milliseconds.
|
|
945
|
-
* @returns The overall difficulty value.
|
|
936
|
+
* The duration of the hitobject.
|
|
946
937
|
*/
|
|
947
|
-
|
|
948
|
-
return
|
|
949
|
-
}
|
|
950
|
-
get greatWindow() {
|
|
951
|
-
return 75 + 5 * (5 - this.overallDifficulty);
|
|
952
|
-
}
|
|
953
|
-
get okWindow() {
|
|
954
|
-
return 150 + 10 * (5 - this.overallDifficulty);
|
|
955
|
-
}
|
|
956
|
-
get mehWindow() {
|
|
957
|
-
return 250 + 10 * (5 - this.overallDifficulty);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
/**
|
|
962
|
-
* Represents the hit window of osu!standard.
|
|
963
|
-
*/
|
|
964
|
-
class OsuHitWindow extends HitWindow {
|
|
965
|
-
/**
|
|
966
|
-
* Calculates the overall difficulty value of a great (300) hit window.
|
|
967
|
-
*
|
|
968
|
-
* @param value The value of the hit window, in milliseconds.
|
|
969
|
-
* @returns The overall difficulty value.
|
|
970
|
-
*/
|
|
971
|
-
static greatWindowToOD(value) {
|
|
972
|
-
return (80 - value) / 6;
|
|
973
|
-
}
|
|
974
|
-
/**
|
|
975
|
-
* Calculates the overall difficulty value of a good (100) hit window.
|
|
976
|
-
*
|
|
977
|
-
* @param value The value of the hit window, in milliseconds.
|
|
978
|
-
* @returns The overall difficulty value.
|
|
979
|
-
*/
|
|
980
|
-
static okWindowToOD(value) {
|
|
981
|
-
return (140 - value) / 8;
|
|
982
|
-
}
|
|
983
|
-
/**
|
|
984
|
-
* Calculates the overall difficulty value of a meh hit window.
|
|
985
|
-
*
|
|
986
|
-
* @param value The value of the hit window, in milliseconds.
|
|
987
|
-
* @returns The overall difficulty value.
|
|
988
|
-
*/
|
|
989
|
-
static mehWindowToOD(value) {
|
|
990
|
-
return (200 - value) / 10;
|
|
991
|
-
}
|
|
992
|
-
get greatWindow() {
|
|
993
|
-
return 80 - 6 * this.overallDifficulty;
|
|
994
|
-
}
|
|
995
|
-
get okWindow() {
|
|
996
|
-
return 140 - 8 * this.overallDifficulty;
|
|
997
|
-
}
|
|
998
|
-
get mehWindow() {
|
|
999
|
-
return 200 - 10 * this.overallDifficulty;
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
/**
|
|
1004
|
-
* Represents a hitobject in a beatmap.
|
|
1005
|
-
*/
|
|
1006
|
-
class HitObject {
|
|
1007
|
-
/**
|
|
1008
|
-
* The position of the hitobject in osu!pixels.
|
|
1009
|
-
*/
|
|
1010
|
-
get position() {
|
|
1011
|
-
return this._position;
|
|
1012
|
-
}
|
|
1013
|
-
set position(value) {
|
|
1014
|
-
this._position = value;
|
|
1015
|
-
}
|
|
1016
|
-
/**
|
|
1017
|
-
* The end position of the hitobject in osu!pixels.
|
|
1018
|
-
*/
|
|
1019
|
-
get endPosition() {
|
|
1020
|
-
return this.position;
|
|
1021
|
-
}
|
|
1022
|
-
/**
|
|
1023
|
-
* The end time of the hitobject.
|
|
1024
|
-
*/
|
|
1025
|
-
get endTime() {
|
|
1026
|
-
return this.startTime;
|
|
1027
|
-
}
|
|
1028
|
-
/**
|
|
1029
|
-
* The duration of the hitobject.
|
|
1030
|
-
*/
|
|
1031
|
-
get duration() {
|
|
1032
|
-
return this.endTime - this.startTime;
|
|
938
|
+
get duration() {
|
|
939
|
+
return this.endTime - this.startTime;
|
|
1033
940
|
}
|
|
1034
941
|
/**
|
|
1035
942
|
* The index of this hitobject in the current combo.
|
|
@@ -1310,66 +1217,309 @@ HitObject.preemptMin = 450;
|
|
|
1310
1217
|
HitObject.controlPointLeniency = 1;
|
|
1311
1218
|
|
|
1312
1219
|
/**
|
|
1313
|
-
* Represents a
|
|
1314
|
-
*
|
|
1315
|
-
* All we need from circles is their position. All positions
|
|
1316
|
-
* stored in the objects are in playfield coordinates (512*384
|
|
1317
|
-
* rectangle).
|
|
1220
|
+
* Represents a mod.
|
|
1318
1221
|
*/
|
|
1319
|
-
class
|
|
1320
|
-
constructor(
|
|
1321
|
-
|
|
1222
|
+
class Mod {
|
|
1223
|
+
constructor() {
|
|
1224
|
+
/**
|
|
1225
|
+
* `Mod`s that are incompatible with this `Mod`.
|
|
1226
|
+
*/
|
|
1227
|
+
this.incompatibleMods = new Set();
|
|
1322
1228
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1229
|
+
/**
|
|
1230
|
+
* Serializes this `Mod` to a `SerializedMod`.
|
|
1231
|
+
*/
|
|
1232
|
+
serialize() {
|
|
1233
|
+
var _a;
|
|
1234
|
+
const serialized = {
|
|
1235
|
+
acronym: this.acronym,
|
|
1236
|
+
settings: (_a = this.serializeSettings()) !== null && _a !== void 0 ? _a : undefined,
|
|
1237
|
+
};
|
|
1238
|
+
if (!serialized.settings) {
|
|
1239
|
+
delete serialized.settings;
|
|
1240
|
+
}
|
|
1241
|
+
return serialized;
|
|
1325
1242
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1243
|
+
/**
|
|
1244
|
+
* Copies the settings of a `SerializedMod` to this `Mod`.
|
|
1245
|
+
*
|
|
1246
|
+
* @param mod The `SerializedMod` to copy the settings from. Must be the same `Mod` type.
|
|
1247
|
+
* @throws {TypeError} If the `SerializedMod` is not the same type as this `Mod`.
|
|
1248
|
+
*/
|
|
1249
|
+
copySettings(mod) {
|
|
1250
|
+
if (mod.acronym !== this.acronym) {
|
|
1251
|
+
throw new TypeError(`Cannot copy settings from ${mod.acronym} to ${this.acronym}`);
|
|
1252
|
+
}
|
|
1336
1253
|
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Whether this `Mod` can be applied to osu!droid.
|
|
1256
|
+
*/
|
|
1257
|
+
isApplicableToDroid() {
|
|
1258
|
+
return "droidRanked" in this;
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Whether this `Mod` can be applied to osu!standard.
|
|
1262
|
+
*/
|
|
1263
|
+
isApplicableToOsu() {
|
|
1264
|
+
return "osuRanked" in this;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Whether this `Mod` can be applied to osu!standard, specifically the osu!stable client.
|
|
1268
|
+
*/
|
|
1269
|
+
isApplicableToOsuStable() {
|
|
1270
|
+
return "bitwise" in this;
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Whether this `Mod` can be applied to a `Beatmap`.
|
|
1274
|
+
*/
|
|
1275
|
+
isApplicableToBeatmap() {
|
|
1276
|
+
return "applyToBeatmap" in this;
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Whether this `Mod` can be applied to a `BeatmapDifficulty`.
|
|
1280
|
+
*/
|
|
1281
|
+
isApplicableToDifficulty() {
|
|
1282
|
+
return "applyToDifficulty" in this;
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Whether this `Mod` can be applied to a `BeatmapDifficulty` relative to other `Mod`s and settings.
|
|
1286
|
+
*/
|
|
1287
|
+
isApplicableToDifficultyWithSettings() {
|
|
1288
|
+
return "applyToDifficultyWithSettings" in this;
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Whether this `Mod` can be applied to a `HitObject`.
|
|
1292
|
+
*/
|
|
1293
|
+
isApplicableToHitObject() {
|
|
1294
|
+
return "applyToHitObject" in this;
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Whether this `Mod` can be applied to a `HitObject` relative to other `Mod`s and settings.
|
|
1298
|
+
*/
|
|
1299
|
+
isApplicableToHitObjectWithSettings() {
|
|
1300
|
+
return "applyToHitObjectWithSettings" in this;
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Whether this `Mod` can be applied to a track's playback rate.
|
|
1304
|
+
*/
|
|
1305
|
+
isApplicableToTrackRate() {
|
|
1306
|
+
return "applyToRate" in this;
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Whether this `Mod` is migratable to a new `Mod` in osu!droid.
|
|
1310
|
+
*/
|
|
1311
|
+
isMigratableDroidMod() {
|
|
1312
|
+
return "migrateDroidMod" in this;
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Serializes the settings of this `Mod` to an object that can be converted to a JSON.
|
|
1316
|
+
*
|
|
1317
|
+
* @returns The serialized settings of this `Mod`, or `null` if there are no settings.
|
|
1318
|
+
*/
|
|
1319
|
+
serializeSettings() {
|
|
1320
|
+
return null;
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Returns the string representation of this `Mod`.
|
|
1324
|
+
*/
|
|
1337
1325
|
toString() {
|
|
1338
|
-
return
|
|
1326
|
+
return this.acronym;
|
|
1339
1327
|
}
|
|
1340
1328
|
}
|
|
1341
1329
|
|
|
1342
1330
|
/**
|
|
1343
|
-
* Represents the
|
|
1331
|
+
* Represents the Relax mod.
|
|
1344
1332
|
*/
|
|
1345
|
-
class
|
|
1346
|
-
constructor(
|
|
1347
|
-
super(
|
|
1333
|
+
class ModRelax extends Mod {
|
|
1334
|
+
constructor() {
|
|
1335
|
+
super();
|
|
1336
|
+
this.acronym = "RX";
|
|
1337
|
+
this.name = "Relax";
|
|
1338
|
+
this.droidRanked = false;
|
|
1339
|
+
this.osuRanked = false;
|
|
1340
|
+
this.bitwise = 1 << 7;
|
|
1341
|
+
this.incompatibleMods.add(ModAuto).add(ModAutopilot);
|
|
1342
|
+
}
|
|
1343
|
+
get isDroidRelevant() {
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
1346
|
+
calculateDroidScoreMultiplier() {
|
|
1347
|
+
return 0.001;
|
|
1348
|
+
}
|
|
1349
|
+
get isOsuRelevant() {
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1352
|
+
get osuScoreMultiplier() {
|
|
1353
|
+
return 0;
|
|
1348
1354
|
}
|
|
1349
1355
|
}
|
|
1350
1356
|
|
|
1351
1357
|
/**
|
|
1352
|
-
*
|
|
1353
|
-
*
|
|
1354
|
-
* No time values are provided (meaning instantaneous hit or miss).
|
|
1358
|
+
* Represents the Autopilot mod.
|
|
1355
1359
|
*/
|
|
1356
|
-
class
|
|
1360
|
+
class ModAutopilot extends Mod {
|
|
1357
1361
|
constructor() {
|
|
1358
|
-
super(
|
|
1362
|
+
super();
|
|
1363
|
+
this.acronym = "AP";
|
|
1364
|
+
this.name = "Autopilot";
|
|
1365
|
+
this.droidRanked = false;
|
|
1366
|
+
this.osuRanked = false;
|
|
1367
|
+
this.bitwise = 1 << 13;
|
|
1368
|
+
this.incompatibleMods.add(ModRelax).add(ModAuto);
|
|
1359
1369
|
}
|
|
1360
|
-
get
|
|
1361
|
-
return
|
|
1370
|
+
get isDroidRelevant() {
|
|
1371
|
+
return true;
|
|
1362
1372
|
}
|
|
1363
|
-
|
|
1364
|
-
return 0;
|
|
1373
|
+
calculateDroidScoreMultiplier() {
|
|
1374
|
+
return 0.001;
|
|
1365
1375
|
}
|
|
1366
|
-
get
|
|
1376
|
+
get isOsuRelevant() {
|
|
1377
|
+
return true;
|
|
1378
|
+
}
|
|
1379
|
+
get osuScoreMultiplier() {
|
|
1367
1380
|
return 0;
|
|
1368
1381
|
}
|
|
1369
1382
|
}
|
|
1370
1383
|
|
|
1371
1384
|
/**
|
|
1372
|
-
* Represents
|
|
1385
|
+
* Represents the Auto mod.
|
|
1386
|
+
*/
|
|
1387
|
+
class ModAuto extends Mod {
|
|
1388
|
+
constructor() {
|
|
1389
|
+
super();
|
|
1390
|
+
this.acronym = "AT";
|
|
1391
|
+
this.name = "Autoplay";
|
|
1392
|
+
this.droidRanked = false;
|
|
1393
|
+
this.osuRanked = false;
|
|
1394
|
+
this.bitwise = 1 << 11;
|
|
1395
|
+
this.incompatibleMods.add(ModAutopilot).add(ModRelax);
|
|
1396
|
+
}
|
|
1397
|
+
get isDroidRelevant() {
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
calculateDroidScoreMultiplier() {
|
|
1401
|
+
return 1;
|
|
1402
|
+
}
|
|
1403
|
+
get isOsuRelevant() {
|
|
1404
|
+
return true;
|
|
1405
|
+
}
|
|
1406
|
+
get osuScoreMultiplier() {
|
|
1407
|
+
return 1;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* Represents a `Mod` that adjusts the playback rate of a track.
|
|
1413
|
+
*/
|
|
1414
|
+
class ModRateAdjust extends Mod {
|
|
1415
|
+
/**
|
|
1416
|
+
* The generic osu!droid score multiplier of this `Mod`.
|
|
1417
|
+
*/
|
|
1418
|
+
get droidScoreMultiplier() {
|
|
1419
|
+
return this.trackRateMultiplier >= 1
|
|
1420
|
+
? 1 + (this.trackRateMultiplier - 1) * 0.24
|
|
1421
|
+
: Math.pow(0.3, (1 - this.trackRateMultiplier) * 4);
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Generic getter to determine if this `ModRateAdjust` is relevant.
|
|
1425
|
+
*/
|
|
1426
|
+
get isRelevant() {
|
|
1427
|
+
return this.trackRateMultiplier !== 1;
|
|
1428
|
+
}
|
|
1429
|
+
applyToRate(time, rate) {
|
|
1430
|
+
return rate * this.trackRateMultiplier;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Represents the Custom Speed mod.
|
|
1436
|
+
*
|
|
1437
|
+
* This is a replacement `Mod` for speed modify in osu!droid and custom rates in osu!lazer.
|
|
1438
|
+
*/
|
|
1439
|
+
class ModCustomSpeed extends ModRateAdjust {
|
|
1440
|
+
constructor(trackRateMultiplier = 1) {
|
|
1441
|
+
super();
|
|
1442
|
+
this.acronym = "CS";
|
|
1443
|
+
this.name = "Custom Speed";
|
|
1444
|
+
this.droidRanked = true;
|
|
1445
|
+
this.osuRanked = false;
|
|
1446
|
+
this.trackRateMultiplier = trackRateMultiplier;
|
|
1447
|
+
}
|
|
1448
|
+
copySettings(mod) {
|
|
1449
|
+
var _a, _b;
|
|
1450
|
+
super.copySettings(mod);
|
|
1451
|
+
this.trackRateMultiplier =
|
|
1452
|
+
(_b = (_a = mod.settings) === null || _a === void 0 ? void 0 : _a.rateMultiplier) !== null && _b !== void 0 ? _b : this.trackRateMultiplier;
|
|
1453
|
+
}
|
|
1454
|
+
get isDroidRelevant() {
|
|
1455
|
+
return this.isRelevant;
|
|
1456
|
+
}
|
|
1457
|
+
calculateDroidScoreMultiplier() {
|
|
1458
|
+
return this.droidScoreMultiplier;
|
|
1459
|
+
}
|
|
1460
|
+
get isOsuRelevant() {
|
|
1461
|
+
return this.isRelevant;
|
|
1462
|
+
}
|
|
1463
|
+
get osuScoreMultiplier() {
|
|
1464
|
+
// Round to the nearest multiple of 0.1.
|
|
1465
|
+
let value = Math.trunc(this.trackRateMultiplier * 10) / 10;
|
|
1466
|
+
// Offset back to 0.
|
|
1467
|
+
--value;
|
|
1468
|
+
return this.trackRateMultiplier >= 1 ? 1 + value / 5 : 0.6 + value;
|
|
1469
|
+
}
|
|
1470
|
+
serializeSettings() {
|
|
1471
|
+
return { rateMultiplier: this.trackRateMultiplier };
|
|
1472
|
+
}
|
|
1473
|
+
toString() {
|
|
1474
|
+
return `${super.toString()} (${this.trackRateMultiplier.toFixed(2)}x)`;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
/**
|
|
1479
|
+
* Represents a hitobject that can be nested within a slider.
|
|
1480
|
+
*/
|
|
1481
|
+
class SliderNestedHitObject extends HitObject {
|
|
1482
|
+
constructor(values) {
|
|
1483
|
+
super(values);
|
|
1484
|
+
this.spanIndex = values.spanIndex;
|
|
1485
|
+
this.spanStartTime = values.spanStartTime;
|
|
1486
|
+
}
|
|
1487
|
+
toString() {
|
|
1488
|
+
return `Position: [${this._position.x}, ${this._position.y}], span index: ${this.spanIndex}, span start time: ${this.spanStartTime}`;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* Represents the head of a slider.
|
|
1494
|
+
*/
|
|
1495
|
+
class SliderHead extends SliderNestedHitObject {
|
|
1496
|
+
constructor(values) {
|
|
1497
|
+
super(Object.assign(Object.assign({}, values), { spanIndex: 0, spanStartTime: values.startTime }));
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* An empty `HitWindow` that does not have any hit windows.
|
|
1503
|
+
*
|
|
1504
|
+
* No time values are provided (meaning instantaneous hit or miss).
|
|
1505
|
+
*/
|
|
1506
|
+
class EmptyHitWindow extends HitWindow {
|
|
1507
|
+
constructor() {
|
|
1508
|
+
super(0);
|
|
1509
|
+
}
|
|
1510
|
+
get greatWindow() {
|
|
1511
|
+
return 0;
|
|
1512
|
+
}
|
|
1513
|
+
get okWindow() {
|
|
1514
|
+
return 0;
|
|
1515
|
+
}
|
|
1516
|
+
get mehWindow() {
|
|
1517
|
+
return 0;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/**
|
|
1522
|
+
* Represents a nested hit object that is at the end of a slider path (either repeat or tail).
|
|
1373
1523
|
*/
|
|
1374
1524
|
class SliderEndCircle extends SliderNestedHitObject {
|
|
1375
1525
|
constructor(values) {
|
|
@@ -1941,799 +2091,75 @@ Slider.baseTickSample = new BankHitSampleInfo("slidertick");
|
|
|
1941
2091
|
Slider.legacyLastTickOffset = 36;
|
|
1942
2092
|
|
|
1943
2093
|
/**
|
|
1944
|
-
* Represents the
|
|
1945
|
-
*/
|
|
1946
|
-
class Playfield {
|
|
1947
|
-
}
|
|
1948
|
-
/**
|
|
1949
|
-
* The size of the playfield, which is 512x384.
|
|
1950
|
-
*/
|
|
1951
|
-
Playfield.baseSize = new Vector2(512, 384);
|
|
1952
|
-
|
|
1953
|
-
/**
|
|
1954
|
-
* Represents a spinner in a beatmap.
|
|
1955
|
-
*
|
|
1956
|
-
* All we need from spinners is their duration. The
|
|
1957
|
-
* position of a spinner is always at 256x192.
|
|
2094
|
+
* Represents the Difficulty Adjust mod.
|
|
1958
2095
|
*/
|
|
1959
|
-
class
|
|
1960
|
-
get
|
|
1961
|
-
return this.
|
|
2096
|
+
class ModDifficultyAdjust extends Mod {
|
|
2097
|
+
get isRelevant() {
|
|
2098
|
+
return (this.cs !== undefined ||
|
|
2099
|
+
this.ar !== undefined ||
|
|
2100
|
+
this.od !== undefined ||
|
|
2101
|
+
this.hp !== undefined);
|
|
1962
2102
|
}
|
|
1963
2103
|
constructor(values) {
|
|
1964
|
-
super(
|
|
1965
|
-
this.
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
this.
|
|
1971
|
-
this.
|
|
1972
|
-
this.
|
|
1973
|
-
}
|
|
1974
|
-
getStackedPosition() {
|
|
1975
|
-
return this.position;
|
|
2104
|
+
super();
|
|
2105
|
+
this.acronym = "DA";
|
|
2106
|
+
this.name = "Difficulty Adjust";
|
|
2107
|
+
this.droidRanked = false;
|
|
2108
|
+
this.osuRanked = false;
|
|
2109
|
+
this.cs = values === null || values === void 0 ? void 0 : values.cs;
|
|
2110
|
+
this.ar = values === null || values === void 0 ? void 0 : values.ar;
|
|
2111
|
+
this.od = values === null || values === void 0 ? void 0 : values.od;
|
|
2112
|
+
this.hp = values === null || values === void 0 ? void 0 : values.hp;
|
|
1976
2113
|
}
|
|
1977
|
-
|
|
1978
|
-
|
|
2114
|
+
copySettings(mod) {
|
|
2115
|
+
var _a, _b, _c, _d;
|
|
2116
|
+
super.copySettings(mod);
|
|
2117
|
+
this.cs = (_a = mod.settings) === null || _a === void 0 ? void 0 : _a.cs;
|
|
2118
|
+
this.ar = (_b = mod.settings) === null || _b === void 0 ? void 0 : _b.ar;
|
|
2119
|
+
this.od = (_c = mod.settings) === null || _c === void 0 ? void 0 : _c.od;
|
|
2120
|
+
this.hp = (_d = mod.settings) === null || _d === void 0 ? void 0 : _d.hp;
|
|
1979
2121
|
}
|
|
1980
|
-
|
|
1981
|
-
return
|
|
2122
|
+
get isDroidRelevant() {
|
|
2123
|
+
return this.isRelevant;
|
|
1982
2124
|
}
|
|
1983
|
-
|
|
1984
|
-
|
|
2125
|
+
calculateDroidScoreMultiplier(difficulty) {
|
|
2126
|
+
// Graph: https://www.desmos.com/calculator/yrggkhrkzz
|
|
2127
|
+
let multiplier = 1;
|
|
2128
|
+
if (this.cs !== undefined) {
|
|
2129
|
+
const diff = this.cs - difficulty.cs;
|
|
2130
|
+
multiplier *=
|
|
2131
|
+
diff >= 0
|
|
2132
|
+
? 1 + 0.0075 * Math.pow(diff, 1.5)
|
|
2133
|
+
: 2 / (1 + Math.exp(-0.5 * diff));
|
|
2134
|
+
}
|
|
2135
|
+
if (this.od !== undefined) {
|
|
2136
|
+
const diff = this.od - difficulty.od;
|
|
2137
|
+
multiplier *=
|
|
2138
|
+
diff >= 0
|
|
2139
|
+
? 1 + 0.005 * Math.pow(diff, 1.3)
|
|
2140
|
+
: 2 / (1 + Math.exp(-0.25 * diff));
|
|
2141
|
+
}
|
|
2142
|
+
return multiplier;
|
|
1985
2143
|
}
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
Spinner.baseSpinnerBonusSample = new BankHitSampleInfo("spinnerbonus");
|
|
1989
|
-
|
|
1990
|
-
/**
|
|
1991
|
-
* Contains information about hit objects of a beatmap.
|
|
1992
|
-
*/
|
|
1993
|
-
class BeatmapHitObjects {
|
|
1994
|
-
constructor() {
|
|
1995
|
-
this._objects = [];
|
|
1996
|
-
this._circles = 0;
|
|
1997
|
-
this._sliders = 0;
|
|
1998
|
-
this._spinners = 0;
|
|
2144
|
+
get isOsuRelevant() {
|
|
2145
|
+
return this.isRelevant;
|
|
1999
2146
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
*/
|
|
2003
|
-
get objects() {
|
|
2004
|
-
return this._objects;
|
|
2147
|
+
get osuScoreMultiplier() {
|
|
2148
|
+
return 0.5;
|
|
2005
2149
|
}
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
* The amount of spinners in the beatmap.
|
|
2020
|
-
*/
|
|
2021
|
-
get spinners() {
|
|
2022
|
-
return this._spinners;
|
|
2023
|
-
}
|
|
2024
|
-
/**
|
|
2025
|
-
* The amount of slider ticks in the beatmap.
|
|
2026
|
-
*
|
|
2027
|
-
* This iterates through all objects and should be stored locally or used sparingly.
|
|
2028
|
-
*/
|
|
2029
|
-
get sliderTicks() {
|
|
2030
|
-
return this.objects.reduce((acc, cur) => (cur instanceof Slider ? acc + cur.ticks : acc), 0);
|
|
2031
|
-
}
|
|
2032
|
-
/**
|
|
2033
|
-
* The amount of sliderends in the beatmap.
|
|
2034
|
-
*/
|
|
2035
|
-
get sliderEnds() {
|
|
2036
|
-
return this.sliders;
|
|
2037
|
-
}
|
|
2038
|
-
/**
|
|
2039
|
-
* The amount of slider repeat points in the beatmap.
|
|
2040
|
-
*
|
|
2041
|
-
* This iterates through all objects and should be stored locally or used sparingly.
|
|
2042
|
-
*/
|
|
2043
|
-
get sliderRepeatPoints() {
|
|
2044
|
-
return this.objects.reduce((acc, cur) => (cur instanceof Slider ? acc + cur.repeatCount : acc), 0);
|
|
2045
|
-
}
|
|
2046
|
-
/**
|
|
2047
|
-
* Adds hitobjects.
|
|
2048
|
-
*
|
|
2049
|
-
* The sorting order of hitobjects will be maintained.
|
|
2050
|
-
*
|
|
2051
|
-
* @param objects The hitobjects to add.
|
|
2052
|
-
*/
|
|
2053
|
-
add(...objects) {
|
|
2054
|
-
for (const object of objects) {
|
|
2055
|
-
// Objects may be out of order *only* if a user has manually edited an .osu file.
|
|
2056
|
-
// Unfortunately there are "ranked" maps in this state (example: https://osu.ppy.sh/s/594828).
|
|
2057
|
-
// Finding index is used to guarantee that the parsing order of hitobjects with equal start times is maintained (stably-sorted).
|
|
2058
|
-
this._objects.splice(this.findInsertionIndex(object.startTime), 0, object);
|
|
2059
|
-
if (object instanceof Circle) {
|
|
2060
|
-
++this._circles;
|
|
2061
|
-
}
|
|
2062
|
-
else if (object instanceof Slider) {
|
|
2063
|
-
++this._sliders;
|
|
2064
|
-
}
|
|
2065
|
-
else {
|
|
2066
|
-
++this._spinners;
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
/**
|
|
2071
|
-
* Removes a hitobject at an index.
|
|
2072
|
-
*
|
|
2073
|
-
* @param index The index of the hitobject to remove.
|
|
2074
|
-
* @returns The hitobject that was removed, `null` if no hitobject was removed.
|
|
2075
|
-
*/
|
|
2076
|
-
removeAt(index) {
|
|
2077
|
-
var _a;
|
|
2078
|
-
const object = (_a = this._objects.splice(index, 1)[0]) !== null && _a !== void 0 ? _a : null;
|
|
2079
|
-
if (object instanceof Circle) {
|
|
2080
|
-
--this._circles;
|
|
2081
|
-
}
|
|
2082
|
-
else if (object instanceof Slider) {
|
|
2083
|
-
--this._sliders;
|
|
2084
|
-
}
|
|
2085
|
-
else if (object instanceof Spinner) {
|
|
2086
|
-
--this._spinners;
|
|
2087
|
-
}
|
|
2088
|
-
return object;
|
|
2089
|
-
}
|
|
2090
|
-
/**
|
|
2091
|
-
* Clears all hitobjects.
|
|
2092
|
-
*/
|
|
2093
|
-
clear() {
|
|
2094
|
-
this._objects.length = 0;
|
|
2095
|
-
this._circles = 0;
|
|
2096
|
-
this._sliders = 0;
|
|
2097
|
-
this._spinners = 0;
|
|
2098
|
-
}
|
|
2099
|
-
/**
|
|
2100
|
-
* Finds the insertion index of a hitobject in a given time.
|
|
2101
|
-
*
|
|
2102
|
-
* @param startTime The start time of the hitobject.
|
|
2103
|
-
*/
|
|
2104
|
-
findInsertionIndex(startTime) {
|
|
2105
|
-
if (this._objects.length === 0 ||
|
|
2106
|
-
startTime < this._objects[0].startTime) {
|
|
2107
|
-
return 0;
|
|
2108
|
-
}
|
|
2109
|
-
if (startTime >= this._objects.at(-1).startTime) {
|
|
2110
|
-
return this._objects.length;
|
|
2111
|
-
}
|
|
2112
|
-
let l = 0;
|
|
2113
|
-
let r = this._objects.length - 2;
|
|
2114
|
-
while (l <= r) {
|
|
2115
|
-
const pivot = l + ((r - l) >> 1);
|
|
2116
|
-
if (this._objects[pivot].startTime < startTime) {
|
|
2117
|
-
l = pivot + 1;
|
|
2118
|
-
}
|
|
2119
|
-
else if (this._objects[pivot].startTime > startTime) {
|
|
2120
|
-
r = pivot - 1;
|
|
2121
|
-
}
|
|
2122
|
-
else {
|
|
2123
|
-
return pivot;
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
return l;
|
|
2127
|
-
}
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
/**
|
|
2131
|
-
* Converts a beatmap for another mode.
|
|
2132
|
-
*/
|
|
2133
|
-
class BeatmapConverter {
|
|
2134
|
-
constructor(beatmap) {
|
|
2135
|
-
this.beatmap = beatmap;
|
|
2136
|
-
}
|
|
2137
|
-
/**
|
|
2138
|
-
* Converts the beatmap.
|
|
2139
|
-
*
|
|
2140
|
-
* @returns The converted beatmap.
|
|
2141
|
-
*/
|
|
2142
|
-
convert() {
|
|
2143
|
-
const converted = new Beatmap(this.beatmap);
|
|
2144
|
-
// Shallow clone isn't enough to ensure we don't mutate some beatmap properties unexpectedly.
|
|
2145
|
-
converted.difficulty = new BeatmapDifficulty(this.beatmap.difficulty);
|
|
2146
|
-
converted.hitObjects = this.convertHitObjects();
|
|
2147
|
-
return converted;
|
|
2148
|
-
}
|
|
2149
|
-
convertHitObjects() {
|
|
2150
|
-
const hitObjects = new BeatmapHitObjects();
|
|
2151
|
-
this.beatmap.hitObjects.objects.forEach((hitObject) => {
|
|
2152
|
-
hitObjects.add(this.convertHitObject(hitObject));
|
|
2153
|
-
});
|
|
2154
|
-
return hitObjects;
|
|
2155
|
-
}
|
|
2156
|
-
convertHitObject(hitObject) {
|
|
2157
|
-
let object;
|
|
2158
|
-
if (hitObject instanceof Circle) {
|
|
2159
|
-
object = new Circle({
|
|
2160
|
-
startTime: hitObject.startTime,
|
|
2161
|
-
position: hitObject.position,
|
|
2162
|
-
newCombo: hitObject.isNewCombo,
|
|
2163
|
-
type: hitObject.type,
|
|
2164
|
-
comboOffset: hitObject.comboOffset,
|
|
2165
|
-
});
|
|
2166
|
-
}
|
|
2167
|
-
else if (hitObject instanceof Slider) {
|
|
2168
|
-
object = new Slider({
|
|
2169
|
-
startTime: hitObject.startTime,
|
|
2170
|
-
position: hitObject.position,
|
|
2171
|
-
newCombo: hitObject.isNewCombo,
|
|
2172
|
-
type: hitObject.type,
|
|
2173
|
-
path: hitObject.path,
|
|
2174
|
-
repeatCount: hitObject.repeatCount,
|
|
2175
|
-
nodeSamples: hitObject.nodeSamples,
|
|
2176
|
-
comboOffset: hitObject.comboOffset,
|
|
2177
|
-
tickDistanceMultiplier:
|
|
2178
|
-
// Prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
|
2179
|
-
// This results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
|
2180
|
-
this.beatmap.formatVersion < 8
|
|
2181
|
-
? 1 /
|
|
2182
|
-
this.beatmap.controlPoints.difficulty.controlPointAt(hitObject.startTime).speedMultiplier
|
|
2183
|
-
: 1,
|
|
2184
|
-
});
|
|
2185
|
-
}
|
|
2186
|
-
else {
|
|
2187
|
-
object = new Spinner({
|
|
2188
|
-
startTime: hitObject.startTime,
|
|
2189
|
-
endTime: hitObject.endTime,
|
|
2190
|
-
type: hitObject.type,
|
|
2191
|
-
});
|
|
2192
|
-
}
|
|
2193
|
-
object.samples = hitObject.samples;
|
|
2194
|
-
object.auxiliarySamples = hitObject.auxiliarySamples;
|
|
2195
|
-
return object;
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
/**
|
|
2200
|
-
* Provides functionality to alter a beatmap after it has been converted.
|
|
2201
|
-
*/
|
|
2202
|
-
class BeatmapProcessor {
|
|
2203
|
-
constructor(beatmap) {
|
|
2204
|
-
this.beatmap = beatmap;
|
|
2205
|
-
}
|
|
2206
|
-
/**
|
|
2207
|
-
* Processes the converted beatmap prior to `HitObject.applyDefaults` being invoked.
|
|
2208
|
-
*
|
|
2209
|
-
* Nested hitobjects generated during `HitObject.applyDefaults` will not be present by this point,
|
|
2210
|
-
* and no mods will have been applied to the hitobjects.
|
|
2211
|
-
*
|
|
2212
|
-
* This can only be used to add alterations to hitobjects generated directly through the conversion process.
|
|
2213
|
-
*/
|
|
2214
|
-
preProcess() {
|
|
2215
|
-
let last = null;
|
|
2216
|
-
for (const object of this.beatmap.hitObjects.objects) {
|
|
2217
|
-
object.updateComboInformation(last);
|
|
2218
|
-
last = object;
|
|
2219
|
-
}
|
|
2220
|
-
// Mark the last object in the beatmap as last in combo.
|
|
2221
|
-
if (last) {
|
|
2222
|
-
last.isLastInCombo = true;
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2225
|
-
/**
|
|
2226
|
-
* Processes the converted beatmap after `HitObject.applyDefaults` has been invoked.
|
|
2227
|
-
*
|
|
2228
|
-
* Nested hitobjects generated during `HitObject.applyDefaults` wil be present by this point,
|
|
2229
|
-
* and mods will have been applied to all hitobjects.
|
|
2230
|
-
*
|
|
2231
|
-
* This should be used to add alterations to hitobjects while they are in their most playable state.
|
|
2232
|
-
*
|
|
2233
|
-
* @param mode The mode to add alterations for.
|
|
2234
|
-
*/
|
|
2235
|
-
postProcess(mode) {
|
|
2236
|
-
const objects = this.beatmap.hitObjects.objects;
|
|
2237
|
-
if (objects.length === 0) {
|
|
2238
|
-
return;
|
|
2239
|
-
}
|
|
2240
|
-
// Reset stacking
|
|
2241
|
-
objects.forEach((h) => {
|
|
2242
|
-
h.stackHeight = 0;
|
|
2243
|
-
});
|
|
2244
|
-
switch (mode) {
|
|
2245
|
-
case exports.Modes.droid:
|
|
2246
|
-
this.applyDroidStacking();
|
|
2247
|
-
break;
|
|
2248
|
-
case exports.Modes.osu:
|
|
2249
|
-
if (this.beatmap.formatVersion >= 6) {
|
|
2250
|
-
this.applyStandardStacking();
|
|
2251
|
-
}
|
|
2252
|
-
else {
|
|
2253
|
-
this.applyStandardOldStacking();
|
|
2254
|
-
}
|
|
2255
|
-
break;
|
|
2256
|
-
}
|
|
2257
|
-
}
|
|
2258
|
-
applyDroidStacking() {
|
|
2259
|
-
const objects = this.beatmap.hitObjects.objects;
|
|
2260
|
-
if (objects.length === 0) {
|
|
2261
|
-
return;
|
|
2262
|
-
}
|
|
2263
|
-
const convertedScale = CircleSizeCalculator.standardScaleToDroidScale(objects[0].scale);
|
|
2264
|
-
for (let i = 0; i < objects.length - 1; ++i) {
|
|
2265
|
-
const current = objects[i];
|
|
2266
|
-
const next = objects[i + 1];
|
|
2267
|
-
if (current instanceof Circle &&
|
|
2268
|
-
next.startTime - current.startTime <
|
|
2269
|
-
2000 * this.beatmap.general.stackLeniency &&
|
|
2270
|
-
next.position.getDistance(current.position) <
|
|
2271
|
-
Math.sqrt(convertedScale)) {
|
|
2272
|
-
next.stackHeight = current.stackHeight + 1;
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
}
|
|
2276
|
-
applyStandardStacking() {
|
|
2277
|
-
const objects = this.beatmap.hitObjects.objects;
|
|
2278
|
-
const startIndex = 0;
|
|
2279
|
-
const endIndex = objects.length - 1;
|
|
2280
|
-
let extendedEndIndex = endIndex;
|
|
2281
|
-
if (endIndex < objects.length - 1) {
|
|
2282
|
-
for (let i = endIndex; i >= startIndex; --i) {
|
|
2283
|
-
let stackBaseIndex = i;
|
|
2284
|
-
for (let n = stackBaseIndex + 1; n < objects.length; ++n) {
|
|
2285
|
-
const stackBaseObject = objects[stackBaseIndex];
|
|
2286
|
-
if (stackBaseObject instanceof Spinner) {
|
|
2287
|
-
break;
|
|
2288
|
-
}
|
|
2289
|
-
const objectN = objects[n];
|
|
2290
|
-
if (objectN instanceof Spinner) {
|
|
2291
|
-
break;
|
|
2292
|
-
}
|
|
2293
|
-
const stackThreshold = objectN.timePreempt *
|
|
2294
|
-
this.beatmap.general.stackLeniency;
|
|
2295
|
-
if (objectN.startTime - stackBaseObject.endTime >
|
|
2296
|
-
stackThreshold) {
|
|
2297
|
-
// We are no longer within stacking range of the next object.
|
|
2298
|
-
break;
|
|
2299
|
-
}
|
|
2300
|
-
const endPositionDistanceCheck = stackBaseObject instanceof Slider
|
|
2301
|
-
? stackBaseObject.endPosition.getDistance(objectN.position) < BeatmapProcessor.stackDistance
|
|
2302
|
-
: false;
|
|
2303
|
-
if (stackBaseObject.position.getDistance(objectN.position) <
|
|
2304
|
-
BeatmapProcessor.stackDistance ||
|
|
2305
|
-
endPositionDistanceCheck) {
|
|
2306
|
-
stackBaseIndex = n;
|
|
2307
|
-
// Hit objects after the specified update range haven't been reset yet
|
|
2308
|
-
objectN.stackHeight = 0;
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
if (stackBaseIndex > extendedEndIndex) {
|
|
2312
|
-
extendedEndIndex = stackBaseIndex;
|
|
2313
|
-
if (extendedEndIndex === objects.length - 1) {
|
|
2314
|
-
break;
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
}
|
|
2319
|
-
// Reverse pass for stack calculation.
|
|
2320
|
-
let extendedStartIndex = startIndex;
|
|
2321
|
-
for (let i = extendedEndIndex; i > startIndex; --i) {
|
|
2322
|
-
let n = i;
|
|
2323
|
-
// We should check every note which has not yet got a stack.
|
|
2324
|
-
// Consider the case we have two inter-wound stacks and this will make sense.
|
|
2325
|
-
//
|
|
2326
|
-
// o <-1 o <-2
|
|
2327
|
-
// o <-3 o <-4
|
|
2328
|
-
//
|
|
2329
|
-
// We first process starting from 4 and handle 2,
|
|
2330
|
-
// then we come backwards on the i loop iteration until we reach 3 and handle 1.
|
|
2331
|
-
// 2 and 1 will be ignored in the i loop because they already have a stack value.
|
|
2332
|
-
let objectI = objects[i];
|
|
2333
|
-
if (objectI.stackHeight !== 0 || objectI instanceof Spinner) {
|
|
2334
|
-
continue;
|
|
2335
|
-
}
|
|
2336
|
-
const stackThreshold = objectI.timePreempt * this.beatmap.general.stackLeniency;
|
|
2337
|
-
// If this object is a hit circle, then we enter this "special" case.
|
|
2338
|
-
// It either ends with a stack of hit circles only, or a stack of hit circles that are underneath a slider.
|
|
2339
|
-
// Any other case is handled by the "instanceof Slider" code below this.
|
|
2340
|
-
if (objectI instanceof Circle) {
|
|
2341
|
-
while (--n >= 0) {
|
|
2342
|
-
const objectN = objects[n];
|
|
2343
|
-
if (objectN instanceof Spinner) {
|
|
2344
|
-
continue;
|
|
2345
|
-
}
|
|
2346
|
-
if (objectI.startTime - objectN.endTime > stackThreshold) {
|
|
2347
|
-
// We are no longer within stacking range of the previous object.
|
|
2348
|
-
break;
|
|
2349
|
-
}
|
|
2350
|
-
// Hit objects before the specified update range haven't been reset yet
|
|
2351
|
-
if (n < extendedStartIndex) {
|
|
2352
|
-
objectN.stackHeight = 0;
|
|
2353
|
-
extendedStartIndex = n;
|
|
2354
|
-
}
|
|
2355
|
-
// This is a special case where hit circles are moved DOWN and RIGHT (negative stacking) if they are under the *last* slider in a stacked pattern.
|
|
2356
|
-
// o==o <- slider is at original location
|
|
2357
|
-
// o <- hitCircle has stack of -1
|
|
2358
|
-
// o <- hitCircle has stack of -2
|
|
2359
|
-
if (objectN instanceof Slider &&
|
|
2360
|
-
objectN.endPosition.getDistance(objectI.position) <
|
|
2361
|
-
BeatmapProcessor.stackDistance) {
|
|
2362
|
-
const offset = objectI.stackHeight - objectN.stackHeight + 1;
|
|
2363
|
-
for (let j = n + 1; j <= i; ++j) {
|
|
2364
|
-
// For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above).
|
|
2365
|
-
const objectJ = objects[j];
|
|
2366
|
-
if (objectN.endPosition.getDistance(objectJ.position) < BeatmapProcessor.stackDistance) {
|
|
2367
|
-
objectJ.stackHeight -= offset;
|
|
2368
|
-
}
|
|
2369
|
-
}
|
|
2370
|
-
// We have hit a slider. We should restart calculation using this as the new base.
|
|
2371
|
-
// Breaking here will mean that the slider still has a stack count of 0, so will be handled in the i-outer-loop.
|
|
2372
|
-
break;
|
|
2373
|
-
}
|
|
2374
|
-
if (objectN.position.getDistance(objectI.position) <
|
|
2375
|
-
BeatmapProcessor.stackDistance) {
|
|
2376
|
-
// Keep processing as if there are no sliders. If we come across a slider, this gets cancelled out.
|
|
2377
|
-
// NOTE: Sliders with start positions stacking are a special case that is also handled here.
|
|
2378
|
-
objectN.stackHeight = objectI.stackHeight + 1;
|
|
2379
|
-
objectI = objectN;
|
|
2380
|
-
}
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
else if (objectI instanceof Slider) {
|
|
2384
|
-
// We have hit the first slider in a possible stack.
|
|
2385
|
-
// From this point on, we ALWAYS stack positive regardless.
|
|
2386
|
-
while (--n >= startIndex) {
|
|
2387
|
-
const objectN = objects[n];
|
|
2388
|
-
if (objectN instanceof Spinner) {
|
|
2389
|
-
continue;
|
|
2390
|
-
}
|
|
2391
|
-
if (objectI.startTime - objectN.startTime >
|
|
2392
|
-
stackThreshold) {
|
|
2393
|
-
// We are no longer within stacking range of the previous object.
|
|
2394
|
-
break;
|
|
2395
|
-
}
|
|
2396
|
-
if (objectN.endPosition.getDistance(objectI.position) <
|
|
2397
|
-
BeatmapProcessor.stackDistance) {
|
|
2398
|
-
objectN.stackHeight = objectI.stackHeight + 1;
|
|
2399
|
-
objectI = objectN;
|
|
2400
|
-
}
|
|
2401
|
-
}
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
2404
|
-
}
|
|
2405
|
-
applyStandardOldStacking() {
|
|
2406
|
-
const objects = this.beatmap.hitObjects.objects;
|
|
2407
|
-
for (let i = 0; i < objects.length; ++i) {
|
|
2408
|
-
const currentObject = objects[i];
|
|
2409
|
-
if (currentObject.stackHeight !== 0 &&
|
|
2410
|
-
!(currentObject instanceof Slider)) {
|
|
2411
|
-
continue;
|
|
2412
|
-
}
|
|
2413
|
-
let startTime = currentObject.endTime;
|
|
2414
|
-
let sliderStack = 0;
|
|
2415
|
-
const stackThreshold = currentObject.timePreempt * this.beatmap.general.stackLeniency;
|
|
2416
|
-
for (let j = i + 1; j < objects.length; ++j) {
|
|
2417
|
-
if (objects[j].startTime - stackThreshold > startTime) {
|
|
2418
|
-
break;
|
|
2419
|
-
}
|
|
2420
|
-
// Note the use of `startTime` in the code below doesn't match osu!stable's use of `endTime`.
|
|
2421
|
-
// This is because in osu!stable's implementation, `UpdateCalculations` is not called on the inner-loop hitobject (j)
|
|
2422
|
-
// and therefore it does not have a correct `endTime`, but instead the default of `endTime = startTime`.
|
|
2423
|
-
//
|
|
2424
|
-
// Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where
|
|
2425
|
-
// if we use `endTime` here it would result in unexpected stacking.
|
|
2426
|
-
//
|
|
2427
|
-
// Reference: https://github.com/ppy/osu/pull/24188
|
|
2428
|
-
if (objects[j].position.getDistance(currentObject.position) <
|
|
2429
|
-
BeatmapProcessor.stackDistance) {
|
|
2430
|
-
++currentObject.stackHeight;
|
|
2431
|
-
startTime = objects[j].startTime;
|
|
2432
|
-
}
|
|
2433
|
-
else if (objects[j].position.getDistance(currentObject.endPosition) <
|
|
2434
|
-
BeatmapProcessor.stackDistance) {
|
|
2435
|
-
// Case for sliders - bump notes down and right, rather than up and left.
|
|
2436
|
-
++sliderStack;
|
|
2437
|
-
objects[j].stackHeight -= sliderStack;
|
|
2438
|
-
startTime = objects[j].startTime;
|
|
2439
|
-
}
|
|
2440
|
-
}
|
|
2441
|
-
}
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
BeatmapProcessor.stackDistance = 3;
|
|
2445
|
-
|
|
2446
|
-
/**
|
|
2447
|
-
* Represents the hit window of osu!droid _with_ the Precise mod.
|
|
2448
|
-
*/
|
|
2449
|
-
class PreciseDroidHitWindow extends HitWindow {
|
|
2450
|
-
/**
|
|
2451
|
-
* Calculates the overall difficulty value of a great (300) hit window.
|
|
2452
|
-
*
|
|
2453
|
-
* @param value The value of the hit window, in milliseconds.
|
|
2454
|
-
* @returns The overall difficulty value.
|
|
2455
|
-
*/
|
|
2456
|
-
static greatWindowToOD(value) {
|
|
2457
|
-
return 5 - (value - 55) / 6;
|
|
2458
|
-
}
|
|
2459
|
-
/**
|
|
2460
|
-
* Calculates the overall difficulty value of a good (100) hit window.
|
|
2461
|
-
*
|
|
2462
|
-
* @param value The value of the hit window, in milliseconds.
|
|
2463
|
-
* @returns The overall difficulty value.
|
|
2464
|
-
*/
|
|
2465
|
-
static okWindowToOD(value) {
|
|
2466
|
-
return 5 - (value - 120) / 8;
|
|
2467
|
-
}
|
|
2468
|
-
/**
|
|
2469
|
-
* Calculates the overall difficulty value of a meh (50) hit window.
|
|
2470
|
-
*
|
|
2471
|
-
* @param value The value of the hit window, in milliseconds.
|
|
2472
|
-
* @returns The overall difficulty value.
|
|
2473
|
-
*/
|
|
2474
|
-
static mehWindowToOD(value) {
|
|
2475
|
-
return 5 - (value - 180) / 10;
|
|
2476
|
-
}
|
|
2477
|
-
get greatWindow() {
|
|
2478
|
-
return 55 + 6 * (5 - this.overallDifficulty);
|
|
2479
|
-
}
|
|
2480
|
-
get okWindow() {
|
|
2481
|
-
return 120 + 8 * (5 - this.overallDifficulty);
|
|
2482
|
-
}
|
|
2483
|
-
get mehWindow() {
|
|
2484
|
-
return 180 + 10 * (5 - this.overallDifficulty);
|
|
2485
|
-
}
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
/**
|
|
2489
|
-
* Represents the Precise mod.
|
|
2490
|
-
*/
|
|
2491
|
-
class ModPrecise extends Mod {
|
|
2492
|
-
constructor() {
|
|
2493
|
-
super(...arguments);
|
|
2494
|
-
this.acronym = "PR";
|
|
2495
|
-
this.name = "Precise";
|
|
2496
|
-
this.droidRanked = true;
|
|
2497
|
-
}
|
|
2498
|
-
get isDroidRelevant() {
|
|
2499
|
-
return true;
|
|
2500
|
-
}
|
|
2501
|
-
calculateDroidScoreMultiplier() {
|
|
2502
|
-
return 1.06;
|
|
2503
|
-
}
|
|
2504
|
-
applyToHitObject(mode, hitObject) {
|
|
2505
|
-
var _a, _b;
|
|
2506
|
-
if (mode !== exports.Modes.droid || hitObject instanceof Spinner) {
|
|
2507
|
-
return;
|
|
2508
|
-
}
|
|
2509
|
-
if (hitObject instanceof Slider) {
|
|
2510
|
-
// For sliders, the hit window is enforced in the head - everything else is an instant hit or miss.
|
|
2511
|
-
hitObject.head.hitWindow = new PreciseDroidHitWindow((_a = hitObject.head.hitWindow) === null || _a === void 0 ? void 0 : _a.overallDifficulty);
|
|
2512
|
-
}
|
|
2513
|
-
else {
|
|
2514
|
-
hitObject.hitWindow = new PreciseDroidHitWindow((_b = hitObject.hitWindow) === null || _b === void 0 ? void 0 : _b.overallDifficulty);
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
|
-
|
|
2519
|
-
/**
|
|
2520
|
-
* Represents the Relax mod.
|
|
2521
|
-
*/
|
|
2522
|
-
class ModRelax extends Mod {
|
|
2523
|
-
constructor() {
|
|
2524
|
-
super();
|
|
2525
|
-
this.acronym = "RX";
|
|
2526
|
-
this.name = "Relax";
|
|
2527
|
-
this.droidRanked = false;
|
|
2528
|
-
this.osuRanked = false;
|
|
2529
|
-
this.bitwise = 1 << 7;
|
|
2530
|
-
this.incompatibleMods.add(ModAuto).add(ModAutopilot);
|
|
2531
|
-
}
|
|
2532
|
-
get isDroidRelevant() {
|
|
2533
|
-
return true;
|
|
2534
|
-
}
|
|
2535
|
-
calculateDroidScoreMultiplier() {
|
|
2536
|
-
return 0.001;
|
|
2537
|
-
}
|
|
2538
|
-
get isOsuRelevant() {
|
|
2539
|
-
return true;
|
|
2540
|
-
}
|
|
2541
|
-
get osuScoreMultiplier() {
|
|
2542
|
-
return 0;
|
|
2543
|
-
}
|
|
2544
|
-
}
|
|
2545
|
-
|
|
2546
|
-
/**
|
|
2547
|
-
* Represents the Autopilot mod.
|
|
2548
|
-
*/
|
|
2549
|
-
class ModAutopilot extends Mod {
|
|
2550
|
-
constructor() {
|
|
2551
|
-
super();
|
|
2552
|
-
this.acronym = "AP";
|
|
2553
|
-
this.name = "Autopilot";
|
|
2554
|
-
this.droidRanked = false;
|
|
2555
|
-
this.osuRanked = false;
|
|
2556
|
-
this.bitwise = 1 << 13;
|
|
2557
|
-
this.incompatibleMods.add(ModRelax).add(ModAuto);
|
|
2558
|
-
}
|
|
2559
|
-
get isDroidRelevant() {
|
|
2560
|
-
return true;
|
|
2561
|
-
}
|
|
2562
|
-
calculateDroidScoreMultiplier() {
|
|
2563
|
-
return 0.001;
|
|
2564
|
-
}
|
|
2565
|
-
get isOsuRelevant() {
|
|
2566
|
-
return true;
|
|
2567
|
-
}
|
|
2568
|
-
get osuScoreMultiplier() {
|
|
2569
|
-
return 0;
|
|
2570
|
-
}
|
|
2571
|
-
}
|
|
2572
|
-
|
|
2573
|
-
/**
|
|
2574
|
-
* Represents the Auto mod.
|
|
2575
|
-
*/
|
|
2576
|
-
class ModAuto extends Mod {
|
|
2577
|
-
constructor() {
|
|
2578
|
-
super();
|
|
2579
|
-
this.acronym = "AT";
|
|
2580
|
-
this.name = "Autoplay";
|
|
2581
|
-
this.droidRanked = false;
|
|
2582
|
-
this.osuRanked = false;
|
|
2583
|
-
this.bitwise = 1 << 11;
|
|
2584
|
-
this.incompatibleMods.add(ModAutopilot).add(ModRelax);
|
|
2585
|
-
}
|
|
2586
|
-
get isDroidRelevant() {
|
|
2587
|
-
return true;
|
|
2588
|
-
}
|
|
2589
|
-
calculateDroidScoreMultiplier() {
|
|
2590
|
-
return 1;
|
|
2591
|
-
}
|
|
2592
|
-
get isOsuRelevant() {
|
|
2593
|
-
return true;
|
|
2594
|
-
}
|
|
2595
|
-
get osuScoreMultiplier() {
|
|
2596
|
-
return 1;
|
|
2597
|
-
}
|
|
2598
|
-
}
|
|
2599
|
-
|
|
2600
|
-
/**
|
|
2601
|
-
* Represents a `Mod` that adjusts the playback rate of a track.
|
|
2602
|
-
*/
|
|
2603
|
-
class ModRateAdjust extends Mod {
|
|
2604
|
-
/**
|
|
2605
|
-
* The generic osu!droid score multiplier of this `Mod`.
|
|
2606
|
-
*/
|
|
2607
|
-
get droidScoreMultiplier() {
|
|
2608
|
-
return this.trackRateMultiplier >= 1
|
|
2609
|
-
? 1 + (this.trackRateMultiplier - 1) * 0.24
|
|
2610
|
-
: Math.pow(0.3, (1 - this.trackRateMultiplier) * 4);
|
|
2611
|
-
}
|
|
2612
|
-
/**
|
|
2613
|
-
* Generic getter to determine if this `ModRateAdjust` is relevant.
|
|
2614
|
-
*/
|
|
2615
|
-
get isRelevant() {
|
|
2616
|
-
return this.trackRateMultiplier !== 1;
|
|
2617
|
-
}
|
|
2618
|
-
applyToRate(time, rate) {
|
|
2619
|
-
return rate * this.trackRateMultiplier;
|
|
2620
|
-
}
|
|
2621
|
-
}
|
|
2622
|
-
|
|
2623
|
-
/**
|
|
2624
|
-
* Represents the Custom Speed mod.
|
|
2625
|
-
*
|
|
2626
|
-
* This is a replacement `Mod` for speed modify in osu!droid and custom rates in osu!lazer.
|
|
2627
|
-
*/
|
|
2628
|
-
class ModCustomSpeed extends ModRateAdjust {
|
|
2629
|
-
constructor(trackRateMultiplier = 1) {
|
|
2630
|
-
super();
|
|
2631
|
-
this.acronym = "CS";
|
|
2632
|
-
this.name = "Custom Speed";
|
|
2633
|
-
this.droidRanked = true;
|
|
2634
|
-
this.osuRanked = false;
|
|
2635
|
-
this.trackRateMultiplier = trackRateMultiplier;
|
|
2636
|
-
}
|
|
2637
|
-
copySettings(mod) {
|
|
2638
|
-
var _a, _b;
|
|
2639
|
-
super.copySettings(mod);
|
|
2640
|
-
this.trackRateMultiplier =
|
|
2641
|
-
(_b = (_a = mod.settings) === null || _a === void 0 ? void 0 : _a.rateMultiplier) !== null && _b !== void 0 ? _b : this.trackRateMultiplier;
|
|
2642
|
-
}
|
|
2643
|
-
get isDroidRelevant() {
|
|
2644
|
-
return this.isRelevant;
|
|
2645
|
-
}
|
|
2646
|
-
calculateDroidScoreMultiplier() {
|
|
2647
|
-
return this.droidScoreMultiplier;
|
|
2648
|
-
}
|
|
2649
|
-
get isOsuRelevant() {
|
|
2650
|
-
return this.isRelevant;
|
|
2651
|
-
}
|
|
2652
|
-
get osuScoreMultiplier() {
|
|
2653
|
-
// Round to the nearest multiple of 0.1.
|
|
2654
|
-
let value = Math.trunc(this.trackRateMultiplier * 10) / 10;
|
|
2655
|
-
// Offset back to 0.
|
|
2656
|
-
--value;
|
|
2657
|
-
return this.trackRateMultiplier >= 1 ? 1 + value / 5 : 0.6 + value;
|
|
2658
|
-
}
|
|
2659
|
-
serializeSettings() {
|
|
2660
|
-
return { rateMultiplier: this.trackRateMultiplier };
|
|
2661
|
-
}
|
|
2662
|
-
toString() {
|
|
2663
|
-
return `${super.toString()} (${this.trackRateMultiplier.toFixed(2)}x)`;
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
/**
|
|
2668
|
-
* Represents the Difficulty Adjust mod.
|
|
2669
|
-
*/
|
|
2670
|
-
class ModDifficultyAdjust extends Mod {
|
|
2671
|
-
get isRelevant() {
|
|
2672
|
-
return (this.cs !== undefined ||
|
|
2673
|
-
this.ar !== undefined ||
|
|
2674
|
-
this.od !== undefined ||
|
|
2675
|
-
this.hp !== undefined);
|
|
2676
|
-
}
|
|
2677
|
-
constructor(values) {
|
|
2678
|
-
super();
|
|
2679
|
-
this.acronym = "DA";
|
|
2680
|
-
this.name = "Difficulty Adjust";
|
|
2681
|
-
this.droidRanked = false;
|
|
2682
|
-
this.osuRanked = false;
|
|
2683
|
-
this.cs = values === null || values === void 0 ? void 0 : values.cs;
|
|
2684
|
-
this.ar = values === null || values === void 0 ? void 0 : values.ar;
|
|
2685
|
-
this.od = values === null || values === void 0 ? void 0 : values.od;
|
|
2686
|
-
this.hp = values === null || values === void 0 ? void 0 : values.hp;
|
|
2687
|
-
}
|
|
2688
|
-
copySettings(mod) {
|
|
2689
|
-
var _a, _b, _c, _d;
|
|
2690
|
-
super.copySettings(mod);
|
|
2691
|
-
this.cs = (_a = mod.settings) === null || _a === void 0 ? void 0 : _a.cs;
|
|
2692
|
-
this.ar = (_b = mod.settings) === null || _b === void 0 ? void 0 : _b.ar;
|
|
2693
|
-
this.od = (_c = mod.settings) === null || _c === void 0 ? void 0 : _c.od;
|
|
2694
|
-
this.hp = (_d = mod.settings) === null || _d === void 0 ? void 0 : _d.hp;
|
|
2695
|
-
}
|
|
2696
|
-
get isDroidRelevant() {
|
|
2697
|
-
return this.isRelevant;
|
|
2698
|
-
}
|
|
2699
|
-
calculateDroidScoreMultiplier(difficulty) {
|
|
2700
|
-
// Graph: https://www.desmos.com/calculator/yrggkhrkzz
|
|
2701
|
-
let multiplier = 1;
|
|
2702
|
-
if (this.cs !== undefined) {
|
|
2703
|
-
const diff = this.cs - difficulty.cs;
|
|
2704
|
-
multiplier *=
|
|
2705
|
-
diff >= 0
|
|
2706
|
-
? 1 + 0.0075 * Math.pow(diff, 1.5)
|
|
2707
|
-
: 2 / (1 + Math.exp(-0.5 * diff));
|
|
2708
|
-
}
|
|
2709
|
-
if (this.od !== undefined) {
|
|
2710
|
-
const diff = this.od - difficulty.od;
|
|
2711
|
-
multiplier *=
|
|
2712
|
-
diff >= 0
|
|
2713
|
-
? 1 + 0.005 * Math.pow(diff, 1.3)
|
|
2714
|
-
: 2 / (1 + Math.exp(-0.25 * diff));
|
|
2715
|
-
}
|
|
2716
|
-
return multiplier;
|
|
2717
|
-
}
|
|
2718
|
-
get isOsuRelevant() {
|
|
2719
|
-
return this.isRelevant;
|
|
2720
|
-
}
|
|
2721
|
-
get osuScoreMultiplier() {
|
|
2722
|
-
return 0.5;
|
|
2723
|
-
}
|
|
2724
|
-
applyToDifficultyWithSettings(_, difficulty, mods) {
|
|
2725
|
-
var _a, _b, _c, _d;
|
|
2726
|
-
difficulty.cs = (_a = this.cs) !== null && _a !== void 0 ? _a : difficulty.cs;
|
|
2727
|
-
difficulty.ar = (_b = this.ar) !== null && _b !== void 0 ? _b : difficulty.ar;
|
|
2728
|
-
difficulty.od = (_c = this.od) !== null && _c !== void 0 ? _c : difficulty.od;
|
|
2729
|
-
difficulty.hp = (_d = this.hp) !== null && _d !== void 0 ? _d : difficulty.hp;
|
|
2730
|
-
// Special case for force AR, where the AR value is kept constant with respect to game time.
|
|
2731
|
-
// This makes the player perceive the AR as is under all speed multipliers.
|
|
2732
|
-
if (this.ar !== undefined) {
|
|
2733
|
-
const preempt = BeatmapDifficulty.difficultyRange(this.ar, HitObject.preemptMax, HitObject.preemptMid, HitObject.preemptMin);
|
|
2734
|
-
const trackRate = this.calculateTrackRate(mods);
|
|
2735
|
-
difficulty.ar = BeatmapDifficulty.inverseDifficultyRange(preempt * trackRate, HitObject.preemptMax, HitObject.preemptMid, HitObject.preemptMin);
|
|
2736
|
-
}
|
|
2150
|
+
applyToDifficultyWithSettings(_, difficulty, mods) {
|
|
2151
|
+
var _a, _b, _c, _d;
|
|
2152
|
+
difficulty.cs = (_a = this.cs) !== null && _a !== void 0 ? _a : difficulty.cs;
|
|
2153
|
+
difficulty.ar = (_b = this.ar) !== null && _b !== void 0 ? _b : difficulty.ar;
|
|
2154
|
+
difficulty.od = (_c = this.od) !== null && _c !== void 0 ? _c : difficulty.od;
|
|
2155
|
+
difficulty.hp = (_d = this.hp) !== null && _d !== void 0 ? _d : difficulty.hp;
|
|
2156
|
+
// Special case for force AR, where the AR value is kept constant with respect to game time.
|
|
2157
|
+
// This makes the player perceive the AR as is under all speed multipliers.
|
|
2158
|
+
if (this.ar !== undefined) {
|
|
2159
|
+
const preempt = BeatmapDifficulty.difficultyRange(this.ar, HitObject.preemptMax, HitObject.preemptMid, HitObject.preemptMin);
|
|
2160
|
+
const trackRate = this.calculateTrackRate(mods.values());
|
|
2161
|
+
difficulty.ar = BeatmapDifficulty.inverseDifficultyRange(preempt * trackRate, HitObject.preemptMax, HitObject.preemptMid, HitObject.preemptMin);
|
|
2162
|
+
}
|
|
2737
2163
|
}
|
|
2738
2164
|
applyToHitObjectWithSettings(_, hitObject, mods) {
|
|
2739
2165
|
// Special case for force AR, where the AR value is kept constant with respect to game time.
|
|
@@ -2772,8 +2198,8 @@ class ModDifficultyAdjust extends Mod {
|
|
|
2772
2198
|
}
|
|
2773
2199
|
applyFadeAdjustment(hitObject, mods) {
|
|
2774
2200
|
// IMPORTANT: These do not use `ModUtil.calculateRateWithMods` to avoid circular dependency.
|
|
2775
|
-
const initialTrackRate = this.calculateTrackRate(mods);
|
|
2776
|
-
const currentTrackRate = this.calculateTrackRate(mods, hitObject.startTime);
|
|
2201
|
+
const initialTrackRate = this.calculateTrackRate(mods.values());
|
|
2202
|
+
const currentTrackRate = this.calculateTrackRate(mods.values(), hitObject.startTime);
|
|
2777
2203
|
// Cancel the rate that was initially applied to timePreempt (via applyToDifficulty above and
|
|
2778
2204
|
// HitObject.applyDefaults) and apply the current one.
|
|
2779
2205
|
hitObject.timePreempt *= currentTrackRate / initialTrackRate;
|
|
@@ -2781,9 +2207,13 @@ class ModDifficultyAdjust extends Mod {
|
|
|
2781
2207
|
}
|
|
2782
2208
|
calculateTrackRate(mods, time = 0) {
|
|
2783
2209
|
// IMPORTANT: This does not use `ModUtil.calculateRateWithMods` to avoid circular dependency.
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2210
|
+
let rate = 1;
|
|
2211
|
+
for (const mod of mods) {
|
|
2212
|
+
if (mod.isApplicableToTrackRate()) {
|
|
2213
|
+
rate = mod.applyToRate(time, rate);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
return rate;
|
|
2787
2217
|
}
|
|
2788
2218
|
toString() {
|
|
2789
2219
|
const settings = [];
|
|
@@ -2887,6 +2317,16 @@ class ModDoubleTime extends ModRateAdjust {
|
|
|
2887
2317
|
}
|
|
2888
2318
|
}
|
|
2889
2319
|
|
|
2320
|
+
/**
|
|
2321
|
+
* Represents the osu! playfield.
|
|
2322
|
+
*/
|
|
2323
|
+
class Playfield {
|
|
2324
|
+
}
|
|
2325
|
+
/**
|
|
2326
|
+
* The size of the playfield, which is 512x384.
|
|
2327
|
+
*/
|
|
2328
|
+
Playfield.baseSize = new Vector2(512, 384);
|
|
2329
|
+
|
|
2890
2330
|
/**
|
|
2891
2331
|
* Types of slider paths.
|
|
2892
2332
|
*/
|
|
@@ -3430,86 +2870,295 @@ class SliderPath {
|
|
|
3430
2870
|
}
|
|
3431
2871
|
|
|
3432
2872
|
/**
|
|
3433
|
-
* Represents the HardRock mod.
|
|
2873
|
+
* Represents the HardRock mod.
|
|
2874
|
+
*/
|
|
2875
|
+
class ModHardRock extends Mod {
|
|
2876
|
+
constructor() {
|
|
2877
|
+
super();
|
|
2878
|
+
this.acronym = "HR";
|
|
2879
|
+
this.name = "HardRock";
|
|
2880
|
+
this.droidRanked = true;
|
|
2881
|
+
this.osuRanked = true;
|
|
2882
|
+
this.bitwise = 1 << 4;
|
|
2883
|
+
this.incompatibleMods.add(ModEasy);
|
|
2884
|
+
}
|
|
2885
|
+
get isDroidRelevant() {
|
|
2886
|
+
return true;
|
|
2887
|
+
}
|
|
2888
|
+
calculateDroidScoreMultiplier() {
|
|
2889
|
+
return 1.06;
|
|
2890
|
+
}
|
|
2891
|
+
get isOsuRelevant() {
|
|
2892
|
+
return true;
|
|
2893
|
+
}
|
|
2894
|
+
get osuScoreMultiplier() {
|
|
2895
|
+
return 1.06;
|
|
2896
|
+
}
|
|
2897
|
+
applyToDifficulty(mode, difficulty) {
|
|
2898
|
+
switch (mode) {
|
|
2899
|
+
case exports.Modes.droid: {
|
|
2900
|
+
const scale = CircleSizeCalculator.droidCSToDroidScale(difficulty.cs);
|
|
2901
|
+
difficulty.cs = CircleSizeCalculator.droidScaleToDroidCS(scale - 0.125);
|
|
2902
|
+
break;
|
|
2903
|
+
}
|
|
2904
|
+
case exports.Modes.osu:
|
|
2905
|
+
// CS uses a custom 1.3 ratio.
|
|
2906
|
+
difficulty.cs = this.applySetting(difficulty.cs, 1.3);
|
|
2907
|
+
break;
|
|
2908
|
+
}
|
|
2909
|
+
difficulty.ar = this.applySetting(difficulty.ar);
|
|
2910
|
+
difficulty.od = this.applySetting(difficulty.od);
|
|
2911
|
+
difficulty.hp = this.applySetting(difficulty.hp);
|
|
2912
|
+
}
|
|
2913
|
+
applyToHitObject(_, hitObject) {
|
|
2914
|
+
// Reflect the position of the hit object.
|
|
2915
|
+
hitObject.position = this.reflectVector(hitObject.position);
|
|
2916
|
+
if (!(hitObject instanceof Slider)) {
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
// Reflect the control points of the slider. This will reflect the positions of head and tail circles.
|
|
2920
|
+
hitObject.path = new SliderPath({
|
|
2921
|
+
pathType: hitObject.path.pathType,
|
|
2922
|
+
controlPoints: hitObject.path.controlPoints.map((v) => this.reflectControlPoint(v)),
|
|
2923
|
+
expectedDistance: hitObject.path.expectedDistance,
|
|
2924
|
+
});
|
|
2925
|
+
// Reflect the position of slider ticks and repeats.
|
|
2926
|
+
hitObject.nestedHitObjects.slice(1, -1).forEach((obj) => {
|
|
2927
|
+
obj.position = this.reflectVector(obj.position);
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2930
|
+
reflectVector(vector) {
|
|
2931
|
+
return new Vector2(vector.x, Playfield.baseSize.y - vector.y);
|
|
2932
|
+
}
|
|
2933
|
+
reflectControlPoint(vector) {
|
|
2934
|
+
return new Vector2(vector.x, -vector.y);
|
|
2935
|
+
}
|
|
2936
|
+
applySetting(value, ratio = 1.4) {
|
|
2937
|
+
return Math.min(value * ratio, 10);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
/**
|
|
2942
|
+
* Represents the Easy mod.
|
|
2943
|
+
*/
|
|
2944
|
+
class ModEasy extends Mod {
|
|
2945
|
+
constructor() {
|
|
2946
|
+
super();
|
|
2947
|
+
this.acronym = "EZ";
|
|
2948
|
+
this.name = "Easy";
|
|
2949
|
+
this.droidRanked = true;
|
|
2950
|
+
this.osuRanked = true;
|
|
2951
|
+
this.bitwise = 1 << 1;
|
|
2952
|
+
this.incompatibleMods.add(ModHardRock);
|
|
2953
|
+
}
|
|
2954
|
+
get isDroidRelevant() {
|
|
2955
|
+
return true;
|
|
2956
|
+
}
|
|
2957
|
+
calculateDroidScoreMultiplier() {
|
|
2958
|
+
return 0.5;
|
|
2959
|
+
}
|
|
2960
|
+
get isOsuRelevant() {
|
|
2961
|
+
return true;
|
|
2962
|
+
}
|
|
2963
|
+
get osuScoreMultiplier() {
|
|
2964
|
+
return 0.5;
|
|
2965
|
+
}
|
|
2966
|
+
applyToDifficulty(mode, difficulty) {
|
|
2967
|
+
switch (mode) {
|
|
2968
|
+
case exports.Modes.droid: {
|
|
2969
|
+
const scale = CircleSizeCalculator.droidCSToDroidScale(difficulty.cs);
|
|
2970
|
+
difficulty.cs = CircleSizeCalculator.droidScaleToDroidCS(scale + 0.125);
|
|
2971
|
+
break;
|
|
2972
|
+
}
|
|
2973
|
+
case exports.Modes.osu:
|
|
2974
|
+
difficulty.cs /= 2;
|
|
2975
|
+
}
|
|
2976
|
+
difficulty.ar /= 2;
|
|
2977
|
+
difficulty.od /= 2;
|
|
2978
|
+
difficulty.hp /= 2;
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
/**
|
|
2983
|
+
* Represents the Flashlight mod.
|
|
2984
|
+
*/
|
|
2985
|
+
class ModFlashlight extends Mod {
|
|
2986
|
+
constructor() {
|
|
2987
|
+
super(...arguments);
|
|
2988
|
+
this.acronym = "FL";
|
|
2989
|
+
this.name = "Flashlight";
|
|
2990
|
+
this.droidRanked = true;
|
|
2991
|
+
this.osuRanked = true;
|
|
2992
|
+
this.bitwise = 1 << 10;
|
|
2993
|
+
/**
|
|
2994
|
+
* The amount of seconds until the Flashlight follow area reaches the cursor.
|
|
2995
|
+
*/
|
|
2996
|
+
this.followDelay = ModFlashlight.defaultFollowDelay;
|
|
2997
|
+
}
|
|
2998
|
+
copySettings(mod) {
|
|
2999
|
+
var _a, _b;
|
|
3000
|
+
super.copySettings(mod);
|
|
3001
|
+
this.followDelay =
|
|
3002
|
+
(_b = (_a = mod.settings) === null || _a === void 0 ? void 0 : _a.areaFollowDelay) !== null && _b !== void 0 ? _b : this.followDelay;
|
|
3003
|
+
}
|
|
3004
|
+
get isDroidRelevant() {
|
|
3005
|
+
return true;
|
|
3006
|
+
}
|
|
3007
|
+
calculateDroidScoreMultiplier() {
|
|
3008
|
+
return 1.12;
|
|
3009
|
+
}
|
|
3010
|
+
get isOsuRelevant() {
|
|
3011
|
+
return true;
|
|
3012
|
+
}
|
|
3013
|
+
get osuScoreMultiplier() {
|
|
3014
|
+
return 1.12;
|
|
3015
|
+
}
|
|
3016
|
+
serializeSettings() {
|
|
3017
|
+
return { areaFollowDelay: this.followDelay };
|
|
3018
|
+
}
|
|
3019
|
+
toString() {
|
|
3020
|
+
if (this.followDelay === ModFlashlight.defaultFollowDelay) {
|
|
3021
|
+
return super.toString();
|
|
3022
|
+
}
|
|
3023
|
+
return `${super.toString()} (${this.followDelay.toFixed(2)}s follow delay)`;
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
/**
|
|
3027
|
+
* The default amount of seconds until the Flashlight follow area reaches the cursor.
|
|
3028
|
+
*/
|
|
3029
|
+
ModFlashlight.defaultFollowDelay = 0.12;
|
|
3030
|
+
|
|
3031
|
+
/**
|
|
3032
|
+
* Represents the Traceable mod.
|
|
3033
|
+
*/
|
|
3034
|
+
class ModTraceable extends Mod {
|
|
3035
|
+
constructor() {
|
|
3036
|
+
super();
|
|
3037
|
+
this.acronym = "TC";
|
|
3038
|
+
this.name = "Traceable";
|
|
3039
|
+
this.droidRanked = false;
|
|
3040
|
+
this.osuRanked = false;
|
|
3041
|
+
this.incompatibleMods.add(ModHidden);
|
|
3042
|
+
}
|
|
3043
|
+
get isDroidRelevant() {
|
|
3044
|
+
return true;
|
|
3045
|
+
}
|
|
3046
|
+
calculateDroidScoreMultiplier() {
|
|
3047
|
+
return 1.06;
|
|
3048
|
+
}
|
|
3049
|
+
get isOsuRelevant() {
|
|
3050
|
+
return true;
|
|
3051
|
+
}
|
|
3052
|
+
get osuScoreMultiplier() {
|
|
3053
|
+
return 1;
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
/**
|
|
3058
|
+
* Represents the Hidden mod.
|
|
3059
|
+
*/
|
|
3060
|
+
class ModHidden extends Mod {
|
|
3061
|
+
constructor() {
|
|
3062
|
+
super();
|
|
3063
|
+
this.acronym = "HD";
|
|
3064
|
+
this.name = "Hidden";
|
|
3065
|
+
this.droidRanked = true;
|
|
3066
|
+
this.osuRanked = true;
|
|
3067
|
+
this.bitwise = 1 << 3;
|
|
3068
|
+
this.incompatibleMods.add(ModTraceable);
|
|
3069
|
+
}
|
|
3070
|
+
get isDroidRelevant() {
|
|
3071
|
+
return true;
|
|
3072
|
+
}
|
|
3073
|
+
calculateDroidScoreMultiplier() {
|
|
3074
|
+
return 1.06;
|
|
3075
|
+
}
|
|
3076
|
+
get isOsuRelevant() {
|
|
3077
|
+
return true;
|
|
3078
|
+
}
|
|
3079
|
+
get osuScoreMultiplier() {
|
|
3080
|
+
return 1.06;
|
|
3081
|
+
}
|
|
3082
|
+
applyToBeatmap(beatmap) {
|
|
3083
|
+
const applyFadeInAdjustment = (hitObject) => {
|
|
3084
|
+
hitObject.timeFadeIn =
|
|
3085
|
+
hitObject.timePreempt * ModHidden.fadeInDurationMultiplier;
|
|
3086
|
+
if (hitObject instanceof Slider) {
|
|
3087
|
+
hitObject.nestedHitObjects.forEach(applyFadeInAdjustment);
|
|
3088
|
+
}
|
|
3089
|
+
};
|
|
3090
|
+
beatmap.hitObjects.objects.forEach(applyFadeInAdjustment);
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
ModHidden.fadeInDurationMultiplier = 0.4;
|
|
3094
|
+
ModHidden.fadeOutDurationMultiplier = 0.3;
|
|
3095
|
+
|
|
3096
|
+
/**
|
|
3097
|
+
* Represents the SuddenDeath mod.
|
|
3098
|
+
*/
|
|
3099
|
+
class ModSuddenDeath extends Mod {
|
|
3100
|
+
constructor() {
|
|
3101
|
+
super();
|
|
3102
|
+
this.acronym = "SD";
|
|
3103
|
+
this.name = "Sudden Death";
|
|
3104
|
+
this.droidRanked = true;
|
|
3105
|
+
this.osuRanked = true;
|
|
3106
|
+
this.bitwise = 1 << 5;
|
|
3107
|
+
this.incompatibleMods.add(ModNoFail).add(ModPerfect);
|
|
3108
|
+
}
|
|
3109
|
+
get isDroidRelevant() {
|
|
3110
|
+
return true;
|
|
3111
|
+
}
|
|
3112
|
+
calculateDroidScoreMultiplier() {
|
|
3113
|
+
return 1;
|
|
3114
|
+
}
|
|
3115
|
+
get isOsuRelevant() {
|
|
3116
|
+
return true;
|
|
3117
|
+
}
|
|
3118
|
+
get osuScoreMultiplier() {
|
|
3119
|
+
return 1;
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
/**
|
|
3124
|
+
* Represents the Perfect mod.
|
|
3434
3125
|
*/
|
|
3435
|
-
class
|
|
3126
|
+
class ModPerfect extends Mod {
|
|
3436
3127
|
constructor() {
|
|
3437
3128
|
super();
|
|
3438
|
-
this.acronym = "
|
|
3439
|
-
this.name = "
|
|
3129
|
+
this.acronym = "PF";
|
|
3130
|
+
this.name = "Perfect";
|
|
3440
3131
|
this.droidRanked = true;
|
|
3441
3132
|
this.osuRanked = true;
|
|
3442
|
-
this.bitwise = 1 <<
|
|
3443
|
-
this.incompatibleMods.add(
|
|
3133
|
+
this.bitwise = 1 << 14;
|
|
3134
|
+
this.incompatibleMods.add(ModNoFail).add(ModSuddenDeath);
|
|
3444
3135
|
}
|
|
3445
3136
|
get isDroidRelevant() {
|
|
3446
3137
|
return true;
|
|
3447
3138
|
}
|
|
3448
3139
|
calculateDroidScoreMultiplier() {
|
|
3449
|
-
return 1
|
|
3140
|
+
return 1;
|
|
3450
3141
|
}
|
|
3451
3142
|
get isOsuRelevant() {
|
|
3452
3143
|
return true;
|
|
3453
3144
|
}
|
|
3454
3145
|
get osuScoreMultiplier() {
|
|
3455
|
-
return 1
|
|
3456
|
-
}
|
|
3457
|
-
applyToDifficulty(mode, difficulty) {
|
|
3458
|
-
switch (mode) {
|
|
3459
|
-
case exports.Modes.droid: {
|
|
3460
|
-
const scale = CircleSizeCalculator.droidCSToDroidScale(difficulty.cs);
|
|
3461
|
-
difficulty.cs = CircleSizeCalculator.droidScaleToDroidCS(scale - 0.125);
|
|
3462
|
-
break;
|
|
3463
|
-
}
|
|
3464
|
-
case exports.Modes.osu:
|
|
3465
|
-
// CS uses a custom 1.3 ratio.
|
|
3466
|
-
difficulty.cs = this.applySetting(difficulty.cs, 1.3);
|
|
3467
|
-
break;
|
|
3468
|
-
}
|
|
3469
|
-
difficulty.ar = this.applySetting(difficulty.ar);
|
|
3470
|
-
difficulty.od = this.applySetting(difficulty.od);
|
|
3471
|
-
difficulty.hp = this.applySetting(difficulty.hp);
|
|
3472
|
-
}
|
|
3473
|
-
applyToHitObject(_, hitObject) {
|
|
3474
|
-
// Reflect the position of the hit object.
|
|
3475
|
-
hitObject.position = this.reflectVector(hitObject.position);
|
|
3476
|
-
if (!(hitObject instanceof Slider)) {
|
|
3477
|
-
return;
|
|
3478
|
-
}
|
|
3479
|
-
// Reflect the control points of the slider. This will reflect the positions of head and tail circles.
|
|
3480
|
-
hitObject.path = new SliderPath({
|
|
3481
|
-
pathType: hitObject.path.pathType,
|
|
3482
|
-
controlPoints: hitObject.path.controlPoints.map((v) => this.reflectControlPoint(v)),
|
|
3483
|
-
expectedDistance: hitObject.path.expectedDistance,
|
|
3484
|
-
});
|
|
3485
|
-
// Reflect the position of slider ticks and repeats.
|
|
3486
|
-
hitObject.nestedHitObjects.slice(1, -1).forEach((obj) => {
|
|
3487
|
-
obj.position = this.reflectVector(obj.position);
|
|
3488
|
-
});
|
|
3489
|
-
}
|
|
3490
|
-
reflectVector(vector) {
|
|
3491
|
-
return new Vector2(vector.x, Playfield.baseSize.y - vector.y);
|
|
3492
|
-
}
|
|
3493
|
-
reflectControlPoint(vector) {
|
|
3494
|
-
return new Vector2(vector.x, -vector.y);
|
|
3495
|
-
}
|
|
3496
|
-
applySetting(value, ratio = 1.4) {
|
|
3497
|
-
return Math.min(value * ratio, 10);
|
|
3146
|
+
return 1;
|
|
3498
3147
|
}
|
|
3499
3148
|
}
|
|
3500
3149
|
|
|
3501
3150
|
/**
|
|
3502
|
-
* Represents the
|
|
3151
|
+
* Represents the NoFail mod.
|
|
3503
3152
|
*/
|
|
3504
|
-
class
|
|
3153
|
+
class ModNoFail extends Mod {
|
|
3505
3154
|
constructor() {
|
|
3506
3155
|
super();
|
|
3507
|
-
this.acronym = "
|
|
3508
|
-
this.name = "
|
|
3156
|
+
this.acronym = "NF";
|
|
3157
|
+
this.name = "NoFail";
|
|
3509
3158
|
this.droidRanked = true;
|
|
3510
3159
|
this.osuRanked = true;
|
|
3511
|
-
this.bitwise = 1 <<
|
|
3512
|
-
this.incompatibleMods.add(
|
|
3160
|
+
this.bitwise = 1 << 0;
|
|
3161
|
+
this.incompatibleMods.add(ModPerfect).add(ModSuddenDeath);
|
|
3513
3162
|
}
|
|
3514
3163
|
get isDroidRelevant() {
|
|
3515
3164
|
return true;
|
|
@@ -3523,88 +3172,213 @@ class ModEasy extends Mod {
|
|
|
3523
3172
|
get osuScoreMultiplier() {
|
|
3524
3173
|
return 0.5;
|
|
3525
3174
|
}
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
/**
|
|
3178
|
+
* Represents a spinner in a beatmap.
|
|
3179
|
+
*
|
|
3180
|
+
* All we need from spinners is their duration. The
|
|
3181
|
+
* position of a spinner is always at 256x192.
|
|
3182
|
+
*/
|
|
3183
|
+
class Spinner extends HitObject {
|
|
3184
|
+
get endTime() {
|
|
3185
|
+
return this._endTime;
|
|
3186
|
+
}
|
|
3187
|
+
constructor(values) {
|
|
3188
|
+
super(Object.assign(Object.assign({}, values), { position: Playfield.baseSize.divide(2) }));
|
|
3189
|
+
this._endTime = values.endTime;
|
|
3190
|
+
}
|
|
3191
|
+
applySamples(controlPoints) {
|
|
3192
|
+
super.applySamples(controlPoints);
|
|
3193
|
+
const samplePoints = controlPoints.sample.between(this.startTime + HitObject.controlPointLeniency, this.endTime + HitObject.controlPointLeniency);
|
|
3194
|
+
this.auxiliarySamples.length = 0;
|
|
3195
|
+
this.auxiliarySamples.push(new SequenceHitSampleInfo(samplePoints.map((s) => new TimedHitSampleInfo(s.time, s.applyTo(Spinner.baseSpinnerSpinSample)))));
|
|
3196
|
+
this.auxiliarySamples.push(new SequenceHitSampleInfo(samplePoints.map((s) => new TimedHitSampleInfo(s.time, s.applyTo(Spinner.baseSpinnerBonusSample)))));
|
|
3197
|
+
}
|
|
3198
|
+
getStackedPosition() {
|
|
3199
|
+
return this.position;
|
|
3200
|
+
}
|
|
3201
|
+
getStackedEndPosition() {
|
|
3202
|
+
return this.position;
|
|
3203
|
+
}
|
|
3204
|
+
createHitWindow() {
|
|
3205
|
+
return new EmptyHitWindow();
|
|
3206
|
+
}
|
|
3207
|
+
toString() {
|
|
3208
|
+
return `Position: [${this._position.x}, ${this._position.y}], duration: ${this.duration}`;
|
|
3539
3209
|
}
|
|
3540
3210
|
}
|
|
3211
|
+
Spinner.baseSpinnerSpinSample = new BankHitSampleInfo("spinnerspin");
|
|
3212
|
+
Spinner.baseSpinnerBonusSample = new BankHitSampleInfo("spinnerbonus");
|
|
3541
3213
|
|
|
3542
3214
|
/**
|
|
3543
|
-
* Represents the
|
|
3215
|
+
* Represents the Precise mod.
|
|
3544
3216
|
*/
|
|
3545
|
-
class
|
|
3217
|
+
class ModPrecise extends Mod {
|
|
3546
3218
|
constructor() {
|
|
3547
3219
|
super(...arguments);
|
|
3548
|
-
this.acronym = "
|
|
3549
|
-
this.name = "
|
|
3220
|
+
this.acronym = "PR";
|
|
3221
|
+
this.name = "Precise";
|
|
3550
3222
|
this.droidRanked = true;
|
|
3551
|
-
this.osuRanked = true;
|
|
3552
|
-
this.bitwise = 1 << 10;
|
|
3553
|
-
/**
|
|
3554
|
-
* The amount of seconds until the Flashlight follow area reaches the cursor.
|
|
3555
|
-
*/
|
|
3556
|
-
this.followDelay = ModFlashlight.defaultFollowDelay;
|
|
3557
3223
|
}
|
|
3558
|
-
|
|
3224
|
+
get isDroidRelevant() {
|
|
3225
|
+
return true;
|
|
3226
|
+
}
|
|
3227
|
+
calculateDroidScoreMultiplier() {
|
|
3228
|
+
return 1.06;
|
|
3229
|
+
}
|
|
3230
|
+
applyToHitObject(mode, hitObject) {
|
|
3559
3231
|
var _a, _b;
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3232
|
+
if (mode !== exports.Modes.droid || hitObject instanceof Spinner) {
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
if (hitObject instanceof Slider) {
|
|
3236
|
+
// For sliders, the hit window is enforced in the head - everything else is an instant hit or miss.
|
|
3237
|
+
hitObject.head.hitWindow = new PreciseDroidHitWindow((_a = hitObject.head.hitWindow) === null || _a === void 0 ? void 0 : _a.overallDifficulty);
|
|
3238
|
+
}
|
|
3239
|
+
else {
|
|
3240
|
+
hitObject.hitWindow = new PreciseDroidHitWindow((_b = hitObject.hitWindow) === null || _b === void 0 ? void 0 : _b.overallDifficulty);
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
/**
|
|
3246
|
+
* Represents the ReallyEasy mod.
|
|
3247
|
+
*/
|
|
3248
|
+
class ModReallyEasy extends Mod {
|
|
3249
|
+
constructor() {
|
|
3250
|
+
super(...arguments);
|
|
3251
|
+
this.acronym = "RE";
|
|
3252
|
+
this.name = "ReallyEasy";
|
|
3253
|
+
this.droidRanked = false;
|
|
3563
3254
|
}
|
|
3564
3255
|
get isDroidRelevant() {
|
|
3565
3256
|
return true;
|
|
3566
3257
|
}
|
|
3567
3258
|
calculateDroidScoreMultiplier() {
|
|
3568
|
-
return
|
|
3259
|
+
return 0.4;
|
|
3260
|
+
}
|
|
3261
|
+
applyToDifficultyWithSettings(mode, difficulty, mods) {
|
|
3262
|
+
var _a;
|
|
3263
|
+
if (mode !== exports.Modes.droid) {
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
const difficultyAdjustMod = mods.get(ModDifficultyAdjust);
|
|
3267
|
+
if ((difficultyAdjustMod === null || difficultyAdjustMod === void 0 ? void 0 : difficultyAdjustMod.ar) === undefined) {
|
|
3268
|
+
if (mods.has(ModEasy)) {
|
|
3269
|
+
difficulty.ar *= 2;
|
|
3270
|
+
difficulty.ar -= 0.5;
|
|
3271
|
+
}
|
|
3272
|
+
const customSpeed = mods.get(ModCustomSpeed);
|
|
3273
|
+
difficulty.ar -= 0.5;
|
|
3274
|
+
difficulty.ar -= ((_a = customSpeed === null || customSpeed === void 0 ? void 0 : customSpeed.trackRateMultiplier) !== null && _a !== void 0 ? _a : 1) - 1;
|
|
3275
|
+
}
|
|
3276
|
+
if ((difficultyAdjustMod === null || difficultyAdjustMod === void 0 ? void 0 : difficultyAdjustMod.cs) === undefined) {
|
|
3277
|
+
const scale = CircleSizeCalculator.droidCSToDroidScale(difficulty.cs);
|
|
3278
|
+
difficulty.cs = CircleSizeCalculator.droidScaleToDroidCS(scale + 0.125);
|
|
3279
|
+
}
|
|
3280
|
+
if ((difficultyAdjustMod === null || difficultyAdjustMod === void 0 ? void 0 : difficultyAdjustMod.od) === undefined) {
|
|
3281
|
+
difficulty.od /= 2;
|
|
3282
|
+
}
|
|
3283
|
+
if ((difficultyAdjustMod === null || difficultyAdjustMod === void 0 ? void 0 : difficultyAdjustMod.hp) === undefined) {
|
|
3284
|
+
difficulty.hp /= 2;
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
/**
|
|
3290
|
+
* Represents the ScoreV2 mod.
|
|
3291
|
+
*/
|
|
3292
|
+
class ModScoreV2 extends Mod {
|
|
3293
|
+
constructor() {
|
|
3294
|
+
super(...arguments);
|
|
3295
|
+
this.acronym = "V2";
|
|
3296
|
+
this.name = "ScoreV2";
|
|
3297
|
+
this.droidRanked = false;
|
|
3298
|
+
this.osuRanked = false;
|
|
3299
|
+
this.bitwise = 1 << 29;
|
|
3300
|
+
}
|
|
3301
|
+
get isDroidRelevant() {
|
|
3302
|
+
return true;
|
|
3303
|
+
}
|
|
3304
|
+
calculateDroidScoreMultiplier() {
|
|
3305
|
+
return 1;
|
|
3306
|
+
}
|
|
3307
|
+
get isOsuRelevant() {
|
|
3308
|
+
return true;
|
|
3309
|
+
}
|
|
3310
|
+
get osuScoreMultiplier() {
|
|
3311
|
+
return 1;
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
/**
|
|
3316
|
+
* Represents the SmallCircle mod.
|
|
3317
|
+
*
|
|
3318
|
+
* This is a legacy osu!droid mod that may still be exist when parsing replays.
|
|
3319
|
+
*/
|
|
3320
|
+
class ModSmallCircle extends Mod {
|
|
3321
|
+
constructor() {
|
|
3322
|
+
super(...arguments);
|
|
3323
|
+
this.acronym = "SC";
|
|
3324
|
+
this.name = "SmallCircle";
|
|
3325
|
+
this.droidRanked = false;
|
|
3326
|
+
}
|
|
3327
|
+
get isDroidRelevant() {
|
|
3328
|
+
return true;
|
|
3329
|
+
}
|
|
3330
|
+
calculateDroidScoreMultiplier() {
|
|
3331
|
+
return 1.06;
|
|
3332
|
+
}
|
|
3333
|
+
migrateDroidMod(difficulty) {
|
|
3334
|
+
return new ModDifficultyAdjust({ cs: difficulty.cs + 4 });
|
|
3335
|
+
}
|
|
3336
|
+
applyToDifficulty(mode, difficulty) {
|
|
3337
|
+
switch (mode) {
|
|
3338
|
+
case exports.Modes.droid: {
|
|
3339
|
+
const scale = CircleSizeCalculator.droidCSToDroidScale(difficulty.cs);
|
|
3340
|
+
difficulty.cs = CircleSizeCalculator.droidScaleToDroidCS(scale -
|
|
3341
|
+
((CircleSizeCalculator.assumedDroidHeight / 480) *
|
|
3342
|
+
(4 * 4.48) *
|
|
3343
|
+
2) /
|
|
3344
|
+
128);
|
|
3345
|
+
break;
|
|
3346
|
+
}
|
|
3347
|
+
case exports.Modes.osu:
|
|
3348
|
+
difficulty.cs += 4;
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
/**
|
|
3354
|
+
* Represents the SpunOut mod.
|
|
3355
|
+
*/
|
|
3356
|
+
class ModSpunOut extends Mod {
|
|
3357
|
+
constructor() {
|
|
3358
|
+
super(...arguments);
|
|
3359
|
+
this.acronym = "SO";
|
|
3360
|
+
this.name = "SpunOut";
|
|
3361
|
+
this.osuRanked = true;
|
|
3362
|
+
this.bitwise = 1 << 12;
|
|
3569
3363
|
}
|
|
3570
3364
|
get isOsuRelevant() {
|
|
3571
3365
|
return true;
|
|
3572
3366
|
}
|
|
3573
3367
|
get osuScoreMultiplier() {
|
|
3574
|
-
return
|
|
3575
|
-
}
|
|
3576
|
-
serializeSettings() {
|
|
3577
|
-
return { areaFollowDelay: this.followDelay };
|
|
3578
|
-
}
|
|
3579
|
-
toString() {
|
|
3580
|
-
if (this.followDelay === ModFlashlight.defaultFollowDelay) {
|
|
3581
|
-
return super.toString();
|
|
3582
|
-
}
|
|
3583
|
-
return `${super.toString()} (${this.followDelay.toFixed(2)}s follow delay)`;
|
|
3368
|
+
return 0.9;
|
|
3584
3369
|
}
|
|
3585
3370
|
}
|
|
3586
|
-
/**
|
|
3587
|
-
* The default amount of seconds until the Flashlight follow area reaches the cursor.
|
|
3588
|
-
*/
|
|
3589
|
-
ModFlashlight.defaultFollowDelay = 0.12;
|
|
3590
3371
|
|
|
3591
3372
|
/**
|
|
3592
|
-
* Represents the
|
|
3373
|
+
* Represents the TouchDevice mod.
|
|
3593
3374
|
*/
|
|
3594
|
-
class
|
|
3375
|
+
class ModTouchDevice extends Mod {
|
|
3595
3376
|
constructor() {
|
|
3596
|
-
super();
|
|
3597
|
-
this.acronym = "
|
|
3598
|
-
this.name = "
|
|
3599
|
-
this.
|
|
3600
|
-
this.
|
|
3601
|
-
this.incompatibleMods.add(ModHidden);
|
|
3602
|
-
}
|
|
3603
|
-
get isDroidRelevant() {
|
|
3604
|
-
return true;
|
|
3605
|
-
}
|
|
3606
|
-
calculateDroidScoreMultiplier() {
|
|
3607
|
-
return 1.06;
|
|
3377
|
+
super(...arguments);
|
|
3378
|
+
this.acronym = "TD";
|
|
3379
|
+
this.name = "TouchDevice";
|
|
3380
|
+
this.osuRanked = true;
|
|
3381
|
+
this.bitwise = 1 << 2;
|
|
3608
3382
|
}
|
|
3609
3383
|
get isOsuRelevant() {
|
|
3610
3384
|
return true;
|
|
@@ -3615,510 +3389,777 @@ class ModTraceable extends Mod {
|
|
|
3615
3389
|
}
|
|
3616
3390
|
|
|
3617
3391
|
/**
|
|
3618
|
-
*
|
|
3392
|
+
* Utilities for mods.
|
|
3619
3393
|
*/
|
|
3620
|
-
class
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3394
|
+
class ModUtil {
|
|
3395
|
+
/**
|
|
3396
|
+
* Gets a list of mods from a PC modbits.
|
|
3397
|
+
*
|
|
3398
|
+
* @param modbits The modbits.
|
|
3399
|
+
* @returns The list of mods.
|
|
3400
|
+
*/
|
|
3401
|
+
static pcModbitsToMods(modbits) {
|
|
3402
|
+
const map = new ModMap();
|
|
3403
|
+
if (modbits === 0) {
|
|
3404
|
+
return map;
|
|
3405
|
+
}
|
|
3406
|
+
for (const modType of this.allMods.values()) {
|
|
3407
|
+
const mod = new modType();
|
|
3408
|
+
if (mod.isApplicableToOsuStable() && (mod.bitwise & modbits) > 0) {
|
|
3409
|
+
map.set(mod);
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
return map;
|
|
3629
3413
|
}
|
|
3630
|
-
|
|
3631
|
-
|
|
3414
|
+
/**
|
|
3415
|
+
* Serializes a list of `Mod`s.
|
|
3416
|
+
*
|
|
3417
|
+
* @param mods The list of `Mod`s to serialize.
|
|
3418
|
+
* @returns The serialized list of `Mod`s.
|
|
3419
|
+
*/
|
|
3420
|
+
static serializeMods(mods) {
|
|
3421
|
+
const serializedMods = [];
|
|
3422
|
+
for (const mod of mods) {
|
|
3423
|
+
serializedMods.push(mod.serialize());
|
|
3424
|
+
}
|
|
3425
|
+
return serializedMods;
|
|
3632
3426
|
}
|
|
3633
|
-
|
|
3634
|
-
|
|
3427
|
+
/**
|
|
3428
|
+
* Deserializes a list of `SerializedMod`s.
|
|
3429
|
+
*
|
|
3430
|
+
* @param mods The list of `SerializedMod`s to deserialize.
|
|
3431
|
+
* @returns The deserialized list of `Mod`s.
|
|
3432
|
+
*/
|
|
3433
|
+
static deserializeMods(mods) {
|
|
3434
|
+
const map = new ModMap();
|
|
3435
|
+
for (const serializedMod of mods) {
|
|
3436
|
+
const modType = this.allMods.get(serializedMod.acronym);
|
|
3437
|
+
if (!modType) {
|
|
3438
|
+
continue;
|
|
3439
|
+
}
|
|
3440
|
+
const mod = new modType();
|
|
3441
|
+
if (serializedMod.settings) {
|
|
3442
|
+
mod.copySettings(serializedMod);
|
|
3443
|
+
}
|
|
3444
|
+
map.set(mod);
|
|
3445
|
+
}
|
|
3446
|
+
return map;
|
|
3635
3447
|
}
|
|
3636
|
-
|
|
3637
|
-
|
|
3448
|
+
/**
|
|
3449
|
+
* Gets a list of mods from a PC mod string, such as "HDHR".
|
|
3450
|
+
*
|
|
3451
|
+
* @param str The string.
|
|
3452
|
+
* @returns The list of mods.
|
|
3453
|
+
*/
|
|
3454
|
+
static pcStringToMods(str) {
|
|
3455
|
+
const map = new ModMap();
|
|
3456
|
+
str = str.toLowerCase();
|
|
3457
|
+
while (str) {
|
|
3458
|
+
let nchars = 1;
|
|
3459
|
+
for (const acronym of this.allMods.keys()) {
|
|
3460
|
+
if (str.startsWith(acronym.toLowerCase())) {
|
|
3461
|
+
const modType = this.allMods.get(acronym);
|
|
3462
|
+
map.set(modType);
|
|
3463
|
+
nchars = acronym.length;
|
|
3464
|
+
break;
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
str = str.slice(nchars);
|
|
3468
|
+
}
|
|
3469
|
+
return map;
|
|
3638
3470
|
}
|
|
3639
|
-
|
|
3640
|
-
|
|
3471
|
+
/**
|
|
3472
|
+
* Converts a list of mods into its osu!standard string counterpart.
|
|
3473
|
+
*
|
|
3474
|
+
* @param mods The array of mods to convert.
|
|
3475
|
+
* @returns The string representing the mods in osu!standard.
|
|
3476
|
+
*/
|
|
3477
|
+
static modsToOsuString(mods) {
|
|
3478
|
+
let str = "";
|
|
3479
|
+
for (const mod of mods) {
|
|
3480
|
+
if (mod instanceof ModDifficultyAdjust) {
|
|
3481
|
+
continue;
|
|
3482
|
+
}
|
|
3483
|
+
str += mod.acronym;
|
|
3484
|
+
}
|
|
3485
|
+
return str;
|
|
3641
3486
|
}
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3487
|
+
/**
|
|
3488
|
+
* Converts an array of `Mod`s into an ordered string based on {@link allMods}.
|
|
3489
|
+
*
|
|
3490
|
+
* @param mods The array of `Mod`s to convert.
|
|
3491
|
+
* @returns The string representing the `Mod`s in ordered form.
|
|
3492
|
+
*/
|
|
3493
|
+
static modsToOrderedString(mods) {
|
|
3494
|
+
const strs = [];
|
|
3495
|
+
for (const modType of this.allMods.values()) {
|
|
3496
|
+
for (const mod of mods) {
|
|
3497
|
+
if (mod instanceof modType) {
|
|
3498
|
+
strs.push(mod.toString());
|
|
3499
|
+
break;
|
|
3500
|
+
}
|
|
3648
3501
|
}
|
|
3649
|
-
}
|
|
3650
|
-
|
|
3502
|
+
}
|
|
3503
|
+
return strs.join();
|
|
3504
|
+
}
|
|
3505
|
+
/**
|
|
3506
|
+
* Removes speed-changing mods from an array of mods.
|
|
3507
|
+
*
|
|
3508
|
+
* @param mods The array of mods.
|
|
3509
|
+
* @returns A new array with speed changing mods filtered out.
|
|
3510
|
+
*/
|
|
3511
|
+
static removeSpeedChangingMods(mods) {
|
|
3512
|
+
return mods.filter((m) => !(m instanceof ModRateAdjust));
|
|
3513
|
+
}
|
|
3514
|
+
/**
|
|
3515
|
+
* Applies the selected `Mod`s to a `BeatmapDifficulty`.
|
|
3516
|
+
*
|
|
3517
|
+
* @param difficulty The `BeatmapDifficulty` to apply the `Mod`s to.
|
|
3518
|
+
* @param mode The game mode to apply the `Mod`s for.
|
|
3519
|
+
* @param mods The selected `Mod`s. Defaults to No Mod.
|
|
3520
|
+
* @param withRateChange Whether to apply rate changes. Defaults to `false`.
|
|
3521
|
+
*/
|
|
3522
|
+
static applyModsToBeatmapDifficulty(difficulty, mode, mods, withRateChange = false) {
|
|
3523
|
+
if (mods !== undefined) {
|
|
3524
|
+
for (const mod of mods.values()) {
|
|
3525
|
+
if (mod.isApplicableToDifficulty()) {
|
|
3526
|
+
mod.applyToDifficulty(mode, difficulty);
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
let rate = 1;
|
|
3531
|
+
if (mods !== undefined) {
|
|
3532
|
+
for (const mod of mods.values()) {
|
|
3533
|
+
if (mod.isApplicableToDifficultyWithSettings()) {
|
|
3534
|
+
mod.applyToDifficultyWithSettings(mode, difficulty, mods);
|
|
3535
|
+
}
|
|
3536
|
+
if (mod.isApplicableToTrackRate()) {
|
|
3537
|
+
rate = mod.applyToRate(0, rate);
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
if (!withRateChange) {
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
// Apply rate adjustments
|
|
3545
|
+
const preempt = BeatmapDifficulty.difficultyRange(difficulty.ar, HitObject.preemptMax, HitObject.preemptMid, HitObject.preemptMin) / rate;
|
|
3546
|
+
difficulty.ar = BeatmapDifficulty.inverseDifficultyRange(preempt, HitObject.preemptMax, HitObject.preemptMid, HitObject.preemptMin);
|
|
3547
|
+
switch (mode) {
|
|
3548
|
+
case exports.Modes.droid:
|
|
3549
|
+
if (mods === null || mods === void 0 ? void 0 : mods.has(ModPrecise)) {
|
|
3550
|
+
const hitWindow = new PreciseDroidHitWindow(difficulty.od);
|
|
3551
|
+
difficulty.od = PreciseDroidHitWindow.greatWindowToOD(hitWindow.greatWindow / rate);
|
|
3552
|
+
}
|
|
3553
|
+
else {
|
|
3554
|
+
const hitWindow = new DroidHitWindow(difficulty.od);
|
|
3555
|
+
difficulty.od = DroidHitWindow.greatWindowToOD(hitWindow.greatWindow / rate);
|
|
3556
|
+
}
|
|
3557
|
+
break;
|
|
3558
|
+
case exports.Modes.osu: {
|
|
3559
|
+
const hitWindow = new OsuHitWindow(difficulty.od);
|
|
3560
|
+
difficulty.od = OsuHitWindow.greatWindowToOD(hitWindow.greatWindow / rate);
|
|
3561
|
+
break;
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
/**
|
|
3566
|
+
* Calculates the playback rate for the track with the selected `Mod`s at the given time.
|
|
3567
|
+
*
|
|
3568
|
+
* @param mods The list of selected `Mod`s.
|
|
3569
|
+
* @param time The time at which the playback rate is queried, in milliseconds. Defaults to 0.
|
|
3570
|
+
* @returns The rate with `Mod`s.
|
|
3571
|
+
*/
|
|
3572
|
+
static calculateRateWithMods(mods, time = 0) {
|
|
3573
|
+
let rate = 1;
|
|
3574
|
+
for (const mod of mods) {
|
|
3575
|
+
if (mod.isApplicableToTrackRate()) {
|
|
3576
|
+
rate = mod.applyToRate(time, rate);
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
return rate;
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
/**
|
|
3583
|
+
* All `Mod`s that exists, mapped by their acronym.
|
|
3584
|
+
*/
|
|
3585
|
+
ModUtil.allMods = (() => {
|
|
3586
|
+
const mods = [
|
|
3587
|
+
// Janky order to keep the order on what players are used to
|
|
3588
|
+
ModAuto,
|
|
3589
|
+
ModRelax,
|
|
3590
|
+
ModAutopilot,
|
|
3591
|
+
ModEasy,
|
|
3592
|
+
ModNoFail,
|
|
3593
|
+
ModHidden,
|
|
3594
|
+
ModTraceable,
|
|
3595
|
+
ModDoubleTime,
|
|
3596
|
+
ModNightCore,
|
|
3597
|
+
ModHalfTime,
|
|
3598
|
+
ModCustomSpeed,
|
|
3599
|
+
ModHardRock,
|
|
3600
|
+
ModDifficultyAdjust,
|
|
3601
|
+
ModFlashlight,
|
|
3602
|
+
ModSuddenDeath,
|
|
3603
|
+
ModPerfect,
|
|
3604
|
+
ModPrecise,
|
|
3605
|
+
ModReallyEasy,
|
|
3606
|
+
ModScoreV2,
|
|
3607
|
+
ModSmallCircle,
|
|
3608
|
+
ModSpunOut,
|
|
3609
|
+
ModTouchDevice,
|
|
3610
|
+
];
|
|
3611
|
+
const map = new Map();
|
|
3612
|
+
for (const mod of mods) {
|
|
3613
|
+
map.set(new mod().acronym, mod);
|
|
3651
3614
|
}
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
ModHidden.fadeOutDurationMultiplier = 0.3;
|
|
3615
|
+
return map;
|
|
3616
|
+
})();
|
|
3655
3617
|
|
|
3656
3618
|
/**
|
|
3657
|
-
*
|
|
3619
|
+
* A map that stores `Mod`s depending on their type.
|
|
3620
|
+
*
|
|
3621
|
+
* This also has additional utilities to eliminate unnecessary `Mod`s.
|
|
3658
3622
|
*/
|
|
3659
|
-
class
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
this.
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3623
|
+
class ModMap extends Map {
|
|
3624
|
+
/**
|
|
3625
|
+
* Whether this map is empty.
|
|
3626
|
+
*/
|
|
3627
|
+
get isEmpty() {
|
|
3628
|
+
return this.size === 0;
|
|
3629
|
+
}
|
|
3630
|
+
constructor(iterable) {
|
|
3631
|
+
if (Array.isArray(iterable)) {
|
|
3632
|
+
for (const [key, value] of iterable) {
|
|
3633
|
+
// Ensure the mod type corresponds to the mod instance.
|
|
3634
|
+
if (key !== value.constructor) {
|
|
3635
|
+
throw new TypeError(`Key ${key.name} does not match value ${value.constructor.name}`);
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
super(iterable);
|
|
3640
|
+
}
|
|
3641
|
+
has(keyOrValue) {
|
|
3642
|
+
const key = keyOrValue instanceof Mod
|
|
3643
|
+
? keyOrValue.constructor
|
|
3644
|
+
: keyOrValue;
|
|
3645
|
+
return super.has(key);
|
|
3646
|
+
}
|
|
3647
|
+
get(key) {
|
|
3648
|
+
return super.get(key);
|
|
3649
|
+
}
|
|
3650
|
+
set(keyOrValue) {
|
|
3651
|
+
const key = (keyOrValue instanceof Mod ? keyOrValue.constructor : keyOrValue);
|
|
3652
|
+
const value = keyOrValue instanceof Mod ? keyOrValue : new key();
|
|
3653
|
+
// Ensure the mod type corresponds to the mod instance.
|
|
3654
|
+
if (key !== value.constructor) {
|
|
3655
|
+
throw new TypeError(`Key ${key.name} does not match value ${value.constructor.name}`);
|
|
3656
|
+
}
|
|
3657
|
+
const existing = this.get(key);
|
|
3658
|
+
// If all difficulty statistics are set, all other difficulty adjusting mods are irrelevant, so we remove them.
|
|
3659
|
+
// This prevents potential abuse cases where score multipliers from non-affecting mods stack (i.e., forcing
|
|
3660
|
+
// all difficulty statistics while using the Hard Rock mod).
|
|
3661
|
+
const removeDifficultyAdjustmentMods = value instanceof ModDifficultyAdjust &&
|
|
3662
|
+
value.cs !== undefined &&
|
|
3663
|
+
value.ar !== undefined &&
|
|
3664
|
+
value.od !== undefined &&
|
|
3665
|
+
value.hp !== undefined;
|
|
3666
|
+
if (removeDifficultyAdjustmentMods) {
|
|
3667
|
+
this.delete(ModEasy);
|
|
3668
|
+
this.delete(ModHardRock);
|
|
3669
|
+
this.delete(ModReallyEasy);
|
|
3670
|
+
this.delete(ModSmallCircle);
|
|
3671
|
+
}
|
|
3672
|
+
// Check if there are any mods that are incompatible with the new mod.
|
|
3673
|
+
// If so, remove them.
|
|
3674
|
+
for (const incompatibleMod of value.incompatibleMods) {
|
|
3675
|
+
for (const mod of this.values()) {
|
|
3676
|
+
if (mod instanceof incompatibleMod) {
|
|
3677
|
+
this.delete(mod.constructor);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
super.set(key, value);
|
|
3682
|
+
return existing !== null && existing !== void 0 ? existing : null;
|
|
3677
3683
|
}
|
|
3678
|
-
|
|
3679
|
-
|
|
3684
|
+
/**
|
|
3685
|
+
* Serializes all `Mod`s that are in this map.
|
|
3686
|
+
*/
|
|
3687
|
+
serializeMods() {
|
|
3688
|
+
return ModUtil.serializeMods(this.values());
|
|
3680
3689
|
}
|
|
3681
3690
|
}
|
|
3682
3691
|
|
|
3683
3692
|
/**
|
|
3684
|
-
* Represents
|
|
3693
|
+
* Represents a circle in a beatmap.
|
|
3694
|
+
*
|
|
3695
|
+
* All we need from circles is their position. All positions
|
|
3696
|
+
* stored in the objects are in playfield coordinates (512*384
|
|
3697
|
+
* rectangle).
|
|
3685
3698
|
*/
|
|
3686
|
-
class
|
|
3687
|
-
constructor() {
|
|
3688
|
-
super();
|
|
3689
|
-
this.acronym = "PF";
|
|
3690
|
-
this.name = "Perfect";
|
|
3691
|
-
this.droidRanked = true;
|
|
3692
|
-
this.osuRanked = true;
|
|
3693
|
-
this.bitwise = 1 << 14;
|
|
3694
|
-
this.incompatibleMods.add(ModNoFail).add(ModSuddenDeath);
|
|
3695
|
-
}
|
|
3696
|
-
get isDroidRelevant() {
|
|
3697
|
-
return true;
|
|
3698
|
-
}
|
|
3699
|
-
calculateDroidScoreMultiplier() {
|
|
3700
|
-
return 1;
|
|
3701
|
-
}
|
|
3702
|
-
get isOsuRelevant() {
|
|
3703
|
-
return true;
|
|
3699
|
+
class Circle extends HitObject {
|
|
3700
|
+
constructor(values) {
|
|
3701
|
+
super(values);
|
|
3704
3702
|
}
|
|
3705
|
-
|
|
3706
|
-
return
|
|
3703
|
+
toString() {
|
|
3704
|
+
return `Position: [${this._position.x}, ${this._position.y}]`;
|
|
3707
3705
|
}
|
|
3708
3706
|
}
|
|
3709
3707
|
|
|
3710
3708
|
/**
|
|
3711
|
-
*
|
|
3709
|
+
* Contains information about hit objects of a beatmap.
|
|
3712
3710
|
*/
|
|
3713
|
-
class
|
|
3711
|
+
class BeatmapHitObjects {
|
|
3714
3712
|
constructor() {
|
|
3715
|
-
|
|
3716
|
-
this.
|
|
3717
|
-
this.
|
|
3718
|
-
this.
|
|
3719
|
-
this.osuRanked = true;
|
|
3720
|
-
this.bitwise = 1 << 0;
|
|
3721
|
-
this.incompatibleMods.add(ModPerfect).add(ModSuddenDeath);
|
|
3713
|
+
this._objects = [];
|
|
3714
|
+
this._circles = 0;
|
|
3715
|
+
this._sliders = 0;
|
|
3716
|
+
this._spinners = 0;
|
|
3722
3717
|
}
|
|
3723
|
-
|
|
3724
|
-
|
|
3718
|
+
/**
|
|
3719
|
+
* The objects of the beatmap.
|
|
3720
|
+
*/
|
|
3721
|
+
get objects() {
|
|
3722
|
+
return this._objects;
|
|
3725
3723
|
}
|
|
3726
|
-
|
|
3727
|
-
|
|
3724
|
+
/**
|
|
3725
|
+
* The amount of circles in the beatmap.
|
|
3726
|
+
*/
|
|
3727
|
+
get circles() {
|
|
3728
|
+
return this._circles;
|
|
3728
3729
|
}
|
|
3729
|
-
|
|
3730
|
-
|
|
3730
|
+
/**
|
|
3731
|
+
* The amount of sliders in the beatmap.
|
|
3732
|
+
*/
|
|
3733
|
+
get sliders() {
|
|
3734
|
+
return this._sliders;
|
|
3731
3735
|
}
|
|
3732
|
-
|
|
3733
|
-
|
|
3736
|
+
/**
|
|
3737
|
+
* The amount of spinners in the beatmap.
|
|
3738
|
+
*/
|
|
3739
|
+
get spinners() {
|
|
3740
|
+
return this._spinners;
|
|
3734
3741
|
}
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
super(...arguments);
|
|
3743
|
-
this.acronym = "RE";
|
|
3744
|
-
this.name = "ReallyEasy";
|
|
3745
|
-
this.droidRanked = false;
|
|
3742
|
+
/**
|
|
3743
|
+
* The amount of slider ticks in the beatmap.
|
|
3744
|
+
*
|
|
3745
|
+
* This iterates through all objects and should be stored locally or used sparingly.
|
|
3746
|
+
*/
|
|
3747
|
+
get sliderTicks() {
|
|
3748
|
+
return this.objects.reduce((acc, cur) => (cur instanceof Slider ? acc + cur.ticks : acc), 0);
|
|
3746
3749
|
}
|
|
3747
|
-
|
|
3748
|
-
|
|
3750
|
+
/**
|
|
3751
|
+
* The amount of sliderends in the beatmap.
|
|
3752
|
+
*/
|
|
3753
|
+
get sliderEnds() {
|
|
3754
|
+
return this.sliders;
|
|
3749
3755
|
}
|
|
3750
|
-
|
|
3751
|
-
|
|
3756
|
+
/**
|
|
3757
|
+
* The amount of slider repeat points in the beatmap.
|
|
3758
|
+
*
|
|
3759
|
+
* This iterates through all objects and should be stored locally or used sparingly.
|
|
3760
|
+
*/
|
|
3761
|
+
get sliderRepeatPoints() {
|
|
3762
|
+
return this.objects.reduce((acc, cur) => (cur instanceof Slider ? acc + cur.repeatCount : acc), 0);
|
|
3752
3763
|
}
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3764
|
+
/**
|
|
3765
|
+
* Adds hitobjects.
|
|
3766
|
+
*
|
|
3767
|
+
* The sorting order of hitobjects will be maintained.
|
|
3768
|
+
*
|
|
3769
|
+
* @param objects The hitobjects to add.
|
|
3770
|
+
*/
|
|
3771
|
+
add(...objects) {
|
|
3772
|
+
for (const object of objects) {
|
|
3773
|
+
// Objects may be out of order *only* if a user has manually edited an .osu file.
|
|
3774
|
+
// Unfortunately there are "ranked" maps in this state (example: https://osu.ppy.sh/s/594828).
|
|
3775
|
+
// Finding index is used to guarantee that the parsing order of hitobjects with equal start times is maintained (stably-sorted).
|
|
3776
|
+
this._objects.splice(this.findInsertionIndex(object.startTime), 0, object);
|
|
3777
|
+
if (object instanceof Circle) {
|
|
3778
|
+
++this._circles;
|
|
3779
|
+
}
|
|
3780
|
+
else if (object instanceof Slider) {
|
|
3781
|
+
++this._sliders;
|
|
3782
|
+
}
|
|
3783
|
+
else {
|
|
3784
|
+
++this._spinners;
|
|
3763
3785
|
}
|
|
3764
|
-
const customSpeed = mods.find((m) => m instanceof ModCustomSpeed);
|
|
3765
|
-
difficulty.ar -= 0.5;
|
|
3766
|
-
difficulty.ar -= ((_a = customSpeed === null || customSpeed === void 0 ? void 0 : customSpeed.trackRateMultiplier) !== null && _a !== void 0 ? _a : 1) - 1;
|
|
3767
3786
|
}
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3787
|
+
}
|
|
3788
|
+
/**
|
|
3789
|
+
* Removes a hitobject at an index.
|
|
3790
|
+
*
|
|
3791
|
+
* @param index The index of the hitobject to remove.
|
|
3792
|
+
* @returns The hitobject that was removed, `null` if no hitobject was removed.
|
|
3793
|
+
*/
|
|
3794
|
+
removeAt(index) {
|
|
3795
|
+
var _a;
|
|
3796
|
+
const object = (_a = this._objects.splice(index, 1)[0]) !== null && _a !== void 0 ? _a : null;
|
|
3797
|
+
if (object instanceof Circle) {
|
|
3798
|
+
--this._circles;
|
|
3771
3799
|
}
|
|
3772
|
-
if (
|
|
3773
|
-
|
|
3800
|
+
else if (object instanceof Slider) {
|
|
3801
|
+
--this._sliders;
|
|
3774
3802
|
}
|
|
3775
|
-
if (
|
|
3776
|
-
|
|
3803
|
+
else if (object instanceof Spinner) {
|
|
3804
|
+
--this._spinners;
|
|
3777
3805
|
}
|
|
3806
|
+
return object;
|
|
3778
3807
|
}
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
constructor() {
|
|
3788
|
-
super(...arguments);
|
|
3789
|
-
this.acronym = "SC";
|
|
3790
|
-
this.name = "SmallCircle";
|
|
3791
|
-
this.droidRanked = false;
|
|
3792
|
-
}
|
|
3793
|
-
get isDroidRelevant() {
|
|
3794
|
-
return true;
|
|
3795
|
-
}
|
|
3796
|
-
calculateDroidScoreMultiplier() {
|
|
3797
|
-
return 1.06;
|
|
3798
|
-
}
|
|
3799
|
-
migrateDroidMod(difficulty) {
|
|
3800
|
-
return new ModDifficultyAdjust({ cs: difficulty.cs + 4 });
|
|
3808
|
+
/**
|
|
3809
|
+
* Clears all hitobjects.
|
|
3810
|
+
*/
|
|
3811
|
+
clear() {
|
|
3812
|
+
this._objects.length = 0;
|
|
3813
|
+
this._circles = 0;
|
|
3814
|
+
this._sliders = 0;
|
|
3815
|
+
this._spinners = 0;
|
|
3801
3816
|
}
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3817
|
+
/**
|
|
3818
|
+
* Finds the insertion index of a hitobject in a given time.
|
|
3819
|
+
*
|
|
3820
|
+
* @param startTime The start time of the hitobject.
|
|
3821
|
+
*/
|
|
3822
|
+
findInsertionIndex(startTime) {
|
|
3823
|
+
if (this._objects.length === 0 ||
|
|
3824
|
+
startTime < this._objects[0].startTime) {
|
|
3825
|
+
return 0;
|
|
3826
|
+
}
|
|
3827
|
+
if (startTime >= this._objects.at(-1).startTime) {
|
|
3828
|
+
return this._objects.length;
|
|
3829
|
+
}
|
|
3830
|
+
let l = 0;
|
|
3831
|
+
let r = this._objects.length - 2;
|
|
3832
|
+
while (l <= r) {
|
|
3833
|
+
const pivot = l + ((r - l) >> 1);
|
|
3834
|
+
if (this._objects[pivot].startTime < startTime) {
|
|
3835
|
+
l = pivot + 1;
|
|
3836
|
+
}
|
|
3837
|
+
else if (this._objects[pivot].startTime > startTime) {
|
|
3838
|
+
r = pivot - 1;
|
|
3839
|
+
}
|
|
3840
|
+
else {
|
|
3841
|
+
return pivot;
|
|
3812
3842
|
}
|
|
3813
|
-
case exports.Modes.osu:
|
|
3814
|
-
difficulty.cs += 4;
|
|
3815
3843
|
}
|
|
3844
|
+
return l;
|
|
3816
3845
|
}
|
|
3817
3846
|
}
|
|
3818
3847
|
|
|
3819
3848
|
/**
|
|
3820
|
-
*
|
|
3849
|
+
* Converts a beatmap for another mode.
|
|
3821
3850
|
*/
|
|
3822
|
-
class
|
|
3823
|
-
constructor() {
|
|
3824
|
-
|
|
3825
|
-
this.acronym = "SO";
|
|
3826
|
-
this.name = "SpunOut";
|
|
3827
|
-
this.osuRanked = true;
|
|
3828
|
-
this.bitwise = 1 << 12;
|
|
3829
|
-
}
|
|
3830
|
-
get isOsuRelevant() {
|
|
3831
|
-
return true;
|
|
3832
|
-
}
|
|
3833
|
-
get osuScoreMultiplier() {
|
|
3834
|
-
return 0.9;
|
|
3851
|
+
class BeatmapConverter {
|
|
3852
|
+
constructor(beatmap) {
|
|
3853
|
+
this.beatmap = beatmap;
|
|
3835
3854
|
}
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
this.bitwise = 1 << 2;
|
|
3855
|
+
/**
|
|
3856
|
+
* Converts the beatmap.
|
|
3857
|
+
*
|
|
3858
|
+
* @returns The converted beatmap.
|
|
3859
|
+
*/
|
|
3860
|
+
convert() {
|
|
3861
|
+
const converted = new Beatmap(this.beatmap);
|
|
3862
|
+
// Shallow clone isn't enough to ensure we don't mutate some beatmap properties unexpectedly.
|
|
3863
|
+
converted.difficulty = new BeatmapDifficulty(this.beatmap.difficulty);
|
|
3864
|
+
converted.hitObjects = this.convertHitObjects();
|
|
3865
|
+
return converted;
|
|
3848
3866
|
}
|
|
3849
|
-
|
|
3850
|
-
|
|
3867
|
+
convertHitObjects() {
|
|
3868
|
+
const hitObjects = new BeatmapHitObjects();
|
|
3869
|
+
this.beatmap.hitObjects.objects.forEach((hitObject) => {
|
|
3870
|
+
hitObjects.add(this.convertHitObject(hitObject));
|
|
3871
|
+
});
|
|
3872
|
+
return hitObjects;
|
|
3851
3873
|
}
|
|
3852
|
-
|
|
3853
|
-
|
|
3874
|
+
convertHitObject(hitObject) {
|
|
3875
|
+
let object;
|
|
3876
|
+
if (hitObject instanceof Circle) {
|
|
3877
|
+
object = new Circle({
|
|
3878
|
+
startTime: hitObject.startTime,
|
|
3879
|
+
position: hitObject.position,
|
|
3880
|
+
newCombo: hitObject.isNewCombo,
|
|
3881
|
+
type: hitObject.type,
|
|
3882
|
+
comboOffset: hitObject.comboOffset,
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
else if (hitObject instanceof Slider) {
|
|
3886
|
+
object = new Slider({
|
|
3887
|
+
startTime: hitObject.startTime,
|
|
3888
|
+
position: hitObject.position,
|
|
3889
|
+
newCombo: hitObject.isNewCombo,
|
|
3890
|
+
type: hitObject.type,
|
|
3891
|
+
path: hitObject.path,
|
|
3892
|
+
repeatCount: hitObject.repeatCount,
|
|
3893
|
+
nodeSamples: hitObject.nodeSamples,
|
|
3894
|
+
comboOffset: hitObject.comboOffset,
|
|
3895
|
+
tickDistanceMultiplier:
|
|
3896
|
+
// Prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
|
3897
|
+
// This results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
|
3898
|
+
this.beatmap.formatVersion < 8
|
|
3899
|
+
? 1 /
|
|
3900
|
+
this.beatmap.controlPoints.difficulty.controlPointAt(hitObject.startTime).speedMultiplier
|
|
3901
|
+
: 1,
|
|
3902
|
+
});
|
|
3903
|
+
}
|
|
3904
|
+
else {
|
|
3905
|
+
object = new Spinner({
|
|
3906
|
+
startTime: hitObject.startTime,
|
|
3907
|
+
endTime: hitObject.endTime,
|
|
3908
|
+
type: hitObject.type,
|
|
3909
|
+
});
|
|
3910
|
+
}
|
|
3911
|
+
object.samples = hitObject.samples;
|
|
3912
|
+
object.auxiliarySamples = hitObject.auxiliarySamples;
|
|
3913
|
+
return object;
|
|
3854
3914
|
}
|
|
3855
3915
|
}
|
|
3856
3916
|
|
|
3857
3917
|
/**
|
|
3858
|
-
*
|
|
3918
|
+
* Provides functionality to alter a beatmap after it has been converted.
|
|
3859
3919
|
*/
|
|
3860
|
-
class
|
|
3920
|
+
class BeatmapProcessor {
|
|
3921
|
+
constructor(beatmap) {
|
|
3922
|
+
this.beatmap = beatmap;
|
|
3923
|
+
}
|
|
3861
3924
|
/**
|
|
3862
|
-
*
|
|
3925
|
+
* Processes the converted beatmap prior to `HitObject.applyDefaults` being invoked.
|
|
3863
3926
|
*
|
|
3864
|
-
*
|
|
3865
|
-
*
|
|
3927
|
+
* Nested hitobjects generated during `HitObject.applyDefaults` will not be present by this point,
|
|
3928
|
+
* and no mods will have been applied to the hitobjects.
|
|
3929
|
+
*
|
|
3930
|
+
* This can only be used to add alterations to hitobjects generated directly through the conversion process.
|
|
3866
3931
|
*/
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3932
|
+
preProcess() {
|
|
3933
|
+
let last = null;
|
|
3934
|
+
for (const object of this.beatmap.hitObjects.objects) {
|
|
3935
|
+
object.updateComboInformation(last);
|
|
3936
|
+
last = object;
|
|
3870
3937
|
}
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
if (mod.isApplicableToOsuStable() && (mod.bitwise & modbits) > 0) {
|
|
3875
|
-
mods.push(mod);
|
|
3876
|
-
}
|
|
3938
|
+
// Mark the last object in the beatmap as last in combo.
|
|
3939
|
+
if (last) {
|
|
3940
|
+
last.isLastInCombo = true;
|
|
3877
3941
|
}
|
|
3878
|
-
return this.processParsingOptions(mods, options);
|
|
3879
3942
|
}
|
|
3880
3943
|
/**
|
|
3881
|
-
*
|
|
3944
|
+
* Processes the converted beatmap after `HitObject.applyDefaults` has been invoked.
|
|
3882
3945
|
*
|
|
3883
|
-
*
|
|
3884
|
-
*
|
|
3946
|
+
* Nested hitobjects generated during `HitObject.applyDefaults` wil be present by this point,
|
|
3947
|
+
* and mods will have been applied to all hitobjects.
|
|
3948
|
+
*
|
|
3949
|
+
* This should be used to add alterations to hitobjects while they are in their most playable state.
|
|
3950
|
+
*
|
|
3951
|
+
* @param mode The mode to add alterations for.
|
|
3885
3952
|
*/
|
|
3886
|
-
|
|
3887
|
-
const
|
|
3888
|
-
|
|
3889
|
-
|
|
3953
|
+
postProcess(mode) {
|
|
3954
|
+
const objects = this.beatmap.hitObjects.objects;
|
|
3955
|
+
if (objects.length === 0) {
|
|
3956
|
+
return;
|
|
3957
|
+
}
|
|
3958
|
+
// Reset stacking
|
|
3959
|
+
objects.forEach((h) => {
|
|
3960
|
+
h.stackHeight = 0;
|
|
3961
|
+
});
|
|
3962
|
+
switch (mode) {
|
|
3963
|
+
case exports.Modes.droid:
|
|
3964
|
+
this.applyDroidStacking();
|
|
3965
|
+
break;
|
|
3966
|
+
case exports.Modes.osu:
|
|
3967
|
+
if (this.beatmap.formatVersion >= 6) {
|
|
3968
|
+
this.applyStandardStacking();
|
|
3969
|
+
}
|
|
3970
|
+
else {
|
|
3971
|
+
this.applyStandardOldStacking();
|
|
3972
|
+
}
|
|
3973
|
+
break;
|
|
3890
3974
|
}
|
|
3891
|
-
return serializedMods;
|
|
3892
3975
|
}
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
mod.copySettings(serializedMod);
|
|
3976
|
+
applyDroidStacking() {
|
|
3977
|
+
const objects = this.beatmap.hitObjects.objects;
|
|
3978
|
+
if (objects.length === 0) {
|
|
3979
|
+
return;
|
|
3980
|
+
}
|
|
3981
|
+
const convertedScale = CircleSizeCalculator.standardScaleToDroidScale(objects[0].scale);
|
|
3982
|
+
for (let i = 0; i < objects.length - 1; ++i) {
|
|
3983
|
+
const current = objects[i];
|
|
3984
|
+
const next = objects[i + 1];
|
|
3985
|
+
if (current instanceof Circle &&
|
|
3986
|
+
next.startTime - current.startTime <
|
|
3987
|
+
2000 * this.beatmap.general.stackLeniency &&
|
|
3988
|
+
next.position.getDistance(current.position) <
|
|
3989
|
+
Math.sqrt(convertedScale)) {
|
|
3990
|
+
next.stackHeight = current.stackHeight + 1;
|
|
3909
3991
|
}
|
|
3910
|
-
deserializedMods.push(mod);
|
|
3911
3992
|
}
|
|
3912
|
-
return deserializedMods;
|
|
3913
3993
|
}
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
const
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3994
|
+
applyStandardStacking() {
|
|
3995
|
+
const objects = this.beatmap.hitObjects.objects;
|
|
3996
|
+
const startIndex = 0;
|
|
3997
|
+
const endIndex = objects.length - 1;
|
|
3998
|
+
let extendedEndIndex = endIndex;
|
|
3999
|
+
if (endIndex < objects.length - 1) {
|
|
4000
|
+
for (let i = endIndex; i >= startIndex; --i) {
|
|
4001
|
+
let stackBaseIndex = i;
|
|
4002
|
+
for (let n = stackBaseIndex + 1; n < objects.length; ++n) {
|
|
4003
|
+
const stackBaseObject = objects[stackBaseIndex];
|
|
4004
|
+
if (stackBaseObject instanceof Spinner) {
|
|
4005
|
+
break;
|
|
4006
|
+
}
|
|
4007
|
+
const objectN = objects[n];
|
|
4008
|
+
if (objectN instanceof Spinner) {
|
|
4009
|
+
break;
|
|
4010
|
+
}
|
|
4011
|
+
const stackThreshold = objectN.timePreempt *
|
|
4012
|
+
this.beatmap.general.stackLeniency;
|
|
4013
|
+
if (objectN.startTime - stackBaseObject.endTime >
|
|
4014
|
+
stackThreshold) {
|
|
4015
|
+
// We are no longer within stacking range of the next object.
|
|
4016
|
+
break;
|
|
4017
|
+
}
|
|
4018
|
+
const endPositionDistanceCheck = stackBaseObject instanceof Slider
|
|
4019
|
+
? stackBaseObject.endPosition.getDistance(objectN.position) < BeatmapProcessor.stackDistance
|
|
4020
|
+
: false;
|
|
4021
|
+
if (stackBaseObject.position.getDistance(objectN.position) <
|
|
4022
|
+
BeatmapProcessor.stackDistance ||
|
|
4023
|
+
endPositionDistanceCheck) {
|
|
4024
|
+
stackBaseIndex = n;
|
|
4025
|
+
// Hit objects after the specified update range haven't been reset yet
|
|
4026
|
+
objectN.stackHeight = 0;
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
if (stackBaseIndex > extendedEndIndex) {
|
|
4030
|
+
extendedEndIndex = stackBaseIndex;
|
|
4031
|
+
if (extendedEndIndex === objects.length - 1) {
|
|
4032
|
+
break;
|
|
4033
|
+
}
|
|
3931
4034
|
}
|
|
3932
4035
|
}
|
|
3933
|
-
str = str.slice(nchars);
|
|
3934
4036
|
}
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
4037
|
+
// Reverse pass for stack calculation.
|
|
4038
|
+
let extendedStartIndex = startIndex;
|
|
4039
|
+
for (let i = extendedEndIndex; i > startIndex; --i) {
|
|
4040
|
+
let n = i;
|
|
4041
|
+
// We should check every note which has not yet got a stack.
|
|
4042
|
+
// Consider the case we have two inter-wound stacks and this will make sense.
|
|
4043
|
+
//
|
|
4044
|
+
// o <-1 o <-2
|
|
4045
|
+
// o <-3 o <-4
|
|
4046
|
+
//
|
|
4047
|
+
// We first process starting from 4 and handle 2,
|
|
4048
|
+
// then we come backwards on the i loop iteration until we reach 3 and handle 1.
|
|
4049
|
+
// 2 and 1 will be ignored in the i loop because they already have a stack value.
|
|
4050
|
+
let objectI = objects[i];
|
|
4051
|
+
if (objectI.stackHeight !== 0 || objectI instanceof Spinner) {
|
|
4052
|
+
continue;
|
|
3947
4053
|
}
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
4054
|
+
const stackThreshold = objectI.timePreempt * this.beatmap.general.stackLeniency;
|
|
4055
|
+
// If this object is a hit circle, then we enter this "special" case.
|
|
4056
|
+
// It either ends with a stack of hit circles only, or a stack of hit circles that are underneath a slider.
|
|
4057
|
+
// Any other case is handled by the "instanceof Slider" code below this.
|
|
4058
|
+
if (objectI instanceof Circle) {
|
|
4059
|
+
while (--n >= 0) {
|
|
4060
|
+
const objectN = objects[n];
|
|
4061
|
+
if (objectN instanceof Spinner) {
|
|
4062
|
+
continue;
|
|
4063
|
+
}
|
|
4064
|
+
if (objectI.startTime - objectN.endTime > stackThreshold) {
|
|
4065
|
+
// We are no longer within stacking range of the previous object.
|
|
4066
|
+
break;
|
|
4067
|
+
}
|
|
4068
|
+
// Hit objects before the specified update range haven't been reset yet
|
|
4069
|
+
if (n < extendedStartIndex) {
|
|
4070
|
+
objectN.stackHeight = 0;
|
|
4071
|
+
extendedStartIndex = n;
|
|
4072
|
+
}
|
|
4073
|
+
// This is a special case where hit circles are moved DOWN and RIGHT (negative stacking) if they are under the *last* slider in a stacked pattern.
|
|
4074
|
+
// o==o <- slider is at original location
|
|
4075
|
+
// o <- hitCircle has stack of -1
|
|
4076
|
+
// o <- hitCircle has stack of -2
|
|
4077
|
+
if (objectN instanceof Slider &&
|
|
4078
|
+
objectN.endPosition.getDistance(objectI.position) <
|
|
4079
|
+
BeatmapProcessor.stackDistance) {
|
|
4080
|
+
const offset = objectI.stackHeight - objectN.stackHeight + 1;
|
|
4081
|
+
for (let j = n + 1; j <= i; ++j) {
|
|
4082
|
+
// For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above).
|
|
4083
|
+
const objectJ = objects[j];
|
|
4084
|
+
if (objectN.endPosition.getDistance(objectJ.position) < BeatmapProcessor.stackDistance) {
|
|
4085
|
+
objectJ.stackHeight -= offset;
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
// We have hit a slider. We should restart calculation using this as the new base.
|
|
4089
|
+
// Breaking here will mean that the slider still has a stack count of 0, so will be handled in the i-outer-loop.
|
|
4090
|
+
break;
|
|
4091
|
+
}
|
|
4092
|
+
if (objectN.position.getDistance(objectI.position) <
|
|
4093
|
+
BeatmapProcessor.stackDistance) {
|
|
4094
|
+
// Keep processing as if there are no sliders. If we come across a slider, this gets cancelled out.
|
|
4095
|
+
// NOTE: Sliders with start positions stacking are a special case that is also handled here.
|
|
4096
|
+
objectN.stackHeight = objectI.stackHeight + 1;
|
|
4097
|
+
objectI = objectN;
|
|
4098
|
+
}
|
|
3964
4099
|
}
|
|
3965
4100
|
}
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
static checkIncompatibleMods(mods) {
|
|
3985
|
-
for (let i = 0; i < mods.length; ++i) {
|
|
3986
|
-
const mod = mods[i];
|
|
3987
|
-
for (const incompatibleMod of mod.incompatibleMods) {
|
|
3988
|
-
if (mods.some((m) => m !== mod && m instanceof incompatibleMod)) {
|
|
3989
|
-
mods = mods.filter((m) =>
|
|
3990
|
-
// Keep the mod itself.
|
|
3991
|
-
m === mod || !(m instanceof incompatibleMod));
|
|
4101
|
+
else if (objectI instanceof Slider) {
|
|
4102
|
+
// We have hit the first slider in a possible stack.
|
|
4103
|
+
// From this point on, we ALWAYS stack positive regardless.
|
|
4104
|
+
while (--n >= startIndex) {
|
|
4105
|
+
const objectN = objects[n];
|
|
4106
|
+
if (objectN instanceof Spinner) {
|
|
4107
|
+
continue;
|
|
4108
|
+
}
|
|
4109
|
+
if (objectI.startTime - objectN.startTime >
|
|
4110
|
+
stackThreshold) {
|
|
4111
|
+
// We are no longer within stacking range of the previous object.
|
|
4112
|
+
break;
|
|
4113
|
+
}
|
|
4114
|
+
if (objectN.endPosition.getDistance(objectI.position) <
|
|
4115
|
+
BeatmapProcessor.stackDistance) {
|
|
4116
|
+
objectN.stackHeight = objectI.stackHeight + 1;
|
|
4117
|
+
objectI = objectN;
|
|
4118
|
+
}
|
|
3992
4119
|
}
|
|
3993
4120
|
}
|
|
3994
4121
|
}
|
|
3995
|
-
return mods;
|
|
3996
|
-
}
|
|
3997
|
-
/**
|
|
3998
|
-
* Removes speed-changing mods from an array of mods.
|
|
3999
|
-
*
|
|
4000
|
-
* @param mods The array of mods.
|
|
4001
|
-
* @returns A new array with speed changing mods filtered out.
|
|
4002
|
-
*/
|
|
4003
|
-
static removeSpeedChangingMods(mods) {
|
|
4004
|
-
return mods.filter((m) => !(m instanceof ModRateAdjust));
|
|
4005
4122
|
}
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
* @param oldStatistics Whether to enforce old statistics. Some `Mod`s behave differently with this flag.
|
|
4014
|
-
*/
|
|
4015
|
-
static applyModsToBeatmapDifficulty(difficulty, mode, mods, withRateChange = false) {
|
|
4016
|
-
for (const mod of mods) {
|
|
4017
|
-
if (mod.isApplicableToDifficulty()) {
|
|
4018
|
-
mod.applyToDifficulty(mode, difficulty);
|
|
4019
|
-
}
|
|
4020
|
-
}
|
|
4021
|
-
let rate = 1;
|
|
4022
|
-
for (const mod of mods) {
|
|
4023
|
-
if (mod.isApplicableToDifficultyWithSettings()) {
|
|
4024
|
-
mod.applyToDifficultyWithSettings(mode, difficulty, mods);
|
|
4025
|
-
}
|
|
4026
|
-
if (mod.isApplicableToTrackRate()) {
|
|
4027
|
-
rate = mod.applyToRate(0, rate);
|
|
4123
|
+
applyStandardOldStacking() {
|
|
4124
|
+
const objects = this.beatmap.hitObjects.objects;
|
|
4125
|
+
for (let i = 0; i < objects.length; ++i) {
|
|
4126
|
+
const currentObject = objects[i];
|
|
4127
|
+
if (currentObject.stackHeight !== 0 &&
|
|
4128
|
+
!(currentObject instanceof Slider)) {
|
|
4129
|
+
continue;
|
|
4028
4130
|
}
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
difficulty.ar = BeatmapDifficulty.inverseDifficultyRange(preempt, HitObject.preemptMax, HitObject.preemptMid, HitObject.preemptMin);
|
|
4036
|
-
switch (mode) {
|
|
4037
|
-
case exports.Modes.droid:
|
|
4038
|
-
if (mods.some((m) => m instanceof ModPrecise)) {
|
|
4039
|
-
const hitWindow = new PreciseDroidHitWindow(difficulty.od);
|
|
4040
|
-
difficulty.od = PreciseDroidHitWindow.greatWindowToOD(hitWindow.greatWindow / rate);
|
|
4131
|
+
let startTime = currentObject.endTime;
|
|
4132
|
+
let sliderStack = 0;
|
|
4133
|
+
const stackThreshold = currentObject.timePreempt * this.beatmap.general.stackLeniency;
|
|
4134
|
+
for (let j = i + 1; j < objects.length; ++j) {
|
|
4135
|
+
if (objects[j].startTime - stackThreshold > startTime) {
|
|
4136
|
+
break;
|
|
4041
4137
|
}
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4138
|
+
// Note the use of `startTime` in the code below doesn't match osu!stable's use of `endTime`.
|
|
4139
|
+
// This is because in osu!stable's implementation, `UpdateCalculations` is not called on the inner-loop hitobject (j)
|
|
4140
|
+
// and therefore it does not have a correct `endTime`, but instead the default of `endTime = startTime`.
|
|
4141
|
+
//
|
|
4142
|
+
// Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where
|
|
4143
|
+
// if we use `endTime` here it would result in unexpected stacking.
|
|
4144
|
+
//
|
|
4145
|
+
// Reference: https://github.com/ppy/osu/pull/24188
|
|
4146
|
+
if (objects[j].position.getDistance(currentObject.position) <
|
|
4147
|
+
BeatmapProcessor.stackDistance) {
|
|
4148
|
+
++currentObject.stackHeight;
|
|
4149
|
+
startTime = objects[j].startTime;
|
|
4150
|
+
}
|
|
4151
|
+
else if (objects[j].position.getDistance(currentObject.endPosition) <
|
|
4152
|
+
BeatmapProcessor.stackDistance) {
|
|
4153
|
+
// Case for sliders - bump notes down and right, rather than up and left.
|
|
4154
|
+
++sliderStack;
|
|
4155
|
+
objects[j].stackHeight -= sliderStack;
|
|
4156
|
+
startTime = objects[j].startTime;
|
|
4045
4157
|
}
|
|
4046
|
-
break;
|
|
4047
|
-
case exports.Modes.osu: {
|
|
4048
|
-
const hitWindow = new OsuHitWindow(difficulty.od);
|
|
4049
|
-
difficulty.od = OsuHitWindow.greatWindowToOD(hitWindow.greatWindow / rate);
|
|
4050
|
-
break;
|
|
4051
|
-
}
|
|
4052
|
-
}
|
|
4053
|
-
}
|
|
4054
|
-
/**
|
|
4055
|
-
* Calculates the playback rate for the track with the selected `Mod`s at the given time.
|
|
4056
|
-
*
|
|
4057
|
-
* @param mods The list of selected `Mod`s.
|
|
4058
|
-
* @param time The time at which the playback rate is queried, in milliseconds. Defaults to 0.
|
|
4059
|
-
* @returns The rate with `Mod`s.
|
|
4060
|
-
*/
|
|
4061
|
-
static calculateRateWithMods(mods, time = 0) {
|
|
4062
|
-
let rate = 1;
|
|
4063
|
-
for (const mod of mods) {
|
|
4064
|
-
if (mod.isApplicableToTrackRate()) {
|
|
4065
|
-
rate = mod.applyToRate(time, rate);
|
|
4066
4158
|
}
|
|
4067
4159
|
}
|
|
4068
|
-
return rate;
|
|
4069
|
-
}
|
|
4070
|
-
/**
|
|
4071
|
-
* Processes parsing options.
|
|
4072
|
-
*
|
|
4073
|
-
* @param mods The mods to process.
|
|
4074
|
-
* @param options The options to process.
|
|
4075
|
-
* @returns The processed mods.
|
|
4076
|
-
*/
|
|
4077
|
-
static processParsingOptions(mods, options) {
|
|
4078
|
-
if ((options === null || options === void 0 ? void 0 : options.checkDuplicate) !== false) {
|
|
4079
|
-
mods = this.checkDuplicateMods(mods);
|
|
4080
|
-
}
|
|
4081
|
-
if ((options === null || options === void 0 ? void 0 : options.checkIncompatible) !== false) {
|
|
4082
|
-
mods = this.checkIncompatibleMods(mods);
|
|
4083
|
-
}
|
|
4084
|
-
return mods;
|
|
4085
4160
|
}
|
|
4086
4161
|
}
|
|
4087
|
-
|
|
4088
|
-
* All `Mod`s that exists, mapped by their acronym.
|
|
4089
|
-
*/
|
|
4090
|
-
ModUtil.allMods = (() => {
|
|
4091
|
-
const mods = [
|
|
4092
|
-
// Janky order to keep the order on what players are used to
|
|
4093
|
-
ModAuto,
|
|
4094
|
-
ModRelax,
|
|
4095
|
-
ModAutopilot,
|
|
4096
|
-
ModEasy,
|
|
4097
|
-
ModNoFail,
|
|
4098
|
-
ModHidden,
|
|
4099
|
-
ModTraceable,
|
|
4100
|
-
ModDoubleTime,
|
|
4101
|
-
ModNightCore,
|
|
4102
|
-
ModHalfTime,
|
|
4103
|
-
ModCustomSpeed,
|
|
4104
|
-
ModHardRock,
|
|
4105
|
-
ModDifficultyAdjust,
|
|
4106
|
-
ModFlashlight,
|
|
4107
|
-
ModSuddenDeath,
|
|
4108
|
-
ModPerfect,
|
|
4109
|
-
ModPrecise,
|
|
4110
|
-
ModReallyEasy,
|
|
4111
|
-
ModScoreV2,
|
|
4112
|
-
ModSmallCircle,
|
|
4113
|
-
ModSpunOut,
|
|
4114
|
-
ModTouchDevice,
|
|
4115
|
-
];
|
|
4116
|
-
const map = new Map();
|
|
4117
|
-
for (const mod of mods) {
|
|
4118
|
-
map.set(new mod().acronym, mod);
|
|
4119
|
-
}
|
|
4120
|
-
return map;
|
|
4121
|
-
})();
|
|
4162
|
+
BeatmapProcessor.stackDistance = 3;
|
|
4122
4163
|
|
|
4123
4164
|
/**
|
|
4124
4165
|
* Represents a beatmap that is in a playable state for a specific game mode.
|
|
@@ -4149,8 +4190,8 @@ class PlayableBeatmap {
|
|
|
4149
4190
|
this.colors = baseBeatmap.colors;
|
|
4150
4191
|
this.hitObjects = baseBeatmap.hitObjects;
|
|
4151
4192
|
this.maxCombo = baseBeatmap.maxCombo;
|
|
4152
|
-
this.mods = mods
|
|
4153
|
-
this.speedMultiplier = ModUtil.calculateRateWithMods(this.mods);
|
|
4193
|
+
this.mods = mods;
|
|
4194
|
+
this.speedMultiplier = ModUtil.calculateRateWithMods(this.mods.values());
|
|
4154
4195
|
}
|
|
4155
4196
|
}
|
|
4156
4197
|
|
|
@@ -4159,7 +4200,7 @@ class PlayableBeatmap {
|
|
|
4159
4200
|
*/
|
|
4160
4201
|
class DroidPlayableBeatmap extends PlayableBeatmap {
|
|
4161
4202
|
createHitWindow() {
|
|
4162
|
-
if (this.mods.
|
|
4203
|
+
if (this.mods.has(ModPrecise)) {
|
|
4163
4204
|
return new PreciseDroidHitWindow(this.difficulty.od);
|
|
4164
4205
|
}
|
|
4165
4206
|
else {
|
|
@@ -4912,15 +4953,17 @@ class Beatmap {
|
|
|
4912
4953
|
*
|
|
4913
4954
|
* @param mods The modifications to calculate for. Defaults to No Mod.
|
|
4914
4955
|
*/
|
|
4915
|
-
maxDroidScore(mods
|
|
4956
|
+
maxDroidScore(mods) {
|
|
4916
4957
|
let scoreMultiplier = 1;
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4958
|
+
if (mods) {
|
|
4959
|
+
for (const mod of mods.values()) {
|
|
4960
|
+
if (mod.isApplicableToDroid()) {
|
|
4961
|
+
scoreMultiplier *= mod.calculateDroidScoreMultiplier(this.difficulty);
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
if (mods.has(ModScoreV2)) {
|
|
4965
|
+
return 1e6 * scoreMultiplier;
|
|
4920
4966
|
}
|
|
4921
|
-
}
|
|
4922
|
-
if (mods.some((m) => m instanceof ModScoreV2)) {
|
|
4923
|
-
return 1e6 * scoreMultiplier;
|
|
4924
4967
|
}
|
|
4925
4968
|
const difficultyMultiplier = 1 +
|
|
4926
4969
|
this.difficulty.od / 10 +
|
|
@@ -4955,17 +4998,19 @@ class Beatmap {
|
|
|
4955
4998
|
*
|
|
4956
4999
|
* @param mods The modifications to calculate for. Defaults to No Mod.
|
|
4957
5000
|
*/
|
|
4958
|
-
maxOsuScore(mods
|
|
5001
|
+
maxOsuScore(mods) {
|
|
4959
5002
|
const accumulatedDiffPoints = this.difficulty.cs + this.difficulty.hp + this.difficulty.od;
|
|
4960
5003
|
let difficultyMultiplier = 2;
|
|
4961
5004
|
let scoreMultiplier = 1;
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
5005
|
+
if (mods) {
|
|
5006
|
+
for (const mod of mods.values()) {
|
|
5007
|
+
if (mod.isApplicableToOsu()) {
|
|
5008
|
+
scoreMultiplier *= mod.osuScoreMultiplier;
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
if (mods.has(ModScoreV2)) {
|
|
5012
|
+
return 1e6 * scoreMultiplier;
|
|
4965
5013
|
}
|
|
4966
|
-
}
|
|
4967
|
-
if (mods.some((m) => m instanceof ModScoreV2)) {
|
|
4968
|
-
return 1e6 * scoreMultiplier;
|
|
4969
5014
|
}
|
|
4970
5015
|
switch (true) {
|
|
4971
5016
|
case accumulatedDiffPoints <= 5:
|
|
@@ -5020,7 +5065,7 @@ class Beatmap {
|
|
|
5020
5065
|
* @param mods The `Mod`s to apply to the `Beatmap`. Defaults to No Mod.
|
|
5021
5066
|
* @return The constructed `DroidPlayableBeatmap`.
|
|
5022
5067
|
*/
|
|
5023
|
-
createDroidPlayableBeatmap(mods =
|
|
5068
|
+
createDroidPlayableBeatmap(mods = new ModMap()) {
|
|
5024
5069
|
return new DroidPlayableBeatmap(this.createPlayableBeatmap(mods, exports.Modes.droid), mods);
|
|
5025
5070
|
}
|
|
5026
5071
|
/**
|
|
@@ -5032,11 +5077,11 @@ class Beatmap {
|
|
|
5032
5077
|
* @param mods The `Mod`s to apply to the `Beatmap`. Defaults to No Mod.
|
|
5033
5078
|
* @return The constructed `OsuPlayableBeatmap`.
|
|
5034
5079
|
*/
|
|
5035
|
-
createOsuPlayableBeatmap(mods =
|
|
5080
|
+
createOsuPlayableBeatmap(mods = new ModMap()) {
|
|
5036
5081
|
return new OsuPlayableBeatmap(this.createPlayableBeatmap(mods, exports.Modes.osu), mods);
|
|
5037
5082
|
}
|
|
5038
5083
|
createPlayableBeatmap(mods, mode) {
|
|
5039
|
-
if (this.mode === mode && mods.
|
|
5084
|
+
if (this.mode === mode && mods.size === 0) {
|
|
5040
5085
|
// Beatmap is already in a playable state.
|
|
5041
5086
|
return this;
|
|
5042
5087
|
}
|
|
@@ -8198,39 +8243,39 @@ class DroidLegacyModConverter {
|
|
|
8198
8243
|
* @returns An array of `Mod`s.
|
|
8199
8244
|
*/
|
|
8200
8245
|
static convert(str, difficulty) {
|
|
8246
|
+
const map = new ModMap();
|
|
8201
8247
|
if (!str) {
|
|
8202
|
-
return
|
|
8248
|
+
return map;
|
|
8203
8249
|
}
|
|
8204
8250
|
const data = str.split("|");
|
|
8205
8251
|
if (!data[0]) {
|
|
8206
|
-
return
|
|
8252
|
+
return map;
|
|
8207
8253
|
}
|
|
8208
|
-
const mods = [];
|
|
8209
8254
|
for (const c of data[0]) {
|
|
8210
|
-
const modType = this.
|
|
8255
|
+
const modType = this.legacyStorableMods.get(c);
|
|
8211
8256
|
if (!modType) {
|
|
8212
8257
|
continue;
|
|
8213
8258
|
}
|
|
8214
8259
|
const mod = new modType();
|
|
8215
8260
|
if (mod.isMigratableDroidMod() && difficulty) {
|
|
8216
|
-
|
|
8261
|
+
map.set(mod.migrateDroidMod(difficulty));
|
|
8217
8262
|
}
|
|
8218
8263
|
else {
|
|
8219
|
-
|
|
8264
|
+
map.set(mod);
|
|
8220
8265
|
}
|
|
8221
8266
|
}
|
|
8222
8267
|
if (data.length > 1) {
|
|
8223
|
-
this.parseExtraModString(
|
|
8268
|
+
this.parseExtraModString(map, data.slice(1));
|
|
8224
8269
|
}
|
|
8225
|
-
return
|
|
8270
|
+
return map;
|
|
8226
8271
|
}
|
|
8227
8272
|
/**
|
|
8228
8273
|
* Parses the extra strings of a mod string.
|
|
8229
8274
|
*
|
|
8230
|
-
* @param
|
|
8275
|
+
* @param map The current `Mod`s.
|
|
8231
8276
|
* @param extraStrings The extra strings to parse.
|
|
8232
8277
|
*/
|
|
8233
|
-
static parseExtraModString(
|
|
8278
|
+
static parseExtraModString(map, extraStrings) {
|
|
8234
8279
|
let customCS;
|
|
8235
8280
|
let customAR;
|
|
8236
8281
|
let customOD;
|
|
@@ -8252,20 +8297,20 @@ class DroidLegacyModConverter {
|
|
|
8252
8297
|
break;
|
|
8253
8298
|
// FL follow delay
|
|
8254
8299
|
case s.startsWith("FLD"): {
|
|
8255
|
-
let flashlight =
|
|
8300
|
+
let flashlight = map.get(ModFlashlight);
|
|
8256
8301
|
if (!flashlight) {
|
|
8257
8302
|
flashlight = new ModFlashlight();
|
|
8258
|
-
|
|
8303
|
+
map.set(flashlight);
|
|
8259
8304
|
}
|
|
8260
8305
|
flashlight.followDelay = parseFloat(s.slice(3));
|
|
8261
8306
|
break;
|
|
8262
8307
|
}
|
|
8263
8308
|
// Speed multiplier
|
|
8264
8309
|
case s.startsWith("x"): {
|
|
8265
|
-
let customSpeed =
|
|
8310
|
+
let customSpeed = map.get(ModCustomSpeed);
|
|
8266
8311
|
if (!customSpeed) {
|
|
8267
8312
|
customSpeed = new ModCustomSpeed();
|
|
8268
|
-
|
|
8313
|
+
map.set(customSpeed);
|
|
8269
8314
|
}
|
|
8270
8315
|
customSpeed.trackRateMultiplier = parseFloat(s.slice(1));
|
|
8271
8316
|
break;
|
|
@@ -8276,7 +8321,7 @@ class DroidLegacyModConverter {
|
|
|
8276
8321
|
customAR !== undefined ||
|
|
8277
8322
|
customOD !== undefined ||
|
|
8278
8323
|
customHP !== undefined) {
|
|
8279
|
-
|
|
8324
|
+
map.set(new ModDifficultyAdjust({
|
|
8280
8325
|
cs: customCS,
|
|
8281
8326
|
ar: customAR,
|
|
8282
8327
|
od: customOD,
|
|
@@ -8288,7 +8333,7 @@ class DroidLegacyModConverter {
|
|
|
8288
8333
|
/**
|
|
8289
8334
|
* All `Mod`s that can be stored in the legacy mods format by their respective encode character.
|
|
8290
8335
|
*/
|
|
8291
|
-
DroidLegacyModConverter.
|
|
8336
|
+
DroidLegacyModConverter.legacyStorableMods = new Map([
|
|
8292
8337
|
["a", ModAuto],
|
|
8293
8338
|
["b", ModTraceable],
|
|
8294
8339
|
["c", ModNightCore],
|
|
@@ -9829,6 +9874,7 @@ exports.ModFlashlight = ModFlashlight;
|
|
|
9829
9874
|
exports.ModHalfTime = ModHalfTime;
|
|
9830
9875
|
exports.ModHardRock = ModHardRock;
|
|
9831
9876
|
exports.ModHidden = ModHidden;
|
|
9877
|
+
exports.ModMap = ModMap;
|
|
9832
9878
|
exports.ModNightCore = ModNightCore;
|
|
9833
9879
|
exports.ModNoFail = ModNoFail;
|
|
9834
9880
|
exports.ModOldNightCore = ModOldNightCore;
|