@rian8337/osu-base 1.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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/dist/beatmap/Beatmap.js +223 -0
  4. package/dist/beatmap/Parser.js +620 -0
  5. package/dist/beatmap/hitobjects/Circle.js +20 -0
  6. package/dist/beatmap/hitobjects/HitObject.js +74 -0
  7. package/dist/beatmap/hitobjects/Slider.js +143 -0
  8. package/dist/beatmap/hitobjects/Spinner.js +26 -0
  9. package/dist/beatmap/hitobjects/sliderObjects/HeadCircle.js +10 -0
  10. package/dist/beatmap/hitobjects/sliderObjects/RepeatPoint.js +21 -0
  11. package/dist/beatmap/hitobjects/sliderObjects/SliderTick.js +21 -0
  12. package/dist/beatmap/hitobjects/sliderObjects/TailCircle.js +10 -0
  13. package/dist/beatmap/timings/BreakPoint.js +34 -0
  14. package/dist/beatmap/timings/DifficultyControlPoint.js +22 -0
  15. package/dist/beatmap/timings/TimingControlPoint.js +22 -0
  16. package/dist/beatmap/timings/TimingPoint.js +12 -0
  17. package/dist/constants/ParserConstants.js +19 -0
  18. package/dist/constants/PathType.js +13 -0
  19. package/dist/constants/modes.js +11 -0
  20. package/dist/constants/objectTypes.js +12 -0
  21. package/dist/constants/rankedStatus.js +16 -0
  22. package/dist/index.js +64 -0
  23. package/dist/mathutil/Interpolation.js +9 -0
  24. package/dist/mathutil/MathUtils.js +41 -0
  25. package/dist/mathutil/Vector2.js +79 -0
  26. package/dist/mods/Mod.js +9 -0
  27. package/dist/mods/ModAuto.js +21 -0
  28. package/dist/mods/ModAutopilot.js +21 -0
  29. package/dist/mods/ModDoubleTime.js +21 -0
  30. package/dist/mods/ModEasy.js +21 -0
  31. package/dist/mods/ModFlashlight.js +21 -0
  32. package/dist/mods/ModHalfTime.js +21 -0
  33. package/dist/mods/ModHardRock.js +21 -0
  34. package/dist/mods/ModHidden.js +21 -0
  35. package/dist/mods/ModNightCore.js +21 -0
  36. package/dist/mods/ModNoFail.js +21 -0
  37. package/dist/mods/ModPerfect.js +21 -0
  38. package/dist/mods/ModPrecise.js +21 -0
  39. package/dist/mods/ModReallyEasy.js +21 -0
  40. package/dist/mods/ModRelax.js +21 -0
  41. package/dist/mods/ModScoreV2.js +21 -0
  42. package/dist/mods/ModSmallCircle.js +21 -0
  43. package/dist/mods/ModSpunOut.js +21 -0
  44. package/dist/mods/ModSuddenDeath.js +21 -0
  45. package/dist/mods/ModTouchDevice.js +21 -0
  46. package/dist/tools/MapInfo.js +559 -0
  47. package/dist/utils/APIRequestBuilder.js +144 -0
  48. package/dist/utils/Accuracy.js +96 -0
  49. package/dist/utils/HitWindow.js +56 -0
  50. package/dist/utils/MapStats.js +212 -0
  51. package/dist/utils/ModUtil.js +137 -0
  52. package/dist/utils/PathApproximator.js +269 -0
  53. package/dist/utils/Precision.js +31 -0
  54. package/dist/utils/SliderPath.js +187 -0
  55. package/dist/utils/Utils.js +53 -0
  56. package/package.json +43 -0
  57. package/typings/index.d.ts +1951 -0
