@rian8337/osu-base 3.0.0-beta.1 → 3.0.0-beta.11

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
@@ -344,9 +344,7 @@ class APIRequestBuilder {
344
344
  * @param param The parameter to remove.
345
345
  */
346
346
  removeParameter(param) {
347
- if (this.params.get(param)) {
348
- this.params.delete(param);
349
- }
347
+ this.params.delete(param);
350
348
  return this;
351
349
  }
352
350
  }
@@ -357,18 +355,6 @@ class DroidAPIRequestBuilder extends APIRequestBuilder {
357
355
  host = "https://osudroid.moe/api/";
358
356
  APIkey = process.env.DROID_API_KEY;
359
357
  APIkeyParam = `apiKey=${this.APIkey}&`;
360
- setEndpoint(endpoint) {
361
- // Compatibility with old API. Can be removed in v3.0.
362
- switch (endpoint) {
363
- case "banscore.php":
364
- endpoint = "single_score_wipe.php";
365
- break;
366
- case "rename.php":
367
- endpoint = "user_rename.php";
368
- break;
369
- }
370
- return super.setEndpoint(endpoint);
371
- }
372
358
  }
373
359
  /**
374
360
  * API request builder for osu!standard.
@@ -379,6 +365,15 @@ class OsuAPIRequestBuilder extends APIRequestBuilder {
379
365
  APIkeyParam = `k=${this.APIkey}&`;
380
366
  }
381
367
 
368
+ /**
369
+ * Mode enum to switch things between osu!droid and osu!standard.
370
+ */
371
+ exports.modes = void 0;
372
+ (function (modes) {
373
+ modes["droid"] = "droid";
374
+ modes["osu"] = "osu";
375
+ })(exports.modes || (exports.modes = {}));
376
+
382
377
  /**
383
378
  * Bitmask constant of object types. This is needed as osu! uses bits to determine object types.
384
379
  */
@@ -396,11 +391,11 @@ exports.objectTypes = void 0;
396
391
  */
