@rian8337/osu-base 2.0.0-alpha.25 → 2.0.0-alpha.31

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/README.md CHANGED
@@ -4,11 +4,33 @@ The base module required for all my osu! modules.
4
4
 
5
5
  # Features
6
6
 
7
- This module provides the required feature for all my osu! related modules, which is the beatmap parser.
7
+ This module serves as the foundation of all my osu! modules. It provides multiple features such as:
8
+
9
+ - Accuracy calculator and estimator
10
+ - Calculate an accuracy value from hit values or estimate hit values from an accuracy value.
11
+ - API request builders
12
+ - Supports osu!droid API and osu! API v1.
13
+ - Beatmap decoder
14
+ - Fully decodes an `.osu` file into a `Beatmap` object that is easier to work with.
15
+ - Beatmap encoder
16
+ - Will encode a beatmap into an `.osu` file with format version 14.
17
+ - Hit window converters
18
+ - Convert an OD value to its hit window values and vice versa.
19
+ - Map statistics calculator
20
+ - Calculate a given map statistics (CS, AR, OD, and HP) with modifications applied (mods, custom speed multiplier, etc).
21
+ - Mod conversion utilities
22
+ - Convert a mod combination string in osu!droid (i.e. `hr`) and osu!standard (i.e. `HDHR`) into an array of mods.
23
+ - Available mods can be looked at the documentation.
24
+ - Storyboard decoder
25
+ - Supports `.osb` and `.osu` files.
26
+ - Storyboard encoder
27
+ - Storyboard will be encoded into a format that is supported by `.osu` files (that is, all variable instances will be replaced).
8
28
 
9
29
  # Specific Requirements
10
30
 
11
- If you want to retrieve a beatmap using osu! API, you need to have an osu! API key set as `OSU_API_KEY` environment variable.
31
+ If you want to use the osu!droid API, you need to have an osu!droid API key set as `DROID_API_KEY` environment variable.
32
+
33
+ If you want to use the osu! API, you need to have an osu! API key set as `OSU_API_KEY` environment variable.
12
34
 
13
35
  See usage for more details.
14
36
 
@@ -26,7 +48,82 @@ yarn add @rian8337/osu-base
26
48
 
27
49
  # Usage
28
50
 
29
- ## Using osu! API
51
+ ## Accuracy Calculator and Estimator
52
+
53
+ ### Calculate Accuracy Value (0-1) from Hit Values
54
+
55
+ ```js
56
+ import { Accuracy } from "@rian8337/osu-base";
57
+
58
+ // If you specified the amount of great hits (`n300`).
59
+ let accuracyValue = new Accuracy({
60
+ n300: 1000,
61
+ n100: 125,
62
+ n50: 10,
63
+ nmiss: 5,
64
+ }).value();
65
+
66
+ console.log(accuracyValue);
67
+
68
+ // If you didn't specify the amount of great hits (`n300`).
69
+ const objectCount = 2000;
70
+
71
+ accuracyValue = new Accuracy({
72
+ n100: 125,
73
+ n50: 10,
74
+ nmiss: 5,
75
+ }).value(objectCount);
76
+ ```
77
+
78
+ ### Hit Values Estimation
79
+
80
+ The way this estimation works is that the estimator will see if the accuracy can be estimated by just using good hit values. Otherwise, the estimator will use meh hit values.
81
+
82
+ Keep in mind that it will not use the miss hit value.
83
+
84
+ ```js
85
+ import { Accuracy } from "@rian8337/osu-base";
86
+
87
+ const accuracy = new Accuracy({
88
+ percent: 99.7,
89
+ nobjects: 2000,
90
+ });
91
+
92
+ console.log(accuracy.n300);
93
+ console.log(accuracy.n100);
94
+ console.log(accuracy.n50);
95
+ ```
96
+
97
+ ## API Request Builders
98
+
99
+ ```js
100
+ import {
101
+ DroidAPIRequestBuilder,
102
+ OsuAPIRequestBuilder,
103
+ } from "@rian8337/osu-base";
104
+
105
+ const droidBuilder = new DroidAPIRequestBuilder()
106
+ .setEndpoint("getuserinfo.php")
107
+ .addParameter("uid", 51076);
108
+
109
+ const droidResult = await droidBuilder.sendRequest();
110
+
111
+ console.log(droidResult);
112
+
113
+ const osuBuilder = new OsuAPIRequestBuilder()
114
+ .setEndpoint("get_beatmaps")
115
+ .addParamter("b", 901854);
116
+
117
+ const osuResult = await osuBuilder.sendRequest();
118
+
119
+ console.log(osuResult);
120
+ ```
121
+
122
+ ## Beatmap Decoder
123
+
124
+ There are two primary ways of using the beatmap decoder.
125
+
126
+ ### Using osu! API
30
127
 
