@rian8337/osu-base 2.0.0-alpha.6 → 2.0.0

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
@@ -815,9 +815,7 @@ class Slider extends HitObject {
815
815
  startTime: finalSpanEndTime,
816
816
  });
817
817
  this.nestedHitObjects.push(this.tail);
818
- this.nestedHitObjects.sort((a, b) => {
819
- return a.startTime - b.startTime;
820
- });
818
+ this.nestedHitObjects.sort((a, b) => a.startTime - b.startTime);
821
819
  }
822
820
  toString() {
823
821
  return `Position: [${this.position.x}, ${this.position.y}], distance: ${this.path.expectedDistance}, repetitions: ${this.repetitions}, slider ticks: ${this.nestedHitObjects.filter((v) => v instanceof SliderTick).length}`;
@@ -1196,6 +1194,8 @@ class ControlPointManager {
1196
1194
  /**
1197
1195
  * Removes a control point.
1198
1196
  *
1197
+ * This method will remove the earliest control point in the array that is equal to the given control point.
1198
+ *
1199
1199
  * @param controlPoint The control point to remove.
1200
1200
  * @returns Whether the control point was removed.
1201
1201
  */
@@ -1872,6 +1872,51 @@ class HitWindow {
1872
1872
  * Represents the hit window of osu!droid.
1873
1873
  */
1874
1874
  class DroidHitWindow extends HitWindow {
1875
+ /**
1876
+ * Calculates the overall difficulty value of a great hit window.
1877
+ *
1878
+ * @param value The value of the hit window, in milliseconds.
1879
+ * @param isPrecise Whether to calculate for Precise mod.
1880
+ * @returns The overall difficulty value.
1881
+ */
1882
+ static hitWindow300ToOD(value, isPrecise) {
1883
+ if (isPrecise) {
1884
+ return 5 - (value - 55) / 6;
1885
+ }
1886
+ else {
1887
+ return 5 - (value - 75) / 5;
1888
+ }
1889
+ }
1890
+ /**
1891
+ * Calculates the overall difficulty value of a good hit window.
1892
+ *
1893
+ * @param value The value of the hit window, in milliseconds.
1894
+ * @param isPrecise Whether to calculate for Precise mod.
1895
+ * @returns The overall difficulty value.
1896
+ */
1897
+ static hitWindow100ToOD(value, isPrecise) {
1898
+ if (isPrecise) {
1899
+ return 5 - (value - 120) / 8;
1900
+ }
1901
+ else {
1902
+ return 5 - (value - 150) / 10;
1903
+ }
1904
+ }
1905
+ /**
1906
+ * Calculates the overall difficulty value of a meh hit window.
1907
+ *
1908
+ * @param value The value of the hit window, in milliseconds.
1909
+ * @param isPrecise Whether to calculate for Precise mod.
1910
+ * @returns The overall difficulty value.
1911
+ */
1912
+ static hitWindow50ToOD(value, isPrecise) {
1913
+ if (isPrecise) {
1914
+ return 5 - (value - 180) / 10;
1915
+ }
1916
+ else {
1917
+ return 5 - (value - 250) / 10;
1918
+ }
1919
+ }
1875
1920
  hitWindowFor300(isPrecise) {
1876
1921
  if (isPrecise) {
1877
1922
  return 55 + 6 * (5 - this.overallDifficulty);
@@ -1901,6 +1946,33 @@ class DroidHitWindow extends HitWindow {
1901
1946
  * Represents the hit window of osu!standard.
1902
1947
  */
1903
1948
  class OsuHitWindow extends HitWindow {
1949
+ /**
1950
+ * Calculates the overall difficulty value of a great hit window.
1951
+ *
1952
+ * @param value The value of the hit window, in milliseconds.
1953
+ * @returns The overall difficulty value.
1954
+ */
1955
+ static hitWindow300ToOD(value) {
1956
+ return (80 - value) / 6;
1957
+ }
1958
+ /**
1959
+ * Calculates the overall difficulty value of a good hit window.
1960
+ *
1961
+ * @param value The value of the hit window, in milliseconds.
1962
+ * @returns The overall difficulty value.
1963
+ */
1964
+ static hitWindow100ToOD(value) {
1965
+ return (140 - value) / 8;
1966
+ }
1967
+ /**
1968
+ * Calculates the overall difficulty value of a meh hit window.
1969
+ *
1970
+ * @param value The value of the hit window, in milliseconds.
1971
+ * @returns The overall difficulty value.
1972
+ */
1973
+ static hitWindow50ToOD(value) {
1974
+ return (200 - value) / 10;
1975
+ }
1904
1976
  hitWindowFor300() {
1905
1977
  return 80 - 6 * this.overallDifficulty;
1906
1978
  }
@@ -2076,6 +2148,8 @@ class ModFlashlight extends Mod {
2076
2148
  * Represents the Hidden mod.
2077
2149
  */
2078
2150
  class ModHidden extends Mod {
2151
+ static fadeInDurationMultiplier = 0.4;
2152
+ static fadeOutDurationMultiplier = 0.3;
2079
2153
  scoreMultiplier = 1.06;
2080
2154
  acronym = "HD";
2081
2155
  name = "Hidden";
@@ -2234,9 +2308,7 @@ class ModUtil {
2234
2308
  * Mods that change the way the map looks.
2235
2309
  */
2236
2310
  static mapChangingMods = [
2237
- new ModDoubleTime(),
2238
- new ModNightCore(),
2239
- new ModHalfTime(),
2311
+ ...this.speedChangingMods,
2240
2312
  new ModEasy(),
2241
2313
  new ModHardRock(),
2242
2314
  new ModSmallCircle(),
@@ -2245,24 +2317,27 @@ class ModUtil {
2245
2317
  * Gets a list of mods from a droid mod string, such as "hd".
2246
2318
  *
2247
2319
  * @param str The string.
2320
+ * @param options Options for parsing behavior.
2248
2321
  */
2249
- static droidStringToMods(str) {
2250
- return this.checkDuplicateMods(this.allMods.filter((m) => m.droidString && str.toLowerCase().includes(m.droidString)));
2322
+ static droidStringToMods(str, options) {
2323
+ return this.processParsingOptions(this.allMods.filter((m) => m.droidString && str.toLowerCase().includes(m.droidString)), options);
2251
2324
  }
2252
2325
  /**
2253
2326
  * Gets a list of mods from a PC modbits.
2254
2327
  *
2255
2328
  * @param modbits The modbits.
2329
+ * @param options Options for parsing behavior.
2256
2330
  */
2257
- static pcModbitsToMods(modbits) {
2258
- return this.checkDuplicateMods(this.allMods.filter((m) => m.bitwise & modbits));
2331
+ static pcModbitsToMods(modbits, options) {
2332
+ return this.processParsingOptions(this.allMods.filter((m) => m.bitwise & modbits), options);
2259
2333
  }
2260
2334
  /**
2261
2335
  * Gets a list of mods from a PC mod string, such as "HDHR".
2262
2336
  *
2263
2337
  * @param str The string.
2338
+ * @param options Options for parsing behavior.
2264
2339
  */
2265
- static pcStringToMods(str) {
2340
+ static pcStringToMods(str, options) {
2266
2341
  const finalMods = [];
2267
2342
  str = str.toLowerCase();
2268
2343
  while (str) {
@@ -2276,14 +2351,25 @@ class ModUtil {
2276
2351
  }
2277
2352
  str = str.slice(nchars);
2278
2353
  }
2279
- return this.checkDuplicateMods(finalMods);
2354
+ return this.processParsingOptions(finalMods, options);
2280
2355
  }
2281
2356
  /**
2282
- * Checks for mods that are duplicate and incompatible with each other.
2357
+ * Checks for mods that are duplicated.
2283
2358
  *
2284
2359
  * @param mods The mods to check for.
2360
+ * @returns Mods that have been filtered.
2285
2361
  */
2286
2362
  static checkDuplicateMods(mods) {
2363
+ return Array.from(new Set(mods));
2364
+ }
2365
+ /**
2366
+ * Checks for mods that are incompatible with each other.
2367
+ *
2368
+ * This will modify the original array.
2369
+ *
2370
+ * @param mods The mods to check for.
2371
+ */
2372
+ static checkIncompatibleMods(mods) {
2287
2373
  for (const incompatibleMod of this.incompatibleMods) {
2288
2374
  const fulfilledMods = mods.filter((m) => incompatibleMod.map((v) => v.acronym).includes(m.acronym));
2289
2375
  if (fulfilledMods.length > 1) {
@@ -2294,8 +2380,22 @@ class ModUtil {
2294
2380
  mods.push(fulfilledMods[0]);
2295
2381
  }
2296
2382
  }
2297
- // Check for duplicate mod entries
2298
- return Array.from(new Set(mods));
2383
+ }
2384
+ /**
2385
+ * Processes parsing options.
2386
+ *
2387
+ * @param mods The mods to process.
2388
+ * @param options The options to process.
2389
+ * @returns The processed mods.
2390
+ */
2391
+ static processParsingOptions(mods, options) {
2392
+ if (options?.checkDuplicate !== false) {
2393
+ mods = this.checkDuplicateMods(mods);
2394
+ }
2395
+ if (options?.checkIncompatible !== false) {
2396
+ this.checkIncompatibleMods(mods);
2397
+ }
2398
+ return mods;
2299
2399
  }
2300
2400
  }
2301
2401
 
@@ -2394,29 +2494,31 @@ class MapStats {
2394
2494
  }
2395
2495
  switch (params?.mode ?? exports.modes.osu) {
2396
2496
  case exports.modes.droid:
2397
- // In droid pre-1.6.8, NC speed multiplier is assumed bugged (1.39)
2497
+ // In droid pre-1.6.8, NC speed multiplier is assumed bugged (1.39).
2398
2498
  if (this.mods.some((m) => m instanceof ModNightCore) &&
2399
2499
  this.oldStatistics) {
2400
2500
  this.speedMultiplier *= 1.39 / 1.5;
2401
2501
  }
2402
2502
  // CS and OD work differently in droid, therefore it
2403
2503
  // needs to be computed regardless of map-changing mods
2404
- // and statistics multiplier
2504
+ // and statistics multiplier.
2405
2505
  if (this.od !== undefined) {
2406
- // apply EZ or HR to OD
2506
+ // Apply EZ or HR to OD.
2407
2507
  this.od = Math.min(this.od * statisticsMultiplier, 10);
2408
- // convert original OD to droid OD
2508
+ // Convert original OD to droid hit window to take
2509
+ // droid hit window and the PR mod in mind.
2409
2510
  const droidToMS = new DroidHitWindow(this.od).hitWindowFor300(this.mods.some((m) => m instanceof ModPrecise)) / this.speedMultiplier;
2410
- this.od = 5 - (droidToMS - 50) / 6;
2511
+ // Convert droid hit window back to original OD.
2512
+ this.od = OsuHitWindow.hitWindow300ToOD(droidToMS);
2411
2513
  }
2412
2514
  // HR and EZ works differently in droid in terms of
2413
- // CS modification (even CS in itself as well)
2515
+ // CS modification (even CS in itself as well).
2414
2516
  //
2415
2517
  // If present mods are found, they need to be removed
2416
2518
  // from the bitwise enum of mods to prevent double
2417
- // calculation
2519
+ // calculation.
2418
2520
  if (this.cs !== undefined) {
2419
- // Assume 681 is height
2521
+ // Assume 681 is height.
2420
2522
  const assumedHeight = 681;
2421
2523
  let scale = ((assumedHeight / 480) * (54.42 - this.cs * 4.48) * 2) /
2422
2524
  128 +
@@ -2433,7 +2535,8 @@ class MapStats {
2433
2535
  if (this.mods.some((m) => m instanceof ModSmallCircle)) {
2434
2536
  scale -= ((assumedHeight / 480) * (4 * 4.48) * 2) / 128;
2435
2537
  }
2436
- const radius = (64 * scale) / ((assumedHeight * 0.85) / 384);
2538
+ const radius = (64 * Math.max(1e-3, scale)) /
2539
+ ((assumedHeight * 0.85) / 384);
2437
2540
  this.cs = Math.min(5 + ((1 - radius / 32) * 5) / 0.7, 10);
2438
2541
  }
2439
2542
  if (this.hp !== undefined) {
@@ -5226,6 +5329,15 @@ class Encoder {
5226
5329
  * The result of the encoding process.
5227
5330
  */
5228
5331
  finalResult = "";
5332
+ /**
5333
+ * The result of the encoding process.
5334
+ */
5335
+ get result() {
5336
+ return this.finalResult;
5337
+ }
5338
+ /**
5339
+ * @param target The target of the encoding process.
5340
+ */
5229
5341
  constructor(target) {
5230
5342
  this.target = target;
5231
5343
  }
@@ -5237,7 +5349,7 @@ class Encoder {
5237
5349
  encode() {
5238
5350
  this.reset();
5239
5351
  this.encodeInternal();
5240
- return this.finalResult;
5352
+ return this;
5241
5353
  }
5242
5354
  /**
5243
5355
  * Writes a line to encoded text.
@@ -5670,6 +5782,12 @@ class StoryboardVariablesEncoder extends StoryboardBaseEncoder {
5670
5782
  }
5671
5783
  }
5672
5784
 
5785
+ /**
5786
+ * A storyboard encoder.
5787
+ *
5788
+ * Note that this storyboard encoder does not encode storyboards, and as such equality with the
5789
+ * original beatmap or storyboard file is not guaranteed (and usually will not be equal).
5790
+ */
5673
5791
  class StoryboardEncoder extends Encoder {
5674
5792
  finalResult = "";
5675
5793
  encoders = [];
@@ -5709,7 +5827,7 @@ class BeatmapEventsEncoder extends BeatmapBaseEncoder {
5709
5827
  this.writeLine(`2,${b.startTime},${b.endTime}`);
5710
5828
  }
5711
5829
  if (this.map.events.storyboard) {
5712
- this.writeLine(new StoryboardEncoder(this.map.events.storyboard, false).encode());
5830
+ this.writeLine(new StoryboardEncoder(this.map.events.storyboard, false).encode().result);
5713
5831
  }
5714
5832
  else {
5715
5833
  this.writeLine("//Storyboard Layer 0 (Background)");
@@ -6125,31 +6243,16 @@ class MapInfo {
6125
6243
  */
6126
6244
  videoAvailable = false;
6127
6245
  /**
6128
- * The parsed beatmap from beatmap parser.
6246
+ * The decoded beatmap from beatmap decoder.
6129
6247
  */
6130
- get map() {
6131
- return Utils.deepCopy(this.cachedBeatmap);
6248
+ get beatmap() {
6249
+ return this.cachedBeatmap;
6132
6250
  }
6133
- cachedBeatmap;
6134
- /**
6135
- * Retrieve a beatmap's general information.
6136
- *
6137
- * Either beatmap ID or MD5 hash of the beatmap must be specified. If both are specified, beatmap ID is taken.
6138
- */
6139
- static async getInformation(params) {
6140
- params.file ??= true;
6141
- const beatmapID = params.beatmapID;
6142
- const hash = params.hash;
6143
- if (!beatmapID && !hash) {
6144
- throw new Error("Beatmap ID or MD5 hash must be defined");
6145
- }
6146
- const apiRequestBuilder = new OsuAPIRequestBuilder().setEndpoint("get_beatmaps");
6147
- if (beatmapID) {
6148
- apiRequestBuilder.addParameter("b", beatmapID);
6149
- }
6150
- else if (hash) {
6151
- apiRequestBuilder.addParameter("h", hash);
6152
- }
6251
+ cachedBeatmap = null;
6252
+ static async getInformation(beatmapIdOrHash, downloadBeatmap) {
6253
+ const apiRequestBuilder = new OsuAPIRequestBuilder()
6254
+ .setEndpoint("get_beatmaps")
6255
+ .addParameter(typeof beatmapIdOrHash === "string" ? "h" : "b", beatmapIdOrHash);
6153
6256
  const map = new MapInfo();
6154
6257
  const result = await apiRequestBuilder.sendRequest();
6155
6258
  if (result.statusCode !== 200) {
@@ -6157,13 +6260,13 @@ class MapInfo {
6157
6260
  }
6158
6261
  const mapinfo = JSON.parse(result.data.toString("utf-8"))[0];
6159
6262
  if (!mapinfo) {
6160
- return map;
6263
+ return null;
6161
6264
  }
6162
6265
  if (parseInt(mapinfo.mode) !== 0) {
6163
- return map;
6266
+ return null;
6164
6267
  }
6165
6268
  map.fillMetadata(mapinfo);
6166
- if (params.file) {
6269
+ if (downloadBeatmap !== false) {
6167
6270
  await map.retrieveBeatmapFile();
6168
6271
  }
6169
6272
  return map;
@@ -6226,15 +6329,23 @@ class MapInfo {
6226
6329
  this.videoAvailable = !!parseInt(mapinfo.video);
6227
6330
  return this;
6228
6331
  }
6332
+ /**
6333
+ * Checks whether the beatmap file has been downloaded.
6334
+ */
6335
+ hasDownloadedBeatmap() {
6336
+ return this.cachedBeatmap !== null;
6337
+ }
6229
6338
  /**
6230
6339
  * Retrieves the .osu file of the beatmap.
6231
6340
  *
6232
- * @param forceDownload Whether or not to download the file regardless if it's already available.
6341
+ * After this, you can use the `hasDownloadedBeatmap` method to check if the beatmap has been downloaded.
6342
+ *
6343
+ * @param force Whether to download the file regardless if it's already available.
6233
6344
  */
6234
- retrieveBeatmapFile(forceDownload) {
6345
+ retrieveBeatmapFile(force) {
6235
6346
  return new Promise((resolve) => {
6236
- if (this.cachedBeatmap && !forceDownload) {
6237
- return resolve(this);
6347
+ if (this.hasDownloadedBeatmap() && !force) {
6348
+ return resolve();
6238
6349
  }
6239
6350
  const url = `https://osu.ppy.sh/osu/${this.beatmapID}`;
6240
6351
  const dataArray = [];
@@ -6244,10 +6355,10 @@ class MapInfo {
6244
6355
  })
6245
6356
  .on("complete", (response) => {
6246
6357
  if (response.statusCode !== 200) {
6247
- return resolve(this);
6358
+ return resolve();
6248
6359
  }
6249
6360
  this.cachedBeatmap = new BeatmapDecoder().decode(Buffer.concat(dataArray).toString("utf8")).result;
6250
- resolve(this);
6361
+ resolve();
6251
6362
  });
6252
6363
  });
6253
6364
  }
@@ -6307,10 +6418,12 @@ class MapInfo {
6307
6418
  *
6308
6419
  * - Option `0`: return map title and mods used if defined
6309
6420
  * - Option `1`: return song source and map download link to beatmap mirrors
6310
- * - Option `2`: return CS, AR, OD, HP
6311
- * - Option `3`: return BPM, map length, max combo
6312
- * - Option `4`: return last update date and map status
6313
- * - Option `5`: return favorite count and play count
6421
+ * - Option `2`: return circle, slider, and spinner count
6422
+ * - Option `3`: return CS, AR, OD, HP, and max score statistics for droid
6423
+ * - Option `4`: return CS, AR, OD, HP, and max score statistics for PC
6424
+ * - Option `5`: return BPM, map length, and max combo
6425
+ * - Option `6`: return last update date and map status
6426
+ * - Option `7`: return favorite count and play count
6314
6427
  *
6315
6428
  * @param option The option to pick.
6316
6429
  * @param stats The custom statistics to apply. This will only be used to apply mods, custom speed multiplier, and force AR.
@@ -6333,13 +6446,9 @@ class MapInfo {
6333
6446
  mapParams.speedMultiplier =
6334
6447
  stats.speedMultiplier ?? mapParams.speedMultiplier;
6335
6448
  }
6336
- const mapStatistics = new MapStats(mapParams).calculate();
6337
- mapStatistics.cs = parseFloat(mapStatistics.cs.toFixed(2));
6338
- mapStatistics.ar = parseFloat(mapStatistics.ar.toFixed(2));
6339
- mapStatistics.od = parseFloat(mapStatistics.od.toFixed(2));
6340
- mapStatistics.hp = parseFloat(mapStatistics.hp.toFixed(2));
6341
6449
  switch (option) {
6342
6450
  case 0: {
6451
+ const mapStatistics = new MapStats(mapParams).calculate();
6343
6452
  let string = `${this.fullTitle}${(mapStatistics.mods.length ?? 0) > 0
6344
6453
  ? ` +${mapStatistics.mods
6345
6454
  .map((m) => m.acronym)
@@ -6383,19 +6492,65 @@ class MapInfo {
6383
6492
  return string;
6384
6493
  }
6385
6494
  case 2:
6386
- return `**Circles**: ${this.circles} - **Sliders**: ${this.sliders} - **Spinners**: ${this.spinners}\n**CS**: ${this.cs}${this.cs === mapStatistics.cs ? "" : ` (${mapStatistics.cs})`} - **AR**: ${this.ar}${this.ar === mapStatistics.ar ? "" : ` (${mapStatistics.ar})`} - **OD**: ${this.od}${this.od === mapStatistics.od ? "" : ` (${mapStatistics.od})`} - **HP**: ${this.hp}${this.hp === mapStatistics.hp ? "" : ` (${mapStatistics.hp})`}`;
6495
+ return `**Circles**: ${this.circles} - **Sliders**: ${this.sliders} - **Spinners**: ${this.spinners}`;
6387
6496
  case 3: {
6388
- const maxScore = this.map?.maxDroidScore(mapStatistics) ?? 0;
6497
+ const droidOriginalStats = new MapStats({
6498
+ cs: this.cs,
6499
+ ar: this.ar,
6500
+ od: this.od,
6501
+ hp: this.hp,
6502
+ }).calculate({ mode: exports.modes.droid });
6503
+ const droidModifiedStats = new MapStats(mapParams).calculate({ mode: exports.modes.droid });
6504
+ droidOriginalStats.cs = MathUtils.round(droidOriginalStats.cs, 2);
6505
+ droidOriginalStats.ar = MathUtils.round(droidOriginalStats.ar, 2);
6506
+ droidOriginalStats.od = MathUtils.round(droidOriginalStats.od, 2);
6507
+ droidOriginalStats.hp = MathUtils.round(droidOriginalStats.hp, 2);
6508
+ droidModifiedStats.cs = MathUtils.round(droidModifiedStats.cs, 2);
6509
+ droidModifiedStats.ar = MathUtils.round(droidModifiedStats.ar, 2);
6510
+ droidModifiedStats.od = MathUtils.round(droidModifiedStats.od, 2);
6511
+ droidModifiedStats.hp = MathUtils.round(droidModifiedStats.hp, 2);
6512
+ const maxScore = this.beatmap?.maxDroidScore(new MapStats(mapParams)) ?? 0;
6513
+ return `**CS**: ${droidOriginalStats.cs}${Precision.almostEqualsNumber(droidOriginalStats.cs, droidModifiedStats.cs)
6514
+ ? ""
6515
+ : ` (${droidModifiedStats.cs})`} - **AR**: ${droidOriginalStats.ar}${Precision.almostEqualsNumber(droidOriginalStats.ar, droidModifiedStats.ar)
6516
+ ? ""
6517
+ : ` (${droidModifiedStats.ar})`} - **OD**: ${droidOriginalStats.od}${Precision.almostEqualsNumber(droidOriginalStats.od, droidModifiedStats.od)
6518
+ ? ""
6519
+ : ` (${droidModifiedStats.od})`} - **HP**: ${droidOriginalStats.hp}${Precision.almostEqualsNumber(droidOriginalStats.hp, droidModifiedStats.hp)
6520
+ ? ""
6521
+ : ` (${droidModifiedStats.hp})`}${maxScore > 0
6522
+ ? `\n**Max Score**: ${maxScore.toLocaleString()}`
6523
+ : ""}`;
6524
+ }
6525
+ case 4: {
6526
+ const mapStatistics = new MapStats(mapParams).calculate();
6527
+ mapStatistics.cs = MathUtils.round(mapStatistics.cs, 2);
6528
+ mapStatistics.ar = MathUtils.round(mapStatistics.ar, 2);
6529
+ mapStatistics.od = MathUtils.round(mapStatistics.od, 2);
6530
+ mapStatistics.hp = MathUtils.round(mapStatistics.hp, 2);
6531
+ const maxScore = this.beatmap?.maxOsuScore(mapStatistics.mods) ?? 0;
6532
+ return `**CS**: ${this.cs}${Precision.almostEqualsNumber(this.cs, mapStatistics.cs)
6533
+ ? ""
6534
+ : ` (${mapStatistics.cs})`} - **AR**: ${this.ar}${Precision.almostEqualsNumber(this.ar, mapStatistics.ar)
6535
+ ? ""
6536
+ : ` (${mapStatistics.ar})`} - **OD**: ${this.od}${Precision.almostEqualsNumber(this.od, mapStatistics.od)
6537
+ ? ""
6538
+ : ` (${mapStatistics.od})`} - **HP**: ${this.hp}${Precision.almostEqualsNumber(this.hp, mapStatistics.hp)
6539
+ ? ""
6540
+ : ` (${mapStatistics.hp})`}${maxScore > 0
6541
+ ? `\n**Max Score**: ${maxScore.toLocaleString()}`
6542
+ : ""}`;
6543
+ }
6544
+ case 5: {
6545
+ const mapStatistics = new MapStats(mapParams).calculate();
6389
6546
  const convertedBPM = this.convertBPM(mapStatistics);
6390
6547
  let string = "**BPM**: ";
6391
- if (this.map) {
6392
- const uninheritedTimingPoints = this.map.controlPoints.timing.points;
6548
+ if (this.beatmap) {
6549
+ const uninheritedTimingPoints = this.beatmap.controlPoints.timing.points;
6393
6550
  if (uninheritedTimingPoints.length === 1) {
6394
6551
  string += `${this.bpm}${!Precision.almostEqualsNumber(this.bpm, convertedBPM)
6395
6552
  ? ` (${convertedBPM})`
6396
- : ""} - **Length**: ${this.convertTime(mapStatistics)} - **Max Combo**: ${this.maxCombo}x${maxScore > 0
6397
- ? `\n**Max Score**: ${maxScore.toLocaleString()}`
6398
- : ""}`;
6553
+ : ""} - **Length**: ${this.convertTime(mapStatistics)} - **Max Combo**: ${this.maxCombo}x`;
6399
6554
  }
6400
6555
  else {
6401
6556
  let maxBPM = this.bpm;
@@ -6422,23 +6577,19 @@ class MapInfo {
6422
6577
  string += `(${convertedBPM}) `;
6423
6578
  }
6424
6579
  }
6425
- string += `- **Length**: ${this.convertTime(mapStatistics)} - **Max Combo**: ${this.maxCombo}x${maxScore > 0
6426
- ? `\n**Max score**: ${maxScore.toLocaleString()}`
6427
- : ""}`;
6580
+ string += `- **Length**: ${this.convertTime(mapStatistics)} - **Max Combo**: ${this.maxCombo}x`;
6428
6581
  }
6429
6582
  }
6430
6583
  else {
6431
6584
  string += `${this.bpm}${!Precision.almostEqualsNumber(this.bpm, convertedBPM)
6432
6585
  ? ` (${convertedBPM})`
6433
- : ""} - **Length**: ${this.convertTime(mapStatistics)} - **Max Combo**: ${this.maxCombo}x${maxScore > 0
6434
- ? `\n**Max score**: ${maxScore.toLocaleString()}`
6435
- : ""}`;
6586
+ : ""} - **Length**: ${this.convertTime(mapStatistics)} - **Max Combo**: ${this.maxCombo}x`;
6436
6587
  }
6437
6588
  return string;
6438
6589
  }
6439
- case 4:
6590
+ case 6:
6440
6591
  return `**Last Update**: ${this.lastUpdate.toUTCString()} | **${this.convertStatus()}**`;
6441
- case 5:
6592
+ case 7:
6442
6593
  return `❤️ **${this.favorites.toLocaleString()}** - ▶️ **${this.plays.toLocaleString()}**`;
6443
6594
  default:
6444
6595
  throw {