397
392
  class Vector2 {
398
393
  /**
399
- * The x position of the vector.
394
+ * The x position of this vector.
400
395
  */
401
396
  x;
402
397
  /**
403
- * The y position of the vector.
398
+ * The y position of this vector.
404
399
  */
405
400
  y;
406
401
  constructor(x, y) {
@@ -408,11 +403,22 @@ class Vector2 {
408
403
  this.y = y;
409
404
  }
410
405
  /**
411
- * Multiplies the vector with another vector.
406
+ * Multiplies this vector with another vector.
407
+ *
408
+ * @param vec The other vector.
409
+ * @returns The multiplied vector.
412
410
  */
413
411
  multiply(vec) {
414
412
  return new Vector2(this.x * vec.x, this.y * vec.y);
415
413
  }
414
+ /**
415
+ * Divides this vector with a scalar.
416
+ *
417
+ * Attempting to divide by 0 will throw an error.
418
+ *
419
+ * @param divideFactor The factor to divide the vector by.
420
+ * @returns The divided vector.
421
+ */
416
422
  divide(divideFactor) {
417
423
  if (divideFactor === 0) {
418
424
  throw new Error("Division by 0");
@@ -420,37 +426,52 @@ class Vector2 {
420
426
  return new Vector2(this.x / divideFactor, this.y / divideFactor);
421
427
  }
422
428
  /**
423
- * Adds the vector with another vector.
429
+ * Adds this vector with another vector.
430
+ *
431
+ * @param vec The other vector.
432
+ * @returns The added vector.
424
433
  */
425
434
  add(vec) {
426
435
  return new Vector2(this.x + vec.x, this.y + vec.y);
427
436
  }
428
437
  /**
429
- * Subtracts the vector with another vector.
438
+ * Subtracts this vector with another vector.
439
+ *
440
+ * @param vec The other vector.
441
+ * @returns The subtracted vector.
430
442
  */
431
443
  subtract(vec) {
432
444
  return new Vector2(this.x - vec.x, this.y - vec.y);
433
445
  }
434
446
  /**
435
- * The length of the vector.
447
+ * The length of this vector.
436
448
  */
437
449
  get length() {
438
450
  return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
439
451
  }
440
452
  /**
441
453
  * Performs a dot multiplication with another vector.
454
+ *
455
+ * @param vec The other vector.
456
+ * @returns The dot product of both vectors.
442
457
  */
443
458
  dot(vec) {
444
459
  return this.x * vec.x + this.y * vec.y;
445
460
  }
446
461
  /**
447
- * Scales the vector.
462
+ * Scales this vector.
463
+ *
464
+ * @param scaleFactor The factor to scale the vector by.
465
+ * @returns The scaled vector.
448
466
  */
449
467
  scale(scaleFactor) {
450
468
  return new Vector2(this.x * scaleFactor, this.y * scaleFactor);
451
469
  }
452
470
  /**
453
471
  * Gets the distance between this vector and another vector.
472
+ *
473
+ * @param vec The other vector.
474
+ * @returns The distance between this vector and the other vector.
454
475
  */
455
476
  getDistance(vec) {
456
477
  const x = this.x - vec.x;
@@ -469,6 +490,7 @@ class Vector2 {
469
490
  * Checks whether this vector is equal to another vector.
470
491
  *
471
492
  * @param other The other vector.
493
+ * @returns Whether this vector is equal to the other vector.
472
494
  */
473
495
  equals(other) {
474
496
  return this.x === other.x && this.y === other.y;
@@ -511,31 +533,6 @@ class HitObject {
511
533
  get duration() {
512
534
  return this.endTime - this.startTime;
513
535
  }
514
- /**
515
- * The stacked position of the hitobject.
516
- */
517
- get stackedPosition() {
518
- if (this.type & exports.objectTypes.spinner) {
519
- return this.position;
520
- }
521
- return this.position.add(this.stackOffset);
522
- }
523
- /**
524
- * The stacked end position of the hitobject.
525
- */
526
- get stackedEndPosition() {
527
- if (this.type & exports.objectTypes.spinner) {
528
- return this.position;
529
- }
530
- return this.endPosition.add(this.stackOffset);
531
- }
532
- /**
533
- * The stack vector to calculate offset for stacked positions.
534
- */
535
- get stackOffset() {
536
- const coordinate = this.stackHeight * this.scale * -6.4;
537
- return new Vector2(coordinate, coordinate);
538
- }
539
536
  /**
540
537
  * Whether this hit object represents a new combo.
541
538
  */
@@ -556,15 +553,13 @@ class HitObject {
556
553
  */
557
554
  stackHeight = 0;
558
555
  /**
559
- * The scale used to calculate stacked position and radius.
556
+ * The osu!droid scale used to calculate stacked position and radius.
560
557
  */
561
- scale = 1;
558
+ droidScale = 1;
562
559
  /**
563
- * The radius of the hitobject.
560
+ * The osu!standard scale used to calculate stacked position and radius.
564
561
  */
565
- get radius() {
566
- return 64 * this.scale;
567
- }
562
+ osuScale = 1;
568
563
  constructor(values) {
569
564
  this.startTime = values.startTime;
570
565
  this.endTime = values.endTime ?? values.startTime;
@@ -574,6 +569,62 @@ class HitObject {
574
569
  this.isNewCombo = values.newCombo ?? false;
575
570
  this.comboOffset = values.comboOffset ?? 0;
576
571
  }
572
+ /**
573
+ * Evaluates the radius of the hitobject.
574
+ *
575
+ * @param mode The gamemode to evaluate for.
576
+ * @returns The radius of the hitobject with respect to the gamemode.
577
+ */
578
+ getRadius(mode) {
579
+ let radius = 64;
580
+ switch (mode) {
581
+ case exports.modes.droid:
582
+ radius *= this.droidScale;
583
+ break;
584
+ case exports.modes.osu:
585
+ radius *= this.osuScale;
586
+ break;
587
+ }
588
+ return radius;
589
+ }
590
+ /**
591
+ * Evaluates the stack offset vector of the hitobject.
592
+ *
593
+ * This is used to calculate offset for stacked positions.
594
+ *
595
+ * @param mode The gamemode to evaluate for.
596
+ * @returns The stack offset with respect to the gamemode.
597
+ */
598
+ getStackOffset(mode) {
599
+ let coordinate = this.stackHeight * -6.4;
600
+ switch (mode) {
601
+ case exports.modes.droid:
602
+ coordinate *= this.droidScale;
603
+ break;
604
+ case exports.modes.osu:
605
+ coordinate *= this.osuScale;
606
+ break;
607
+ }
608
+ return new Vector2(coordinate, coordinate);
609
+ }
610
+ /**
611
+ * Evaluates the stacked position of the hitobject.
612
+ *
613
+ * @param mode The gamemode to evaluate for.
614
+ * @returns The stacked position with respect to the gamemode.
615
+ */
616
+ getStackedPosition(mode) {
617
+ return this.evaluateStackedPosition(this.position, mode);
618
+ }
619
+ /**
620
+ * Evaluates the stacked end position of the hitobject.
621
+ *
622
+ * @param mode The gamemode to evaluate for.
623
+ * @returns The stacked end position with respect to the gamemode.
624
+ */
625
+ getStackedEndPosition(mode) {
626
+ return this.evaluateStackedPosition(this.endPosition, mode);
627
+ }
577
628
  /**
578
629
  * Returns the hitobject type.
579
630
  */
@@ -590,6 +641,19 @@ class HitObject {
590
641
  }
591
642
  return res.substring(0, Math.max(0, res.length - 3));
592
643
  }
644
+ /**
645
+ * Evaluates the stacked position of the specified position.
646
+ *
647
+ * @param position The position to evaluate.
648
+ * @param mode The gamemode to evaluate for.
649
+ * @returns The stacked position.
650
+ */
651
+ evaluateStackedPosition(position, mode) {
652
+ if (this.type & exports.objectTypes.spinner) {
653
+ return position;
654
+ }
655
+ return position.add(this.getStackOffset(mode));
656
+ }
593
657
  }
594
658
 
595
659
  /**
@@ -1193,15 +1257,7 @@ class ControlPointManager {
1193
1257
  }
1194
1258
  existing = this.controlPointAt(controlPoint.time);
1195
1259
  }
1196
- // Get the index at which to add the control point.
1197
- for (let i = 0; i < this._points.length; ++i) {
1198
- if (this._points[i].time >= controlPoint.time) {
1199
- this._points.splice(i - 1, 0, controlPoint);
1200
- return true;
1201
- }
1202
- }
1203
- // Append the control point if it hasn't been added yet.
1204
- this._points.push(controlPoint);
1260
+ this._points.splice(this.findInsertionIndex(controlPoint.time), 0, controlPoint);
1205
1261
  return true;
1206
1262
  }
1207
1263
  /**
@@ -1235,6 +1291,12 @@ class ControlPointManager {
1235
1291
  removeAt(index) {
1236
1292
  return this._points.splice(index, 1)[0];
1237
1293
  }
1294
+ /**
1295
+ * Clears all control points of this type.
1296
+ */
1297
+ clear() {
1298
+ this._points.length = 0;
1299
+ }
1238
1300
  /**
1239
1301
  * Binary searches one of the control point lists to find the active control point at the given time.
1240
1302
  *
@@ -1274,9 +1336,37 @@ class ControlPointManager {
1274
1336
  return this._points[pivot];
1275
1337
  }
1276
1338
  }
1277
- // l will be the first control point with time > this.controlPoints[l].time, but we want the one before it
1339
+ // l will be the first control point with time > this._points[l].time, but we want the one before it
1278
1340
  return this._points[l - 1];
1279
1341
  }
1342
+ /**
1343
+ * Finds the insertion index of a control point in a given time.
1344
+ *
1345
+ * @param time The start time of the control point.
1346
+ */
1347
+ findInsertionIndex(time) {
1348
+ if (this._points.length === 0 || time < this._points[0].time) {
1349
+ return 0;
1350
+ }
1351
+ if (time >= this._points.at(-1).time) {
1352
+ return this._points.length;
1353
+ }
1354
+ let l = 0;
1355
+ let r = this._points.length - 2;
1356
+ while (l <= r) {
1357
+ const pivot = l + ((r - l) >> 1);
1358
+ if (this._points[pivot].time < time) {
1359
+ l = pivot + 1;
1360
+ }
1361
+ else if (this._points[pivot].time > time) {
1362
+ r = pivot - 1;
1363
+ }
1364
+ else {
1365
+ return pivot;
1366
+ }
1367
+ }
1368
+ return l;
1369
+ }
1280
1370
  }
1281
1371
 
1282
1372
  /**
@@ -1437,7 +1527,7 @@ class SampleControlPointManager extends ControlPointManager {
1437
1527
  }
1438
1528
 
1439
1529
  /**
1440
- * Contains information about timing and control points of a beatmap.
1530
+ * Contains information about timing (control) points of a beatmap.
1441
1531
  */
1442
1532
  class BeatmapControlPoints {
1443
1533
  /**
@@ -1456,6 +1546,15 @@ class BeatmapControlPoints {
1456
1546
  * The manager for sample control points of the beatmap.
1457
1547
  */
1458
1548
  sample = new SampleControlPointManager();
1549
+ /**
1550
+ * Clears all control points in the beatmap.
1551
+ */
1552
+ clear() {
1553
+ this.timing.clear();
1554
+ this.difficulty.clear();
1555
+ this.effect.clear();
1556
+ this.sample.clear();
1557
+ }
1459
1558
  }
1460
1559
 
1461
1560
  /**
@@ -1573,7 +1672,7 @@ class BeatmapHitObjects {
1573
1672
  }
1574
1673
  }
1575
1674
  }
1576
- else if (object instanceof Spinner) {
1675
+ else {
1577
1676
  ++this._spinners;
1578
1677
  }
1579
1678
  }
@@ -1622,12 +1721,28 @@ class BeatmapHitObjects {
1622
1721
  * @param startTime The start time of the hitobject.
1623
1722
  */
1624
1723
  findInsertionIndex(startTime) {
1625
- for (let i = 0; i < this.objects.length; ++i) {
1626
- if (this.objects[i].startTime > startTime) {
1627
- return i;
1724
+ if (this._objects.length === 0 ||
1725
+ startTime < this._objects[0].startTime) {
1726
+ return 0;
1727
+ }
1728
+ if (startTime >= this._objects.at(-1).startTime) {
1729
+ return this._objects.length;
1730
+ }
1731
+ let l = 0;
1732
+ let r = this._objects.length - 2;
1733
+ while (l <= r) {
1734
+ const pivot = l + ((r - l) >> 1);
1735
+ if (this._objects[pivot].startTime < startTime) {
1736
+ l = pivot + 1;
1737
+ }
1738
+ else if (this._objects[pivot].startTime > startTime) {
1739
+ r = pivot - 1;
1740
+ }
1741
+ else {
1742
+ return pivot;
1628
1743
  }
1629
1744
  }
1630
- return this.objects.length;
1745
+ return l;
1631
1746
  }
1632
1747
  }
1633
1748
 
@@ -1893,15 +2008,6 @@ class BeatmapBackground {
1893
2008
  }
1894
2009
  }
1895
2010
 
1896
- /**
1897
- * Mode enum to switch things between osu!droid and osu!standard.
1898
- */
1899
- exports.modes = void 0;
1900
- (function (modes) {
1901
- modes["droid"] = "droid";
1902
- modes["osu"] = "osu";
1903
- })(exports.modes || (exports.modes = {}));
1904
-
1905
2011
  class HitWindow {
1906
2012
  /**
1907
2013
  * The overall difficulty of this hit window.
@@ -2373,7 +2479,7 @@ class ModUtil {
2373
2479
  * @param options Options for parsing behavior.
2374
2480
  */
2375
2481
  static pcModbitsToMods(modbits, options) {
2376
- return this.processParsingOptions(this.allMods.filter((m) => m.isApplicableToOsu() && (m.bitwise & modbits)), options);
2482
+ return this.processParsingOptions(this.allMods.filter((m) => m.isApplicableToOsu() && m.bitwise & modbits), options);
2377
2483
  }
2378
2484
  /**
2379
2485
  * Gets a list of mods from a PC mod string, such as "HDHR".
@@ -2416,8 +2522,7 @@ class ModUtil {
2416
2522
  for (const incompatibleMod of this.incompatibleMods) {
2417
2523
  const fulfilledMods = mods.filter((m) => incompatibleMod.some((v) => m.acronym === v.acronym));
2418
2524
  if (fulfilledMods.length > 1) {
2419
- mods = mods.filter((m) => incompatibleMod
2420
- .every((v) => m.acronym !== v.acronym));
2525
+ mods = mods.filter((m) => incompatibleMod.every((v) => m.acronym !== v.acronym));
2421
2526
  // Keep the first selected mod
2422
2527
  mods.push(fulfilledMods[0]);
2423
2528
  }
@@ -2743,6 +2848,49 @@ class Precision {
2743
2848
  return (this.almostEqualsNumber(vec1.x, vec2.x, acceptableDifference) &&
2744
2849
  this.almostEqualsNumber(vec1.y, vec2.y, acceptableDifference));
2745
2850
  }
2851
+ /**
2852
+ * Checks whether two real numbers are almost equal.
2853
+ *
2854
+ * @param a The first number.
2855
+ * @param b The second number.
2856
+ * @param maximumError The accuracy required for being almost equal. Defaults to `10 * 2^(-53)`.
2857
+ * @returns Whether the two values differ by no more than 10 * 2^(-52).
2858
+ */
2859
+ static almostEqualRelative(a, b, maximumError = 10 * Math.pow(2, -53)) {
2860
+ return this.almostEqualNormRelative(a, b, a - b, maximumError);
2861
+ }
2862
+ /**
2863
+ * Compares two numbers and determines if they are equal within the specified maximum error.
2864
+ *
2865
+ * @param a The norm of the first value (can be negative).
2866
+ * @param b The norm of the second value (can be negative).
2867
+ * @param diff The norm of the difference of the two values (can be negative).
2868
+ * @param maximumError The accuracy required for being almost equal.
2869
+ * @returns Whether both numbers are almost equal up to the specified maximum error.
2870
+ */
2871
+ static almostEqualNormRelative(a, b, diff, maximumError) {
2872
+ // If A or B are infinity (positive or negative) then
2873
+ // only return true if they are exactly equal to each other -
2874
+ // that is, if they are both infinities of the same sign.
2875
+ if (!Number.isFinite(a) || !Number.isFinite(b)) {
2876
+ return a === b;
2877
+ }
2878
+ // If A or B are a NAN, return false. NANs are equal to nothing,
2879
+ // not even themselves.
2880
+ if (Number.isNaN(a) || Number.isNaN(b)) {
2881
+ return false;
2882
+ }
2883
+ // If one is almost zero, fall back to absolute equality.
2884
+ const doublePrecision = Math.pow(2, -53);
2885
+ if (Math.abs(a) < doublePrecision || Math.abs(b) < doublePrecision) {
2886
+ return Math.abs(diff) < maximumError;
2887
+ }
2888
+ if ((a === 0 && Math.abs(b) < maximumError) ||
2889
+ (b === 0 && Math.abs(a) < maximumError)) {
2890
+ return true;
2891
+ }
2892
+ return (Math.abs(diff) < maximumError * Math.max(Math.abs(a), Math.abs(b)));
2893
+ }
2746
2894
  }
2747
2895
 
2748
2896
  /**
@@ -4302,7 +4450,12 @@ class RGBColor {
4302
4450
  * Returns a string representation of the color.
4303
4451
  */
4304
4452
  toString() {
4305
- return `${this.r},${this.g},${this.b}`;
4453
+ if (this.a === 1) {
4454
+ return `${this.r},${this.g},${this.b}`;
4455
+ }
4456
+ else {
4457
+ return `${this.r},${this.g},${this.b},${this.a}`;
4458
+ }
4306
4459
  }
4307
4460
  /**
4308
4461
  * Checks whether this color is equal to another color.
@@ -4310,7 +4463,10 @@ class RGBColor {
4310
4463
  * @param other The other color.
4311
4464
  */
4312
4465
  equals(other) {
4313
- return this.r === other.r && this.g === other.g && this.b === other.b;
4466
+ return (this.r === other.r &&
4467
+ this.g === other.g &&
4468
+ this.b === other.b &&
4469
+ this.a === other.a);
4314
4470
  }
4315
4471
  }
4316
4472
 
@@ -4664,21 +4820,11 @@ class CommandTimelineGroup {
4664
4820
  this.flipHorizontal,
4665
4821
  this.flipVertical,
4666
4822
  ];
4667
- /**
4668
- * The earliest visible time. Will be `null` unless this group's first alpha command has a start value of zero.
4669
- */
4670
- get earliestDisplayedTime() {
4671
- const first = this.alpha.commands.at(0);
4672
- return first?.startValue === 0 ? first.startTime : null;
4673
- }
4674
4823
  /**
4675
4824
  * The start time of commands.
4676
4825
  */
4677
4826
  get commandsStartTime() {
4678
- // If the first alpha command starts at zero it should be given priority over anything else.
4679
- // This is due to it creating a state where the target is not present before that time, causing any other events to not be visible.
4680
- return (this.earliestDisplayedTime ??
4681
- Math.min(...this.timelines.map((t) => t.startTime)));
4827
+ return Math.min(...this.timelines.map((t) => t.startTime));
4682
4828
  }
4683
4829
  /**
4684
4830
  * The end time of commands.
@@ -4719,7 +4865,7 @@ class CommandTimelineGroup {
4719
4865
  /**
4720
4866
  * Gets the commands from a command timeline.
4721
4867
  *
4722
- * @param timelineSelector A function that returns a command timeline.
4868
+ * @param timelineSelector A function to select the command timeline to retrieve commands from.
4723
4869
  * @param offset The offset to apply to all commands.
4724
4870
  */
4725
4871
  getCommands(timelineSelector, offset = 0) {
@@ -4831,20 +4977,14 @@ class StoryboardElement {
4831
4977
  * Represents a storyboard sprite.
4832
4978
  */
4833
4979
  class StoryboardSprite extends StoryboardElement {
4834
- _loops = [];
4835
- _triggers = [];
4836
4980
  /**
4837
4981
  * The loop commands of the sprite.
4838
4982
  */
4839
- get loops() {
4840
- return this._loops;
4841
- }
4983
+ loops = [];
4842
4984
  /**
4843
4985
  * The trigger commands of the sprite.
4844
4986
  */
4845
- get triggers() {
4846
- return this._triggers;
4847
- }
4987
+ triggers = [];
4848
4988
  /**
4849
4989
  * The origin of the sprite.
4850
4990
  */
@@ -4858,28 +4998,57 @@ class StoryboardSprite extends StoryboardElement {
4858
4998
  */
4859
4999
  timelineGroup = new CommandTimelineGroup();
4860
5000
  get startTime() {
4861
- // Check for presence affecting commands as an initial pass.
4862
- let earliestStartTime = this.timelineGroup.earliestDisplayedTime ??
4863
- Number.POSITIVE_INFINITY;
4864
- for (const l of this._loops) {
4865
- const { earliestDisplayedTime: loopEarliestDisplayTime } = l;
4866
- if (loopEarliestDisplayTime !== null) {
4867
- earliestStartTime = Math.min(earliestStartTime, l.loopStartTime + loopEarliestDisplayTime);
5001
+ // To get the initial start time, we need to check whether the first alpha command to exist (across all loops) has a start value of zero.
5002
+ // A start value of zero governs, above all else, the first valid display time of a sprite.
5003
+ //
5004
+ // You can imagine that the first command of each type decides that type's start value, so if the initial alpha is zero,
5005
+ // anything before that point can be ignored (the sprite is not visible after all).
5006
+ const alphaCommands = [];
5007
+ let command = this.timelineGroup.alpha.commands[0];
5008
+ if (command) {
5009
+ alphaCommands.push({
5010
+ startTime: command.startTime,
5011
+ isZeroStartValue: command.startValue === 0,
5012
+ });
5013
+ }
5014
+ for (const l of this.loops) {
5015
+ command = l.alpha.commands[0];
5016
+ if (command) {
5017
+ alphaCommands.push({
5018
+ startTime: command.startTime + l.loopStartTime,
5019
+ isZeroStartValue: command.startValue === 0,
5020
+ });
4868
5021
  }
4869
5022
  }
4870
- return earliestStartTime !== Number.POSITIVE_INFINITY
4871
- ? earliestStartTime
4872
- : Math.min(this.timelineGroup.startTime, ...this._loops.map((l) => l.startTime));
5023
+ if (alphaCommands.length > 0) {
5024
+ const firstAlpha = alphaCommands.sort((a, b) => a.startTime - b.startTime)[0];
5025
+ if (firstAlpha.isZeroStartValue) {
5026
+ return firstAlpha.startTime;
5027
+ }
5028
+ }
5029
+ return this.earliestTransformTime;
5030
+ }
5031
+ /**
5032
+ * The time at which the first transformation occurs.
5033
+ */
5034
+ get earliestTransformTime() {
5035
+ // If we got to this point, either no alpha commands were present, or the earliest had a non-zero start value.
5036
+ // The sprite's start time will be determined by the earliest command, regardless of type.
5037
+ let earliestStartTime = this.timelineGroup.startTime;
5038
+ for (const l of this.loops) {
5039
+ earliestStartTime = Math.min(earliestStartTime, l.startTime);
5040
+ }
5041
+ return earliestStartTime;
4873
5042
  }
4874
5043
  get endTime() {
4875
- return Math.max(this.timelineGroup.endTime, ...this._loops.map((l) => l.endTime));
5044
+ return Math.max(this.timelineGroup.endTime, ...this.loops.map((l) => l.endTime));
4876
5045
  }
4877
5046
  /**
4878
5047
  * Whether this sprite has at least one command.
4879
5048
  */
4880
5049
  get hasCommands() {
4881
5050
  return (this.timelineGroup.hasCommands ||
4882
- this._loops.some((l) => l.hasCommands));
5051
+ this.loops.some((l) => l.hasCommands));
4883
5052
  }
4884
5053
  constructor(path, origin, initialPosition) {
4885
5054
  super(path);
@@ -4895,7 +5064,7 @@ class StoryboardSprite extends StoryboardElement {
4895
5064
  */
4896
5065
  addLoop(startTime, repeatCount) {
4897
5066
  const loop = new CommandLoop(startTime, repeatCount);
4898
- this._loops.push(loop);
5067
+ this.loops.push(loop);
4899
5068
  return loop;
4900
5069
  }
4901
5070
  /**
@@ -4909,7 +5078,7 @@ class StoryboardSprite extends StoryboardElement {
4909
5078
  */
4910
5079
  addTrigger(triggerName, startTime, endTime, groupNumber) {
4911
5080
  const trigger = new CommandTrigger(triggerName, startTime, endTime, groupNumber);
4912
- this._triggers.push(trigger);
5081
+ this.triggers.push(trigger);
4913
5082
  return trigger;
4914
5083
  }
4915
5084
  toString() {
@@ -5349,16 +5518,23 @@ class BeatmapDecoder extends Decoder {
5349
5518
  else {
5350
5519
  hitObjectsDecoder.applyStackingOld();
5351
5520
  }
5352
- const circleSize = new MapStats({
5521
+ const droidCircleSize = new MapStats({
5522
+ cs: this.finalResult.difficulty.cs,
5523
+ mods,
5524
+ }).calculate({ mode: exports.modes.droid }).cs;
5525
+ const droidScale = (1 - (0.7 * (droidCircleSize - 5)) / 5) / 2;
5526
+ const osuCircleSize = new MapStats({
5353
5527
  cs: this.finalResult.difficulty.cs,
5354
5528
  mods,
5355
- }).calculate().cs;
5356
- const scale = (1 - (0.7 * (circleSize - 5)) / 5) / 2;
5529
+ }).calculate({ mode: exports.modes.osu }).cs;
5530
+ const osuScale = (1 - (0.7 * (osuCircleSize - 5)) / 5) / 2;
5357
5531
  this.finalResult.hitObjects.objects.forEach((h) => {
5358
- h.scale = scale;
5532
+ h.droidScale = droidScale;
5533
+ h.osuScale = osuScale;
5359
5534
  if (h instanceof Slider) {
5360
5535
  h.nestedHitObjects.forEach((n) => {
5361
- n.scale = scale;
5536
+ n.droidScale = droidScale;
5537
+ n.osuScale = osuScale;
5362
5538
  });
5363
5539
  }
5364
5540
  });
@@ -6118,6 +6294,236 @@ class BeatmapEncoder extends Encoder {
6118
6294
  }
6119
6295
  }
6120
6296
 
6297
+ class ZeroCrossingBracketing {
6298
+ /**
6299
+ * Detect a range containing at least one root.
6300
+ *
6301
+ * This iterative method stops when two values with opposite signs are found.
6302
+ *
6303
+ * @param f The function to detect roots from.
6304
+ * @param bounds The upper and lower value of the range.
6305
+ * @param factor The growing factor of research. Defaults to 1.6.
6306
+ * @param maxIterations Maximum number of iterations. Defaults to 50.
6307
+ * @returns Whether the bracketing operation succeeded.
6308
+ */
6309
+ static expand(f, bounds, factor = 1.6, maxIterations = 50) {
6310
+ const originalUpperBound = bounds.upperBound;
6311
+ const originalLowerBound = bounds.lowerBound;
6312
+ if (originalLowerBound >= originalUpperBound) {
6313
+ throw new RangeError("Upper bound must be greater than lower bound.");
6314
+ }
6315
+ let fmin = f(originalLowerBound);
6316
+ let fmax = f(originalUpperBound);
6317
+ for (let i = 0; i < maxIterations; ++i) {
6318
+ if (Math.sign(fmin) !== Math.sign(fmax)) {
6319
+ return true;
6320
+ }
6321
+ if (Math.abs(fmin) < Math.abs(fmax)) {
6322
+ bounds.lowerBound +=
6323
+ factor * (bounds.lowerBound - bounds.upperBound);
6324
+ fmin = f(bounds.lowerBound);
6325
+ }
6326
+ else {
6327
+ bounds.upperBound +=
6328
+ factor * (bounds.upperBound - bounds.lowerBound);
6329
+ fmax = f(bounds.upperBound);
6330
+ }
6331
+ }
6332
+ bounds.lowerBound = originalLowerBound;
6333
+ bounds.upperBound = originalUpperBound;
6334
+ return false;
6335
+ }
6336
+ static reduce(f, bounds, subdivisions = 1000) {
6337
+ const originalUpperBound = bounds.upperBound;
6338
+ const originalLowerBound = bounds.lowerBound;
6339
+ if (originalLowerBound >= originalUpperBound) {
6340
+ throw new RangeError("Upper bound must be greater than lower bound.");
6341
+ }
6342
+ // TODO: Consider binary-style search instead of linear scan
6343
+ const fmin = f(bounds.lowerBound);
6344
+ const fmax = f(bounds.upperBound);
6345
+ if (Math.sign(fmin) != Math.sign(fmax)) {
6346
+ return true;
6347
+ }
6348
+ const subdiv = (bounds.upperBound - bounds.lowerBound) / subdivisions;
6349
+ let smin = bounds.lowerBound;
6350
+ const sign = Math.sign(fmin);
6351
+ for (let i = 0; i < subdivisions; ++i) {
6352
+ const smax = smin + subdiv;
6353
+ const sfmax = f(smax);
6354
+ if (!Number.isFinite(sfmax)) {
6355
+ // expand interval to include pole
6356
+ smin = smax;
6357
+ continue;
6358
+ }
6359
+ if (Math.sign(sfmax) != sign) {
6360
+ bounds.upperBound = smax;
6361
+ bounds.lowerBound = smin;
6362
+ return true;
6363
+ }
6364
+ smin = smax;
6365
+ }
6366
+ bounds.lowerBound = originalLowerBound;
6367
+ bounds.upperBound = originalUpperBound;
6368
+ return false;
6369
+ }
6370
+ static expandReduce(f, bounds, expansionFactor = 1.6, expansionMaxIterations = 50, reduceSubdivisions = 100) {
6371
+ return (this.expand(f, bounds, expansionFactor, expansionMaxIterations) ||
6372
+ this.reduce(f, bounds, reduceSubdivisions));
6373
+ }
6374
+ }
6375
+
6376
+ /**
6377
+ * Algorithm by Brent, Van Wijngaarden, Dekker et al.
6378
+ *
6379
+ * Implementation inspired by Press, Teukolsky, Vetterling, and Flannery, "Numerical Recipes in C", 2nd edition, Cambridge University Press.
6380
+ */
6381
+ class Brent {
6382
+ /**
6383
+ * Finds a solution to the equation f(x) = 0.
6384
+ *
6385
+ * @param f The function to find roots from.
6386
+ * @param bounds The upper and lower root bounds.
6387
+ * @param accuracy The desired accuracy. The root will be refined until the accuracy or the maximum number of iterations is reached. Defaults to 1e-8. Must be greater than 0.
6388
+ * @param maxIterations The maximum number of iterations. Defaults to 100.
6389
+ * @param expandFactor The factor at which to expand the bounds, if needed. Defaults to 1.6.
6390
+ * @param maxExpandIterations The maximum number of expand iterations. Defaults to 100.
6391
+ * @returns The root with the specified accuracy. Throws an error if the algorithm failed to converge.
6392
+ */
6393
+ static findRootExpand(f, bounds, accuracy = 1e-8, maxIterations = 100, expandFactor = 1.6, maxExpandIterations = 100) {
6394
+ ZeroCrossingBracketing.expandReduce(f, bounds, expandFactor, maxExpandIterations, maxExpandIterations * 10);
6395
+ return this.findRoot(f, bounds, accuracy, maxIterations);
6396
+ }
6397
+ /**
6398
+ * Finds a solution to the equation f(x) = 0.
6399
+ *
6400
+ * @param f The function to find roots from.
6401
+ * @param bounds The upper and lower root bounds.
6402
+ * @param accuracy The desired accuracy. The root will be refined until the accuracy or the maximum number of iterations is reached. Defaults to 1e-8. Must be greater than 0.
6403
+ * @param maxIterations The maximum number of iterations. Defaults to 100.
6404
+ * @returns The root with the specified accuracy. Throws an error if the algorithm failed to converge.
6405
+ */
6406
+ static findRoot(f, bounds, accuracy = 1e-8, maxIterations = 100) {
6407
+ const root = this.tryFindRoot(f, bounds, accuracy, maxIterations);
6408
+ if (root === null) {
6409
+ throw new Error("The algorithm has failed, exceeded the number of iterations allowed or there is no root within the provided bounds.");
6410
+ }
6411
+ return root;
6412
+ }
6413
+ /**
6414
+ * Finds a solution to the equation f(x) = 0.
6415
+ *
6416
+ * @param f The function to find roots from.
6417
+ * @param bounds The upper and lower root bounds.
6418
+ * @param accuracy The desired accuracy. The root will be refined until the accuracy or the maximum number of iterations is reached. Must be greater than 0.
6419
+ * @param maxIterations The maximum number of iterations. Usually 100.
6420
+ * @returns The root with the specified accuracy, `null` if not found.
6421
+ */
6422
+ static tryFindRoot(f, bounds, accuracy, maxIterations) {
6423
+ if (accuracy <= 0) {
6424
+ throw new RangeError("Accuracy must be greater than 0.");
6425
+ }
6426
+ let { lowerBound, upperBound } = bounds;
6427
+ let fmin = f(lowerBound);
6428
+ let fmax = f(upperBound);
6429
+ let froot = fmax;
6430
+ let d = 0;
6431
+ let e = 0;
6432
+ let root = upperBound;
6433
+ let xMid = Number.NaN;
6434
+ // Root must be bracketed.
6435
+ if (Math.sign(fmin) === Math.sign(fmax)) {
6436
+ return null;
6437
+ }
6438
+ for (let i = 0; i <= maxIterations; ++i) {
6439
+ // Adjust bounds.
6440
+ if (Math.sign(froot) === Math.sign(fmax)) {
6441
+ upperBound = lowerBound;
6442
+ fmax = fmin;
6443
+ e = d = root - lowerBound;
6444
+ }
6445
+ if (Math.abs(fmax) < Math.abs(froot)) {
6446
+ lowerBound = root;
6447
+ root = upperBound;
6448
+ upperBound = lowerBound;
6449
+ fmin = froot;
6450
+ froot = fmax;
6451
+ fmax = fmin;
6452
+ }
6453
+ // Convergence check
6454
+ const xAcc1 = 2 * Math.pow(2, -53) * Math.abs(root) + accuracy / 2;
6455
+ const xMidOld = xMid;
6456
+ xMid = (upperBound - root) / 2;
6457
+ if (Math.abs(xMid) <= xAcc1 ||
6458
+ Precision.almostEqualNormRelative(froot, 0, froot, accuracy)) {
6459
+ return root;
6460
+ }
6461
+ if (xMid === xMidOld) {
6462
+ // accuracy not sufficient, but cannot be improved further
6463
+ return null;
6464
+ }
6465
+ if (Math.abs(e) >= xAcc1 && Math.abs(fmin) > Math.abs(froot)) {
6466
+ // Attempt inverse quadratic interpolation
6467
+ const s = froot / fmin;
6468
+ let p;
6469
+ let q;
6470
+ if (Precision.almostEqualRelative(lowerBound, upperBound)) {
6471
+ p = 2 * xMid * s;
6472
+ q = 1 - s;
6473
+ }
6474
+ else {
6475
+ q = fmin / fmax;
6476
+ const r = froot / fmax;
6477
+ p =
6478
+ s *
6479
+ (2 * xMid * q * (q - r) -
6480
+ (root - lowerBound) * (r - 1));
6481
+ q = (q - 1) * (r - 1) * (s - 1);
6482
+ }
6483
+ if (p > 0) {
6484
+ // Check whether in bounds
6485
+ q = -q;
6486
+ }
6487
+ p = Math.abs(p);
6488
+ if (2 * p <
6489
+ Math.min(3 * xMid * q - Math.abs(xAcc1 * q), Math.abs(e * q))) {
6490
+ // Accept interpolation
6491
+ e = d;
6492
+ d = p / q;
6493
+ }
6494
+ else {
6495
+ // Interpolation failed, use bisection
6496
+ d = xMid;
6497
+ e = d;
6498
+ }
6499
+ }
6500
+ else {
6501
+ // Bounds decreasing too slowly, use bisection
6502
+ d = xMid;
6503
+ e = d;
6504
+ }
6505
+ lowerBound = root;
6506
+ fmin = froot;
6507
+ if (Math.abs(d) > xAcc1) {
6508
+ root += d;
6509
+ }
6510
+ else {
6511
+ root += this.sign(xAcc1, xMid);
6512
+ }
6513
+ froot = f(root);
6514
+ }
6515
+ return null;
6516
+ }
6517
+ /**
6518
+ * Helper method useful for preventing rounding errors.
6519
+ *
6520
+ * @returns a * sign(b)
6521
+ */
6522
+ static sign(a, b) {
6523
+ return b >= 0 ? (a >= 0 ? a : -a) : a >= 0 ? -a : a;
6524
+ }
6525
+ }
6526
+
6121
6527
  /**
6122
6528
  * Types of easing.
6123
6529
  *
@@ -6583,6 +6989,29 @@ class ErrorFunction {
6583
6989
  }
6584
6990
  return this.erfImp(x, false);
6585
6991
  }
6992
+ /**
6993
+ * Calculates the complementary error function.
6994
+ *
6995
+ * @param x The value to evaluate.
6996
+ * @returns The complementary error function evaluated at given value, or:
6997
+ * - 0 if `x === Number.POSITIVE_INFINITY`;
6998
+ * - 2 if `x === Number.NEGATIVE_INFINITY`.
6999
+ */
7000
+ static erfc(x) {
7001
+ if (x === 0) {
7002
+ return 1;
7003
+ }
7004
+ if (x === Number.POSITIVE_INFINITY) {
7005
+ return 0;
7006
+ }
7007
+ if (x === Number.NEGATIVE_INFINITY) {
7008
+ return 2;
7009
+ }
7010
+ if (Number.isNaN(x)) {
7011
+ return Number.NaN;
7012
+ }
7013
+ return this.erfImp(x, true);
7014
+ }
6586
7015
  /**
6587
7016
  * Calculates the inverse error function evaluated at z.
6588
7017
  *
@@ -7426,6 +7855,16 @@ class MapInfo {
7426
7855
  }
7427
7856
  }
7428
7857
 
7858
+ /**
7859
+ * Represents the osu! playfield.
7860
+ */
7861
+ class Playfield {
7862
+ /**
7863
+ * The size of the playfield, which is 512x384.
7864
+ */
7865
+ static baseSize = new Vector2(512, 384);
7866
+ }
7867
+
7429
7868
  dotenv.config();
7430
7869
 
7431
7870
  exports.Accuracy = Accuracy;
@@ -7444,6 +7883,7 @@ exports.BeatmapMetadata = BeatmapMetadata;
7444
7883
  exports.BeatmapVideo = BeatmapVideo;
7445
7884
  exports.BlendingParameters = BlendingParameters;
7446
7885
  exports.BreakPoint = BreakPoint;
7886
+ exports.Brent = Brent;
7447
7887
  exports.Circle = Circle;
7448
7888
  exports.Command = Command;
7449
7889
  exports.CommandLoop = CommandLoop;
@@ -7488,6 +7928,7 @@ exports.ModUtil = ModUtil;
7488
7928
  exports.OsuAPIRequestBuilder = OsuAPIRequestBuilder;
7489
7929
  exports.OsuHitWindow = OsuHitWindow;
7490
7930
  exports.PathApproximator = PathApproximator;
7931
+ exports.Playfield = Playfield;
7491
7932
  exports.Polynomial = Polynomial;
7492
7933
  exports.Precision = Precision;
7493
7934
  exports.RGBColor = RGBColor;
@@ -7513,4 +7954,5 @@ exports.TimingControlPoint = TimingControlPoint;
7513
7954
  exports.TimingControlPointManager = TimingControlPointManager;
7514
7955
  exports.Utils = Utils;
7515
7956
  exports.Vector2 = Vector2;
7957
+ exports.ZeroCrossingBracketing = ZeroCrossingBracketing;
7516
7958
  //# sourceMappingURL=index.js.map