31
128
  ```js
32
129
  import { MapInfo } from "@rian8337/osu-base";
@@ -47,22 +144,173 @@ if (!beatmapInfo.title) {
47
144
  console.log(beatmapInfo.map);
48
145
  ```
49
146
 
50
- ### Not retrieving beatmap file
147
+ #### Not retrieving beatmap file
51
148
 
52
- You can also opt out from downloading the beatmap (`.osu`) file if you just want to retrieve information from the API by setting `file` to `false`, however a parsed beatmap will not be provided.
149
+ You can also opt out from downloading the beatmap (`.osu`) file if you just want to retrieve information from the API by setting `file` to `false`, however a decoded beatmap will not be provided.
53
150
 
54
- ## Using without osu! API
151
+ ### Not using osu! API
55
152
 
56
153
  ```js
57
154
  import { readFile } from "fs";
58
- import { Parser } from "@rian8337/osu-base";
155
+ import { BeatmapDecoder } from "@rian8337/osu-base";
59
156
 
60
157
  readFile("path/to/file.osu", { encoding: "utf-8" }, (err, data) => {
61
158
  if (err) throw err;
62
159
 
63
- const parser = new Parser().parse(data);
160
+ const decoder = new BeatmapDecoder().decode(data);
64
161
 
65
- // Parsed beatmap can be accessed via the `map` field
66
- console.log(parser.map);
162
+ // Decoded beatmap can be accessed via the `result` field
163
+ console.log(decoder.result);
67
164
  });
68
165
  ```