@@ -0,0 +1,559 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MapInfo = void 0;
7
+ const request_1 = __importDefault(require("request"));
8
+ const MapStats_1 = require("../utils/MapStats");
9
+ const Parser_1 = require("../beatmap/Parser");
10
+ const rankedStatus_1 = require("../constants/rankedStatus");
11
+ const Slider_1 = require("../beatmap/hitobjects/Slider");
12
+ const SliderTick_1 = require("../beatmap/hitobjects/sliderObjects/SliderTick");
13
+ const APIRequestBuilder_1 = require("../utils/APIRequestBuilder");
14
+ const Precision_1 = require("../utils/Precision");
15
+ const Utils_1 = require("../utils/Utils");
16
+ /**
17
+ * Represents a beatmap with general information.
18
+ */
19
+ class MapInfo {
20
+ constructor() {
21
+ /**
22
+ * The title of the song of the beatmap.
23
+ */
24
+ this.title = "";
25
+ /**
26
+ * The artist of the song of the beatmap.
27
+ */
28
+ this.artist = "";
29
+ /**
30
+ * The creator of the beatmap.
31
+ */
32
+ this.creator = "";
33
+ /**
34
+ * The difficulty name of the beatmap.
35
+ */
36
+ this.version = "";
37
+ /**
38
+ * The source of the song, if any.
39
+ */
40
+ this.source = "";
41
+ /**
42
+ * The ranking status of the beatmap.
43
+ */
44
+ this.approved = 0;
45
+ /**
46
+ * The ID of the beatmap.
47
+ */
48
+ this.beatmapID = 0;
49
+ /**
50
+ * The ID of the beatmapset containing the beatmap.
51
+ */
52
+ this.beatmapsetID = 0;
53
+ /**
54
+ * The amount of times the beatmap has been played.
55
+ */
56
+ this.plays = 0;
57
+ /**
58
+ * The amount of times the beatmap has been favorited.
59
+ */
60
+ this.favorites = 0;
61
+ /**
62
+ * The date of which the beatmap was submitted.
63
+ */
64
+ this.submitDate = new Date(0);
65
+ /**
66
+ * The date of which the beatmap was last updated.
67
+ */
68
+ this.lastUpdate = new Date(0);
69
+ /**
70
+ * The duration of the beatmap not including breaks.
71
+ */
72
+ this.hitLength = 0;
73
+ /**
74
+ * The duration of the beatmap including breaks.
75
+ */
76
+ this.totalLength = 0;
77
+ /**
78
+ * The BPM of the beatmap.
79
+ */
80
+ this.bpm = 0;
81
+ /**
82
+ * The amount of circles in the beatmap.
83
+ */
84
+ this.circles = 0;
85
+ /**
86
+ * The amount of sliders in the beatmap.
87
+ */
88
+ this.sliders = 0;
89
+ /**
90
+ * The amount of spinners in the beatmap.
91
+ */
92
+ this.spinners = 0;
93
+ /**
94
+ * The maximum combo of the beatmap.
95
+ */
96
+ this.maxCombo = 0;
97
+ /**
98
+ * The circle size of the beatmap.
99
+ */
100
+ this.cs = 0;
101
+ /**
102
+ * The approach rate of the beatmap.
103
+ */
104
+ this.ar = 0;
105
+ /**
106
+ * The overall difficulty of the beatmap.
107
+ */
108
+ this.od = 0;
109
+ /**
110
+ * The health drain rate of the beatmap.
111
+ */
112
+ this.hp = 0;
113
+ /**
114
+ * The beatmap packs that contain this beatmap, represented by their ID.
115
+ */
116
+ this.packs = [];
117
+ /**
118
+ * The aim difficulty rating of the beatmap.
119
+ */
120
+ this.aimDifficulty = 0;
121
+ /**
122
+ * The speed difficulty rating of the beatmap.
123
+ */
124
+ this.speedDifficulty = 0;
125
+ /**
126
+ * The generic difficulty rating of the beatmap.
127
+ */
128
+ this.totalDifficulty = 0;
129
+ /**
130
+ * The MD5 hash of the beatmap.
131
+ */
132
+ this.hash = "";
133
+ /**
134
+ * Whether or not this beatmap has a storyboard.
135
+ */
136
+ this.storyboardAvailable = false;
137
+ /**
138
+ * Whether or not this beatmap has a video.
139
+ */
140
+ this.videoAvailable = false;
141
+ }
142
+ /**
143
+ * The full title of the beatmap, which is `Artist - Title (Creator) [Difficulty Name]`.
144
+ */
145
+ get fullTitle() {
146
+ return `${this.artist} - ${this.title} (${this.creator}) [${this.version}]`;
147
+ }
148
+ /**
149
+ * The amount of objects in the beatmap.
150
+ */
151
+ get objects() {
152
+ return this.circles + this.sliders + this.spinners;
153
+ }
154
+ /**
155
+ * The parsed beatmap from beatmap parser.
156
+ */
157
+ get map() {
158
+ return Utils_1.Utils.deepCopy(this.cachedBeatmap);
159
+ }
160
+ /**
161
+ * Retrieve a beatmap's general information.
162
+ *
163
+ * Either beatmap ID or MD5 hash of the beatmap must be specified. If both are specified, beatmap ID is taken.
164
+ */
165
+ static async getInformation(params) {
166
+ params.file ??= true;
167
+ const beatmapID = params.beatmapID;
168
+ const hash = params.hash;
169
+ if (!beatmapID && !hash) {
170
+ throw new Error("Beatmap ID or MD5 hash must be defined");
171
+ }
172
+ const apiRequestBuilder = new APIRequestBuilder_1.OsuAPIRequestBuilder().setEndpoint("get_beatmaps");
173
+ if (beatmapID) {
174
+ apiRequestBuilder.addParameter("b", beatmapID);
175
+ }
176
+ else if (hash) {
177
+ apiRequestBuilder.addParameter("h", hash);
178
+ }
179
+ const map = new MapInfo();
180
+ const result = await apiRequestBuilder.sendRequest();
181
+ if (result.statusCode !== 200) {
182
+ throw new Error("API error");
183
+ }
184
+ const mapinfo = JSON.parse(result.data.toString("utf-8"))[0];
185
+ if (!mapinfo) {
186
+ return map;
187
+ }
188
+ if (parseInt(mapinfo.mode) !== 0) {
189
+ return map;
190
+ }
191
+ map.fillMetadata(mapinfo);
192
+ if (params.file) {
193
+ await map.retrieveBeatmapFile();
194
+ }
195
+ return map;
196
+ }
197
+ /**
198
+ * Fills the current instance with map data.
199
+ *
200
+ * @param mapinfo The map data.
201
+ */
202
+ fillMetadata(mapinfo) {
203
+ this.title = mapinfo.title;
204
+ this.artist = mapinfo.artist;
205
+ this.creator = mapinfo.creator;
206
+ this.version = mapinfo.version;
207
+ this.source = mapinfo.source;
208
+ this.approved = parseInt(mapinfo.approved);
209
+ this.beatmapID = parseInt(mapinfo.beatmap_id);
210
+ this.beatmapsetID = parseInt(mapinfo.beatmapset_id);
211
+ this.plays = parseInt(mapinfo.playcount);
212
+ this.favorites = parseInt(mapinfo.favourite_count);
213
+ const t = mapinfo.last_update
214
+ .split(/[- :]/)
215
+ .map((e) => parseInt(e));
216
+ this.lastUpdate = new Date(Date.UTC(t[0], t[1] - 1, t[2], t[3], t[4], t[5]));
217
+ const s = mapinfo.submit_date
218
+ .split(/[- :]/)
219
+ .map((e) => parseInt(e));
220
+ this.submitDate = new Date(Date.UTC(s[0], s[1] - 1, s[2], s[3], s[4], s[5]));
221
+ this.hitLength = parseInt(mapinfo.hit_length);
222
+ this.totalLength = parseInt(mapinfo.total_length);
223
+ this.bpm = parseFloat(mapinfo.bpm);
224
+ this.circles = mapinfo.count_normal
225
+ ? parseInt(mapinfo.count_normal)
226
+ : 0;
227
+ this.sliders = mapinfo.count_slider
228
+ ? parseInt(mapinfo.count_slider)
229
+ : 0;
230
+ this.spinners = mapinfo.count_spinner
231
+ ? parseInt(mapinfo.count_spinner)
232
+ : 0;
233
+ this.maxCombo = parseInt(mapinfo.max_combo);
234
+ this.cs = parseFloat(mapinfo.diff_size);
235
+ this.ar = parseFloat(mapinfo.diff_approach);
236
+ this.od = parseFloat(mapinfo.diff_overall);
237
+ this.hp = parseFloat(mapinfo.diff_drain);
238
+ if (mapinfo.packs) {
239
+ this.packs = mapinfo.packs.split(",").map((pack) => pack.trim());
240
+ }
241
+ this.aimDifficulty = mapinfo.diff_aim
242
+ ? parseFloat(mapinfo.diff_aim)
243
+ : 0;
244
+ this.speedDifficulty = mapinfo.diff_speed
245
+ ? parseFloat(mapinfo.diff_speed)
246
+ : 0;
247
+ this.totalDifficulty = mapinfo.difficultyrating
248
+ ? parseFloat(mapinfo.difficultyrating)
249
+ : 0;
250
+ this.hash = mapinfo.file_md5;
251
+ this.storyboardAvailable = !!parseInt(mapinfo.storyboard);
252
+ this.videoAvailable = !!parseInt(mapinfo.video);
253
+ return this;
254
+ }
255
+ /**
256
+ * Retrieves the .osu file of the beatmap.
257
+ *
258
+ * @param forceDownload Whether or not to download the file regardless if it's already available.
259
+ */
260
+ retrieveBeatmapFile(forceDownload) {
261
+ return new Promise((resolve) => {
262
+ if (this.cachedBeatmap && !forceDownload) {
263
+ return resolve(this);
264
+ }
265
+ const url = `https://osu.ppy.sh/osu/${this.beatmapID}`;
266
+ const dataArray = [];
267
+ (0, request_1.default)(url, { timeout: 10000 })
268
+ .on("data", (chunk) => {
269
+ dataArray.push(Buffer.from(chunk));
270
+ })
271
+ .on("complete", (response) => {
272
+ if (response.statusCode !== 200) {
273
+ return resolve(this);
274
+ }
275
+ this.cachedBeatmap = new Parser_1.Parser().parse(Buffer.concat(dataArray).toString("utf8")).map;
276
+ resolve(this);
277
+ });
278
+ });
279
+ }
280
+ /**
281
+ * Converts the beatmap's BPM if speed-changing mods are applied.
282
+ */
283
+ convertBPM(stats) {
284
+ let bpm = this.bpm;
285
+ bpm *= stats.speedMultiplier;
286
+ return parseFloat(bpm.toFixed(2));
287
+ }
288
+ /**
289
+ * Converts the beatmap's status into a string.
290
+ */
291
+ convertStatus() {
292
+ let status = "Unknown";
293
+ for (const stat in rankedStatus_1.rankedStatus) {
294
+ if (rankedStatus_1.rankedStatus[stat] ===
295
+ this.approved) {
296
+ status = stat;
297
+ break;
298
+ }
299
+ }
300
+ return status !== "WIP"
301
+ ? status.charAt(0) + status.slice(1).toLowerCase()
302
+ : status;
303
+ }
304
+ /**
305
+ * Converts the beatmap's length if speed-changing mods are applied.
306
+ */
307
+ convertTime(stats) {
308
+ let hitLength = this.hitLength;
309
+ let totalLength = this.totalLength;
310
+ hitLength /= stats.speedMultiplier;
311
+ totalLength /= stats.speedMultiplier;
312
+ return `${this.timeString(this.hitLength)}${this.hitLength === hitLength
313
+ ? ""
314
+ : ` (${this.timeString(hitLength)})`}/${this.timeString(this.totalLength)}${this.totalLength === totalLength
315
+ ? ""
316
+ : ` (${this.timeString(totalLength)})`}`;
317
+ }
318
+ /**
319
+ * Time string parsing function for statistics utility.
320
+ */
321
+ timeString(second) {
322
+ let str = new Date(1000 * Math.ceil(second))
323
+ .toISOString()
324
+ .substr(11, 8)
325
+ .replace(/^[0:]+/, "");
326
+ if (second < 60) {
327
+ str = "0:" + str;
328
+ }
329
+ return str;
330
+ }
331
+ /**
332
+ * Shows the beatmap's statistics based on applied statistics and option.
333
+ *
334
+ * - Option `0`: return map title and mods used if defined
335
+ * - Option `1`: return song source and map download link to beatmap mirrors
336
+ * - Option `2`: return CS, AR, OD, HP
337
+ * - Option `3`: return BPM, map length, max combo
338
+ * - Option `4`: return last update date and map status
339
+ * - Option `5`: return favorite count and play count
340
+ *
341
+ * @param option The option to pick.
342
+ * @param stats The custom statistics to apply. This will only be used to apply mods, custom speed multiplier, and force AR.
343
+ */
344
+ showStatistics(option, stats) {
345
+ const mapParams = {
346
+ cs: this.cs,
347
+ ar: this.ar,
348
+ od: this.od,
349
+ hp: this.hp,
350
+ mods: stats?.mods ?? [],
351
+ isForceAR: false,
352
+ speedMultiplier: 1,
353
+ };
354
+ if (stats) {
355
+ if (stats.isForceAR) {
356
+ mapParams.ar = stats.ar ?? mapParams.ar;
357
+ }
358
+ mapParams.isForceAR = stats.isForceAR ?? mapParams.isForceAR;
359
+ mapParams.speedMultiplier =
360
+ stats.speedMultiplier ?? mapParams.speedMultiplier;
361
+ }
362
+ const mapStatistics = new MapStats_1.MapStats(mapParams).calculate();
363
+ mapStatistics.cs = parseFloat(mapStatistics.cs.toFixed(2));
364
+ mapStatistics.ar = parseFloat(mapStatistics.ar.toFixed(2));
365
+ mapStatistics.od = parseFloat(mapStatistics.od.toFixed(2));
366
+ mapStatistics.hp = parseFloat(mapStatistics.hp.toFixed(2));
367
+ switch (option) {
368
+ case 0: {
369
+ let string = `${this.fullTitle}${(mapStatistics.mods.length ?? 0) > 0
370
+ ? ` +${mapStatistics.mods
371
+ .map((m) => m.acronym)
372
+ .join("")}`
373
+ : ""}`;
374
+ if (mapParams.speedMultiplier !== 1 ||
375
+ mapStatistics.isForceAR) {
376
+ string += " (";
377
+ if (mapStatistics.isForceAR) {
378
+ string += `AR${mapStatistics.ar}`;
379
+ }
380
+ if (mapParams.speedMultiplier !== 1) {
381
+ if (mapStatistics.isForceAR) {
382
+ string += ", ";
383
+ }
384
+ string += `${mapParams.speedMultiplier}x`;
385
+ }
386
+ string += ")";
387
+ }
388
+ return string;
389
+ }
390
+ case 1: {
391
+ let string = `${this.source ? `**Source**: ${this.source}\n` : ""}**Download**: [osu!](https://osu.ppy.sh/d/${this.beatmapsetID})${this.videoAvailable
392
+ ? ` [(no video)](https://osu.ppy.sh/d/${this.beatmapsetID}n)`
393
+ : ""} - [Chimu](https://chimu.moe/en/d/${this.beatmapsetID}) - [Sayobot](https://txy1.sayobot.cn/beatmaps/download/full/${this.beatmapsetID})${this.videoAvailable
394
+ ? ` [(no video)](https://txy1.sayobot.cn/beatmaps/download/novideo/${this.beatmapsetID})`
395
+ : ""} - [Beatconnect](https://beatconnect.io/b/${this.beatmapsetID}/) - [Nerina](https://nerina.pw/d/${this.beatmapsetID})${this.approved >= rankedStatus_1.rankedStatus.RANKED &&
396
+ this.approved !== rankedStatus_1.rankedStatus.QUALIFIED
397
+ ? ` - [Ripple](https://storage.ripple.moe/d/${this.beatmapsetID})`
398
+ : ""}`;
399
+ if (this.packs.length > 0) {
400
+ string += "\n**Beatmap Pack**: ";
401
+ for (let i = 0; i < this.packs.length; i++) {
402
+ string += `[${this.packs[i]}](https://osu.ppy.sh/beatmaps/packs/${this.packs[i]})`;
403
+ if (i + 1 < this.packs.length) {
404
+ string += " - ";
405
+ }
406
+ }
407
+ }
408
+ string += `\n🖼️ ${this.storyboardAvailable ? "✅" : "❎"} **|** 🎞️ ${this.videoAvailable ? "✅" : "❎"}`;
409
+ return string;
410
+ }
411
+ case 2:
412
+ 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})`}`;
413
+ case 3: {
414
+ const maxScore = this.maxScore(mapStatistics);
415
+ const convertedBPM = this.convertBPM(mapStatistics);
416
+ let string = "**BPM**: ";
417
+ if (this.map) {
418
+ const uninheritedTimingPoints = this.map.timingPoints;
419
+ if (uninheritedTimingPoints.length === 1) {
420
+ string += `${this.bpm}${!Precision_1.Precision.almostEqualsNumber(this.bpm, convertedBPM)
421
+ ? ` (${convertedBPM})`
422
+ : ""} - **Length**: ${this.convertTime(mapStatistics)} - **Max Combo**: ${this.maxCombo}x${maxScore > 0
423
+ ? `\n**Max Score**: ${maxScore.toLocaleString()}`
424
+ : ""}`;
425
+ }
426
+ else {
427
+ let maxBPM = this.bpm;
428
+ let minBPM = this.bpm;
429
+ for (const t of uninheritedTimingPoints) {
430
+ const bpm = parseFloat((60000 / t.msPerBeat).toFixed(2));
431
+ maxBPM = Math.max(maxBPM, bpm);
432
+ minBPM = Math.min(minBPM, bpm);
433
+ }
434
+ maxBPM = Math.round(maxBPM);
435
+ minBPM = Math.round(minBPM);
436
+ const speedMulMinBPM = Math.round(minBPM * mapStatistics.speedMultiplier);
437
+ const speedMulMaxBPM = Math.round(maxBPM * mapStatistics.speedMultiplier);
438
+ string +=
439
+ Precision_1.Precision.almostEqualsNumber(minBPM, this.bpm) &&
440
+ Precision_1.Precision.almostEqualsNumber(maxBPM, this.bpm)
441
+ ? `${this.bpm} `
442
+ : `${minBPM}-${maxBPM} (${this.bpm}) `;
443
+ if (!Precision_1.Precision.almostEqualsNumber(this.bpm, convertedBPM)) {
444
+ if (!Precision_1.Precision.almostEqualsNumber(speedMulMinBPM, speedMulMaxBPM)) {
445
+ string += `(${speedMulMinBPM}-${speedMulMaxBPM} (${convertedBPM})) `;
446
+ }
447
+ else {
448
+ string += `(${convertedBPM}) `;
449
+ }
450
+ }
451
+ string += `- **Length**: ${this.convertTime(mapStatistics)} - **Max Combo**: ${this.maxCombo}x${maxScore > 0
452
+ ? `\n**Max score**: ${maxScore.toLocaleString()}`
453
+ : ""}`;
454
+ }
455
+ }
456
+ else {
457
+ string += `${this.bpm}${!Precision_1.Precision.almostEqualsNumber(this.bpm, convertedBPM)
458
+ ? ` (${convertedBPM})`
459
+ : ""} - **Length**: ${this.convertTime(mapStatistics)} - **Max Combo**: ${this.maxCombo}x${maxScore > 0
460
+ ? `\n**Max score**: ${maxScore.toLocaleString()}`
461
+ : ""}`;
462
+ }
463
+ return string;
464
+ }
465
+ case 4:
466
+ return `**Last Update**: ${this.lastUpdate.toUTCString()} | **${this.convertStatus()}**`;
467
+ case 5:
468
+ return `❤️ **${this.favorites.toLocaleString()}** - ▶️ **${this.plays.toLocaleString()}**`;
469
+ default:
470
+ throw {
471
+ name: "NotSupportedError",
472
+ message: `This mode (${option}) is not supported`,
473
+ };
474
+ }
475
+ }
476
+ /**
477
+ * Returns a color integer based on the beatmap's ranking status.
478
+ *
479
+ * Useful to make embed messages.
480
+ */
481
+ get statusColor() {
482
+ switch (this.approved) {
483
+ case -2:
484
+ return 16711711; // Graveyard: red
485
+ case -1:
486
+ return 9442302; // WIP: purple
487
+ case 0:
488
+ return 16312092; // Pending: yellow
489
+ case 1:
490
+ return 2483712; // Ranked: green
491
+ case 2:
492
+ return 16741376; // Approved: tosca
493
+ case 3:
494
+ return 5301186; // Qualified: light blue
495
+ case 4:
496
+ return 16711796; // Loved: pink
497
+ default:
498
+ return 0;
499
+ }
500
+ }
501
+ /**
502
+ * Calculates the osu!droid maximum score of the beatmap.
503
+ *
504
+ * This requires .osu file to be downloaded.
505
+ */
506
+ maxScore(stats) {
507
+ if (!this.map) {
508
+ return 0;
509
+ }
510
+ const difficultyMultiplier = 1 + this.od / 10 + this.hp / 10 + (this.cs - 3) / 4;
511
+ // score multiplier
512
+ let scoreMultiplier = 1;
513
+ if (stats.mods.every((m) => m.droidRanked)) {
514
+ let scoreSpeedMultiplier = 1;
515
+ const speedMultiplier = stats.speedMultiplier;
516
+ if (speedMultiplier > 1) {
517
+ scoreSpeedMultiplier += (speedMultiplier - 1) * 0.24;
518
+ }
519
+ else if (speedMultiplier < 1) {
520
+ scoreSpeedMultiplier = Math.pow(0.3, (1 - speedMultiplier) * 4);
521
+ }
522
+ scoreMultiplier =
523
+ stats.mods.reduce((a, v) => a * v.scoreMultiplier, 1) *
524
+ scoreSpeedMultiplier;
525
+ }
526
+ else {
527
+ scoreMultiplier = 0;
528
+ }
529
+ const objects = this.map.objects;
530
+ let combo = 0;
531
+ let score = 0;
532
+ for (let i = 0; i < objects.length; ++i) {
533
+ const object = objects[i];
534
+ if (!(object instanceof Slider_1.Slider)) {
535
+ score += Math.floor(300 +
536
+ (300 * combo * difficultyMultiplier * scoreMultiplier) /
537
+ 25);
538
+ ++combo;
539
+ continue;
540
+ }
541
+ const tickCount = object.nestedHitObjects.filter((v) => v instanceof SliderTick_1.SliderTick).length;
542
+ // Apply sliderhead, slider repeats, and slider ticks
543
+ score += 30 * (object.repeatPoints + 1) + 10 * tickCount;
544
+ combo += tickCount + (object.repeatPoints + 1);
545
+ // Apply sliderend
546
+ score += Math.floor(300 +
547
+ (300 * combo * difficultyMultiplier * scoreMultiplier) / 25);
548
+ ++combo;
549
+ }
550
+ return score;
551
+ }
552
+ /**
553
+ * Returns a string representative of the class.
554
+ */
555
+ toString() {
556
+ return `${this.fullTitle}\nCS: ${this.cs} - AR: ${this.ar} - OD: ${this.od} - HP: ${this.hp}\nBPM: ${this.bpm} - Length: ${this.hitLength}/${this.totalLength} - Max Combo: ${this.maxCombo}\nLast Update: ${this.lastUpdate}`;
557
+ }
558
+ }
559
+ exports.MapInfo = MapInfo;
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.OsuAPIRequestBuilder = exports.DroidAPIRequestBuilder = void 0;
7
+ const request_1 = __importDefault(require("request"));
8
+ const Utils_1 = require("./Utils");
9
+ class APIRequestBuilder {
10
+ constructor() {
11
+ /**
12
+ * Whether or not to include the API key in the request URL.
13
+ */
14
+ this.requiresAPIkey = true;
15
+ /**
16
+ * The endpoint of this builder.
17
+ */
18
+ this.endpoint = "";
19
+ /**
20
+ * The parameters of this builder.
21
+ */
22
+ this.params = new Map();
23
+ this.fetchAttempts = 0;
24
+ }
25
+ /**
26
+ * Sets if this builder includes the API key in the request URL.
27
+ *
28
+ * @param requireAPIkey Whether or not to include the API key in the request URL.
29
+ */
30
+ setRequireAPIkey(requireAPIkey) {
31
+ this.requiresAPIkey = requireAPIkey;
32
+ return this;
33
+ }
34
+ /**
35
+ * Builds the URL to request the API.
36
+ */
37
+ buildURL() {
38
+ let url = this.host + this.endpoint;
39
+ if (this instanceof DroidAPIRequestBuilder &&
40
+ this.endpoint === "upload") {
41
+ url += "/";
42
+ for (const [, value] of this.params.entries()) {
43
+ url += value;
44
+ }
45
+ return url;
46
+ }
47
+ url += "?";
48
+ if (this.requiresAPIkey) {
49
+ if (!this.APIkey) {
50
+ throw new Error("An API key is not specified as environment variable");
51
+ }
52
+ url += this.APIkeyParam;
53
+ }
54
+ for (const [param, value] of this.params.entries()) {
55
+ url += `${param}=${encodeURIComponent(value)}&`;
56
+ }
57
+ return url;
58
+ }
59
+ /**
60
+ * Sends a request to the API using built parameters.
61
+ *
62
+ * If the request fails, it will be redone 5 times.
63
+ */
64
+ sendRequest() {
65
+ return new Promise((resolve) => {
66
+ const url = this.buildURL();
67
+ const dataArray = [];
68
+ (0, request_1.default)(url)
69
+ .on("data", (chunk) => {
70
+ dataArray.push(Buffer.from(chunk));
71
+ })
72
+ .on("complete", async (response) => {
73
+ ++this.fetchAttempts;
74
+ if (response.statusCode !== 200 && this.fetchAttempts < 5) {
75
+ console.error(`Request to ${url} failed; ${this.fetchAttempts} attempts so far; retrying`);
76
+ await Utils_1.Utils.sleep(0.2);
77
+ return resolve(this.sendRequest());
78
+ }
79
+ return resolve({
80
+ data: Buffer.concat(dataArray),
81
+ statusCode: response.statusCode,
82
+ });
83
+ })
84
+ .on("error", (e) => {
85
+ throw e;
86
+ });
87
+ });
88
+ }
89
+ /**
90
+ * Adds a parameter to the builder.
91
+ *
92
+ * @param param The parameter to add.
93
+ * @param value The value to add for the parameter.
94
+ */
95
+ addParameter(param, value) {
96
+ this.params.set(param, value);
97
+ return this;
98
+ }
99
+ /**
100
+ * Removes a parameter from the builder.
101
+ *
102
+ * @param param The parameter to remove.
103
+ */
104
+ removeParameter(param) {
105
+ if (this.params.get(param)) {
106
+ this.params.delete(param);
107
+ }
108
+ return this;
109
+ }
110
+ }
111
+ /**
112
+ * API request builder for osu!droid.
113
+ */
114
+ class DroidAPIRequestBuilder extends APIRequestBuilder {
115
+ constructor() {
116
+ super(...arguments);
117
+ this.host = "http://ops.dgsrz.com/api/";
118
+ this.APIkey = process.env
119
+ .DROID_API_KEY;
120
+ this.APIkeyParam = `apiKey=${this.APIkey}&`;
121
+ }
122
+ setEndpoint(endpoint) {
123
+ this.endpoint = endpoint;
124
+ return this;
125
+ }
126
+ }
127
+ exports.DroidAPIRequestBuilder = DroidAPIRequestBuilder;
128
+ /**
129
+ * API request builder for osu!standard.
130
+ */
131
+ class OsuAPIRequestBuilder extends APIRequestBuilder {
132
+ constructor() {
133
+ super(...arguments);
134
+ this.host = "https://osu.ppy.sh/api/";
135
+ this.APIkey = process.env
136
+ .OSU_API_KEY;
137
+ this.APIkeyParam = `k=${this.APIkey}&`;
138
+ }
139
+ setEndpoint(endpoint) {
140
+ this.endpoint = endpoint;
141
+ return this;
142
+ }
143
+ }
144
+ exports.OsuAPIRequestBuilder = OsuAPIRequestBuilder;