@rian8337/osu-base 4.0.0-beta.55 → 4.0.0-beta.58
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 +1470 -1422
- package/package.json +2 -2
- package/typings/index.d.ts +71 -74
package/dist/index.js
CHANGED
|
@@ -388,143 +388,6 @@ exports.Modes = void 0;
|
|
|
388
388
|
Modes["osu"] = "osu";
|
|
389
389
|
})(exports.Modes || (exports.Modes = {}));
|
|
390
390
|
|
|
391
|
-
/**
|
|
392
|
-
* Represents a mod.
|
|
393
|
-
*/
|
|
394
|
-
class Mod {
|
|
395
|
-
constructor() {
|
|
396
|
-
/**
|
|
397
|
-
* `Mod`s that are incompatible with this `Mod`.
|
|
398
|
-
*/
|
|
399
|
-
this.incompatibleMods = new Set();
|
|
400
|
-
}
|
|
401
|
-
/**
|
|
402
|
-
* Serializes this `Mod` to a `SerializedMod`.
|
|
403
|
-
*/
|
|
404
|
-
serialize() {
|
|
405
|
-
var _a;
|
|
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;
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Copies the settings of a `SerializedMod` to this `Mod`.
|
|
417
|
-
*
|
|
418
|
-
* @param mod The `SerializedMod` to copy the settings from. Must be the same `Mod` type.
|
|
419
|
-
* @throws {TypeError} If the `SerializedMod` is not the same type as this `Mod`.
|
|
420
|
-
*/
|
|
421
|
-
copySettings(mod) {
|
|
422
|
-
if (mod.acronym !== this.acronym) {
|
|
423
|
-
throw new TypeError(`Cannot copy settings from ${mod.acronym} to ${this.acronym}`);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
/**
|
|
427
|
-
* Whether this `Mod` can be applied to osu!droid.
|
|
428
|
-
*/
|
|
429
|
-
isApplicableToDroid() {
|
|
430
|
-
return "droidRanked" in this;
|
|
431
|
-
}
|
|
432
|
-
/**
|
|
433
|
-
* Whether this `Mod` can be applied to osu!standard.
|
|
434
|
-
*/
|
|
435
|
-
isApplicableToOsu() {
|
|
436
|
-
return "osuRanked" in this;
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Whether this `Mod` can be applied to osu!standard, specifically the osu!stable client.
|
|
440
|
-
*/
|
|
441
|
-
isApplicableToOsuStable() {
|
|
442
|
-
return "bitwise" in this;
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Whether this `Mod` can be applied to a `Beatmap`.
|
|
446
|
-
*/
|
|
447
|
-
isApplicableToBeatmap() {
|
|
448
|
-
return "applyToBeatmap" in this;
|
|
449
|
-
}
|
|
450
|
-
/**
|
|
451
|
-
* Whether this `Mod` can be applied to a `BeatmapDifficulty`.
|
|
452
|
-
*/
|
|
453
|
-
isApplicableToDifficulty() {
|
|
454
|
-
return "applyToDifficulty" in this;
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* Whether this `Mod` can be applied to a `BeatmapDifficulty` relative to other `Mod`s and settings.
|
|
458
|
-
*/
|
|
459
|
-
isApplicableToDifficultyWithSettings() {
|
|
460
|
-
return "applyToDifficultyWithSettings" in this;
|
|
461
|
-
}
|
|
462
|
-
/**
|
|
463
|
-
* Whether this `Mod` can be applied to a `HitObject`.
|
|
464
|
-
*/
|
|
465
|
-
isApplicableToHitObject() {
|
|
466
|
-
return "applyToHitObject" in this;
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* Whether this `Mod` can be applied to a `HitObject` relative to other `Mod`s and settings.
|
|
470
|
-
*/
|
|
471
|
-
isApplicableToHitObjectWithSettings() {
|
|
472
|
-
return "applyToHitObjectWithSettings" in this;
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Whether this `Mod` can be applied to a track's playback rate.
|
|
476
|
-
*/
|
|
477
|
-
isApplicableToTrackRate() {
|
|
478
|
-
return "applyToRate" in this;
|
|
479
|
-
}
|
|
480
|
-
/**
|
|
481
|
-
* Whether this `Mod` is migratable to a new `Mod` in osu!droid.
|
|
482
|
-
*/
|
|
483
|
-
isMigratableDroidMod() {
|
|
484
|
-
return "migrateDroidMod" in this;
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Serializes the settings of this `Mod` to an object that can be converted to a JSON.
|
|
488
|
-
*
|
|
489
|
-
* @returns The serialized settings of this `Mod`, or `null` if there are no settings.
|
|
490
|
-
*/
|
|
491
|
-
serializeSettings() {
|
|
492
|
-
return null;
|
|
493
|
-
}
|
|
494
|
-
/**
|
|
495
|
-
* Returns the string representation of this `Mod`.
|
|
496
|
-
*/
|
|
497
|
-
toString() {
|
|
498
|
-
return this.acronym;
|
|
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;
|
|
516
|
-
}
|
|
517
|
-
calculateDroidScoreMultiplier() {
|
|
518
|
-
return 1;
|
|
519
|
-
}
|
|
520
|
-
get isOsuRelevant() {
|
|
521
|
-
return true;
|
|
522
|
-
}
|
|
523
|
-
get osuScoreMultiplier() {
|
|
524
|
-
return 1;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
391
|
/**
|
|
529
392
|
* Bitmask constant of object types. This is needed as osu! uses bits to determine object types.
|
|
530
393
|
*/
|
|
@@ -766,18 +629,20 @@ class CircleSizeCalculator {
|
|
|
766
629
|
* @param mods The mods to apply.
|
|
767
630
|
* @returns The calculated osu!droid scale.
|
|
768
631
|
*/
|
|
769
|
-
static droidCSToDroidScale(cs, mods
|
|
632
|
+
static droidCSToDroidScale(cs, mods) {
|
|
770
633
|
// Create a dummy beatmap difficulty for circle size calculation.
|
|
771
634
|
const difficulty = new BeatmapDifficulty();
|
|
772
635
|
difficulty.cs = cs;
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
mod.
|
|
636
|
+
if (mods !== undefined) {
|
|
637
|
+
for (const mod of mods.values()) {
|
|
638
|
+
if (mod.isApplicableToDifficulty()) {
|
|
639
|
+
mod.applyToDifficulty(exports.Modes.droid, difficulty);
|
|
640
|
+
}
|
|
776
641
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
642
|
+
for (const mod of mods.values()) {
|
|
643
|
+
if (mod.isApplicableToDifficultyWithSettings()) {
|
|
644
|
+
mod.applyToDifficultyWithSettings(exports.Modes.droid, difficulty, mods);
|
|
645
|
+
}
|
|
781
646
|
}
|
|
782
647
|
}
|
|
783
648
|
return Math.max(((this.assumedDroidHeight / 480) *
|
|
@@ -1310,68 +1175,311 @@ HitObject.preemptMin = 450;
|
|
|
1310
1175
|
HitObject.controlPointLeniency = 1;
|
|
1311
1176
|
|
|
1312
1177
|
/**
|
|
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).
|
|
1178
|
+
* Represents a mod.
|
|
1318
1179
|
*/
|
|
1319
|
-
class
|
|
1320
|
-
constructor(
|
|
1321
|
-
|
|
1180
|
+
class Mod {
|
|
1181
|
+
constructor() {
|
|
1182
|
+
/**
|
|
1183
|
+
* `Mod`s that are incompatible with this `Mod`.
|
|
1184
|
+
*/
|
|
1185
|
+
this.incompatibleMods = new Set();
|
|
1322
1186
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1187
|
+
/**
|
|
1188
|
+
* Serializes this `Mod` to a `SerializedMod`.
|
|
1189
|
+
*/
|
|
1190
|
+
serialize() {
|
|
1191
|
+
var _a;
|
|
1192
|
+
const serialized = {
|
|
1193
|
+
acronym: this.acronym,
|
|
1194
|
+
settings: (_a = this.serializeSettings()) !== null && _a !== void 0 ? _a : undefined,
|
|
1195
|
+
};
|
|
1196
|
+
if (!serialized.settings) {
|
|
1197
|
+
delete serialized.settings;
|
|
1198
|
+
}
|
|
1199
|
+
return serialized;
|
|
1325
1200
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1201
|
+
/**
|
|
1202
|
+
* Copies the settings of a `SerializedMod` to this `Mod`.
|
|
1203
|
+
*
|
|
1204
|
+
* @param mod The `SerializedMod` to copy the settings from. Must be the same `Mod` type.
|
|
1205
|
+
* @throws {TypeError} If the `SerializedMod` is not the same type as this `Mod`.
|
|
1206
|
+
*/
|
|
1207
|
+
copySettings(mod) {
|
|
1208
|
+
if (mod.acronym !== this.acronym) {
|
|
1209
|
+
throw new TypeError(`Cannot copy settings from ${mod.acronym} to ${this.acronym}`);
|
|
1210
|
+
}
|
|
1336
1211
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1212
|
+
/**
|
|
1213
|
+
* Whether this `Mod` can be applied to osu!droid.
|
|
1214
|
+
*/
|
|
1215
|
+
isApplicableToDroid() {
|
|
1216
|
+
return "droidRanked" in this;
|
|
1339
1217
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
class SliderHead extends SliderNestedHitObject {
|
|
1346
|
-
constructor(values) {
|
|
1347
|
-
super(Object.assign(Object.assign({}, values), { spanIndex: 0, spanStartTime: values.startTime }));
|
|
1218
|
+
/**
|
|
1219
|
+
* Whether this `Mod` can be applied to osu!standard.
|
|
1220
|
+
*/
|
|
1221
|
+
isApplicableToOsu() {
|
|
1222
|
+
return "osuRanked" in this;
|
|
1348
1223
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
* No time values are provided (meaning instantaneous hit or miss).
|
|
1355
|
-
*/
|
|
1356
|
-
class EmptyHitWindow extends HitWindow {
|
|
1357
|
-
constructor() {
|
|
1358
|
-
super(0);
|
|
1224
|
+
/**
|
|
1225
|
+
* Whether this `Mod` can be applied to osu!standard, specifically the osu!stable client.
|
|
1226
|
+
*/
|
|
1227
|
+
isApplicableToOsuStable() {
|
|
1228
|
+
return "bitwise" in this;
|
|
1359
1229
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1230
|
+
/**
|
|
1231
|
+
* Whether this `Mod` can be applied to a `Beatmap`.
|
|
1232
|
+
*/
|
|
1233
|
+
isApplicableToBeatmap() {
|
|
1234
|
+
return "applyToBeatmap" in this;
|
|
1362
1235
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1236
|
+
/**
|
|
1237
|
+
* Whether this `Mod` can be applied to a `BeatmapDifficulty`.
|
|
1238
|
+
*/
|
|
1239
|
+
isApplicableToDifficulty() {
|
|
1240
|
+
return "applyToDifficulty" in this;
|
|
1365
1241
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1242
|
+
/**
|
|
1243
|
+
* Whether this `Mod` can be applied to a `BeatmapDifficulty` relative to other `Mod`s and settings.
|
|
1244
|
+
*/
|
|
1245
|
+
isApplicableToDifficultyWithSettings() {
|
|
1246
|
+
return "applyToDifficultyWithSettings" in this;
|
|
1368
1247
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1248
|
+
/**
|
|
1249
|
+
* Whether this `Mod` can be applied to a `HitObject`.
|
|
1250
|
+
*/
|
|
1251
|
+
isApplicableToHitObject() {
|
|
1252
|
+
return "applyToHitObject" in this;
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Whether this `Mod` can be applied to a `HitObject` relative to other `Mod`s and settings.
|
|
1256
|
+
*/
|
|
1257
|
+
isApplicableToHitObjectWithSettings() {
|
|
1258
|
+
return "applyToHitObjectWithSettings" in this;
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Whether this `Mod` can be applied to a track's playback rate.
|
|
1262
|
+
*/
|
|
1263
|
+
isApplicableToTrackRate() {
|
|
1264
|
+
return "applyToRate" in this;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Whether this `Mod` is migratable to a new `Mod` in osu!droid.
|
|
1268
|
+
*/
|
|
1269
|
+
isMigratableDroidMod() {
|
|
1270
|
+
return "migrateDroidMod" in this;
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Serializes the settings of this `Mod` to an object that can be converted to a JSON.
|
|
1274
|
+
*
|
|
1275
|
+
* @returns The serialized settings of this `Mod`, or `null` if there are no settings.
|
|
1276
|
+
*/
|
|
1277
|
+
serializeSettings() {
|
|
1278
|
+
return null;
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Returns the string representation of this `Mod`.
|
|
1282
|
+
*/
|
|
1283
|
+
toString() {
|
|
1284
|
+
return this.acronym;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* Represents the Relax mod.
|
|
1290
|
+
*/
|
|
1291
|
+
class ModRelax extends Mod {
|
|
1292
|
+
constructor() {
|
|
1293
|
+
super();
|
|
1294
|
+
this.acronym = "RX";
|
|
1295
|
+
this.name = "Relax";
|
|
1296
|
+
this.droidRanked = false;
|
|
1297
|
+
this.osuRanked = false;
|
|
1298
|
+
this.bitwise = 1 << 7;
|
|
1299
|
+
this.incompatibleMods.add(ModAuto).add(ModAutopilot);
|
|
1300
|
+
}
|
|
1301
|
+
get isDroidRelevant() {
|
|
1302
|
+
return true;
|
|
1303
|
+
}
|
|
1304
|
+
calculateDroidScoreMultiplier() {
|
|
1305
|
+
return 0.001;
|
|
1306
|
+
}
|
|
1307
|
+
get isOsuRelevant() {
|
|
1308
|
+
return true;
|
|
1309
|
+
}
|
|
1310
|
+
get osuScoreMultiplier() {
|
|
1311
|
+
return 0;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Represents the Autopilot mod.
|
|
1317
|
+
*/
|
|
1318
|
+
class ModAutopilot extends Mod {
|
|
1319
|
+
constructor() {
|
|
1320
|
+
super();
|
|
1321
|
+
this.acronym = "AP";
|
|
1322
|
+
this.name = "Autopilot";
|
|
1323
|
+
this.droidRanked = false;
|
|
1324
|
+
this.osuRanked = false;
|
|
1325
|
+
this.bitwise = 1 << 13;
|
|
1326
|
+
this.incompatibleMods.add(ModRelax).add(ModAuto);
|
|
1327
|
+
}
|
|
1328
|
+
get isDroidRelevant() {
|
|
1329
|
+
return true;
|
|
1330
|
+
}
|
|
1331
|
+
calculateDroidScoreMultiplier() {
|
|
1332
|
+
return 0.001;
|
|
1333
|
+
}
|
|
1334
|
+
get isOsuRelevant() {
|
|
1335
|
+
return true;
|
|
1336
|
+
}
|
|
1337
|
+
get osuScoreMultiplier() {
|
|
1338
|
+
return 0;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Represents the Auto mod.
|
|
1344
|
+
*/
|
|
1345
|
+
class ModAuto extends Mod {
|
|
1346
|
+
constructor() {
|
|
1347
|
+
super();
|
|
1348
|
+
this.acronym = "AT";
|
|
1349
|
+
this.name = "Autoplay";
|
|
1350
|
+
this.droidRanked = false;
|
|
1351
|
+
this.osuRanked = false;
|
|
1352
|
+
this.bitwise = 1 << 11;
|
|
1353
|
+
this.incompatibleMods.add(ModAutopilot).add(ModRelax);
|
|
1354
|
+
}
|
|
1355
|
+
get isDroidRelevant() {
|
|
1356
|
+
return true;
|
|
1357
|
+
}
|
|
1358
|
+
calculateDroidScoreMultiplier() {
|
|
1359
|
+
return 1;
|
|
1360
|
+
}
|
|
1361
|
+
get isOsuRelevant() {
|
|
1362
|
+
return true;
|
|
1363
|
+
}
|
|
1364
|
+
get osuScoreMultiplier() {
|
|
1365
|
+
return 1;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* Represents a `Mod` that adjusts the playback rate of a track.
|
|
1371
|
+
*/
|
|
1372
|
+
class ModRateAdjust extends Mod {
|
|
1373
|
+
/**
|
|
1374
|
+
* The generic osu!droid score multiplier of this `Mod`.
|
|
1375
|
+
*/
|
|
1376
|
+
get droidScoreMultiplier() {
|
|
1377
|
+
return this.trackRateMultiplier >= 1
|
|
1378
|
+
? 1 + (this.trackRateMultiplier - 1) * 0.24
|
|
1379
|
+
: Math.pow(0.3, (1 - this.trackRateMultiplier) * 4);
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Generic getter to determine if this `ModRateAdjust` is relevant.
|
|
1383
|
+
*/
|
|
1384
|
+
get isRelevant() {
|
|
1385
|
+
return this.trackRateMultiplier !== 1;
|
|
1386
|
+
}
|
|
1387
|
+
applyToRate(time, rate) {
|
|
1388
|
+
return rate * this.trackRateMultiplier;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* Represents the Custom Speed mod.
|
|
1394
|
+
*
|
|
1395
|
+
* This is a replacement `Mod` for speed modify in osu!droid and custom rates in osu!lazer.
|
|
1396
|
+
*/
|
|
1397
|
+
class ModCustomSpeed extends ModRateAdjust {
|
|
1398
|
+
constructor(trackRateMultiplier = 1) {
|
|
1399
|
+
super();
|
|
1400
|
+
this.acronym = "CS";
|
|
1401
|
+
this.name = "Custom Speed";
|
|
1402
|
+
this.droidRanked = true;
|
|
1403
|
+
this.osuRanked = false;
|
|
1404
|
+
this.trackRateMultiplier = trackRateMultiplier;
|
|
1405
|
+
}
|
|
1406
|
+
copySettings(mod) {
|
|
1407
|
+
var _a, _b;
|
|
1408
|
+
super.copySettings(mod);
|
|
1409
|
+
this.trackRateMultiplier =
|
|
1410
|
+
(_b = (_a = mod.settings) === null || _a === void 0 ? void 0 : _a.rateMultiplier) !== null && _b !== void 0 ? _b : this.trackRateMultiplier;
|
|
1411
|
+
}
|
|
1412
|
+
get isDroidRelevant() {
|
|
1413
|
+
return this.isRelevant;
|
|
1414
|
+
}
|
|
1415
|
+
calculateDroidScoreMultiplier() {
|
|
1416
|
+
return this.droidScoreMultiplier;
|
|
1417
|
+
}
|
|
1418
|
+
get isOsuRelevant() {
|
|
1419
|
+
return this.isRelevant;
|
|
1420
|
+
}
|
|
1421
|
+
get osuScoreMultiplier() {
|
|
1422
|
+
// Round to the nearest multiple of 0.1.
|
|
1423
|
+
let value = Math.trunc(this.trackRateMultiplier * 10) / 10;
|
|
1424
|
+
// Offset back to 0.
|
|
1425
|
+
--value;
|
|
1426
|
+
return this.trackRateMultiplier >= 1 ? 1 + value / 5 : 0.6 + value;
|
|
1427
|
+
}
|
|
1428
|
+
serializeSettings() {
|
|
1429
|
+
return { rateMultiplier: this.trackRateMultiplier };
|
|
1430
|
+
}
|
|
1431
|
+
toString() {
|
|
1432
|
+
return `${super.toString()} (${this.trackRateMultiplier.toFixed(2)}x)`;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Represents a hitobject that can be nested within a slider.
|
|
1438
|
+
*/
|
|
1439
|
+
class SliderNestedHitObject extends HitObject {
|
|
1440
|
+
constructor(values) {
|
|
1441
|
+
super(values);
|
|
1442
|
+
this.spanIndex = values.spanIndex;
|
|
1443
|
+
this.spanStartTime = values.spanStartTime;
|
|
1444
|
+
}
|
|
1445
|
+
toString() {
|
|
1446
|
+
return `Position: [${this._position.x}, ${this._position.y}], span index: ${this.spanIndex}, span start time: ${this.spanStartTime}`;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* Represents the head of a slider.
|
|
1452
|
+
*/
|
|
1453
|
+
class SliderHead extends SliderNestedHitObject {
|
|
1454
|
+
constructor(values) {
|
|
1455
|
+
super(Object.assign(Object.assign({}, values), { spanIndex: 0, spanStartTime: values.startTime }));
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/**
|
|
1460
|
+
* An empty `HitWindow` that does not have any hit windows.
|
|
1461
|
+
*
|
|
1462
|
+
* No time values are provided (meaning instantaneous hit or miss).
|
|
1463
|
+
*/
|
|
1464
|
+
class EmptyHitWindow extends HitWindow {
|
|
1465
|
+
constructor() {
|
|
1466
|
+
super(0);
|
|
1467
|
+
}
|
|
1468
|
+
get greatWindow() {
|
|
1469
|
+
return 0;
|
|
1470
|
+
}
|
|
1471
|
+
get okWindow() {
|
|
1472
|
+
return 0;
|
|
1473
|
+
}
|
|
1474
|
+
get mehWindow() {
|
|
1475
|
+
return 0;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/**
|
|
1480
|
+
* Represents a nested hit object that is at the end of a slider path (either repeat or tail).
|
|
1481
|
+
*/
|
|
1482
|
+
class SliderEndCircle extends SliderNestedHitObject {
|
|
1375
1483
|
constructor(values) {
|
|
1376
1484
|
super(values);
|
|
1377
1485
|
this.sliderSpanDuration = values.sliderSpanDuration;
|
|
@@ -1941,731 +2049,7 @@ Slider.baseTickSample = new BankHitSampleInfo("slidertick");
|
|
|
1941
2049
|
Slider.legacyLastTickOffset = 36;
|
|
1942
2050
|
|
|
1943
2051
|
/**
|
|
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.
|
|
1958
|
-
*/
|
|
1959
|
-
class Spinner extends HitObject {
|
|
1960
|
-
get endTime() {
|
|
1961
|
-
return this._endTime;
|
|
1962
|
-
}
|
|
1963
|
-
constructor(values) {
|
|
1964
|
-
super(Object.assign(Object.assign({}, values), { position: Playfield.baseSize.divide(2) }));
|
|
1965
|
-
this._endTime = values.endTime;
|
|
1966
|
-
}
|
|
1967
|
-
applySamples(controlPoints) {
|
|
1968
|
-
super.applySamples(controlPoints);
|
|
1969
|
-
const samplePoints = controlPoints.sample.between(this.startTime + HitObject.controlPointLeniency, this.endTime + HitObject.controlPointLeniency);
|
|
1970
|
-
this.auxiliarySamples.length = 0;
|
|
1971
|
-
this.auxiliarySamples.push(new SequenceHitSampleInfo(samplePoints.map((s) => new TimedHitSampleInfo(s.time, s.applyTo(Spinner.baseSpinnerSpinSample)))));
|
|
1972
|
-
this.auxiliarySamples.push(new SequenceHitSampleInfo(samplePoints.map((s) => new TimedHitSampleInfo(s.time, s.applyTo(Spinner.baseSpinnerBonusSample)))));
|
|
1973
|
-
}
|
|
1974
|
-
getStackedPosition() {
|
|
1975
|
-
return this.position;
|
|
1976
|
-
}
|
|
1977
|
-
getStackedEndPosition() {
|
|
1978
|
-
return this.position;
|
|
1979
|
-
}
|
|
1980
|
-
createHitWindow() {
|
|
1981
|
-
return new EmptyHitWindow();
|
|
1982
|
-
}
|
|
1983
|
-
toString() {
|
|
1984
|
-
return `Position: [${this._position.x}, ${this._position.y}], duration: ${this.duration}`;
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
Spinner.baseSpinnerSpinSample = new BankHitSampleInfo("spinnerspin");
|
|
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;
|
|
1999
|
-
}
|
|
2000
|
-
/**
|
|
2001
|
-
* The objects of the beatmap.
|
|
2002
|
-
*/
|
|
2003
|
-
get objects() {
|
|
2004
|
-
return this._objects;
|
|
2005
|
-
}
|
|
2006
|
-
/**
|
|
2007
|
-
* The amount of circles in the beatmap.
|
|
2008
|
-
*/
|
|
2009
|
-
get circles() {
|
|
2010
|
-
return this._circles;
|
|
2011
|
-
}
|
|
2012
|
-
/**
|
|
2013
|
-
* The amount of sliders in the beatmap.
|
|
2014
|
-
*/
|
|
2015
|
-
get sliders() {
|
|
2016
|
-
return this._sliders;
|
|
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.
|
|
2052
|
+
* Represents the Difficulty Adjust mod.
|
|
2669
2053
|
*/
|
|
2670
2054
|
class ModDifficultyAdjust extends Mod {
|
|
2671
2055
|
get isRelevant() {
|
|
@@ -2721,7 +2105,7 @@ class ModDifficultyAdjust extends Mod {
|
|
|
2721
2105
|
get osuScoreMultiplier() {
|
|
2722
2106
|
return 0.5;
|
|
2723
2107
|
}
|
|
2724
|
-
applyToDifficultyWithSettings(
|
|
2108
|
+
applyToDifficultyWithSettings(_, difficulty, mods) {
|
|
2725
2109
|
var _a, _b, _c, _d;
|
|
2726
2110
|
difficulty.cs = (_a = this.cs) !== null && _a !== void 0 ? _a : difficulty.cs;
|
|
2727
2111
|
difficulty.ar = (_b = this.ar) !== null && _b !== void 0 ? _b : difficulty.ar;
|
|
@@ -2731,19 +2115,22 @@ class ModDifficultyAdjust extends Mod {
|
|
|
2731
2115
|
// This makes the player perceive the AR as is under all speed multipliers.
|
|
2732
2116
|
if (this.ar !== undefined) {
|
|
2733
2117
|
const preempt = BeatmapDifficulty.difficultyRange(this.ar, HitObject.preemptMax, HitObject.preemptMid, HitObject.preemptMin);
|
|
2734
|
-
const trackRate = this.calculateTrackRate(mods);
|
|
2118
|
+
const trackRate = this.calculateTrackRate(mods.values());
|
|
2735
2119
|
difficulty.ar = BeatmapDifficulty.inverseDifficultyRange(preempt * trackRate, HitObject.preemptMax, HitObject.preemptMid, HitObject.preemptMin);
|
|
2736
2120
|
}
|
|
2737
2121
|
}
|
|
2738
|
-
applyToHitObjectWithSettings(
|
|
2122
|
+
applyToHitObjectWithSettings(_, hitObject, mods) {
|
|
2739
2123
|
// Special case for force AR, where the AR value is kept constant with respect to game time.
|
|
2740
2124
|
// This makes the player perceive the fade in animation as is under all speed multipliers.
|
|
2741
2125
|
if (this.ar === undefined) {
|
|
2742
2126
|
return;
|
|
2743
2127
|
}
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2128
|
+
this.applyFadeAdjustment(hitObject, mods);
|
|
2129
|
+
if (hitObject instanceof Slider) {
|
|
2130
|
+
for (const nested of hitObject.nestedHitObjects) {
|
|
2131
|
+
this.applyFadeAdjustment(nested, mods);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2747
2134
|
}
|
|
2748
2135
|
serializeSettings() {
|
|
2749
2136
|
if (this.cs === undefined &&
|
|
@@ -2767,11 +2154,24 @@ class ModDifficultyAdjust extends Mod {
|
|
|
2767
2154
|
}
|
|
2768
2155
|
return settings;
|
|
2769
2156
|
}
|
|
2157
|
+
applyFadeAdjustment(hitObject, mods) {
|
|
2158
|
+
// IMPORTANT: These do not use `ModUtil.calculateRateWithMods` to avoid circular dependency.
|
|
2159
|
+
const initialTrackRate = this.calculateTrackRate(mods.values());
|
|
2160
|
+
const currentTrackRate = this.calculateTrackRate(mods.values(), hitObject.startTime);
|
|
2161
|
+
// Cancel the rate that was initially applied to timePreempt (via applyToDifficulty above and
|
|
2162
|
+
// HitObject.applyDefaults) and apply the current one.
|
|
2163
|
+
hitObject.timePreempt *= currentTrackRate / initialTrackRate;
|
|
2164
|
+
hitObject.timeFadeIn *= currentTrackRate;
|
|
2165
|
+
}
|
|
2770
2166
|
calculateTrackRate(mods, time = 0) {
|
|
2771
2167
|
// IMPORTANT: This does not use `ModUtil.calculateRateWithMods` to avoid circular dependency.
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2168
|
+
let rate = 1;
|
|
2169
|
+
for (const mod of mods) {
|
|
2170
|
+
if (mod.isApplicableToTrackRate()) {
|
|
2171
|
+
rate = mod.applyToRate(time, rate);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
return rate;
|
|
2775
2175
|
}
|
|
2776
2176
|
toString() {
|
|
2777
2177
|
const settings = [];
|
|
@@ -2875,6 +2275,16 @@ class ModDoubleTime extends ModRateAdjust {
|
|
|
2875
2275
|
}
|
|
2876
2276
|
}
|
|
2877
2277
|
|
|
2278
|
+
/**
|
|
2279
|
+
* Represents the osu! playfield.
|
|
2280
|
+
*/
|
|
2281
|
+
class Playfield {
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* The size of the playfield, which is 512x384.
|
|
2285
|
+
*/
|
|
2286
|
+
Playfield.baseSize = new Vector2(512, 384);
|
|
2287
|
+
|
|
2878
2288
|
/**
|
|
2879
2289
|
* Types of slider paths.
|
|
2880
2290
|
*/
|
|
@@ -3418,17 +2828,202 @@ class SliderPath {
|
|
|
3418
2828
|
}
|
|
3419
2829
|
|
|
3420
2830
|
/**
|
|
3421
|
-
* Represents the HardRock mod.
|
|
2831
|
+
* Represents the HardRock mod.
|
|
2832
|
+
*/
|
|
2833
|
+
class ModHardRock extends Mod {
|
|
2834
|
+
constructor() {
|
|
2835
|
+
super();
|
|
2836
|
+
this.acronym = "HR";
|
|
2837
|
+
this.name = "HardRock";
|
|
2838
|
+
this.droidRanked = true;
|
|
2839
|
+
this.osuRanked = true;
|
|
2840
|
+
this.bitwise = 1 << 4;
|
|
2841
|
+
this.incompatibleMods.add(ModEasy);
|
|
2842
|
+
}
|
|
2843
|
+
get isDroidRelevant() {
|
|
2844
|
+
return true;
|
|
2845
|
+
}
|
|
2846
|
+
calculateDroidScoreMultiplier() {
|
|
2847
|
+
return 1.06;
|
|
2848
|
+
}
|
|
2849
|
+
get isOsuRelevant() {
|
|
2850
|
+
return true;
|
|
2851
|
+
}
|
|
2852
|
+
get osuScoreMultiplier() {
|
|
2853
|
+
return 1.06;
|
|
2854
|
+
}
|
|
2855
|
+
applyToDifficulty(mode, difficulty) {
|
|
2856
|
+
switch (mode) {
|
|
2857
|
+
case exports.Modes.droid: {
|
|
2858
|
+
const scale = CircleSizeCalculator.droidCSToDroidScale(difficulty.cs);
|
|
2859
|
+
difficulty.cs = CircleSizeCalculator.droidScaleToDroidCS(scale - 0.125);
|
|
2860
|
+
break;
|
|
2861
|
+
}
|
|
2862
|
+
case exports.Modes.osu:
|
|
2863
|
+
// CS uses a custom 1.3 ratio.
|
|
2864
|
+
difficulty.cs = this.applySetting(difficulty.cs, 1.3);
|
|
2865
|
+
break;
|
|
2866
|
+
}
|
|
2867
|
+
difficulty.ar = this.applySetting(difficulty.ar);
|
|
2868
|
+
difficulty.od = this.applySetting(difficulty.od);
|
|
2869
|
+
difficulty.hp = this.applySetting(difficulty.hp);
|
|
2870
|
+
}
|
|
2871
|
+
applyToHitObject(_, hitObject) {
|
|
2872
|
+
// Reflect the position of the hit object.
|
|
2873
|
+
hitObject.position = this.reflectVector(hitObject.position);
|
|
2874
|
+
if (!(hitObject instanceof Slider)) {
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
// Reflect the control points of the slider. This will reflect the positions of head and tail circles.
|
|
2878
|
+
hitObject.path = new SliderPath({
|
|
2879
|
+
pathType: hitObject.path.pathType,
|
|
2880
|
+
controlPoints: hitObject.path.controlPoints.map((v) => this.reflectControlPoint(v)),
|
|
2881
|
+
expectedDistance: hitObject.path.expectedDistance,
|
|
2882
|
+
});
|
|
2883
|
+
// Reflect the position of slider ticks and repeats.
|
|
2884
|
+
hitObject.nestedHitObjects.slice(1, -1).forEach((obj) => {
|
|
2885
|
+
obj.position = this.reflectVector(obj.position);
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
reflectVector(vector) {
|
|
2889
|
+
return new Vector2(vector.x, Playfield.baseSize.y - vector.y);
|
|
2890
|
+
}
|
|
2891
|
+
reflectControlPoint(vector) {
|
|
2892
|
+
return new Vector2(vector.x, -vector.y);
|
|
2893
|
+
}
|
|
2894
|
+
applySetting(value, ratio = 1.4) {
|
|
2895
|
+
return Math.min(value * ratio, 10);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
/**
|
|
2900
|
+
* Represents the Easy mod.
|
|
2901
|
+
*/
|
|
2902
|
+
class ModEasy extends Mod {
|
|
2903
|
+
constructor() {
|
|
2904
|
+
super();
|
|
2905
|
+
this.acronym = "EZ";
|
|
2906
|
+
this.name = "Easy";
|
|
2907
|
+
this.droidRanked = true;
|
|
2908
|
+
this.osuRanked = true;
|
|
2909
|
+
this.bitwise = 1 << 1;
|
|
2910
|
+
this.incompatibleMods.add(ModHardRock);
|
|
2911
|
+
}
|
|
2912
|
+
get isDroidRelevant() {
|
|
2913
|
+
return true;
|
|
2914
|
+
}
|
|
2915
|
+
calculateDroidScoreMultiplier() {
|
|
2916
|
+
return 0.5;
|
|
2917
|
+
}
|
|
2918
|
+
get isOsuRelevant() {
|
|
2919
|
+
return true;
|
|
2920
|
+
}
|
|
2921
|
+
get osuScoreMultiplier() {
|
|
2922
|
+
return 0.5;
|
|
2923
|
+
}
|
|
2924
|
+
applyToDifficulty(mode, difficulty) {
|
|
2925
|
+
switch (mode) {
|
|
2926
|
+
case exports.Modes.droid: {
|
|
2927
|
+
const scale = CircleSizeCalculator.droidCSToDroidScale(difficulty.cs);
|
|
2928
|
+
difficulty.cs = CircleSizeCalculator.droidScaleToDroidCS(scale + 0.125);
|
|
2929
|
+
break;
|
|
2930
|
+
}
|
|
2931
|
+
case exports.Modes.osu:
|
|
2932
|
+
difficulty.cs /= 2;
|
|
2933
|
+
}
|
|
2934
|
+
difficulty.ar /= 2;
|
|
2935
|
+
difficulty.od /= 2;
|
|
2936
|
+
difficulty.hp /= 2;
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
/**
|
|
2941
|
+
* Represents the Flashlight mod.
|
|
2942
|
+
*/
|
|
2943
|
+
class ModFlashlight extends Mod {
|
|
2944
|
+
constructor() {
|
|
2945
|
+
super(...arguments);
|
|
2946
|
+
this.acronym = "FL";
|
|
2947
|
+
this.name = "Flashlight";
|
|
2948
|
+
this.droidRanked = true;
|
|
2949
|
+
this.osuRanked = true;
|
|
2950
|
+
this.bitwise = 1 << 10;
|
|
2951
|
+
/**
|
|
2952
|
+
* The amount of seconds until the Flashlight follow area reaches the cursor.
|
|
2953
|
+
*/
|
|
2954
|
+
this.followDelay = ModFlashlight.defaultFollowDelay;
|
|
2955
|
+
}
|
|
2956
|
+
copySettings(mod) {
|
|
2957
|
+
var _a, _b;
|
|
2958
|
+
super.copySettings(mod);
|
|
2959
|
+
this.followDelay =
|
|
2960
|
+
(_b = (_a = mod.settings) === null || _a === void 0 ? void 0 : _a.areaFollowDelay) !== null && _b !== void 0 ? _b : this.followDelay;
|
|
2961
|
+
}
|
|
2962
|
+
get isDroidRelevant() {
|
|
2963
|
+
return true;
|
|
2964
|
+
}
|
|
2965
|
+
calculateDroidScoreMultiplier() {
|
|
2966
|
+
return 1.12;
|
|
2967
|
+
}
|
|
2968
|
+
get isOsuRelevant() {
|
|
2969
|
+
return true;
|
|
2970
|
+
}
|
|
2971
|
+
get osuScoreMultiplier() {
|
|
2972
|
+
return 1.12;
|
|
2973
|
+
}
|
|
2974
|
+
serializeSettings() {
|
|
2975
|
+
return { areaFollowDelay: this.followDelay };
|
|
2976
|
+
}
|
|
2977
|
+
toString() {
|
|
2978
|
+
if (this.followDelay === ModFlashlight.defaultFollowDelay) {
|
|
2979
|
+
return super.toString();
|
|
2980
|
+
}
|
|
2981
|
+
return `${super.toString()} (${this.followDelay.toFixed(2)}s follow delay)`;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
/**
|
|
2985
|
+
* The default amount of seconds until the Flashlight follow area reaches the cursor.
|
|
2986
|
+
*/
|
|
2987
|
+
ModFlashlight.defaultFollowDelay = 0.12;
|
|
2988
|
+
|
|
2989
|
+
/**
|
|
2990
|
+
* Represents the Traceable mod.
|
|
2991
|
+
*/
|
|
2992
|
+
class ModTraceable extends Mod {
|
|
2993
|
+
constructor() {
|
|
2994
|
+
super();
|
|
2995
|
+
this.acronym = "TC";
|
|
2996
|
+
this.name = "Traceable";
|
|
2997
|
+
this.droidRanked = false;
|
|
2998
|
+
this.osuRanked = false;
|
|
2999
|
+
this.incompatibleMods.add(ModHidden);
|
|
3000
|
+
}
|
|
3001
|
+
get isDroidRelevant() {
|
|
3002
|
+
return true;
|
|
3003
|
+
}
|
|
3004
|
+
calculateDroidScoreMultiplier() {
|
|
3005
|
+
return 1.06;
|
|
3006
|
+
}
|
|
3007
|
+
get isOsuRelevant() {
|
|
3008
|
+
return true;
|
|
3009
|
+
}
|
|
3010
|
+
get osuScoreMultiplier() {
|
|
3011
|
+
return 1;
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
/**
|
|
3016
|
+
* Represents the Hidden mod.
|
|
3422
3017
|
*/
|
|
3423
|
-
class
|
|
3018
|
+
class ModHidden extends Mod {
|
|
3424
3019
|
constructor() {
|
|
3425
3020
|
super();
|
|
3426
|
-
this.acronym = "
|
|
3427
|
-
this.name = "
|
|
3021
|
+
this.acronym = "HD";
|
|
3022
|
+
this.name = "Hidden";
|
|
3428
3023
|
this.droidRanked = true;
|
|
3429
3024
|
this.osuRanked = true;
|
|
3430
|
-
this.bitwise = 1 <<
|
|
3431
|
-
this.incompatibleMods.add(
|
|
3025
|
+
this.bitwise = 1 << 3;
|
|
3026
|
+
this.incompatibleMods.add(ModTraceable);
|
|
3432
3027
|
}
|
|
3433
3028
|
get isDroidRelevant() {
|
|
3434
3029
|
return true;
|
|
@@ -3442,62 +3037,86 @@ class ModHardRock extends Mod {
|
|
|
3442
3037
|
get osuScoreMultiplier() {
|
|
3443
3038
|
return 1.06;
|
|
3444
3039
|
}
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3040
|
+
applyToBeatmap(beatmap) {
|
|
3041
|
+
const applyFadeInAdjustment = (hitObject) => {
|
|
3042
|
+
hitObject.timeFadeIn =
|
|
3043
|
+
hitObject.timePreempt * ModHidden.fadeInDurationMultiplier;
|
|
3044
|
+
if (hitObject instanceof Slider) {
|
|
3045
|
+
hitObject.nestedHitObjects.forEach(applyFadeInAdjustment);
|
|
3451
3046
|
}
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
difficulty.cs = this.applySetting(difficulty.cs, 1.3);
|
|
3455
|
-
break;
|
|
3456
|
-
}
|
|
3457
|
-
difficulty.ar = this.applySetting(difficulty.ar);
|
|
3458
|
-
difficulty.od = this.applySetting(difficulty.od);
|
|
3459
|
-
difficulty.hp = this.applySetting(difficulty.hp);
|
|
3047
|
+
};
|
|
3048
|
+
beatmap.hitObjects.objects.forEach(applyFadeInAdjustment);
|
|
3460
3049
|
}
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3050
|
+
}
|
|
3051
|
+
ModHidden.fadeInDurationMultiplier = 0.4;
|
|
3052
|
+
ModHidden.fadeOutDurationMultiplier = 0.3;
|
|
3053
|
+
|
|
3054
|
+
/**
|
|
3055
|
+
* Represents the SuddenDeath mod.
|
|
3056
|
+
*/
|
|
3057
|
+
class ModSuddenDeath extends Mod {
|
|
3058
|
+
constructor() {
|
|
3059
|
+
super();
|
|
3060
|
+
this.acronym = "SD";
|
|
3061
|
+
this.name = "Sudden Death";
|
|
3062
|
+
this.droidRanked = true;
|
|
3063
|
+
this.osuRanked = true;
|
|
3064
|
+
this.bitwise = 1 << 5;
|
|
3065
|
+
this.incompatibleMods.add(ModNoFail).add(ModPerfect);
|
|
3477
3066
|
}
|
|
3478
|
-
|
|
3479
|
-
return
|
|
3067
|
+
get isDroidRelevant() {
|
|
3068
|
+
return true;
|
|
3480
3069
|
}
|
|
3481
|
-
|
|
3482
|
-
return
|
|
3070
|
+
calculateDroidScoreMultiplier() {
|
|
3071
|
+
return 1;
|
|
3483
3072
|
}
|
|
3484
|
-
|
|
3485
|
-
return
|
|
3073
|
+
get isOsuRelevant() {
|
|
3074
|
+
return true;
|
|
3075
|
+
}
|
|
3076
|
+
get osuScoreMultiplier() {
|
|
3077
|
+
return 1;
|
|
3486
3078
|
}
|
|
3487
3079
|
}
|
|
3488
3080
|
|
|
3489
3081
|
/**
|
|
3490
|
-
* Represents the
|
|
3082
|
+
* Represents the Perfect mod.
|
|
3491
3083
|
*/
|
|
3492
|
-
class
|
|
3084
|
+
class ModPerfect extends Mod {
|
|
3493
3085
|
constructor() {
|
|
3494
3086
|
super();
|
|
3495
|
-
this.acronym = "
|
|
3496
|
-
this.name = "
|
|
3087
|
+
this.acronym = "PF";
|
|
3088
|
+
this.name = "Perfect";
|
|
3497
3089
|
this.droidRanked = true;
|
|
3498
3090
|
this.osuRanked = true;
|
|
3499
|
-
this.bitwise = 1 <<
|
|
3500
|
-
this.incompatibleMods.add(
|
|
3091
|
+
this.bitwise = 1 << 14;
|
|
3092
|
+
this.incompatibleMods.add(ModNoFail).add(ModSuddenDeath);
|
|
3093
|
+
}
|
|
3094
|
+
get isDroidRelevant() {
|
|
3095
|
+
return true;
|
|
3096
|
+
}
|
|
3097
|
+
calculateDroidScoreMultiplier() {
|
|
3098
|
+
return 1;
|
|
3099
|
+
}
|
|
3100
|
+
get isOsuRelevant() {
|
|
3101
|
+
return true;
|
|
3102
|
+
}
|
|
3103
|
+
get osuScoreMultiplier() {
|
|
3104
|
+
return 1;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
/**
|
|
3109
|
+
* Represents the NoFail mod.
|
|
3110
|
+
*/
|
|
3111
|
+
class ModNoFail extends Mod {
|
|
3112
|
+
constructor() {
|
|
3113
|
+
super();
|
|
3114
|
+
this.acronym = "NF";
|
|
3115
|
+
this.name = "NoFail";
|
|
3116
|
+
this.droidRanked = true;
|
|
3117
|
+
this.osuRanked = true;
|
|
3118
|
+
this.bitwise = 1 << 0;
|
|
3119
|
+
this.incompatibleMods.add(ModPerfect).add(ModSuddenDeath);
|
|
3501
3120
|
}
|
|
3502
3121
|
get isDroidRelevant() {
|
|
3503
3122
|
return true;
|
|
@@ -3511,88 +3130,179 @@ class ModEasy extends Mod {
|
|
|
3511
3130
|
get osuScoreMultiplier() {
|
|
3512
3131
|
return 0.5;
|
|
3513
3132
|
}
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
/**
|
|
3136
|
+
* Represents a spinner in a beatmap.
|
|
3137
|
+
*
|
|
3138
|
+
* All we need from spinners is their duration. The
|
|
3139
|
+
* position of a spinner is always at 256x192.
|
|
3140
|
+
*/
|
|
3141
|
+
class Spinner extends HitObject {
|
|
3142
|
+
get endTime() {
|
|
3143
|
+
return this._endTime;
|
|
3144
|
+
}
|
|
3145
|
+
constructor(values) {
|
|
3146
|
+
super(Object.assign(Object.assign({}, values), { position: Playfield.baseSize.divide(2) }));
|
|
3147
|
+
this._endTime = values.endTime;
|
|
3148
|
+
}
|
|
3149
|
+
applySamples(controlPoints) {
|
|
3150
|
+
super.applySamples(controlPoints);
|
|
3151
|
+
const samplePoints = controlPoints.sample.between(this.startTime + HitObject.controlPointLeniency, this.endTime + HitObject.controlPointLeniency);
|
|
3152
|
+
this.auxiliarySamples.length = 0;
|
|
3153
|
+
this.auxiliarySamples.push(new SequenceHitSampleInfo(samplePoints.map((s) => new TimedHitSampleInfo(s.time, s.applyTo(Spinner.baseSpinnerSpinSample)))));
|
|
3154
|
+
this.auxiliarySamples.push(new SequenceHitSampleInfo(samplePoints.map((s) => new TimedHitSampleInfo(s.time, s.applyTo(Spinner.baseSpinnerBonusSample)))));
|
|
3155
|
+
}
|
|
3156
|
+
getStackedPosition() {
|
|
3157
|
+
return this.position;
|
|
3158
|
+
}
|
|
3159
|
+
getStackedEndPosition() {
|
|
3160
|
+
return this.position;
|
|
3161
|
+
}
|
|
3162
|
+
createHitWindow() {
|
|
3163
|
+
return new EmptyHitWindow();
|
|
3164
|
+
}
|
|
3165
|
+
toString() {
|
|
3166
|
+
return `Position: [${this._position.x}, ${this._position.y}], duration: ${this.duration}`;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
Spinner.baseSpinnerSpinSample = new BankHitSampleInfo("spinnerspin");
|
|
3170
|
+
Spinner.baseSpinnerBonusSample = new BankHitSampleInfo("spinnerbonus");
|
|
3171
|
+
|
|
3172
|
+
/**
|
|
3173
|
+
* Represents the hit window of osu!droid _with_ the Precise mod.
|
|
3174
|
+
*/
|
|
3175
|
+
class PreciseDroidHitWindow extends HitWindow {
|
|
3176
|
+
/**
|
|
3177
|
+
* Calculates the overall difficulty value of a great (300) hit window.
|
|
3178
|
+
*
|
|
3179
|
+
* @param value The value of the hit window, in milliseconds.
|
|
3180
|
+
* @returns The overall difficulty value.
|
|
3181
|
+
*/
|
|
3182
|
+
static greatWindowToOD(value) {
|
|
3183
|
+
return 5 - (value - 55) / 6;
|
|
3184
|
+
}
|
|
3185
|
+
/**
|
|
3186
|
+
* Calculates the overall difficulty value of a good (100) hit window.
|
|
3187
|
+
*
|
|
3188
|
+
* @param value The value of the hit window, in milliseconds.
|
|
3189
|
+
* @returns The overall difficulty value.
|
|
3190
|
+
*/
|
|
3191
|
+
static okWindowToOD(value) {
|
|
3192
|
+
return 5 - (value - 120) / 8;
|
|
3193
|
+
}
|
|
3194
|
+
/**
|
|
3195
|
+
* Calculates the overall difficulty value of a meh (50) hit window.
|
|
3196
|
+
*
|
|
3197
|
+
* @param value The value of the hit window, in milliseconds.
|
|
3198
|
+
* @returns The overall difficulty value.
|
|
3199
|
+
*/
|
|
3200
|
+
static mehWindowToOD(value) {
|
|
3201
|
+
return 5 - (value - 180) / 10;
|
|
3202
|
+
}
|
|
3203
|
+
get greatWindow() {
|
|
3204
|
+
return 55 + 6 * (5 - this.overallDifficulty);
|
|
3205
|
+
}
|
|
3206
|
+
get okWindow() {
|
|
3207
|
+
return 120 + 8 * (5 - this.overallDifficulty);
|
|
3208
|
+
}
|
|
3209
|
+
get mehWindow() {
|
|
3210
|
+
return 180 + 10 * (5 - this.overallDifficulty);
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
/**
|
|
3215
|
+
* Represents the Precise mod.
|
|
3216
|
+
*/
|
|
3217
|
+
class ModPrecise extends Mod {
|
|
3218
|
+
constructor() {
|
|
3219
|
+
super(...arguments);
|
|
3220
|
+
this.acronym = "PR";
|
|
3221
|
+
this.name = "Precise";
|
|
3222
|
+
this.droidRanked = true;
|
|
3223
|
+
}
|
|
3224
|
+
get isDroidRelevant() {
|
|
3225
|
+
return true;
|
|
3226
|
+
}
|
|
3227
|
+
calculateDroidScoreMultiplier() {
|
|
3228
|
+
return 1.06;
|
|
3229
|
+
}
|
|
3230
|
+
applyToHitObject(mode, hitObject) {
|
|
3231
|
+
var _a, _b;
|
|
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);
|
|
3523
3241
|
}
|
|
3524
|
-
difficulty.ar /= 2;
|
|
3525
|
-
difficulty.od /= 2;
|
|
3526
|
-
difficulty.hp /= 2;
|
|
3527
3242
|
}
|
|
3528
3243
|
}
|
|
3529
3244
|
|
|
3530
3245
|
/**
|
|
3531
|
-
* Represents the
|
|
3246
|
+
* Represents the ReallyEasy mod.
|
|
3532
3247
|
*/
|
|
3533
|
-
class
|
|
3248
|
+
class ModReallyEasy extends Mod {
|
|
3534
3249
|
constructor() {
|
|
3535
3250
|
super(...arguments);
|
|
3536
|
-
this.acronym = "
|
|
3537
|
-
this.name = "
|
|
3538
|
-
this.droidRanked =
|
|
3539
|
-
this.osuRanked = true;
|
|
3540
|
-
this.bitwise = 1 << 10;
|
|
3541
|
-
/**
|
|
3542
|
-
* The amount of seconds until the Flashlight follow area reaches the cursor.
|
|
3543
|
-
*/
|
|
3544
|
-
this.followDelay = ModFlashlight.defaultFollowDelay;
|
|
3545
|
-
}
|
|
3546
|
-
copySettings(mod) {
|
|
3547
|
-
var _a, _b;
|
|
3548
|
-
super.copySettings(mod);
|
|
3549
|
-
this.followDelay =
|
|
3550
|
-
(_b = (_a = mod.settings) === null || _a === void 0 ? void 0 : _a.areaFollowDelay) !== null && _b !== void 0 ? _b : this.followDelay;
|
|
3251
|
+
this.acronym = "RE";
|
|
3252
|
+
this.name = "ReallyEasy";
|
|
3253
|
+
this.droidRanked = false;
|
|
3551
3254
|
}
|
|
3552
3255
|
get isDroidRelevant() {
|
|
3553
3256
|
return true;
|
|
3554
3257
|
}
|
|
3555
3258
|
calculateDroidScoreMultiplier() {
|
|
3556
|
-
return
|
|
3557
|
-
}
|
|
3558
|
-
get isOsuRelevant() {
|
|
3559
|
-
return true;
|
|
3560
|
-
}
|
|
3561
|
-
get osuScoreMultiplier() {
|
|
3562
|
-
return 1.12;
|
|
3563
|
-
}
|
|
3564
|
-
serializeSettings() {
|
|
3565
|
-
return { areaFollowDelay: this.followDelay };
|
|
3259
|
+
return 0.4;
|
|
3566
3260
|
}
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
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;
|
|
3570
3285
|
}
|
|
3571
|
-
return `${super.toString()} (${this.followDelay.toFixed(2)}s follow delay)`;
|
|
3572
3286
|
}
|
|
3573
3287
|
}
|
|
3574
|
-
/**
|
|
3575
|
-
* The default amount of seconds until the Flashlight follow area reaches the cursor.
|
|
3576
|
-
*/
|
|
3577
|
-
ModFlashlight.defaultFollowDelay = 0.12;
|
|
3578
3288
|
|
|
3579
3289
|
/**
|
|
3580
|
-
* Represents the
|
|
3290
|
+
* Represents the ScoreV2 mod.
|
|
3581
3291
|
*/
|
|
3582
|
-
class
|
|
3292
|
+
class ModScoreV2 extends Mod {
|
|
3583
3293
|
constructor() {
|
|
3584
|
-
super();
|
|
3585
|
-
this.acronym = "
|
|
3586
|
-
this.name = "
|
|
3294
|
+
super(...arguments);
|
|
3295
|
+
this.acronym = "V2";
|
|
3296
|
+
this.name = "ScoreV2";
|
|
3587
3297
|
this.droidRanked = false;
|
|
3588
3298
|
this.osuRanked = false;
|
|
3589
|
-
this.
|
|
3299
|
+
this.bitwise = 1 << 29;
|
|
3590
3300
|
}
|
|
3591
3301
|
get isDroidRelevant() {
|
|
3592
3302
|
return true;
|
|
3593
3303
|
}
|
|
3594
3304
|
calculateDroidScoreMultiplier() {
|
|
3595
|
-
return 1
|
|
3305
|
+
return 1;
|
|
3596
3306
|
}
|
|
3597
3307
|
get isOsuRelevant() {
|
|
3598
3308
|
return true;
|
|
@@ -3603,17 +3313,16 @@ class ModTraceable extends Mod {
|
|
|
3603
3313
|
}
|
|
3604
3314
|
|
|
3605
3315
|
/**
|
|
3606
|
-
* Represents the
|
|
3316
|
+
* Represents the SmallCircle mod.
|
|
3317
|
+
*
|
|
3318
|
+
* This is a legacy osu!droid mod that may still be exist when parsing replays.
|
|
3607
3319
|
*/
|
|
3608
|
-
class
|
|
3320
|
+
class ModSmallCircle extends Mod {
|
|
3609
3321
|
constructor() {
|
|
3610
|
-
super();
|
|
3611
|
-
this.acronym = "
|
|
3612
|
-
this.name = "
|
|
3613
|
-
this.droidRanked =
|
|
3614
|
-
this.osuRanked = true;
|
|
3615
|
-
this.bitwise = 1 << 3;
|
|
3616
|
-
this.incompatibleMods.add(ModTraceable);
|
|
3322
|
+
super(...arguments);
|
|
3323
|
+
this.acronym = "SC";
|
|
3324
|
+
this.name = "SmallCircle";
|
|
3325
|
+
this.droidRanked = false;
|
|
3617
3326
|
}
|
|
3618
3327
|
get isDroidRelevant() {
|
|
3619
3328
|
return true;
|
|
@@ -3621,71 +3330,55 @@ class ModHidden extends Mod {
|
|
|
3621
3330
|
calculateDroidScoreMultiplier() {
|
|
3622
3331
|
return 1.06;
|
|
3623
3332
|
}
|
|
3624
|
-
|
|
3625
|
-
return
|
|
3626
|
-
}
|
|
3627
|
-
get osuScoreMultiplier() {
|
|
3628
|
-
return 1.06;
|
|
3333
|
+
migrateDroidMod(difficulty) {
|
|
3334
|
+
return new ModDifficultyAdjust({ cs: difficulty.cs + 4 });
|
|
3629
3335
|
}
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
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;
|
|
3636
3346
|
}
|
|
3637
|
-
|
|
3638
|
-
|
|
3347
|
+
case exports.Modes.osu:
|
|
3348
|
+
difficulty.cs += 4;
|
|
3349
|
+
}
|
|
3639
3350
|
}
|
|
3640
3351
|
}
|
|
3641
|
-
ModHidden.fadeInDurationMultiplier = 0.4;
|
|
3642
|
-
ModHidden.fadeOutDurationMultiplier = 0.3;
|
|
3643
3352
|
|
|
3644
3353
|
/**
|
|
3645
|
-
* Represents the
|
|
3354
|
+
* Represents the SpunOut mod.
|
|
3646
3355
|
*/
|
|
3647
|
-
class
|
|
3356
|
+
class ModSpunOut extends Mod {
|
|
3648
3357
|
constructor() {
|
|
3649
|
-
super();
|
|
3650
|
-
this.acronym = "
|
|
3651
|
-
this.name = "
|
|
3652
|
-
this.droidRanked = true;
|
|
3358
|
+
super(...arguments);
|
|
3359
|
+
this.acronym = "SO";
|
|
3360
|
+
this.name = "SpunOut";
|
|
3653
3361
|
this.osuRanked = true;
|
|
3654
|
-
this.bitwise = 1 <<
|
|
3655
|
-
this.incompatibleMods.add(ModNoFail).add(ModPerfect);
|
|
3656
|
-
}
|
|
3657
|
-
get isDroidRelevant() {
|
|
3658
|
-
return true;
|
|
3659
|
-
}
|
|
3660
|
-
calculateDroidScoreMultiplier() {
|
|
3661
|
-
return 1;
|
|
3362
|
+
this.bitwise = 1 << 12;
|
|
3662
3363
|
}
|
|
3663
3364
|
get isOsuRelevant() {
|
|
3664
3365
|
return true;
|
|
3665
3366
|
}
|
|
3666
3367
|
get osuScoreMultiplier() {
|
|
3667
|
-
return
|
|
3368
|
+
return 0.9;
|
|
3668
3369
|
}
|
|
3669
3370
|
}
|
|
3670
3371
|
|
|
3671
3372
|
/**
|
|
3672
|
-
* Represents the
|
|
3373
|
+
* Represents the TouchDevice mod.
|
|
3673
3374
|
*/
|
|
3674
|
-
class
|
|
3375
|
+
class ModTouchDevice extends Mod {
|
|
3675
3376
|
constructor() {
|
|
3676
|
-
super();
|
|
3677
|
-
this.acronym = "
|
|
3678
|
-
this.name = "
|
|
3679
|
-
this.droidRanked = true;
|
|
3377
|
+
super(...arguments);
|
|
3378
|
+
this.acronym = "TD";
|
|
3379
|
+
this.name = "TouchDevice";
|
|
3680
3380
|
this.osuRanked = true;
|
|
3681
|
-
this.bitwise = 1 <<
|
|
3682
|
-
this.incompatibleMods.add(ModNoFail).add(ModSuddenDeath);
|
|
3683
|
-
}
|
|
3684
|
-
get isDroidRelevant() {
|
|
3685
|
-
return true;
|
|
3686
|
-
}
|
|
3687
|
-
calculateDroidScoreMultiplier() {
|
|
3688
|
-
return 1;
|
|
3381
|
+
this.bitwise = 1 << 2;
|
|
3689
3382
|
}
|
|
3690
3383
|
get isOsuRelevant() {
|
|
3691
3384
|
return true;
|
|
@@ -3696,417 +3389,771 @@ class ModPerfect extends Mod {
|
|
|
3696
3389
|
}
|
|
3697
3390
|
|
|
3698
3391
|
/**
|
|
3699
|
-
*
|
|
3392
|
+
* Utilities for mods.
|
|
3700
3393
|
*/
|
|
3701
|
-
class
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
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;
|
|
3716
3413
|
}
|
|
3717
|
-
|
|
3718
|
-
|
|
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;
|
|
3719
3426
|
}
|
|
3720
|
-
|
|
3721
|
-
|
|
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;
|
|
3722
3447
|
}
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
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;
|
|
3734
3470
|
}
|
|
3735
|
-
|
|
3736
|
-
|
|
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;
|
|
3737
3486
|
}
|
|
3738
|
-
|
|
3739
|
-
|
|
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
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
return strs.join();
|
|
3740
3504
|
}
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
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
|
+
}
|
|
3745
3529
|
}
|
|
3746
|
-
|
|
3747
|
-
if (
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
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
|
+
}
|
|
3751
3539
|
}
|
|
3752
|
-
const customSpeed = mods.find((m) => m instanceof ModCustomSpeed);
|
|
3753
|
-
difficulty.ar -= 0.5;
|
|
3754
|
-
difficulty.ar -= ((_a = customSpeed === null || customSpeed === void 0 ? void 0 : customSpeed.trackRateMultiplier) !== null && _a !== void 0 ? _a : 1) - 1;
|
|
3755
3540
|
}
|
|
3756
|
-
if (
|
|
3757
|
-
|
|
3758
|
-
difficulty.cs = CircleSizeCalculator.droidScaleToDroidCS(scale + 0.125);
|
|
3541
|
+
if (!withRateChange) {
|
|
3542
|
+
return;
|
|
3759
3543
|
}
|
|
3760
|
-
|
|
3761
|
-
|
|
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
|
+
}
|
|
3762
3563
|
}
|
|
3763
|
-
|
|
3764
|
-
|
|
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
|
+
}
|
|
3765
3578
|
}
|
|
3579
|
+
return rate;
|
|
3766
3580
|
}
|
|
3767
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);
|
|
3614
|
+
}
|
|
3615
|
+
return map;
|
|
3616
|
+
})();
|
|
3768
3617
|
|
|
3769
3618
|
/**
|
|
3770
|
-
*
|
|
3619
|
+
* A map that stores `Mod`s depending on their type.
|
|
3771
3620
|
*
|
|
3772
|
-
* This
|
|
3621
|
+
* This also has additional utilities to eliminate unnecessary `Mod`s.
|
|
3773
3622
|
*/
|
|
3774
|
-
class
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
this.
|
|
3780
|
-
}
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
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
|
+
get(key) {
|
|
3642
|
+
return super.get(key);
|
|
3643
|
+
}
|
|
3644
|
+
set(keyOrValue) {
|
|
3645
|
+
const key = (keyOrValue instanceof Mod ? keyOrValue.constructor : keyOrValue);
|
|
3646
|
+
const value = keyOrValue instanceof Mod ? keyOrValue : new key();
|
|
3647
|
+
// Ensure the mod type corresponds to the mod instance.
|
|
3648
|
+
if (key !== value.constructor) {
|
|
3649
|
+
throw new TypeError(`Key ${key.name} does not match value ${value.constructor.name}`);
|
|
3650
|
+
}
|
|
3651
|
+
const existing = this.get(key);
|
|
3652
|
+
// If all difficulty statistics are set, all other difficulty adjusting mods are irrelevant, so we remove them.
|
|
3653
|
+
// This prevents potential abuse cases where score multipliers from non-affecting mods stack (i.e., forcing
|
|
3654
|
+
// all difficulty statistics while using the Hard Rock mod).
|
|
3655
|
+
const removeDifficultyAdjustmentMods = value instanceof ModDifficultyAdjust &&
|
|
3656
|
+
value.cs !== undefined &&
|
|
3657
|
+
value.ar !== undefined &&
|
|
3658
|
+
value.od !== undefined &&
|
|
3659
|
+
value.hp !== undefined;
|
|
3660
|
+
if (removeDifficultyAdjustmentMods) {
|
|
3661
|
+
this.delete(ModEasy);
|
|
3662
|
+
this.delete(ModHardRock);
|
|
3663
|
+
this.delete(ModReallyEasy);
|
|
3664
|
+
this.delete(ModSmallCircle);
|
|
3665
|
+
}
|
|
3666
|
+
// Check if there are any mods that are incompatible with the new mod.
|
|
3667
|
+
// If so, remove them.
|
|
3668
|
+
for (const incompatibleMod of value.incompatibleMods) {
|
|
3669
|
+
for (const mod of this.values()) {
|
|
3670
|
+
if (mod instanceof incompatibleMod) {
|
|
3671
|
+
this.delete(mod.constructor);
|
|
3672
|
+
}
|
|
3800
3673
|
}
|
|
3801
|
-
case exports.Modes.osu:
|
|
3802
|
-
difficulty.cs += 4;
|
|
3803
3674
|
}
|
|
3675
|
+
super.set(key, value);
|
|
3676
|
+
return existing !== null && existing !== void 0 ? existing : null;
|
|
3677
|
+
}
|
|
3678
|
+
/**
|
|
3679
|
+
* Serializes all `Mod`s that are in this map.
|
|
3680
|
+
*/
|
|
3681
|
+
serializeMods() {
|
|
3682
|
+
return ModUtil.serializeMods(this.values());
|
|
3804
3683
|
}
|
|
3805
3684
|
}
|
|
3806
3685
|
|
|
3807
3686
|
/**
|
|
3808
|
-
* Represents
|
|
3687
|
+
* Represents a circle in a beatmap.
|
|
3688
|
+
*
|
|
3689
|
+
* All we need from circles is their position. All positions
|
|
3690
|
+
* stored in the objects are in playfield coordinates (512*384
|
|
3691
|
+
* rectangle).
|
|
3809
3692
|
*/
|
|
3810
|
-
class
|
|
3811
|
-
constructor() {
|
|
3812
|
-
super(
|
|
3813
|
-
this.acronym = "SO";
|
|
3814
|
-
this.name = "SpunOut";
|
|
3815
|
-
this.osuRanked = true;
|
|
3816
|
-
this.bitwise = 1 << 12;
|
|
3817
|
-
}
|
|
3818
|
-
get isOsuRelevant() {
|
|
3819
|
-
return true;
|
|
3693
|
+
class Circle extends HitObject {
|
|
3694
|
+
constructor(values) {
|
|
3695
|
+
super(values);
|
|
3820
3696
|
}
|
|
3821
|
-
|
|
3822
|
-
return
|
|
3697
|
+
toString() {
|
|
3698
|
+
return `Position: [${this._position.x}, ${this._position.y}]`;
|
|
3823
3699
|
}
|
|
3824
3700
|
}
|
|
3825
3701
|
|
|
3826
3702
|
/**
|
|
3827
|
-
*
|
|
3703
|
+
* Contains information about hit objects of a beatmap.
|
|
3828
3704
|
*/
|
|
3829
|
-
class
|
|
3705
|
+
class BeatmapHitObjects {
|
|
3830
3706
|
constructor() {
|
|
3831
|
-
|
|
3832
|
-
this.
|
|
3833
|
-
this.
|
|
3834
|
-
this.
|
|
3835
|
-
this.bitwise = 1 << 2;
|
|
3707
|
+
this._objects = [];
|
|
3708
|
+
this._circles = 0;
|
|
3709
|
+
this._sliders = 0;
|
|
3710
|
+
this._spinners = 0;
|
|
3836
3711
|
}
|
|
3837
|
-
|
|
3838
|
-
|
|
3712
|
+
/**
|
|
3713
|
+
* The objects of the beatmap.
|
|
3714
|
+
*/
|
|
3715
|
+
get objects() {
|
|
3716
|
+
return this._objects;
|
|
3839
3717
|
}
|
|
3840
|
-
|
|
3841
|
-
|
|
3718
|
+
/**
|
|
3719
|
+
* The amount of circles in the beatmap.
|
|
3720
|
+
*/
|
|
3721
|
+
get circles() {
|
|
3722
|
+
return this._circles;
|
|
3842
3723
|
}
|
|
3843
|
-
}
|
|
3844
|
-
|
|
3845
|
-
/**
|
|
3846
|
-
* Utilities for mods.
|
|
3847
|
-
*/
|
|
3848
|
-
class ModUtil {
|
|
3849
3724
|
/**
|
|
3850
|
-
*
|
|
3725
|
+
* The amount of sliders in the beatmap.
|
|
3726
|
+
*/
|
|
3727
|
+
get sliders() {
|
|
3728
|
+
return this._sliders;
|
|
3729
|
+
}
|
|
3730
|
+
/**
|
|
3731
|
+
* The amount of spinners in the beatmap.
|
|
3732
|
+
*/
|
|
3733
|
+
get spinners() {
|
|
3734
|
+
return this._spinners;
|
|
3735
|
+
}
|
|
3736
|
+
/**
|
|
3737
|
+
* The amount of slider ticks in the beatmap.
|
|
3851
3738
|
*
|
|
3852
|
-
*
|
|
3853
|
-
* @param options Options for parsing behavior.
|
|
3739
|
+
* This iterates through all objects and should be stored locally or used sparingly.
|
|
3854
3740
|
*/
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
return [];
|
|
3858
|
-
}
|
|
3859
|
-
const mods = [];
|
|
3860
|
-
for (const modType of this.allMods.values()) {
|
|
3861
|
-
const mod = new modType();
|
|
3862
|
-
if (mod.isApplicableToOsuStable() && (mod.bitwise & modbits) > 0) {
|
|
3863
|
-
mods.push(mod);
|
|
3864
|
-
}
|
|
3865
|
-
}
|
|
3866
|
-
return this.processParsingOptions(mods, options);
|
|
3741
|
+
get sliderTicks() {
|
|
3742
|
+
return this.objects.reduce((acc, cur) => (cur instanceof Slider ? acc + cur.ticks : acc), 0);
|
|
3867
3743
|
}
|
|
3868
3744
|
/**
|
|
3869
|
-
*
|
|
3745
|
+
* The amount of sliderends in the beatmap.
|
|
3746
|
+
*/
|
|
3747
|
+
get sliderEnds() {
|
|
3748
|
+
return this.sliders;
|
|
3749
|
+
}
|
|
3750
|
+
/**
|
|
3751
|
+
* The amount of slider repeat points in the beatmap.
|
|
3870
3752
|
*
|
|
3871
|
-
*
|
|
3872
|
-
* @returns The serialized list of `Mod`s.
|
|
3753
|
+
* This iterates through all objects and should be stored locally or used sparingly.
|
|
3873
3754
|
*/
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
for (const mod of mods) {
|
|
3877
|
-
serializedMods.push(mod.serialize());
|
|
3878
|
-
}
|
|
3879
|
-
return serializedMods;
|
|
3755
|
+
get sliderRepeatPoints() {
|
|
3756
|
+
return this.objects.reduce((acc, cur) => (cur instanceof Slider ? acc + cur.repeatCount : acc), 0);
|
|
3880
3757
|
}
|
|
3881
3758
|
/**
|
|
3882
|
-
*
|
|
3759
|
+
* Adds hitobjects.
|
|
3883
3760
|
*
|
|
3884
|
-
*
|
|
3885
|
-
*
|
|
3761
|
+
* The sorting order of hitobjects will be maintained.
|
|
3762
|
+
*
|
|
3763
|
+
* @param objects The hitobjects to add.
|
|
3886
3764
|
*/
|
|
3887
|
-
|
|
3888
|
-
const
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3765
|
+
add(...objects) {
|
|
3766
|
+
for (const object of objects) {
|
|
3767
|
+
// Objects may be out of order *only* if a user has manually edited an .osu file.
|
|
3768
|
+
// Unfortunately there are "ranked" maps in this state (example: https://osu.ppy.sh/s/594828).
|
|
3769
|
+
// Finding index is used to guarantee that the parsing order of hitobjects with equal start times is maintained (stably-sorted).
|
|
3770
|
+
this._objects.splice(this.findInsertionIndex(object.startTime), 0, object);
|
|
3771
|
+
if (object instanceof Circle) {
|
|
3772
|
+
++this._circles;
|
|
3893
3773
|
}
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3774
|
+
else if (object instanceof Slider) {
|
|
3775
|
+
++this._sliders;
|
|
3776
|
+
}
|
|
3777
|
+
else {
|
|
3778
|
+
++this._spinners;
|
|
3897
3779
|
}
|
|
3898
|
-
deserializedMods.push(mod);
|
|
3899
3780
|
}
|
|
3900
|
-
return deserializedMods;
|
|
3901
3781
|
}
|
|
3902
3782
|
/**
|
|
3903
|
-
*
|
|
3783
|
+
* Removes a hitobject at an index.
|
|
3904
3784
|
*
|
|
3905
|
-
* @param
|
|
3906
|
-
* @
|
|
3785
|
+
* @param index The index of the hitobject to remove.
|
|
3786
|
+
* @returns The hitobject that was removed, `null` if no hitobject was removed.
|
|
3907
3787
|
*/
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
for (const acronym of this.allMods.keys()) {
|
|
3914
|
-
if (str.startsWith(acronym.toLowerCase())) {
|
|
3915
|
-
const modType = this.allMods.get(acronym);
|
|
3916
|
-
finalMods.push(new modType());
|
|
3917
|
-
nchars = acronym.length;
|
|
3918
|
-
break;
|
|
3919
|
-
}
|
|
3920
|
-
}
|
|
3921
|
-
str = str.slice(nchars);
|
|
3788
|
+
removeAt(index) {
|
|
3789
|
+
var _a;
|
|
3790
|
+
const object = (_a = this._objects.splice(index, 1)[0]) !== null && _a !== void 0 ? _a : null;
|
|
3791
|
+
if (object instanceof Circle) {
|
|
3792
|
+
--this._circles;
|
|
3922
3793
|
}
|
|
3923
|
-
|
|
3794
|
+
else if (object instanceof Slider) {
|
|
3795
|
+
--this._sliders;
|
|
3796
|
+
}
|
|
3797
|
+
else if (object instanceof Spinner) {
|
|
3798
|
+
--this._spinners;
|
|
3799
|
+
}
|
|
3800
|
+
return object;
|
|
3924
3801
|
}
|
|
3925
3802
|
/**
|
|
3926
|
-
*
|
|
3927
|
-
*
|
|
3928
|
-
* @param mods The array of mods to convert.
|
|
3929
|
-
* @returns The string representing the mods in osu!standard.
|
|
3803
|
+
* Clears all hitobjects.
|
|
3930
3804
|
*/
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
return a + v.acronym;
|
|
3937
|
-
}, "");
|
|
3805
|
+
clear() {
|
|
3806
|
+
this._objects.length = 0;
|
|
3807
|
+
this._circles = 0;
|
|
3808
|
+
this._sliders = 0;
|
|
3809
|
+
this._spinners = 0;
|
|
3938
3810
|
}
|
|
3939
3811
|
/**
|
|
3940
|
-
*
|
|
3812
|
+
* Finds the insertion index of a hitobject in a given time.
|
|
3941
3813
|
*
|
|
3942
|
-
* @param
|
|
3943
|
-
* @returns The string representing the `Mod`s in ordered form.
|
|
3814
|
+
* @param startTime The start time of the hitobject.
|
|
3944
3815
|
*/
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3816
|
+
findInsertionIndex(startTime) {
|
|
3817
|
+
if (this._objects.length === 0 ||
|
|
3818
|
+
startTime < this._objects[0].startTime) {
|
|
3819
|
+
return 0;
|
|
3820
|
+
}
|
|
3821
|
+
if (startTime >= this._objects.at(-1).startTime) {
|
|
3822
|
+
return this._objects.length;
|
|
3823
|
+
}
|
|
3824
|
+
let l = 0;
|
|
3825
|
+
let r = this._objects.length - 2;
|
|
3826
|
+
while (l <= r) {
|
|
3827
|
+
const pivot = l + ((r - l) >> 1);
|
|
3828
|
+
if (this._objects[pivot].startTime < startTime) {
|
|
3829
|
+
l = pivot + 1;
|
|
3830
|
+
}
|
|
3831
|
+
else if (this._objects[pivot].startTime > startTime) {
|
|
3832
|
+
r = pivot - 1;
|
|
3833
|
+
}
|
|
3834
|
+
else {
|
|
3835
|
+
return pivot;
|
|
3953
3836
|
}
|
|
3954
3837
|
}
|
|
3955
|
-
return
|
|
3838
|
+
return l;
|
|
3956
3839
|
}
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
/**
|
|
3843
|
+
* Converts a beatmap for another mode.
|
|
3844
|
+
*/
|
|
3845
|
+
class BeatmapConverter {
|
|
3846
|
+
constructor(beatmap) {
|
|
3847
|
+
this.beatmap = beatmap;
|
|
3965
3848
|
}
|
|
3966
3849
|
/**
|
|
3967
|
-
*
|
|
3850
|
+
* Converts the beatmap.
|
|
3968
3851
|
*
|
|
3969
|
-
* @
|
|
3970
|
-
* @returns Mods that have been filtered.
|
|
3852
|
+
* @returns The converted beatmap.
|
|
3971
3853
|
*/
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3854
|
+
convert() {
|
|
3855
|
+
const converted = new Beatmap(this.beatmap);
|
|
3856
|
+
// Shallow clone isn't enough to ensure we don't mutate some beatmap properties unexpectedly.
|
|
3857
|
+
converted.difficulty = new BeatmapDifficulty(this.beatmap.difficulty);
|
|
3858
|
+
converted.hitObjects = this.convertHitObjects();
|
|
3859
|
+
return converted;
|
|
3860
|
+
}
|
|
3861
|
+
convertHitObjects() {
|
|
3862
|
+
const hitObjects = new BeatmapHitObjects();
|
|
3863
|
+
this.beatmap.hitObjects.objects.forEach((hitObject) => {
|
|
3864
|
+
hitObjects.add(this.convertHitObject(hitObject));
|
|
3865
|
+
});
|
|
3866
|
+
return hitObjects;
|
|
3867
|
+
}
|
|
3868
|
+
convertHitObject(hitObject) {
|
|
3869
|
+
let object;
|
|
3870
|
+
if (hitObject instanceof Circle) {
|
|
3871
|
+
object = new Circle({
|
|
3872
|
+
startTime: hitObject.startTime,
|
|
3873
|
+
position: hitObject.position,
|
|
3874
|
+
newCombo: hitObject.isNewCombo,
|
|
3875
|
+
type: hitObject.type,
|
|
3876
|
+
comboOffset: hitObject.comboOffset,
|
|
3877
|
+
});
|
|
3878
|
+
}
|
|
3879
|
+
else if (hitObject instanceof Slider) {
|
|
3880
|
+
object = new Slider({
|
|
3881
|
+
startTime: hitObject.startTime,
|
|
3882
|
+
position: hitObject.position,
|
|
3883
|
+
newCombo: hitObject.isNewCombo,
|
|
3884
|
+
type: hitObject.type,
|
|
3885
|
+
path: hitObject.path,
|
|
3886
|
+
repeatCount: hitObject.repeatCount,
|
|
3887
|
+
nodeSamples: hitObject.nodeSamples,
|
|
3888
|
+
comboOffset: hitObject.comboOffset,
|
|
3889
|
+
tickDistanceMultiplier:
|
|
3890
|
+
// Prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
|
3891
|
+
// This results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
|
3892
|
+
this.beatmap.formatVersion < 8
|
|
3893
|
+
? 1 /
|
|
3894
|
+
this.beatmap.controlPoints.difficulty.controlPointAt(hitObject.startTime).speedMultiplier
|
|
3895
|
+
: 1,
|
|
3896
|
+
});
|
|
3897
|
+
}
|
|
3898
|
+
else {
|
|
3899
|
+
object = new Spinner({
|
|
3900
|
+
startTime: hitObject.startTime,
|
|
3901
|
+
endTime: hitObject.endTime,
|
|
3902
|
+
type: hitObject.type,
|
|
3903
|
+
});
|
|
3982
3904
|
}
|
|
3983
|
-
|
|
3905
|
+
object.samples = hitObject.samples;
|
|
3906
|
+
object.auxiliarySamples = hitObject.auxiliarySamples;
|
|
3907
|
+
return object;
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
/**
|
|
3912
|
+
* Provides functionality to alter a beatmap after it has been converted.
|
|
3913
|
+
*/
|
|
3914
|
+
class BeatmapProcessor {
|
|
3915
|
+
constructor(beatmap) {
|
|
3916
|
+
this.beatmap = beatmap;
|
|
3984
3917
|
}
|
|
3985
3918
|
/**
|
|
3986
|
-
*
|
|
3919
|
+
* Processes the converted beatmap prior to `HitObject.applyDefaults` being invoked.
|
|
3987
3920
|
*
|
|
3988
|
-
*
|
|
3989
|
-
*
|
|
3921
|
+
* Nested hitobjects generated during `HitObject.applyDefaults` will not be present by this point,
|
|
3922
|
+
* and no mods will have been applied to the hitobjects.
|
|
3923
|
+
*
|
|
3924
|
+
* This can only be used to add alterations to hitobjects generated directly through the conversion process.
|
|
3990
3925
|
*/
|
|
3991
|
-
|
|
3992
|
-
|
|
3926
|
+
preProcess() {
|
|
3927
|
+
let last = null;
|
|
3928
|
+
for (const object of this.beatmap.hitObjects.objects) {
|
|
3929
|
+
object.updateComboInformation(last);
|
|
3930
|
+
last = object;
|
|
3931
|
+
}
|
|
3932
|
+
// Mark the last object in the beatmap as last in combo.
|
|
3933
|
+
if (last) {
|
|
3934
|
+
last.isLastInCombo = true;
|
|
3935
|
+
}
|
|
3993
3936
|
}
|
|
3994
3937
|
/**
|
|
3995
|
-
*
|
|
3938
|
+
* Processes the converted beatmap after `HitObject.applyDefaults` has been invoked.
|
|
3996
3939
|
*
|
|
3997
|
-
*
|
|
3998
|
-
*
|
|
3999
|
-
*
|
|
4000
|
-
*
|
|
4001
|
-
*
|
|
3940
|
+
* Nested hitobjects generated during `HitObject.applyDefaults` wil be present by this point,
|
|
3941
|
+
* and mods will have been applied to all hitobjects.
|
|
3942
|
+
*
|
|
3943
|
+
* This should be used to add alterations to hitobjects while they are in their most playable state.
|
|
3944
|
+
*
|
|
3945
|
+
* @param mode The mode to add alterations for.
|
|
4002
3946
|
*/
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
mod.applyToDifficulty(mode, difficulty);
|
|
4007
|
-
}
|
|
4008
|
-
}
|
|
4009
|
-
let rate = 1;
|
|
4010
|
-
for (const mod of mods) {
|
|
4011
|
-
if (mod.isApplicableToDifficultyWithSettings()) {
|
|
4012
|
-
mod.applyToDifficultyWithSettings(mode, difficulty, mods);
|
|
4013
|
-
}
|
|
4014
|
-
if (mod.isApplicableToTrackRate()) {
|
|
4015
|
-
rate = mod.applyToRate(0, rate);
|
|
4016
|
-
}
|
|
4017
|
-
}
|
|
4018
|
-
if (!withRateChange) {
|
|
3947
|
+
postProcess(mode) {
|
|
3948
|
+
const objects = this.beatmap.hitObjects.objects;
|
|
3949
|
+
if (objects.length === 0) {
|
|
4019
3950
|
return;
|
|
4020
3951
|
}
|
|
4021
|
-
//
|
|
4022
|
-
|
|
4023
|
-
|
|
3952
|
+
// Reset stacking
|
|
3953
|
+
objects.forEach((h) => {
|
|
3954
|
+
h.stackHeight = 0;
|
|
3955
|
+
});
|
|
4024
3956
|
switch (mode) {
|
|
4025
3957
|
case exports.Modes.droid:
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
3958
|
+
this.applyDroidStacking();
|
|
3959
|
+
break;
|
|
3960
|
+
case exports.Modes.osu:
|
|
3961
|
+
if (this.beatmap.formatVersion >= 6) {
|
|
3962
|
+
this.applyStandardStacking();
|
|
4029
3963
|
}
|
|
4030
3964
|
else {
|
|
4031
|
-
|
|
4032
|
-
difficulty.od = DroidHitWindow.greatWindowToOD(hitWindow.greatWindow / rate);
|
|
3965
|
+
this.applyStandardOldStacking();
|
|
4033
3966
|
}
|
|
4034
3967
|
break;
|
|
4035
|
-
case exports.Modes.osu: {
|
|
4036
|
-
const hitWindow = new OsuHitWindow(difficulty.od);
|
|
4037
|
-
difficulty.od = OsuHitWindow.greatWindowToOD(hitWindow.greatWindow / rate);
|
|
4038
|
-
break;
|
|
4039
|
-
}
|
|
4040
3968
|
}
|
|
4041
3969
|
}
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
3970
|
+
applyDroidStacking() {
|
|
3971
|
+
const objects = this.beatmap.hitObjects.objects;
|
|
3972
|
+
if (objects.length === 0) {
|
|
3973
|
+
return;
|
|
3974
|
+
}
|
|
3975
|
+
const convertedScale = CircleSizeCalculator.standardScaleToDroidScale(objects[0].scale);
|
|
3976
|
+
for (let i = 0; i < objects.length - 1; ++i) {
|
|
3977
|
+
const current = objects[i];
|
|
3978
|
+
const next = objects[i + 1];
|
|
3979
|
+
if (current instanceof Circle &&
|
|
3980
|
+
next.startTime - current.startTime <
|
|
3981
|
+
2000 * this.beatmap.general.stackLeniency &&
|
|
3982
|
+
next.position.getDistance(current.position) <
|
|
3983
|
+
Math.sqrt(convertedScale)) {
|
|
3984
|
+
next.stackHeight = current.stackHeight + 1;
|
|
4054
3985
|
}
|
|
4055
3986
|
}
|
|
4056
|
-
return rate;
|
|
4057
3987
|
}
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
3988
|
+
applyStandardStacking() {
|
|
3989
|
+
const objects = this.beatmap.hitObjects.objects;
|
|
3990
|
+
const startIndex = 0;
|
|
3991
|
+
const endIndex = objects.length - 1;
|
|
3992
|
+
let extendedEndIndex = endIndex;
|
|
3993
|
+
if (endIndex < objects.length - 1) {
|
|
3994
|
+
for (let i = endIndex; i >= startIndex; --i) {
|
|
3995
|
+
let stackBaseIndex = i;
|
|
3996
|
+
for (let n = stackBaseIndex + 1; n < objects.length; ++n) {
|
|
3997
|
+
const stackBaseObject = objects[stackBaseIndex];
|
|
3998
|
+
if (stackBaseObject instanceof Spinner) {
|
|
3999
|
+
break;
|
|
4000
|
+
}
|
|
4001
|
+
const objectN = objects[n];
|
|
4002
|
+
if (objectN instanceof Spinner) {
|
|
4003
|
+
break;
|
|
4004
|
+
}
|
|
4005
|
+
const stackThreshold = objectN.timePreempt *
|
|
4006
|
+
this.beatmap.general.stackLeniency;
|
|
4007
|
+
if (objectN.startTime - stackBaseObject.endTime >
|
|
4008
|
+
stackThreshold) {
|
|
4009
|
+
// We are no longer within stacking range of the next object.
|
|
4010
|
+
break;
|
|
4011
|
+
}
|
|
4012
|
+
const endPositionDistanceCheck = stackBaseObject instanceof Slider
|
|
4013
|
+
? stackBaseObject.endPosition.getDistance(objectN.position) < BeatmapProcessor.stackDistance
|
|
4014
|
+
: false;
|
|
4015
|
+
if (stackBaseObject.position.getDistance(objectN.position) <
|
|
4016
|
+
BeatmapProcessor.stackDistance ||
|
|
4017
|
+
endPositionDistanceCheck) {
|
|
4018
|
+
stackBaseIndex = n;
|
|
4019
|
+
// Hit objects after the specified update range haven't been reset yet
|
|
4020
|
+
objectN.stackHeight = 0;
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
if (stackBaseIndex > extendedEndIndex) {
|
|
4024
|
+
extendedEndIndex = stackBaseIndex;
|
|
4025
|
+
if (extendedEndIndex === objects.length - 1) {
|
|
4026
|
+
break;
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
// Reverse pass for stack calculation.
|
|
4032
|
+
let extendedStartIndex = startIndex;
|
|
4033
|
+
for (let i = extendedEndIndex; i > startIndex; --i) {
|
|
4034
|
+
let n = i;
|
|
4035
|
+
// We should check every note which has not yet got a stack.
|
|
4036
|
+
// Consider the case we have two inter-wound stacks and this will make sense.
|
|
4037
|
+
//
|
|
4038
|
+
// o <-1 o <-2
|
|
4039
|
+
// o <-3 o <-4
|
|
4040
|
+
//
|
|
4041
|
+
// We first process starting from 4 and handle 2,
|
|
4042
|
+
// then we come backwards on the i loop iteration until we reach 3 and handle 1.
|
|
4043
|
+
// 2 and 1 will be ignored in the i loop because they already have a stack value.
|
|
4044
|
+
let objectI = objects[i];
|
|
4045
|
+
if (objectI.stackHeight !== 0 || objectI instanceof Spinner) {
|
|
4046
|
+
continue;
|
|
4047
|
+
}
|
|
4048
|
+
const stackThreshold = objectI.timePreempt * this.beatmap.general.stackLeniency;
|
|
4049
|
+
// If this object is a hit circle, then we enter this "special" case.
|
|
4050
|
+
// It either ends with a stack of hit circles only, or a stack of hit circles that are underneath a slider.
|
|
4051
|
+
// Any other case is handled by the "instanceof Slider" code below this.
|
|
4052
|
+
if (objectI instanceof Circle) {
|
|
4053
|
+
while (--n >= 0) {
|
|
4054
|
+
const objectN = objects[n];
|
|
4055
|
+
if (objectN instanceof Spinner) {
|
|
4056
|
+
continue;
|
|
4057
|
+
}
|
|
4058
|
+
if (objectI.startTime - objectN.endTime > stackThreshold) {
|
|
4059
|
+
// We are no longer within stacking range of the previous object.
|
|
4060
|
+
break;
|
|
4061
|
+
}
|
|
4062
|
+
// Hit objects before the specified update range haven't been reset yet
|
|
4063
|
+
if (n < extendedStartIndex) {
|
|
4064
|
+
objectN.stackHeight = 0;
|
|
4065
|
+
extendedStartIndex = n;
|
|
4066
|
+
}
|
|
4067
|
+
// 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.
|
|
4068
|
+
// o==o <- slider is at original location
|
|
4069
|
+
// o <- hitCircle has stack of -1
|
|
4070
|
+
// o <- hitCircle has stack of -2
|
|
4071
|
+
if (objectN instanceof Slider &&
|
|
4072
|
+
objectN.endPosition.getDistance(objectI.position) <
|
|
4073
|
+
BeatmapProcessor.stackDistance) {
|
|
4074
|
+
const offset = objectI.stackHeight - objectN.stackHeight + 1;
|
|
4075
|
+
for (let j = n + 1; j <= i; ++j) {
|
|
4076
|
+
// For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above).
|
|
4077
|
+
const objectJ = objects[j];
|
|
4078
|
+
if (objectN.endPosition.getDistance(objectJ.position) < BeatmapProcessor.stackDistance) {
|
|
4079
|
+
objectJ.stackHeight -= offset;
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
// We have hit a slider. We should restart calculation using this as the new base.
|
|
4083
|
+
// Breaking here will mean that the slider still has a stack count of 0, so will be handled in the i-outer-loop.
|
|
4084
|
+
break;
|
|
4085
|
+
}
|
|
4086
|
+
if (objectN.position.getDistance(objectI.position) <
|
|
4087
|
+
BeatmapProcessor.stackDistance) {
|
|
4088
|
+
// Keep processing as if there are no sliders. If we come across a slider, this gets cancelled out.
|
|
4089
|
+
// NOTE: Sliders with start positions stacking are a special case that is also handled here.
|
|
4090
|
+
objectN.stackHeight = objectI.stackHeight + 1;
|
|
4091
|
+
objectI = objectN;
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
else if (objectI instanceof Slider) {
|
|
4096
|
+
// We have hit the first slider in a possible stack.
|
|
4097
|
+
// From this point on, we ALWAYS stack positive regardless.
|
|
4098
|
+
while (--n >= startIndex) {
|
|
4099
|
+
const objectN = objects[n];
|
|
4100
|
+
if (objectN instanceof Spinner) {
|
|
4101
|
+
continue;
|
|
4102
|
+
}
|
|
4103
|
+
if (objectI.startTime - objectN.startTime >
|
|
4104
|
+
stackThreshold) {
|
|
4105
|
+
// We are no longer within stacking range of the previous object.
|
|
4106
|
+
break;
|
|
4107
|
+
}
|
|
4108
|
+
if (objectN.endPosition.getDistance(objectI.position) <
|
|
4109
|
+
BeatmapProcessor.stackDistance) {
|
|
4110
|
+
objectN.stackHeight = objectI.stackHeight + 1;
|
|
4111
|
+
objectI = objectN;
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4068
4115
|
}
|
|
4069
|
-
|
|
4070
|
-
|
|
4116
|
+
}
|
|
4117
|
+
applyStandardOldStacking() {
|
|
4118
|
+
const objects = this.beatmap.hitObjects.objects;
|
|
4119
|
+
for (let i = 0; i < objects.length; ++i) {
|
|
4120
|
+
const currentObject = objects[i];
|
|
4121
|
+
if (currentObject.stackHeight !== 0 &&
|
|
4122
|
+
!(currentObject instanceof Slider)) {
|
|
4123
|
+
continue;
|
|
4124
|
+
}
|
|
4125
|
+
let startTime = currentObject.endTime;
|
|
4126
|
+
let sliderStack = 0;
|
|
4127
|
+
const stackThreshold = currentObject.timePreempt * this.beatmap.general.stackLeniency;
|
|
4128
|
+
for (let j = i + 1; j < objects.length; ++j) {
|
|
4129
|
+
if (objects[j].startTime - stackThreshold > startTime) {
|
|
4130
|
+
break;
|
|
4131
|
+
}
|
|
4132
|
+
// Note the use of `startTime` in the code below doesn't match osu!stable's use of `endTime`.
|
|
4133
|
+
// This is because in osu!stable's implementation, `UpdateCalculations` is not called on the inner-loop hitobject (j)
|
|
4134
|
+
// and therefore it does not have a correct `endTime`, but instead the default of `endTime = startTime`.
|
|
4135
|
+
//
|
|
4136
|
+
// Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where
|
|
4137
|
+
// if we use `endTime` here it would result in unexpected stacking.
|
|
4138
|
+
//
|
|
4139
|
+
// Reference: https://github.com/ppy/osu/pull/24188
|
|
4140
|
+
if (objects[j].position.getDistance(currentObject.position) <
|
|
4141
|
+
BeatmapProcessor.stackDistance) {
|
|
4142
|
+
++currentObject.stackHeight;
|
|
4143
|
+
startTime = objects[j].startTime;
|
|
4144
|
+
}
|
|
4145
|
+
else if (objects[j].position.getDistance(currentObject.endPosition) <
|
|
4146
|
+
BeatmapProcessor.stackDistance) {
|
|
4147
|
+
// Case for sliders - bump notes down and right, rather than up and left.
|
|
4148
|
+
++sliderStack;
|
|
4149
|
+
objects[j].stackHeight -= sliderStack;
|
|
4150
|
+
startTime = objects[j].startTime;
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4071
4153
|
}
|
|
4072
|
-
return mods;
|
|
4073
4154
|
}
|
|
4074
4155
|
}
|
|
4075
|
-
|
|
4076
|
-
* All `Mod`s that exists, mapped by their acronym.
|
|
4077
|
-
*/
|
|
4078
|
-
ModUtil.allMods = (() => {
|
|
4079
|
-
const mods = [
|
|
4080
|
-
// Janky order to keep the order on what players are used to
|
|
4081
|
-
ModAuto,
|
|
4082
|
-
ModRelax,
|
|
4083
|
-
ModAutopilot,
|
|
4084
|
-
ModEasy,
|
|
4085
|
-
ModNoFail,
|
|
4086
|
-
ModHidden,
|
|
4087
|
-
ModTraceable,
|
|
4088
|
-
ModDoubleTime,
|
|
4089
|
-
ModNightCore,
|
|
4090
|
-
ModHalfTime,
|
|
4091
|
-
ModCustomSpeed,
|
|
4092
|
-
ModHardRock,
|
|
4093
|
-
ModDifficultyAdjust,
|
|
4094
|
-
ModFlashlight,
|
|
4095
|
-
ModSuddenDeath,
|
|
4096
|
-
ModPerfect,
|
|
4097
|
-
ModPrecise,
|
|
4098
|
-
ModReallyEasy,
|
|
4099
|
-
ModScoreV2,
|
|
4100
|
-
ModSmallCircle,
|
|
4101
|
-
ModSpunOut,
|
|
4102
|
-
ModTouchDevice,
|
|
4103
|
-
];
|
|
4104
|
-
const map = new Map();
|
|
4105
|
-
for (const mod of mods) {
|
|
4106
|
-
map.set(new mod().acronym, mod);
|
|
4107
|
-
}
|
|
4108
|
-
return map;
|
|
4109
|
-
})();
|
|
4156
|
+
BeatmapProcessor.stackDistance = 3;
|
|
4110
4157
|
|
|
4111
4158
|
/**
|
|
4112
4159
|
* Represents a beatmap that is in a playable state for a specific game mode.
|
|
@@ -4137,8 +4184,8 @@ class PlayableBeatmap {
|
|
|
4137
4184
|
this.colors = baseBeatmap.colors;
|
|
4138
4185
|
this.hitObjects = baseBeatmap.hitObjects;
|
|
4139
4186
|
this.maxCombo = baseBeatmap.maxCombo;
|
|
4140
|
-
this.mods = mods
|
|
4141
|
-
this.speedMultiplier = ModUtil.calculateRateWithMods(this.mods);
|
|
4187
|
+
this.mods = mods;
|
|
4188
|
+
this.speedMultiplier = ModUtil.calculateRateWithMods(this.mods.values());
|
|
4142
4189
|
}
|
|
4143
4190
|
}
|
|
4144
4191
|
|
|
@@ -4147,7 +4194,7 @@ class PlayableBeatmap {
|
|
|
4147
4194
|
*/
|
|
4148
4195
|
class DroidPlayableBeatmap extends PlayableBeatmap {
|
|
4149
4196
|
createHitWindow() {
|
|
4150
|
-
if (this.mods.
|
|
4197
|
+
if (this.mods.has(ModPrecise)) {
|
|
4151
4198
|
return new PreciseDroidHitWindow(this.difficulty.od);
|
|
4152
4199
|
}
|
|
4153
4200
|
else {
|
|
@@ -5008,7 +5055,7 @@ class Beatmap {
|
|
|
5008
5055
|
* @param mods The `Mod`s to apply to the `Beatmap`. Defaults to No Mod.
|
|
5009
5056
|
* @return The constructed `DroidPlayableBeatmap`.
|
|
5010
5057
|
*/
|
|
5011
|
-
createDroidPlayableBeatmap(mods =
|
|
5058
|
+
createDroidPlayableBeatmap(mods = new ModMap()) {
|
|
5012
5059
|
return new DroidPlayableBeatmap(this.createPlayableBeatmap(mods, exports.Modes.droid), mods);
|
|
5013
5060
|
}
|
|
5014
5061
|
/**
|
|
@@ -5020,11 +5067,11 @@ class Beatmap {
|
|
|
5020
5067
|
* @param mods The `Mod`s to apply to the `Beatmap`. Defaults to No Mod.
|
|
5021
5068
|
* @return The constructed `OsuPlayableBeatmap`.
|
|
5022
5069
|
*/
|
|
5023
|
-
createOsuPlayableBeatmap(mods =
|
|
5070
|
+
createOsuPlayableBeatmap(mods = new ModMap()) {
|
|
5024
5071
|
return new OsuPlayableBeatmap(this.createPlayableBeatmap(mods, exports.Modes.osu), mods);
|
|
5025
5072
|
}
|
|
5026
5073
|
createPlayableBeatmap(mods, mode) {
|
|
5027
|
-
if (this.mode === mode && mods.
|
|
5074
|
+
if (this.mode === mode && mods.size === 0) {
|
|
5028
5075
|
// Beatmap is already in a playable state.
|
|
5029
5076
|
return this;
|
|
5030
5077
|
}
|
|
@@ -8186,14 +8233,14 @@ class DroidLegacyModConverter {
|
|
|
8186
8233
|
* @returns An array of `Mod`s.
|
|
8187
8234
|
*/
|
|
8188
8235
|
static convert(str, difficulty) {
|
|
8236
|
+
const map = new ModMap();
|
|
8189
8237
|
if (!str) {
|
|
8190
|
-
return
|
|
8238
|
+
return map;
|
|
8191
8239
|
}
|
|
8192
8240
|
const data = str.split("|");
|
|
8193
8241
|
if (!data[0]) {
|
|
8194
|
-
return
|
|
8242
|
+
return map;
|
|
8195
8243
|
}
|
|
8196
|
-
const mods = [];
|
|
8197
8244
|
for (const c of data[0]) {
|
|
8198
8245
|
const modType = this.droidLegacyStorableMods.get(c);
|
|
8199
8246
|
if (!modType) {
|
|
@@ -8201,24 +8248,24 @@ class DroidLegacyModConverter {
|
|
|
8201
8248
|
}
|
|
8202
8249
|
const mod = new modType();
|
|
8203
8250
|
if (mod.isMigratableDroidMod() && difficulty) {
|
|
8204
|
-
|
|
8251
|
+
map.set(mod.migrateDroidMod(difficulty));
|
|
8205
8252
|
}
|
|
8206
8253
|
else {
|
|
8207
|
-
|
|
8254
|
+
map.set(mod);
|
|
8208
8255
|
}
|
|
8209
8256
|
}
|
|
8210
8257
|
if (data.length > 1) {
|
|
8211
|
-
this.parseExtraModString(
|
|
8258
|
+
this.parseExtraModString(map, data.slice(1));
|
|
8212
8259
|
}
|
|
8213
|
-
return
|
|
8260
|
+
return map;
|
|
8214
8261
|
}
|
|
8215
8262
|
/**
|
|
8216
8263
|
* Parses the extra strings of a mod string.
|
|
8217
8264
|
*
|
|
8218
|
-
* @param
|
|
8265
|
+
* @param map The current `Mod`s.
|
|
8219
8266
|
* @param extraStrings The extra strings to parse.
|
|
8220
8267
|
*/
|
|
8221
|
-
static parseExtraModString(
|
|
8268
|
+
static parseExtraModString(map, extraStrings) {
|
|
8222
8269
|
let customCS;
|
|
8223
8270
|
let customAR;
|
|
8224
8271
|
let customOD;
|
|
@@ -8240,20 +8287,20 @@ class DroidLegacyModConverter {
|
|
|
8240
8287
|
break;
|
|
8241
8288
|
// FL follow delay
|
|
8242
8289
|
case s.startsWith("FLD"): {
|
|
8243
|
-
let flashlight =
|
|
8290
|
+
let flashlight = map.get(ModFlashlight);
|
|
8244
8291
|
if (!flashlight) {
|
|
8245
8292
|
flashlight = new ModFlashlight();
|
|
8246
|
-
|
|
8293
|
+
map.set(flashlight);
|
|
8247
8294
|
}
|
|
8248
8295
|
flashlight.followDelay = parseFloat(s.slice(3));
|
|
8249
8296
|
break;
|
|
8250
8297
|
}
|
|
8251
8298
|
// Speed multiplier
|
|
8252
8299
|
case s.startsWith("x"): {
|
|
8253
|
-
let customSpeed =
|
|
8300
|
+
let customSpeed = map.get(ModCustomSpeed);
|
|
8254
8301
|
if (!customSpeed) {
|
|
8255
8302
|
customSpeed = new ModCustomSpeed();
|
|
8256
|
-
|
|
8303
|
+
map.set(customSpeed);
|
|
8257
8304
|
}
|
|
8258
8305
|
customSpeed.trackRateMultiplier = parseFloat(s.slice(1));
|
|
8259
8306
|
break;
|
|
@@ -8264,7 +8311,7 @@ class DroidLegacyModConverter {
|
|
|
8264
8311
|
customAR !== undefined ||
|
|
8265
8312
|
customOD !== undefined ||
|
|
8266
8313
|
customHP !== undefined) {
|
|
8267
|
-
|
|
8314
|
+
map.set(new ModDifficultyAdjust({
|
|
8268
8315
|
cs: customCS,
|
|
8269
8316
|
ar: customAR,
|
|
8270
8317
|
od: customOD,
|
|
@@ -9817,6 +9864,7 @@ exports.ModFlashlight = ModFlashlight;
|
|
|
9817
9864
|
exports.ModHalfTime = ModHalfTime;
|
|
9818
9865
|
exports.ModHardRock = ModHardRock;
|
|
9819
9866
|
exports.ModHidden = ModHidden;
|
|
9867
|
+
exports.ModMap = ModMap;
|
|
9820
9868
|
exports.ModNightCore = ModNightCore;
|
|
9821
9869
|
exports.ModNoFail = ModNoFail;
|
|
9822
9870
|
exports.ModOldNightCore = ModOldNightCore;
|