166
+
167
+ ## Beatmap Encoder
168
+
169
+ ```js
170
+ import { Beatmap, BeatmapEncoder } from "@rian8337/osu-base";
171
+
172
+ const beatmap = new Beatmap();
173
+
174
+ const encoder = new BeatmapEncoder(beatmap).encode();
175
+
176
+ // Encoded beatmap can be accessed via the `result` field
177
+ console.log(encoder.result);
178
+ ```
179
+
180
+ ## Hit Window Converters
181
+
182
+ ### OD to Hit Window
183
+
184
+ ```js
185
+ import { DroidHitWindow, OsuHitWindow } from "@rian8337/osu-base";
186
+
187
+ // Convert OD to osu!droid hit window
188
+ const droidWindow = new DroidHitWindow(10);
189
+
190
+ console.log(droidHitWindow.hitWindowFor300());
191
+ console.log(droidHitWindow.hitWindowFor100());
192
+ console.log(droidHitWindow.hitWindowFor50());
193
+
194
+ // Calculating for the Precise mod in mind is also possible
195
+ console.log(droidHitWindow.hitWindowFor300(true));
196
+ console.log(droidHitWindow.hitWindowFor100(true));
197
+ console.log(droidHitWindow.hitWindowFor50(true));
198
+
199
+ // Convert OD to osu!standard hit window
200
+ const osuWindow = new OsuHitWindow(10);
201
+
202
+ console.log(osuWindow.hitWindowFor300());
203
+ console.log(osuWindow.hitWindowFor100());
204
+ console.log(osuWindow.hitWindowFor50());
205
+ ```
206
+
207
+ ### Hit Window to OD
208
+
209
+ ```js
210
+ import { DroidHitWindow, OsuHitWindow } from "@rian8337/osu-base";
211
+
212
+ // Convert hit window to osu!droid OD
213
+ console.log(DroidHitWindow.hitWindow300ToOD(50));
214
+ console.log(DroidHitWindow.hitWindow100ToOD(100));
215
+ console.log(DroidHitWindow.hitWindow50ToOD(200));
216
+
217
+ // Calculating for the Precise mod in mind is also possible
218
+ console.log(DroidHitWindow.hitWindow300ToOD(25, true));
219
+ console.log(DroidHitWindow.hitWindow100ToOD(80, true));
220
+ console.log(DroidHitWindow.hitWindow50ToOD(130, true));
221
+
222
+ // Convert hit window to osu!standard OD
223
+ console.log(OsuHitWindow.hitWindow300ToOD(20));
224
+ console.log(OsuHitWindow.hitWindow100ToOD(60));
225
+ console.log(OsuHitWindow.hitWindow50ToOD(100));
226
+ ```
227
+
228
+ ## Map Statistics Calculator
229
+
230
+ The map statistics calculator can only be used once per instance.
231
+
232
+ ### General Usage
233
+
234
+ ```js
235
+ import { MapStats } from "@rian8337/osu-base";
236
+
237
+ const stats = new MapStats({
238
+ cs: 4,
239
+ ar: 9,
240
+ od: 8,
241
+ hp: 6,
242
+ }).calculate();
243
+
244
+ console.log(stats);
245
+ ```
246
+
247
+ Every value is optional.
248
+
249
+ ### Available Options
250
+
251
+ You can specify more options to alter the final result of the calculation:
252
+
253
+ - Mods
254
+ - Custom speed multiplier
255
+ - Force AR (whether to keep the AR at its original value)
256
+ - Game mode (switch between osu!droid and osu!standard, defaults to osu!standard)
257
+
258
+ ```js
259
+ import { MapStats, ModUtil, modes } from "@rian8337/osu-base";
260
+
261
+ const stats = new MapStats({
262
+ cs: 4,
263
+ ar: 9,
264
+ od: 8,
265
+ hp: 6,
266
+ mods: ModUtil.pcStringToMods("HDHR"),
267
+ speedMultiplier: 1.25,
268
+ isForceAR: true,
269
+ }).calculate({ mode: modes.osu });
270
+
271
+ console.log(stats);
272
+ ```
273
+
274
+ ## Mod Conversion Utilities
275
+
276
+ ```js
277
+ import { ModUtil } from "@rian8337/osu-base";
278
+
279
+ // Convert droid mod string into an array of mods
280
+ console.log(ModUtil.droidStringToMods("hr"));
281
+
282
+ // Convert PC modbits into an array of mods
283
+ console.log(ModUtil.pcModbitsToMods(12));
284
+
285
+ // Convert PC mod string into an array mods
286
+ console.log(ModUtil.pcStringToMods("HDHR"));
287
+ ```
288
+
289
+ ## Storyboard Decoder
290
+
291
+ ```js
292
+ import { readFile } from "fs";
293
+ import { StoryboardDecoder } from "@rian8337/osu-base";
294
+
295
+ readFile("path/to/file.osb", { encoding: "utf-8" }, (err, data) => {
296
+ if (err) throw err;
297
+
298
+ const decoder = new StoryboardDecoder().decode(data);
299
+
300
+ // Decoded storyboard can be accessed via the `result` field
301
+ console.log(decoder.result);
302
+ });
303
+ ```
304
+
305
+ ## Storyboard Encoder
306
+
307
+ ```js
308
+ import { Storyboard, StoryboardEncoder } from "@rian8337/osu-base";
309
+
310
+ const storyboard = new Storyboard();
311
+
312
+ const encoder = new StoryboardEncoder(storyboard).encode();
313
+
314
+ // Encoded storyboard can be accessed via the `result` field
315
+ console.log(encoder.result);
316
+ ```
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}`;
@@ -1872,6 +1870,51 @@ class HitWindow {
1872
1870
  * Represents the hit window of osu!droid.
1873
1871
  */
1874
1872
  class DroidHitWindow extends HitWindow {
1873
+ /**
1874
+ * Calculates the overall difficulty value of a great hit window.
1875
+ *
1876
+ * @param value The value of the hit window, in milliseconds.
1877
+ * @param isPrecise Whether to calculate for Precise mod.
1878
+ * @returns The overall difficulty value.
1879
+ */
1880
+ static hitWindow300ToOD(value, isPrecise) {
1881
+ if (isPrecise) {
1882
+ return 5 - (value - 55) / 6;
1883
+ }
1884
+ else {
1885
+ return 5 - (value - 75) / 5;
1886
+ }
1887
+ }
1888
+ /**
1889
+ * Calculates the overall difficulty value of a good hit window.
1890
+ *
1891
+ * @param value The value of the hit window, in milliseconds.
1892
+ * @param isPrecise Whether to calculate for Precise mod.
1893
+ * @returns The overall difficulty value.
1894
+ */
1895
+ static hitWindow100ToOD(value, isPrecise) {
1896
+ if (isPrecise) {
1897
+ return 5 - (value - 120) / 8;
1898
+ }
1899
+ else {
1900
+ return 5 - (value - 150) / 10;
1901
+ }
1902
+ }
1903
+ /**
1904
+ * Calculates the overall difficulty value of a meh hit window.
1905
+ *
1906
+ * @param value The value of the hit window, in milliseconds.
1907
+ * @param isPrecise Whether to calculate for Precise mod.
1908
+ * @returns The overall difficulty value.
1909
+ */
1910
+ static hitWindow50ToOD(value, isPrecise) {
1911
+ if (isPrecise) {
1912
+ return 5 - (value - 180) / 10;
1913
+ }
1914
+ else {
1915
+ return 5 - (value - 250) / 10;
1916
+ }
1917
+ }
1875
1918
  hitWindowFor300(isPrecise) {
1876
1919
  if (isPrecise) {
1877
1920
  return 55 + 6 * (5 - this.overallDifficulty);
@@ -1901,6 +1944,33 @@ class DroidHitWindow extends HitWindow {
1901
1944
  * Represents the hit window of osu!standard.
1902
1945
  */
1903
1946
  class OsuHitWindow extends HitWindow {
1947
+ /**
1948
+ * Calculates the overall difficulty value of a great hit window.
1949
+ *
1950
+ * @param value The value of the hit window, in milliseconds.
1951
+ * @returns The overall difficulty value.
1952
+ */
1953
+ static hitWindow300ToOD(value) {
1954
+ return (80 - value) / 6;
1955
+ }
1956
+ /**
1957
+ * Calculates the overall difficulty value of a good hit window.
1958
+ *
1959
+ * @param value The value of the hit window, in milliseconds.
1960
+ * @returns The overall difficulty value.
1961
+ */
1962
+ static hitWindow100ToOD(value) {
1963
+ return (140 - value) / 8;
1964
+ }
1965
+ /**
1966
+ * Calculates the overall difficulty value of a meh hit window.
1967
+ *
1968
+ * @param value The value of the hit window, in milliseconds.
1969
+ * @returns The overall difficulty value.
1970
+ */
1971
+ static hitWindow50ToOD(value) {
1972
+ return (200 - value) / 10;
1973
+ }
1904
1974
  hitWindowFor300() {
1905
1975
  return 80 - 6 * this.overallDifficulty;
1906
1976
  }
@@ -2076,6 +2146,8 @@ class ModFlashlight extends Mod {
2076
2146
  * Represents the Hidden mod.
2077
2147
  */
2078
2148
  class ModHidden extends Mod {
2149
+ static fadeInDurationMultiplier = 0.4;
2150
+ static fadeOutDurationMultiplier = 0.3;
2079
2151
  scoreMultiplier = 1.06;
2080
2152
  acronym = "HD";
2081
2153
  name = "Hidden";
@@ -2234,9 +2306,7 @@ class ModUtil {
2234
2306
  * Mods that change the way the map looks.
2235
2307
  */
2236
2308
  static mapChangingMods = [
2237
- new ModDoubleTime(),
2238
- new ModNightCore(),
2239
- new ModHalfTime(),
2309
+ ...this.speedChangingMods,
2240
2310
  new ModEasy(),
2241
2311
  new ModHardRock(),
2242
2312
  new ModSmallCircle(),
@@ -2394,29 +2464,31 @@ class MapStats {
2394
2464
  }
2395
2465
  switch (params?.mode ?? exports.modes.osu) {
2396
2466
  case exports.modes.droid:
2397
- // In droid pre-1.6.8, NC speed multiplier is assumed bugged (1.39)
2467
+ // In droid pre-1.6.8, NC speed multiplier is assumed bugged (1.39).
2398
2468
  if (this.mods.some((m) => m instanceof ModNightCore) &&
2399
2469
  this.oldStatistics) {
2400
2470
  this.speedMultiplier *= 1.39 / 1.5;
2401
2471
  }
2402
2472
  // CS and OD work differently in droid, therefore it
2403
2473
  // needs to be computed regardless of map-changing mods
2404
- // and statistics multiplier
2474
+ // and statistics multiplier.
2405
2475
  if (this.od !== undefined) {
2406
- // apply EZ or HR to OD
2476
+ // Apply EZ or HR to OD.
2407
2477
  this.od = Math.min(this.od * statisticsMultiplier, 10);
2408
- // convert original OD to droid OD
2478
+ // Convert original OD to droid hit window to take
2479
+ // droid hit window and the PR mod in mind.
2409
2480
  const droidToMS = new DroidHitWindow(this.od).hitWindowFor300(this.mods.some((m) => m instanceof ModPrecise)) / this.speedMultiplier;
2410
- this.od = 5 - (droidToMS - 50) / 6;
2481
+ // Convert droid hit window back to original OD.
2482
+ this.od = OsuHitWindow.hitWindow300ToOD(droidToMS);
2411
2483
  }
2412
2484
  // HR and EZ works differently in droid in terms of
2413
- // CS modification (even CS in itself as well)
2485
+ // CS modification (even CS in itself as well).
2414
2486
  //
2415
2487
  // If present mods are found, they need to be removed
2416
2488
  // from the bitwise enum of mods to prevent double
2417
- // calculation
2489
+ // calculation.
2418
2490
  if (this.cs !== undefined) {
2419
- // Assume 681 is height
2491
+ // Assume 681 is height.
2420
2492
  const assumedHeight = 681;
2421
2493
  let scale = ((assumedHeight / 480) * (54.42 - this.cs * 4.48) * 2) /
2422
2494
  128 +
@@ -5226,6 +5298,15 @@ class Encoder {
5226
5298
  * The result of the encoding process.
5227
5299
  */
5228
5300
  finalResult = "";
5301
+ /**
5302
+ * The result of the encoding process.
5303
+ */
5304
+ get result() {
5305
+ return this.finalResult;
5306
+ }
5307
+ /**
5308
+ * @param target The target of the encoding process.
5309
+ */
5229
5310
  constructor(target) {
5230
5311
  this.target = target;
5231
5312
  }
@@ -5237,7 +5318,7 @@ class Encoder {
5237
5318
  encode() {
5238
5319
  this.reset();
5239
5320
  this.encodeInternal();
5240
- return this.finalResult;
5321
+ return this;
5241
5322
  }
5242
5323
  /**
5243
5324
  * Writes a line to encoded text.
@@ -5715,7 +5796,7 @@ class BeatmapEventsEncoder extends BeatmapBaseEncoder {
5715
5796
  this.writeLine(`2,${b.startTime},${b.endTime}`);
5716
5797
  }
5717
5798
  if (this.map.events.storyboard) {
5718
- this.writeLine(new StoryboardEncoder(this.map.events.storyboard, false).encode());
5799
+ this.writeLine(new StoryboardEncoder(this.map.events.storyboard, false).encode().result);
5719
5800
  }
5720
5801
  else {
5721
5802
  this.writeLine("//Storyboard Layer 0 (Background)");
@@ -6314,9 +6395,9 @@ class MapInfo {
6314
6395
  * - Option `0`: return map title and mods used if defined
6315
6396
  * - Option `1`: return song source and map download link to beatmap mirrors
6316
6397
  * - Option `2`: return circle, slider, and spinner count
6317
- * - Option `3`: return CS, AR, OD, and HP statistics for droid
6318
- * - Option `4`: return CS, AR, OD, and HP statistics for PC
6319
- * - Option `5`: return BPM, map length, max combo, and max score
6398
+ * - Option `3`: return CS, AR, OD, HP, and max score statistics for droid
6399
+ * - Option `4`: return CS, AR, OD, HP, and max score statistics for PC
6400
+ * - Option `5`: return BPM, map length, and max combo
6320
6401
  * - Option `6`: return last update date and map status
6321
6402
  * - Option `7`: return favorite count and play count
6322
6403
  *
@@ -6404,6 +6485,7 @@ class MapInfo {
6404
6485
  droidModifiedStats.ar = MathUtils.round(droidModifiedStats.ar, 2);
6405
6486
  droidModifiedStats.od = MathUtils.round(droidModifiedStats.od, 2);
6406
6487
  droidModifiedStats.hp = MathUtils.round(droidModifiedStats.hp, 2);
6488
+ const maxScore = this.map?.maxDroidScore(new MapStats(mapParams)) ?? 0;
6407
6489
  return `**CS**: ${droidOriginalStats.cs}${Precision.almostEqualsNumber(droidOriginalStats.cs, droidModifiedStats.cs)
6408
6490
  ? ""
6409
6491
  : ` (${droidModifiedStats.cs})`} - **AR**: ${droidOriginalStats.ar}${Precision.almostEqualsNumber(droidOriginalStats.ar, droidModifiedStats.ar)
@@ -6412,7 +6494,9 @@ class MapInfo {
6412
6494
  ? ""
6413
6495
  : ` (${droidModifiedStats.od})`} - **HP**: ${droidOriginalStats.hp}${Precision.almostEqualsNumber(droidOriginalStats.hp, droidModifiedStats.hp)
6414
6496
  ? ""
6415
- : ` (${droidModifiedStats.hp})`}`;
6497
+ : ` (${droidModifiedStats.hp})`}${maxScore > 0
6498
+ ? `\n**Max Score**: ${maxScore.toLocaleString()}`
6499
+ : ""}`;
6416
6500
  }
