@rian8337/osu-base 4.0.0-beta.23 → 4.0.0-beta.25

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 CHANGED
@@ -1043,6 +1043,30 @@ function convertApproachRateMilliseconds(ms) {
1043
1043
  * Represents a hitobject in a beatmap.
1044
1044
  */
1045
1045
  class HitObject {
1046
+ /**
1047
+ * The position of the hitobject in osu!pixels.
1048
+ */
1049
+ get position() {
1050
+ return this._position;
1051
+ }
1052
+ /**
1053
+ * The position of the hitobject in osu!pixels.
1054
+ */
1055
+ set position(value) {
1056
+ this._position = value;
1057
+ }
1058
+ /**
1059
+ * The end position of the hitobject in osu!pixels.
1060
+ */
1061
+ get endPosition() {
1062
+ return this.position;
1063
+ }
1064
+ /**
1065
+ * The end time of the hitobject.
1066
+ */
1067
+ get endTime() {
1068
+ return this.startTime;
1069
+ }
1046
1070
  /**
1047
1071
  * The duration of the hitobject.
1048
1072
  */
@@ -1090,13 +1114,13 @@ class HitObject {
1090
1114
  return res.substring(0, Math.max(0, res.length - 3));
1091
1115
  }
1092
1116
  /**
1093
- * The radius of this hitobject,
1117
+ * The radius of this hitobject in osu!pixels.
1094
1118
  */
1095
1119
  get radius() {
1096
- return HitObject.baseRadius * this._scale;
1120
+ return HitObject.baseRadius * this.scale;
1097
1121
  }
1098
1122
  constructor(values) {
1099
- var _a, _b, _c, _d, _e;
1123
+ var _a, _b, _c;
1100
1124
  /**
1101
1125
  * The samples to be played when this hitobject is hit.
1102
1126
  *
@@ -1125,12 +1149,10 @@ class HitObject {
1125
1149
  */
1126
1150
  this.timeFadeIn = 400;
1127
1151
  this.startTime = values.startTime;
1128
- this.endTime = (_a = values.endTime) !== null && _a !== void 0 ? _a : values.startTime;
1129
- this.type = (_b = values.type) !== null && _b !== void 0 ? _b : exports.ObjectTypes.circle;
1130
- this.position = values.position;
1131
- this.endPosition = (_c = values.endPosition) !== null && _c !== void 0 ? _c : this.position;
1132
- this.isNewCombo = (_d = values.newCombo) !== null && _d !== void 0 ? _d : false;
1133
- this.comboOffset = (_e = values.comboOffset) !== null && _e !== void 0 ? _e : 0;
1152
+ this.type = (_a = values.type) !== null && _a !== void 0 ? _a : exports.ObjectTypes.circle;
1153
+ this._position = values.position;
1154
+ this.isNewCombo = (_b = values.newCombo) !== null && _b !== void 0 ? _b : false;
1155
+ this.comboOffset = (_c = values.comboOffset) !== null && _c !== void 0 ? _c : 0;
1134
1156
  }
1135
1157
  /**
1136
1158
  * Applies default values to this hitobject.
@@ -1151,11 +1173,11 @@ class HitObject {
1151
1173
  const cs = calculateDroidDifficultyStatistics({
1152
1174
  circleSize: difficulty.cs,
1153
1175
  }).circleSize;
1154
- this._scale = CircleSizeCalculator.standardCSToStandardScale(cs, true);
1176
+ this.scale = CircleSizeCalculator.standardCSToStandardScale(cs, true);
1155
1177
  break;
1156
1178
  }
1157
1179
  case exports.Modes.osu:
1158
- this._scale = CircleSizeCalculator.standardCSToStandardScale(difficulty.cs, true);
1180
+ this.scale = CircleSizeCalculator.standardCSToStandardScale(difficulty.cs, true);
1159
1181
  break;
1160
1182
  }
1161
1183
  }
@@ -1179,9 +1201,11 @@ class HitObject {
1179
1201
  getStackOffset(mode) {
1180
1202
  switch (mode) {
1181
1203
  case exports.Modes.droid:
1182
- return new Vector2(this._stackHeight * this._scale * 4);
1204
+ return new Vector2(this.stackHeight *
1205
+ CircleSizeCalculator.standardScaleToDroidScale(this.scale, true) *
1206
+ 4);
1183
1207
  case exports.Modes.osu:
1184
- return new Vector2(this._stackHeight * this._scale * -6.4);
1208
+ return new Vector2(this.stackHeight * this.scale * -6.4);
1185
1209
  }
1186
1210
  }
1187
1211
  /**
@@ -1252,7 +1276,7 @@ class SliderNestedHitObject extends HitObject {
1252
1276
  this.spanStartTime = values.spanStartTime;
1253
1277
  }
1254
1278
  toString() {
1255
- return `Position: [${this.position.x}, ${this.position.y}], span index: ${this.spanIndex}, span start time: ${this.spanStartTime}`;
1279
+ return `Position: [${this._position.x}, ${this._position.y}], span index: ${this.spanIndex}, span start time: ${this.spanStartTime}`;
1256
1280
  }
1257
1281
  }
1258
1282
 
@@ -1283,10 +1307,83 @@ class SliderTick extends SliderNestedHitObject {
1283
1307
  class SliderTail extends SliderNestedHitObject {
1284
1308
  }
1285
1309
 
1310
+ /**
1311
+ * Describes a value that can be cached.
1312
+ */
1313
+ class Cached {
1314
+ /**
1315
+ * The cached value.
1316
+ */
1317
+ get value() {
1318
+ if (!this._isValid) {
1319
+ throw new Error("May not query value of an invalid cache.");
1320
+ }
1321
+ return this._value;
1322
+ }
1323
+ /**
1324
+ * The cached value.
1325
+ */
1326
+ set value(value) {
1327
+ this._value = value;
1328
+ this._isValid = true;
1329
+ }
1330
+ /**
1331
+ * Whether the cache is valid.
1332
+ */
1333
+ get isValid() {
1334
+ return this._isValid;
1335
+ }
1336
+ constructor(value) {
1337
+ this._isValid = true;
1338
+ this._value = value;
1339
+ }
1340
+ /**
1341
+ * Invalidates the cache of this `Cached`.
1342
+ *
1343
+ * @return `true` if the cache was invalidated from a valid state.
1344
+ */
1345
+ invalidate() {
1346
+ if (this._isValid) {
1347
+ this._isValid = false;
1348
+ return true;
1349
+ }
1350
+ return false;
1351
+ }
1352
+ }
1353
+
1286
1354
  /**
1287
1355
  * Represents a slider in a beatmap.
1288
1356
  */
1289
1357
  class Slider extends HitObject {
1358
+ get position() {
1359
+ return super.position;
1360
+ }
1361
+ set position(value) {
1362
+ super.position = value;
1363
+ this.updateNestedPositions();
1364
+ }
1365
+ get endTime() {
1366
+ return (this.startTime + (this.spanCount * this.distance) / this.velocity);
1367
+ }
1368
+ get endPosition() {
1369
+ if (!this.endPositionCache.isValid) {
1370
+ this.endPositionCache.value = this.position.add(this.curvePositionAt(1));
1371
+ }
1372
+ return this.endPositionCache.value;
1373
+ }
1374
+ /**
1375
+ * The slider's path.
1376
+ */
1377
+ get path() {
1378
+ return this._path;
1379
+ }
1380
+ /**
1381
+ * The slider's path.
1382
+ */
1383
+ set path(value) {
1384
+ this._path = value;
1385
+ this.updateNestedPositions();
1386
+ }
1290
1387
  /**
1291
1388
  * The slider's velocity.
1292
1389
  */
@@ -1330,6 +1427,18 @@ class Slider extends HitObject {
1330
1427
  get spanDuration() {
1331
1428
  return this.duration / this.spanCount;
1332
1429
  }
1430
+ /**
1431
+ * The slider's head.
1432
+ */
1433
+ get head() {
1434
+ return this._head;
1435
+ }
1436
+ /**
1437
+ * The slider's tail.
1438
+ */
1439
+ get tail() {
1440
+ return this._tail;
1441
+ }
1333
1442
  /**
1334
1443
  * The amount of slider ticks in this slider.
1335
1444
  *
@@ -1387,31 +1496,21 @@ class Slider extends HitObject {
1387
1496
  * as few movements as possible. This is set and used by difficulty calculation.
1388
1497
  */
1389
1498
  this.lazyTravelTime = 0;
1390
- this.path = values.path;
1499
+ this._path = values.path;
1391
1500
  this.nodeSamples = values.nodeSamples;
1392
1501
  this._repeatCount = values.repeatCount;
1393
- this.endPosition = this.position.add(this.curvePositionAt(1));
1502
+ this.endPositionCache = new Cached(this.position.add(this.curvePositionAt(1)));
1394
1503
  this.tickDistanceMultiplier = values.tickDistanceMultiplier;
1395
- this.head = new SliderHead({
1396
- position: this.position,
1504
+ this._head = new SliderHead({
1505
+ position: this._position,
1397
1506
  startTime: this.startTime,
1398
1507
  });
1399
- this.tail = new SliderTail({
1508
+ this._tail = new SliderTail({
1400
1509
  position: this.endPosition,
1401
1510
  startTime: this.endTime,
1402
1511
  spanIndex: this.spanCount - 1,
1403
1512
  spanStartTime: this.startTime + this.spanDuration * this.spanCount,
1404
1513
  });
1405
- // Create sliding samples
1406
- const bankSamples = this.samples.filter((v) => v instanceof BankHitSampleInfo);
1407
- const normalSample = bankSamples.find((v) => v.name === BankHitSampleInfo.HIT_NORMAL);
1408
- if (normalSample) {
1409
- this.auxiliarySamples.push(new BankHitSampleInfo("sliderslide", normalSample.bank, normalSample.customSampleBank, normalSample.volume, normalSample.isLayered));
1410
- }
1411
- const whistleSample = bankSamples.find((v) => v.name === BankHitSampleInfo.HIT_WHISTLE);
1412
- if (whistleSample) {
1413
- this.auxiliarySamples.push(new BankHitSampleInfo("sliderwhistle", whistleSample.bank, whistleSample.customSampleBank, whistleSample.volume, whistleSample.isLayered));
1414
- }
1415
1514
  }
1416
1515
  applyDefaults(controlPoints, difficulty, mode) {
1417
1516
  super.applyDefaults(controlPoints, difficulty, mode);
@@ -1426,20 +1525,28 @@ class Slider extends HitObject {
1426
1525
  (timingPoint.msPerBeat * bpmMultiplier);
1427
1526
  // WARNING: this is intentionally not computed as `BASE_SCORING_DISTANCE * difficulty.sliderMultiplier`
1428
1527
  // for backwards compatibility reasons (intentionally introducing floating point errors to match osu!stable).
1429
- const scoringDistance = this._velocity * timingPoint.msPerBeat;
1528
+ const scoringDistance = this.velocity * timingPoint.msPerBeat;
1430
1529
  this.generateTicks = difficultyPoint.generateTicks;
1431
1530
  this._tickDistance = this.generateTicks
1432
1531
  ? (scoringDistance / difficulty.sliderTickRate) *
1433
1532
  this.tickDistanceMultiplier
1434
1533
  : Number.POSITIVE_INFINITY;
1435
- this.endTime =
1436
- this.startTime +
1437
- (this.spanCount * this.path.expectedDistance) / this._velocity;
1438
1534
  this.createNestedHitObjects(mode);
1439
1535
  this.nestedHitObjects.forEach((v) => v.applyDefaults(controlPoints, difficulty, mode));
1440
1536
  }
1441
1537
  applySamples(controlPoints) {
1442
1538
  super.applySamples(controlPoints);
1539
+ // Create sliding samples
1540
+ this.auxiliarySamples.length = 0;
1541
+ const bankSamples = this.samples.filter((v) => v instanceof BankHitSampleInfo);
1542
+ const normalSample = bankSamples.find((v) => v.name === BankHitSampleInfo.HIT_NORMAL);
1543
+ if (normalSample) {
1544
+ this.auxiliarySamples.push(new BankHitSampleInfo("sliderslide", normalSample.bank, normalSample.customSampleBank, normalSample.volume, normalSample.isLayered));
1545
+ }
1546
+ const whistleSample = bankSamples.find((v) => v.name === BankHitSampleInfo.HIT_WHISTLE);
1547
+ if (whistleSample) {
1548
+ this.auxiliarySamples.push(new BankHitSampleInfo("sliderwhistle", whistleSample.bank, whistleSample.customSampleBank, whistleSample.volume, whistleSample.isLayered));
1549
+ }
1443
1550
  this.nodeSamples.forEach((nodeSample, i) => {
1444
1551
  const time = this.startTime +
1445
1552
  i * this.spanDuration +
@@ -1481,7 +1588,7 @@ class Slider extends HitObject {
1481
1588
  }
1482
1589
  createNestedHitObjects(mode) {
1483
1590
  this.nestedHitObjects.length = 0;
1484
- this.head = new SliderHead({
1591
+ this._head = new SliderHead({
1485
1592
  position: this.position,
1486
1593
  startTime: this.startTime,
1487
1594
  });
@@ -1490,9 +1597,9 @@ class Slider extends HitObject {
1490
1597
  // This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage.
1491
1598
  const maxLength = 100000;
1492
1599
  const length = Math.min(maxLength, this.path.expectedDistance);
1493
- const tickDistance = MathUtils.clamp(this._tickDistance, 0, length);
1600
+ const tickDistance = MathUtils.clamp(this.tickDistance, 0, length);
1494
1601
  if (tickDistance !== 0 && this.generateTicks) {
1495
- const minDistanceFromEnd = this._velocity * 10;
1602
+ const minDistanceFromEnd = this.velocity * 10;
1496
1603
  for (let span = 0; span < this.spanCount; ++span) {
1497
1604
  const spanStartTime = this.startTime + span * this.spanDuration;
1498
1605
  const reversed = span % 2 === 1;
@@ -1534,7 +1641,7 @@ class Slider extends HitObject {
1534
1641
  }
1535
1642
  switch (mode) {
1536
1643
  case exports.Modes.droid:
1537
- this.tail = new SliderTail({
1644
+ this._tail = new SliderTail({
1538
1645
  position: this.endPosition,
1539
1646
  startTime: this.endTime,
1540
1647
  spanIndex: this.spanCount - 1,
@@ -1554,7 +1661,7 @@ class Slider extends HitObject {
1554
1661
  const finalSpanEndTime = Math.max(this.startTime + this.duration / 2, finalSpanStartTime +
1555
1662
  this.spanDuration -
1556
1663
  Slider.legacyLastTickOffset);
1557
- this.tail = new SliderTail({
1664
+ this._tail = new SliderTail({
1558
1665
  position: this.endPosition,
1559
1666
  startTime: finalSpanEndTime,
1560
1667
  spanIndex: this.spanCount - 1,
@@ -1568,6 +1675,7 @@ class Slider extends HitObject {
1568
1675
  this.updateNestedSamples();
1569
1676
  }
1570
1677
  updateNestedPositions() {
1678
+ this.endPositionCache.invalidate();
1571
1679
  this.head.position = this.position;
1572
1680
  this.tail.position = this.endPosition;
1573
1681
  }
@@ -2253,7 +2361,7 @@ class Circle extends HitObject {
2253
2361
  super(values);
2254
2362
  }
2255
2363
  toString() {
2256
- return `Position: [${this.position.x}, ${this.position.y}]`;
2364
+ return `Position: [${this._position.x}, ${this._position.y}]`;
2257
2365
  }
2258
2366
  }
2259
2367
 
@@ -2264,8 +2372,16 @@ class Circle extends HitObject {
2264
2372
  * position of a spinner is always at 256x192.
2265
2373
  */
2266
2374
  class Spinner extends HitObject {
2375
+ get endTime() {
2376
+ return this._endTime;
2377
+ }
2267
2378
  constructor(values) {
2268
2379
  super(Object.assign(Object.assign({}, values), { position: new Vector2(256, 192) }));
2380
+ this._endTime = values.endTime;
2381
+ }
2382
+ applySamples(controlPoints) {
2383
+ super.applySamples(controlPoints);
2384
+ this.auxiliarySamples.length = 0;
2269
2385
  const bankSample = this.samples.find((v) => v instanceof BankHitSampleInfo);
2270
2386
  if (bankSample) {
2271
2387
  this.auxiliarySamples.push(new BankHitSampleInfo("spinnerspin", bankSample.bank, bankSample.customSampleBank, bankSample.volume, bankSample.isLayered));
@@ -2279,7 +2395,7 @@ class Spinner extends HitObject {
2279
2395
  return this.position;
2280
2396
  }
2281
2397
  toString() {
2282
- return `Position: [${this.position.x}, ${this.position.y}], duration: ${this.duration}`;
2398
+ return `Position: [${this._position.x}, ${this._position.y}], duration: ${this.duration}`;
2283
2399
  }
2284
2400
  }
2285
2401
 
@@ -2424,293 +2540,59 @@ class BeatmapHitObjects {
2424
2540
  }
2425
2541
 
2426
2542
  /**
2427
- * Represents a beatmap with advanced information.
2543
+ * Provides functionality to alter a beatmap after it has been converted.
2428
2544
  */
2429
- class Beatmap {
2430
- constructor(shallowCopy) {
2431
- if (shallowCopy) {
2432
- this.formatVersion = shallowCopy.formatVersion;
2433
- this.general = shallowCopy.general;
2434
- this.editor = shallowCopy.editor;
2435
- this.metadata = shallowCopy.metadata;
2436
- this.difficulty = shallowCopy.difficulty;
2437
- this.events = shallowCopy.events;
2438
- this.controlPoints = shallowCopy.controlPoints;
2439
- this.colors = shallowCopy.colors;
2440
- this.hitObjects = shallowCopy.hitObjects;
2441
- return;
2442
- }
2443
- this.formatVersion = 1;
2444
- this.general = new BeatmapGeneral();
2445
- this.editor = new BeatmapEditor();
2446
- this.metadata = new BeatmapMetadata();
2447
- this.difficulty = new BeatmapDifficulty();
2448
- this.events = new BeatmapEvents();
2449
- this.controlPoints = new BeatmapControlPoints();
2450
- this.colors = new BeatmapColor();
2451
- this.hitObjects = new BeatmapHitObjects();
2452
- }
2453
- /**
2454
- * The maximum combo of the beatmap.
2455
- */
2456
- get maxCombo() {
2457
- return (this.hitObjects.circles +
2458
- this.hitObjects.sliders +
2459
- this.hitObjects.sliderTicks +
2460
- this.hitObjects.sliderRepeatPoints +
2461
- this.hitObjects.sliderEnds +
2462
- this.hitObjects.spinners);
2463
- }
2464
- /**
2465
- * The most common beat length of the beatmap.
2466
- */
2467
- get mostCommonBeatLength() {
2468
- var _a, _b, _c, _d, _e;
2469
- // The last playable time in the beatmap - the last timing point extends to this time.
2470
- // Note: This is more accurate and may present different results because osu-stable didn't have the ability to calculate slider durations in this context.
2471
- const lastTime = (_d = (_b = (_a = this.hitObjects.objects[this.hitObjects.objects.length - 1]) === null || _a === void 0 ? void 0 : _a.endTime) !== null && _b !== void 0 ? _b : (_c = this.controlPoints.timing.points[this.controlPoints.timing.points.length - 1]) === null || _c === void 0 ? void 0 : _c.time) !== null && _d !== void 0 ? _d : 0;
2472
- const mostCommon =
2473
- // Construct a set of {beatLength, duration} objects for each individual timing point.
2474
- this.controlPoints.timing.points
2475
- .map((t, i, a) => {
2476
- if (t.time > lastTime) {
2477
- return { beatLength: t.msPerBeat, duration: 0 };
2478
- }
2479
- // osu-stable forced the first control point to start at 0.
2480
- const currentTime = i === 0 ? 0 : t.time;
2481
- const nextTime = i === a.length - 1 ? lastTime : a[i + 1].time;
2482
- return {
2483
- beatLength: t.msPerBeat,
2484
- duration: nextTime - currentTime,
2485
- };
2486
- })
2487
- // Get the most common one, or 0 as a suitable default.
2488
- .sort((a, b) => b.duration - a.duration)[0];
2489
- return (_e = mostCommon === null || mostCommon === void 0 ? void 0 : mostCommon.beatLength) !== null && _e !== void 0 ? _e : 0;
2545
+ class BeatmapProcessor {
2546
+ constructor(beatmap) {
2547
+ this.beatmap = beatmap;
2490
2548
  }
2491
2549
  /**
2492
- * Returns a time combined with beatmap-wide time offset.
2550
+ * Processes the converted beatmap after `HitObject.applyDefaults` has been invoked.
2493
2551
  *
2494
- * BeatmapVersion 4 and lower had an incorrect offset. Stable has this set as 24ms off.
2552
+ * Nested hitobjects generated during `HitObject.applyDefaults` wil be present by this point,
2553
+ * and mods will have been applied to all hitobjects.
2495
2554
  *
2496
- * @param time The time.
2497
- */
2498
- getOffsetTime(time) {
2499
- return time + (this.formatVersion < 5 ? 24 : 0);
2500
- }
2501
- /**
2502
- * Calculates the osu!droid maximum score of the beatmap without taking spinner bonus into account.
2555
+ * This should be used to add alterations to hitobjects while they are in their most playable state.
2503
2556
  *
2504
- * @param mods The modifications to calculate for. Defaults to No Mod.
2505
- * @param customSpeedMultiplier The custom speed multiplier of the beatmap. Defaults to 1.
2557
+ * @param mode The mode to add alterations for.
2506
2558
  */
2507
- maxDroidScore(mods = [], customSpeedMultiplier = 1) {
2508
- let scoreMultiplier = 1;
2509
- for (const mod of mods) {
2510
- if (mod.isApplicableToDroid()) {
2511
- scoreMultiplier *= mod.droidScoreMultiplier;
2512
- }
2559
+ postProcess(mode) {
2560
+ const objects = this.beatmap.hitObjects.objects;
2561
+ if (objects.length === 0) {
2562
+ return;
2513
2563
  }
2514
- if (customSpeedMultiplier >= 1) {
2515
- scoreMultiplier *= 1 + (customSpeedMultiplier - 1) * 0.24;
2564
+ // Reset stacking
2565
+ objects.forEach((h) => {
2566
+ h.stackHeight = 0;
2567
+ });
2568
+ switch (mode) {
2569
+ case exports.Modes.droid:
2570
+ this.applyDroidStacking();
2571
+ break;
2572
+ case exports.Modes.osu:
2573
+ if (this.beatmap.formatVersion >= 6) {
2574
+ this.applyStandardStacking();
2575
+ }
2576
+ else {
2577
+ this.applyStandardOldStacking();
2578
+ }
2579
+ break;
2516
2580
  }
2517
- else {
2518
- scoreMultiplier *= Math.pow(0.3, (1 - customSpeedMultiplier) * 4);
2581
+ }
2582
+ applyDroidStacking() {
2583
+ const objects = this.beatmap.hitObjects.objects;
2584
+ if (objects.length === 0) {
2585
+ return;
2519
2586
  }
2520
- const difficultyMultiplier = 1 +
2521
- this.difficulty.od / 10 +
2522
- this.difficulty.hp / 10 +
2523
- (this.difficulty.cs - 3) / 4;
2524
- let combo = 0;
2525
- let score = 0;
2526
- for (const object of this.hitObjects.objects) {
2527
- if (!(object instanceof Slider)) {
2528
- score += Math.floor(300 + (300 * combo * difficultyMultiplier) / 25);
2529
- ++combo;
2530
- continue;
2531
- }
2532
- const { ticks } = object;
2533
- // Apply slider head.
2534
- score += 30;
2535
- ++combo;
2536
- // Apply slider repeats.
2537
- score += 30 * object.repeatCount;
2538
- combo += object.repeatCount;
2539
- // Apply slider ticks.
2540
- score += 10 * ticks;
2541
- combo += ticks;
2542
- // Apply slider end.
2543
- score += Math.floor(300 + (300 * combo * difficultyMultiplier) / 25);
2544
- ++combo;
2545
- }
2546
- return Math.floor(score * scoreMultiplier);
2547
- }
2548
- /**
2549
- * Calculates the osu!standard maximum score of the beatmap without taking spinner bonus into account.
2550
- *
2551
- * @param mods The modifications to calculate for. Defaults to No Mod.
2552
- */
2553
- maxOsuScore(mods = []) {
2554
- const accumulatedDiffPoints = this.difficulty.cs + this.difficulty.hp + this.difficulty.od;
2555
- let difficultyMultiplier = 2;
2556
- let scoreMultiplier = 1;
2557
- for (const mod of mods) {
2558
- if (mod.isApplicableToOsu()) {
2559
- scoreMultiplier *= mod.pcScoreMultiplier;
2560
- }
2561
- }
2562
- switch (true) {
2563
- case accumulatedDiffPoints <= 5:
2564
- difficultyMultiplier = 2;
2565
- break;
2566
- case accumulatedDiffPoints <= 12:
2567
- difficultyMultiplier = 3;
2568
- break;
2569
- case accumulatedDiffPoints <= 17:
2570
- difficultyMultiplier = 4;
2571
- break;
2572
- case accumulatedDiffPoints <= 24:
2573
- difficultyMultiplier = 5;
2574
- break;
2575
- case accumulatedDiffPoints >= 25:
2576
- difficultyMultiplier = 6;
2577
- break;
2578
- }
2579
- let combo = 0;
2580
- let score = 0;
2581
- for (const object of this.hitObjects.objects) {
2582
- if (!(object instanceof Slider)) {
2583
- score += Math.floor(300 +
2584
- (300 * combo * difficultyMultiplier * scoreMultiplier) /
2585
- 25);
2586
- ++combo;
2587
- continue;
2588
- }
2589
- const { ticks } = object;
2590
- // Apply slider head.
2591
- score += 30;
2592
- ++combo;
2593
- // Apply slider repeats.
2594
- score += 30 * object.repeatCount;
2595
- combo += object.repeatCount;
2596
- // Apply slider ticks.
2597
- score += 10 * ticks;
2598
- combo += ticks;
2599
- // Apply slider end.
2600
- score += Math.floor(300 +
2601
- (300 * combo * difficultyMultiplier * scoreMultiplier) / 25);
2602
- ++combo;
2603
- }
2604
- return score;
2605
- }
2606
- /**
2607
- * Returns a string representative of the class.
2608
- */
2609
- toString() {
2610
- let res = this.metadata.artist + " - " + this.metadata.title + " [";
2611
- if (this.metadata.titleUnicode || this.metadata.artistUnicode) {
2612
- res +=
2613
- "(" +
2614
- this.metadata.artistUnicode +
2615
- " - " +
2616
- this.metadata.titleUnicode +
2617
- ")";
2618
- }
2619
- res +=
2620
- this.metadata.version +
2621
- "] mapped by " +
2622
- this.metadata.creator +
2623
- "\n" +
2624
- "\n" +
2625
- "AR" +
2626
- MathUtils.round(this.difficulty.ar, 2) +
2627
- " " +
2628
- "OD" +
2629
- MathUtils.round(this.difficulty.od, 2) +
2630
- " " +
2631
- "CS" +
2632
- MathUtils.round(this.difficulty.cs, 2) +
2633
- " " +
2634
- "HP" +
2635
- MathUtils.round(this.difficulty.hp, 2) +
2636
- "\n" +
2637
- this.hitObjects.circles +
2638
- " circles, " +
2639
- this.hitObjects.sliders +
2640
- " sliders, " +
2641
- this.hitObjects.spinners +
2642
- " spinners" +
2643
- "\n" +
2644
- this.maxCombo +
2645
- " max combo";
2646
- return res;
2647
- }
2648
- }
2649
-
2650
- /**
2651
- * Represents a beatmap's background.
2652
- */
2653
- class BeatmapBackground {
2654
- constructor(filename, offset) {
2655
- this.filename = filename;
2656
- this.offset = offset;
2657
- }
2658
- }
2659
-
2660
- /**
2661
- * Provides functionality to alter a beatmap after it has been converted.
2662
- */
2663
- class BeatmapProcessor {
2664
- constructor(beatmap) {
2665
- this.beatmap = beatmap;
2666
- }
2667
- /**
2668
- * Processes the converted beatmap after `HitObject.applyDefaults` has been invoked.
2669
- *
2670
- * Nested hitobjects generated during `HitObject.applyDefaults` wil be present by this point,
2671
- * and mods will have been applied to all hitobjects.
2672
- *
2673
- * This should be used to add alterations to hitobjects while they are in their most playable state.
2674
- *
2675
- * @param mode The mode to add alterations for.
2676
- */
2677
- postProcess(mode) {
2678
- const objects = this.beatmap.hitObjects.objects;
2679
- if (objects.length === 0) {
2680
- return;
2681
- }
2682
- // Reset stacking
2683
- objects.forEach((h) => {
2684
- h.stackHeight = 0;
2685
- });
2686
- switch (mode) {
2687
- case exports.Modes.droid:
2688
- this.applyDroidStacking();
2689
- break;
2690
- case exports.Modes.osu:
2691
- if (this.beatmap.formatVersion >= 6) {
2692
- this.applyStandardStacking();
2693
- }
2694
- else {
2695
- this.applyStandardOldStacking();
2696
- }
2697
- break;
2698
- }
2699
- }
2700
- applyDroidStacking() {
2701
- const objects = this.beatmap.hitObjects.objects;
2702
- if (objects.length === 0) {
2703
- return;
2704
- }
2705
- const convertedScale = CircleSizeCalculator.standardScaleToDroidScale(objects[0].scale);
2706
- for (let i = 0; i < objects.length - 1; ++i) {
2707
- const current = objects[i];
2708
- const next = objects[i + 1];
2709
- if (next.startTime - current.startTime <
2710
- 2000 * this.beatmap.general.stackLeniency &&
2711
- next.position.getDistance(current.position) <
2712
- Math.sqrt(convertedScale)) {
2713
- next.stackHeight = current.stackHeight + 1;
2587
+ const convertedScale = CircleSizeCalculator.standardScaleToDroidScale(objects[0].scale);
2588
+ for (let i = 0; i < objects.length - 1; ++i) {
2589
+ const current = objects[i];
2590
+ const next = objects[i + 1];
2591
+ if (next.startTime - current.startTime <
2592
+ 2000 * this.beatmap.general.stackLeniency &&
2593
+ next.position.getDistance(current.position) <
2594
+ Math.sqrt(convertedScale)) {
2595
+ next.stackHeight = current.stackHeight + 1;
2714
2596
  }
2715
2597
  }
2716
2598
  }
@@ -2843,70 +2725,312 @@ class BeatmapProcessor {
2843
2725
  }
2844
2726
  }
2845
2727
  }
2846
- applyStandardOldStacking() {
2847
- const objects = this.beatmap.hitObjects.objects;
2848
- for (let i = 0; i < objects.length; ++i) {
2849
- const currentObject = objects[i];
2850
- if (currentObject.stackHeight !== 0 &&
2851
- !(currentObject instanceof Slider)) {
2852
- continue;
2728
+ applyStandardOldStacking() {
2729
+ const objects = this.beatmap.hitObjects.objects;
2730
+ for (let i = 0; i < objects.length; ++i) {
2731
+ const currentObject = objects[i];
2732
+ if (currentObject.stackHeight !== 0 &&
2733
+ !(currentObject instanceof Slider)) {
2734
+ continue;
2735
+ }
2736
+ let startTime = currentObject.endTime;
2737
+ let sliderStack = 0;
2738
+ const stackThreshold = currentObject.timePreempt * this.beatmap.general.stackLeniency;
2739
+ for (let j = i + 1; j < objects.length; ++j) {
2740
+ if (objects[j].startTime - stackThreshold > startTime) {
2741
+ break;
2742
+ }
2743
+ // Note the use of `startTime` in the code below doesn't match osu!stable's use of `endTime`.
2744
+ // This is because in osu!stable's implementation, `UpdateCalculations` is not called on the inner-loop hitobject (j)
2745
+ // and therefore it does not have a correct `endTime`, but instead the default of `endTime = startTime`.
2746
+ //
2747
+ // Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where
2748
+ // if we use `endTime` here it would result in unexpected stacking.
2749
+ //
2750
+ // Reference: https://github.com/ppy/osu/pull/24188
2751
+ if (objects[j].position.getDistance(currentObject.position) <
2752
+ BeatmapProcessor.stackDistance) {
2753
+ ++currentObject.stackHeight;
2754
+ startTime = objects[j].startTime;
2755
+ }
2756
+ else if (objects[j].position.getDistance(currentObject.endPosition) <
2757
+ BeatmapProcessor.stackDistance) {
2758
+ // Case for sliders - bump notes down and right, rather than up and left.
2759
+ ++sliderStack;
2760
+ objects[j].stackHeight -= sliderStack;
2761
+ startTime = objects[j].startTime;
2762
+ }
2763
+ }
2764
+ }
2765
+ }
2766
+ }
2767
+ BeatmapProcessor.stackDistance = 3;
2768
+
2769
+ /**
2770
+ * Converts a beatmap for another mode.
2771
+ */
2772
+ class BeatmapConverter {
2773
+ constructor(beatmap) {
2774
+ this.beatmap = beatmap;
2775
+ }
2776
+ /**
2777
+ * Converts the beatmap.
2778
+ *
2779
+ * @returns The converted beatmap.
2780
+ */
2781
+ convert() {
2782
+ const converted = new Beatmap(this.beatmap);
2783
+ // Shallow clone isn't enough to ensure we don't mutate some beatmap properties unexpectedly.
2784
+ converted.difficulty = new BeatmapDifficulty(this.beatmap.difficulty);
2785
+ converted.hitObjects = this.convertHitObjects();
2786
+ return converted;
2787
+ }
2788
+ convertHitObjects() {
2789
+ const hitObjects = new BeatmapHitObjects();
2790
+ this.beatmap.hitObjects.objects.forEach((hitObject) => {
2791
+ hitObjects.add(this.convertHitObject(hitObject));
2792
+ });
2793
+ return hitObjects;
2794
+ }
2795
+ convertHitObject(hitObject) {
2796
+ let object;
2797
+ if (hitObject instanceof Circle) {
2798
+ object = new Circle({
2799
+ startTime: hitObject.startTime,
2800
+ position: hitObject.position,
2801
+ newCombo: hitObject.isNewCombo,
2802
+ type: hitObject.type,
2803
+ comboOffset: hitObject.comboOffset,
2804
+ });
2805
+ }
2806
+ else if (hitObject instanceof Slider) {
2807
+ object = new Slider({
2808
+ startTime: hitObject.startTime,
2809
+ position: hitObject.position,
2810
+ newCombo: hitObject.isNewCombo,
2811
+ type: hitObject.type,
2812
+ path: hitObject.path,
2813
+ repeatCount: hitObject.repeatCount,
2814
+ nodeSamples: hitObject.nodeSamples,
2815
+ comboOffset: hitObject.comboOffset,
2816
+ tickDistanceMultiplier:
2817
+ // Prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
2818
+ // This results in more (or less) ticks being generated in <v8 maps for the same time duration.
2819
+ this.beatmap.formatVersion < 8
2820
+ ? 1 /
2821
+ this.beatmap.controlPoints.difficulty.controlPointAt(hitObject.startTime).speedMultiplier
2822
+ : 1,
2823
+ });
2824
+ }
2825
+ else {
2826
+ object = new Spinner({
2827
+ startTime: hitObject.startTime,
2828
+ endTime: hitObject.endTime,
2829
+ type: hitObject.type,
2830
+ });
2831
+ }
2832
+ object.samples = hitObject.samples;
2833
+ object.auxiliarySamples = hitObject.auxiliarySamples;
2834
+ return object;
2835
+ }
2836
+ }
2837
+
2838
+ /**
2839
+ * Represents a beatmap with advanced information.
2840
+ */
2841
+ class Beatmap {
2842
+ constructor(shallowCopy) {
2843
+ if (shallowCopy) {
2844
+ this.formatVersion = shallowCopy.formatVersion;
2845
+ this.general = shallowCopy.general;
2846
+ this.editor = shallowCopy.editor;
2847
+ this.metadata = shallowCopy.metadata;
2848
+ this.difficulty = shallowCopy.difficulty;
2849
+ this.events = shallowCopy.events;
2850
+ this.controlPoints = shallowCopy.controlPoints;
2851
+ this.colors = shallowCopy.colors;
2852
+ this.hitObjects = shallowCopy.hitObjects;
2853
+ return;
2854
+ }
2855
+ this.formatVersion = 1;
2856
+ this.general = new BeatmapGeneral();
2857
+ this.editor = new BeatmapEditor();
2858
+ this.metadata = new BeatmapMetadata();
2859
+ this.difficulty = new BeatmapDifficulty();
2860
+ this.events = new BeatmapEvents();
2861
+ this.controlPoints = new BeatmapControlPoints();
2862
+ this.colors = new BeatmapColor();
2863
+ this.hitObjects = new BeatmapHitObjects();
2864
+ }
2865
+ /**
2866
+ * The maximum combo of the beatmap.
2867
+ */
2868
+ get maxCombo() {
2869
+ return (this.hitObjects.circles +
2870
+ this.hitObjects.sliders +
2871
+ this.hitObjects.sliderTicks +
2872
+ this.hitObjects.sliderRepeatPoints +
2873
+ this.hitObjects.sliderEnds +
2874
+ this.hitObjects.spinners);
2875
+ }
2876
+ /**
2877
+ * The most common beat length of the beatmap.
2878
+ */
2879
+ get mostCommonBeatLength() {
2880
+ var _a, _b, _c, _d, _e;
2881
+ // The last playable time in the beatmap - the last timing point extends to this time.
2882
+ // Note: This is more accurate and may present different results because osu-stable didn't have the ability to calculate slider durations in this context.
2883
+ const lastTime = (_d = (_b = (_a = this.hitObjects.objects[this.hitObjects.objects.length - 1]) === null || _a === void 0 ? void 0 : _a.endTime) !== null && _b !== void 0 ? _b : (_c = this.controlPoints.timing.points[this.controlPoints.timing.points.length - 1]) === null || _c === void 0 ? void 0 : _c.time) !== null && _d !== void 0 ? _d : 0;
2884
+ const mostCommon =
2885
+ // Construct a set of {beatLength, duration} objects for each individual timing point.
2886
+ this.controlPoints.timing.points
2887
+ .map((t, i, a) => {
2888
+ if (t.time > lastTime) {
2889
+ return { beatLength: t.msPerBeat, duration: 0 };
2890
+ }
2891
+ // osu-stable forced the first control point to start at 0.
2892
+ const currentTime = i === 0 ? 0 : t.time;
2893
+ const nextTime = i === a.length - 1 ? lastTime : a[i + 1].time;
2894
+ return {
2895
+ beatLength: t.msPerBeat,
2896
+ duration: nextTime - currentTime,
2897
+ };
2898
+ })
2899
+ // Get the most common one, or 0 as a suitable default.
2900
+ .sort((a, b) => b.duration - a.duration)[0];
2901
+ return (_e = mostCommon === null || mostCommon === void 0 ? void 0 : mostCommon.beatLength) !== null && _e !== void 0 ? _e : 0;
2902
+ }
2903
+ /**
2904
+ * Returns a time combined with beatmap-wide time offset.
2905
+ *
2906
+ * BeatmapVersion 4 and lower had an incorrect offset. Stable has this set as 24ms off.
2907
+ *
2908
+ * @param time The time.
2909
+ */
2910
+ getOffsetTime(time) {
2911
+ return time + (this.formatVersion < 5 ? 24 : 0);
2912
+ }
2913
+ /**
2914
+ * Calculates the osu!droid maximum score of the beatmap without taking spinner bonus into account.
2915
+ *
2916
+ * @param mods The modifications to calculate for. Defaults to No Mod.
2917
+ * @param customSpeedMultiplier The custom speed multiplier of the beatmap. Defaults to 1.
2918
+ */
2919
+ maxDroidScore(mods = [], customSpeedMultiplier = 1) {
2920
+ let scoreMultiplier = 1;
2921
+ for (const mod of mods) {
2922
+ if (mod.isApplicableToDroid()) {
2923
+ scoreMultiplier *= mod.droidScoreMultiplier;
2924
+ }
2925
+ }
2926
+ if (customSpeedMultiplier >= 1) {
2927
+ scoreMultiplier *= 1 + (customSpeedMultiplier - 1) * 0.24;
2928
+ }
2929
+ else {
2930
+ scoreMultiplier *= Math.pow(0.3, (1 - customSpeedMultiplier) * 4);
2931
+ }
2932
+ const difficultyMultiplier = 1 +
2933
+ this.difficulty.od / 10 +
2934
+ this.difficulty.hp / 10 +
2935
+ (this.difficulty.cs - 3) / 4;
2936
+ let combo = 0;
2937
+ let score = 0;
2938
+ for (const object of this.hitObjects.objects) {
2939
+ if (!(object instanceof Slider)) {
2940
+ score += Math.floor(300 + (300 * combo * difficultyMultiplier) / 25);
2941
+ ++combo;
2942
+ continue;
2943
+ }
2944
+ const { ticks } = object;
2945
+ // Apply slider head.
2946
+ score += 30;
2947
+ ++combo;
2948
+ // Apply slider repeats.
2949
+ score += 30 * object.repeatCount;
2950
+ combo += object.repeatCount;
2951
+ // Apply slider ticks.
2952
+ score += 10 * ticks;
2953
+ combo += ticks;
2954
+ // Apply slider end.
2955
+ score += Math.floor(300 + (300 * combo * difficultyMultiplier) / 25);
2956
+ ++combo;
2957
+ }
2958
+ return Math.floor(score * scoreMultiplier);
2959
+ }
2960
+ /**
2961
+ * Calculates the osu!standard maximum score of the beatmap without taking spinner bonus into account.
2962
+ *
2963
+ * @param mods The modifications to calculate for. Defaults to No Mod.
2964
+ */
2965
+ maxOsuScore(mods = []) {
2966
+ const accumulatedDiffPoints = this.difficulty.cs + this.difficulty.hp + this.difficulty.od;
2967
+ let difficultyMultiplier = 2;
2968
+ let scoreMultiplier = 1;
2969
+ for (const mod of mods) {
2970
+ if (mod.isApplicableToOsu()) {
2971
+ scoreMultiplier *= mod.pcScoreMultiplier;
2853
2972
  }
2854
- let startTime = currentObject.endTime;
2855
- let sliderStack = 0;
2856
- const stackThreshold = currentObject.timePreempt * this.beatmap.general.stackLeniency;
2857
- for (let j = i + 1; j < objects.length; ++j) {
2858
- if (objects[j].startTime - stackThreshold > startTime) {
2859
- break;
2860
- }
2861
- // Note the use of `startTime` in the code below doesn't match osu!stable's use of `endTime`.
2862
- // This is because in osu!stable's implementation, `UpdateCalculations` is not called on the inner-loop hitobject (j)
2863
- // and therefore it does not have a correct `endTime`, but instead the default of `endTime = startTime`.
2864
- //
2865
- // Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where
2866
- // if we use `endTime` here it would result in unexpected stacking.
2867
- //
2868
- // Reference: https://github.com/ppy/osu/pull/24188
2869
- if (objects[j].position.getDistance(currentObject.position) <
2870
- BeatmapProcessor.stackDistance) {
2871
- ++currentObject.stackHeight;
2872
- startTime = objects[j].startTime;
2873
- }
2874
- else if (objects[j].position.getDistance(currentObject.endPosition) <
2875
- BeatmapProcessor.stackDistance) {
2876
- // Case for sliders - bump notes down and right, rather than up and left.
2877
- ++sliderStack;
2878
- objects[j].stackHeight -= sliderStack;
2879
- startTime = objects[j].startTime;
2880
- }
2973
+ }
2974
+ switch (true) {
2975
+ case accumulatedDiffPoints <= 5:
2976
+ difficultyMultiplier = 2;
2977
+ break;
2978
+ case accumulatedDiffPoints <= 12:
2979
+ difficultyMultiplier = 3;
2980
+ break;
2981
+ case accumulatedDiffPoints <= 17:
2982
+ difficultyMultiplier = 4;
2983
+ break;
2984
+ case accumulatedDiffPoints <= 24:
2985
+ difficultyMultiplier = 5;
2986
+ break;
2987
+ case accumulatedDiffPoints >= 25:
2988
+ difficultyMultiplier = 6;
2989
+ break;
2990
+ }
2991
+ let combo = 0;
2992
+ let score = 0;
2993
+ for (const object of this.hitObjects.objects) {
2994
+ if (!(object instanceof Slider)) {
2995
+ score += Math.floor(300 +
2996
+ (300 * combo * difficultyMultiplier * scoreMultiplier) /
2997
+ 25);
2998
+ ++combo;
2999
+ continue;
2881
3000
  }
3001
+ const { ticks } = object;
3002
+ // Apply slider head.
3003
+ score += 30;
3004
+ ++combo;
3005
+ // Apply slider repeats.
3006
+ score += 30 * object.repeatCount;
3007
+ combo += object.repeatCount;
3008
+ // Apply slider ticks.
3009
+ score += 10 * ticks;
3010
+ combo += ticks;
3011
+ // Apply slider end.
3012
+ score += Math.floor(300 +
3013
+ (300 * combo * difficultyMultiplier * scoreMultiplier) / 25);
3014
+ ++combo;
2882
3015
  }
2883
- }
2884
- }
2885
- BeatmapProcessor.stackDistance = 3;
2886
-
2887
- /**
2888
- * Converts a beatmap for another mode.
2889
- */
2890
- class BeatmapConverter {
2891
- constructor(beatmap) {
2892
- this.beatmap = beatmap;
3016
+ return score;
2893
3017
  }
2894
3018
  /**
2895
- * Converts the beatmap.
3019
+ * Constructs a playable `Beatmap` from this `Beatmap`.
2896
3020
  *
2897
- * @param options The options to use for conversion.
2898
- * @returns The converted beatmap.
3021
+ * The returned `Beatmap` is in a playable state - all `HitObject` and `BeatmapDifficulty` `Mod`s
3022
+ * have been applied, and `HitObject`s have been fully constructed.
3023
+ *
3024
+ * @param options The options to use.
3025
+ * @return The constructed `Beatmap`.
2899
3026
  */
2900
- convert(options) {
3027
+ createPlayableBeatmap(options) {
2901
3028
  var _a, _b, _c;
2902
3029
  const mods = (_a = options === null || options === void 0 ? void 0 : options.mods) !== null && _a !== void 0 ? _a : [];
2903
3030
  const mode = (_b = options === null || options === void 0 ? void 0 : options.mode) !== null && _b !== void 0 ? _b : exports.Modes.osu;
2904
3031
  const customSpeedMultiplier = (_c = options === null || options === void 0 ? void 0 : options.customSpeedMultiplier) !== null && _c !== void 0 ? _c : 1;
2905
3032
  // Convert
2906
- const converted = new Beatmap(this.beatmap);
2907
- // Shallow clone isn't enough to ensure we don't mutate some beatmap properties unexpectedly.
2908
- converted.difficulty = new BeatmapDifficulty(this.beatmap.difficulty);
2909
- converted.hitObjects = this.convertHitObjects();
3033
+ const converted = new BeatmapConverter(this).convert();
2910
3034
  // Apply difficulty mods
2911
3035
  mods.forEach((mod) => {
2912
3036
  if (mod.isApplicableToDifficulty()) {
@@ -2921,7 +3045,6 @@ class BeatmapConverter {
2921
3045
  mod.applyToDifficultyWithSettings(mode, converted.difficulty, mods, customSpeedMultiplier);
2922
3046
  }
2923
3047
  });
2924
- const processor = new BeatmapProcessor(converted);
2925
3048
  // Compute default values for hit objects, including creating nested hit objects in-case they're needed.
2926
3049
  converted.hitObjects.objects.forEach((hitObject) => hitObject.applyDefaults(converted.controlPoints, converted.difficulty, mode));
2927
3050
  mods.forEach((mod) => {
@@ -2931,7 +3054,7 @@ class BeatmapConverter {
2931
3054
  }
2932
3055
  }
2933
3056
  });
2934
- processor.postProcess(mode);
3057
+ new BeatmapProcessor(converted).postProcess(mode);
2935
3058
  mods.forEach((mod) => {
2936
3059
  if (mod.isApplicableToBeatmap()) {
2937
3060
  mod.applyToBeatmap(converted);
@@ -2939,52 +3062,57 @@ class BeatmapConverter {
2939
3062
  });
2940
3063
  return converted;
2941
3064
  }
2942
- convertHitObjects() {
2943
- const hitObjects = new BeatmapHitObjects();
2944
- this.beatmap.hitObjects.objects.forEach((hitObject) => {
2945
- hitObjects.add(this.convertHitObject(hitObject));
2946
- });
2947
- return hitObjects;
2948
- }
2949
- convertHitObject(hitObject) {
2950
- let object;
2951
- if (hitObject instanceof Circle) {
2952
- object = new Circle({
2953
- startTime: hitObject.startTime,
2954
- position: hitObject.position,
2955
- newCombo: hitObject.isNewCombo,
2956
- type: hitObject.type,
2957
- comboOffset: hitObject.comboOffset,
2958
- });
2959
- }
2960
- else if (hitObject instanceof Slider) {
2961
- object = new Slider({
2962
- startTime: hitObject.startTime,
2963
- position: hitObject.position,
2964
- newCombo: hitObject.isNewCombo,
2965
- type: hitObject.type,
2966
- path: hitObject.path,
2967
- repeatCount: hitObject.repeatCount,
2968
- nodeSamples: hitObject.nodeSamples,
2969
- comboOffset: hitObject.comboOffset,
2970
- tickDistanceMultiplier:
2971
- // Prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
2972
- // This results in more (or less) ticks being generated in <v8 maps for the same time duration.
2973
- this.beatmap.formatVersion < 8
2974
- ? 1 /
2975
- this.beatmap.controlPoints.difficulty.controlPointAt(hitObject.startTime).speedMultiplier
2976
- : 1,
2977
- });
2978
- }
2979
- else {
2980
- object = new Spinner({
2981
- startTime: hitObject.startTime,
2982
- endTime: hitObject.endTime,
2983
- type: hitObject.type,
2984
- });
3065
+ /**
3066
+ * Returns a string representative of the class.
3067
+ */
3068
+ toString() {
3069
+ let res = this.metadata.artist + " - " + this.metadata.title + " [";
3070
+ if (this.metadata.titleUnicode || this.metadata.artistUnicode) {
3071
+ res +=
3072
+ "(" +
3073
+ this.metadata.artistUnicode +
3074
+ " - " +
3075
+ this.metadata.titleUnicode +
3076
+ ")";
2985
3077
  }
2986
- object.samples = hitObject.samples;
2987
- return object;
3078
+ res +=
3079
+ this.metadata.version +
3080
+ "] mapped by " +
3081
+ this.metadata.creator +
3082
+ "\n" +
3083
+ "\n" +
3084
+ "AR" +
3085
+ MathUtils.round(this.difficulty.ar, 2) +
3086
+ " " +
3087
+ "OD" +
3088
+ MathUtils.round(this.difficulty.od, 2) +
3089
+ " " +
3090
+ "CS" +
3091
+ MathUtils.round(this.difficulty.cs, 2) +
3092
+ " " +
3093
+ "HP" +
3094
+ MathUtils.round(this.difficulty.hp, 2) +
3095
+ "\n" +
3096
+ this.hitObjects.circles +
3097
+ " circles, " +
3098
+ this.hitObjects.sliders +
3099
+ " sliders, " +
3100
+ this.hitObjects.spinners +
3101
+ " spinners" +
3102
+ "\n" +
3103
+ this.maxCombo +
3104
+ " max combo";
3105
+ return res;
3106
+ }
3107
+ }
3108
+
3109
+ /**
3110
+ * Represents a beatmap's background.
3111
+ */
3112
+ class BeatmapBackground {
3113
+ constructor(filename, offset) {
3114
+ this.filename = filename;
3115
+ this.offset = offset;
2988
3116
  }
2989
3117
  }
2990
3118
 
@@ -7404,6 +7532,20 @@ class Interpolation {
7404
7532
  }
7405
7533
  }
7406
7534
 
7535
+ /**
7536
+ * Ranking status of a beatmap.
7537
+ */
7538
+ exports.RankedStatus = void 0;
7539
+ (function (RankedStatus) {
7540
+ RankedStatus[RankedStatus["graveyard"] = -2] = "graveyard";
7541
+ RankedStatus[RankedStatus["wip"] = -1] = "wip";
7542
+ RankedStatus[RankedStatus["pending"] = 0] = "pending";
7543
+ RankedStatus[RankedStatus["ranked"] = 1] = "ranked";
7544
+ RankedStatus[RankedStatus["approved"] = 2] = "approved";
7545
+ RankedStatus[RankedStatus["qualified"] = 3] = "qualified";
7546
+ RankedStatus[RankedStatus["loved"] = 4] = "loved";
7547
+ })(exports.RankedStatus || (exports.RankedStatus = {}));
7548
+
7407
7549
  /**
7408
7550
  * An API request builder for osu!standard.
7409
7551
  */
@@ -7448,7 +7590,7 @@ class MapInfo {
7448
7590
  /**
7449
7591
  * The ranking status of the beatmap.
7450
7592
  */
7451
- this.approved = 0;
7593
+ this.approved = exports.RankedStatus.pending;
7452
7594
  /**
7453
7595
  * The ID of the beatmap.
7454
7596
  */
@@ -7520,7 +7662,7 @@ class MapInfo {
7520
7662
  /**
7521
7663
  * The maximum combo of the beatmap.
7522
7664
  */
7523
- this.maxCombo = 0;
7665
+ this.maxCombo = null;
7524
7666
  /**
7525
7667
  * The circle size of the beatmap.
7526
7668
  */
@@ -7676,7 +7818,8 @@ class MapInfo {
7676
7818
  map.spinners = mapinfo.count_spinner
7677
7819
  ? parseInt(mapinfo.count_spinner)
7678
7820
  : 0;
7679
- map.maxCombo = parseInt(mapinfo.max_combo);
7821
+ map.maxCombo =
7822
+ mapinfo.max_combo !== null ? parseInt(mapinfo.max_combo) : null;
7680
7823
  map.cs = parseFloat(mapinfo.diff_size);
7681
7824
  map.ar = parseFloat(mapinfo.diff_approach);
7682
7825
  map.od = parseFloat(mapinfo.diff_overall);
@@ -7707,7 +7850,7 @@ class MapInfo {
7707
7850
  * @returns The raw API response represented by this `MapInfo`.
7708
7851
  */
7709
7852
  toAPIResponse() {
7710
- var _a, _b, _c, _d, _e, _f;
7853
+ var _a, _b, _c, _d, _e, _f, _g, _h;
7711
7854
  const padDateNumber = (num) => num.toString().padStart(2, "0");
7712
7855
  const convertDate = (date) => `${date.getUTCFullYear()}-${padDateNumber(date.getUTCMonth() + 1)}-${padDateNumber(date.getUTCDate())} ${padDateNumber(date.getUTCHours())}:${padDateNumber(date.getUTCMinutes())}:${padDateNumber(date.getUTCSeconds())}`;
7713
7856
  return {
@@ -7748,7 +7891,7 @@ class MapInfo {
7748
7891
  count_normal: this.circles.toString(),
7749
7892
  count_slider: this.sliders.toString(),
7750
7893
  count_spinner: this.spinners.toString(),
7751
- max_combo: this.maxCombo.toString(),
7894
+ max_combo: (_h = (_g = this.maxCombo) === null || _g === void 0 ? void 0 : _g.toString()) !== null && _h !== void 0 ? _h : null,
7752
7895
  storyboard: this.storyboardAvailable ? "1" : "0",
7753
7896
  video: this.videoAvailable ? "1" : "0",
7754
7897
  download_unavailable: this.downloadAvailable ? "0" : "1",
@@ -7882,6 +8025,16 @@ class ModFlashlight extends Mod {
7882
8025
  }
7883
8026
  }
7884
8027
 
8028
+ /**
8029
+ * Represents the osu! playfield.
8030
+ */
8031
+ class Playfield {
8032
+ }
8033
+ /**
8034
+ * The size of the playfield, which is 512x384.
8035
+ */
8036
+ Playfield.baseSize = new Vector2(512, 384);
8037
+
7885
8038
  /**
7886
8039
  * Represents the HardRock mod.
7887
8040
  */
@@ -7914,6 +8067,29 @@ class ModHardRock extends Mod {
7914
8067
  difficulty.od = this.applySetting(difficulty.od);
7915
8068
  difficulty.hp = this.applySetting(difficulty.hp);
7916
8069
  }
8070
+ applyToHitObject(_, hitObject) {
8071
+ // Reflect the position of the hit object.
8072
+ hitObject.position = this.reflectVector(hitObject.position);
8073
+ if (!(hitObject instanceof Slider)) {
8074
+ return;
8075
+ }
8076
+ // Reflect the control points of the slider. This will reflect the positions of head and tail circles.
8077
+ hitObject.path = new SliderPath({
8078
+ pathType: hitObject.path.pathType,
8079
+ controlPoints: hitObject.path.controlPoints.map((v) => this.reflectControlPoint(v)),
8080
+ expectedDistance: hitObject.path.expectedDistance,
8081
+ });
8082
+ // Reflect the position of slider ticks and repeats.
8083
+ hitObject.nestedHitObjects.forEach((obj) => {
8084
+ obj.position = this.reflectVector(obj.position);
8085
+ });
8086
+ }
8087
+ reflectVector(vector) {
8088
+ return new Vector2(vector.x, Playfield.baseSize.y - vector.y);
8089
+ }
8090
+ reflectControlPoint(vector) {
8091
+ return new Vector2(vector.x, -vector.y);
8092
+ }
7917
8093
  applySetting(value, ratio = 1.4) {
7918
8094
  return Math.min(value * ratio, 10);
7919
8095
  }
@@ -8341,30 +8517,6 @@ class NormalDistribution {
8341
8517
  }
8342
8518
  }
8343
8519
 
8344
- /**
8345
- * Represents the osu! playfield.
8346
- */
8347
- class Playfield {
8348
- }
8349
- /**
8350
- * The size of the playfield, which is 512x384.
8351
- */
8352
- Playfield.baseSize = new Vector2(512, 384);
8353
-
8354
- /**
8355
- * Ranking status of a beatmap.
8356
- */
8357
- exports.RankedStatus = void 0;
8358
- (function (RankedStatus) {
8359
- RankedStatus[RankedStatus["graveyard"] = -2] = "graveyard";
8360
- RankedStatus[RankedStatus["wip"] = -1] = "wip";
8361
- RankedStatus[RankedStatus["pending"] = 0] = "pending";
8362
- RankedStatus[RankedStatus["ranked"] = 1] = "ranked";
8363
- RankedStatus[RankedStatus["approved"] = 2] = "approved";
8364
- RankedStatus[RankedStatus["qualified"] = 3] = "qualified";
8365
- RankedStatus[RankedStatus["loved"] = 4] = "loved";
8366
- })(exports.RankedStatus || (exports.RankedStatus = {}));
8367
-
8368
8520
  dotenv.config();
8369
8521
 
8370
8522
  exports.AR0_MS = AR0_MS;