6417
6501
  case 4: {
6418
6502
  const mapStatistics = new MapStats(mapParams).calculate();
@@ -6420,6 +6504,7 @@ class MapInfo {
6420
6504
  mapStatistics.ar = MathUtils.round(mapStatistics.ar, 2);
6421
6505
  mapStatistics.od = MathUtils.round(mapStatistics.od, 2);
6422
6506
  mapStatistics.hp = MathUtils.round(mapStatistics.hp, 2);
6507
+ const maxScore = this.map?.maxOsuScore(mapStatistics.mods) ?? 0;
6423
6508
  return `**CS**: ${this.cs}${Precision.almostEqualsNumber(this.cs, mapStatistics.cs)
6424
6509
  ? ""
6425
6510
  : ` (${mapStatistics.cs})`} - **AR**: ${this.ar}${Precision.almostEqualsNumber(this.ar, mapStatistics.ar)
@@ -6428,12 +6513,12 @@ class MapInfo {
6428
6513
  ? ""
6429
6514
  : ` (${mapStatistics.od})`} - **HP**: ${this.hp}${Precision.almostEqualsNumber(this.hp, mapStatistics.hp)
6430
6515
  ? ""
6431
- : ` (${mapStatistics.hp})`}`;
6516
+ : ` (${mapStatistics.hp})`}${maxScore > 0
6517
+ ? `\n**Max Score**: ${maxScore.toLocaleString()}`
6518
+ : ""}`;
6432
6519
  }
6433
6520
  case 5: {
6434
6521
  const mapStatistics = new MapStats(mapParams).calculate();
6435
- const droidMaxScore = this.map?.maxDroidScore(mapStatistics) ?? 0;
6436
- const osuMaxScore = this.map?.maxOsuScore(mapStatistics.mods) ?? 0;
6437
6522
  const convertedBPM = this.convertBPM(mapStatistics);
6438
6523
  let string = "**BPM**: ";
6439
6524
  if (this.map) {
@@ -6441,12 +6526,7 @@ class MapInfo {
6441
6526
  if (uninheritedTimingPoints.length === 1) {
6442
6527
  string += `${this.bpm}${!Precision.almostEqualsNumber(this.bpm, convertedBPM)
6443
6528
  ? ` (${convertedBPM})`
6444
- : ""} - **Length**: ${this.convertTime(mapStatistics)} - **Max Combo**: ${this.maxCombo}x${droidMaxScore > 0
6445
- ? `\n**Max Droid Score**: ${droidMaxScore.toLocaleString()}`
6446
- : ""}${osuMaxScore > 0
6447
- ? (droidMaxScore > 0 ? " - " : "\n") +
6448
- `**Max Standard Score**: ${osuMaxScore.toLocaleString()}`
6449
- : ""}`;
6529
+ : ""} - **Length**: ${this.convertTime(mapStatistics)} - **Max Combo**: ${this.maxCombo}x`;
6450
6530
  }
6451
6531
  else {
6452
6532
  let maxBPM = this.bpm;
@@ -6473,25 +6553,13 @@ class MapInfo {
6473
6553
  string += `(${convertedBPM}) `;
6474
6554
  }
6475
6555
  }
6476
- string += `- **Length**: ${this.convertTime(mapStatistics)} - **Max Combo**: ${this.maxCombo}x${droidMaxScore > 0
6477
- ? `\n**Max Droid Score**: ${droidMaxScore.toLocaleString()}`
6478
- : ""}${osuMaxScore > 0
6479
- ? (droidMaxScore > 0 ? " - " : "\n") +
6480
- `**Max Standard Score**: ${osuMaxScore.toLocaleString()}`
6481
- : ""}`;
6556
+ string += `- **Length**: ${this.convertTime(mapStatistics)} - **Max Combo**: ${this.maxCombo}x`;
6482
6557
  }
6483
6558
  }
6484
6559
  else {
6485
6560
  string += `${this.bpm}${!Precision.almostEqualsNumber(this.bpm, convertedBPM)
6486
6561
  ? ` (${convertedBPM})`
6487
- : ""} - **Length**: ${this.convertTime(mapStatistics)} - **Max Combo**: ${this.maxCombo}x${droidMaxScore > 0
6488
- ? `${droidMaxScore > 0
6489
- ? `\n**Max Droid Score**: ${droidMaxScore.toLocaleString()}`
6490
- : ""}${osuMaxScore > 0
6491
- ? (droidMaxScore > 0 ? " - " : "\n") +
6492
- `**Max Standard Score**: ${osuMaxScore.toLocaleString()}`
6493
- : ""}`
6494
- : ""}`;
6562
+ : ""} - **Length**: ${this.convertTime(mapStatistics)} - **Max Combo**: ${this.maxCombo}x`;
6495
6563
  }
6496
6564
  return string;
6497
6565
  }