@rian8337/osu-difficulty-calculator 4.0.0-beta.95 → 4.0.0-beta.97

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
@@ -60,6 +60,118 @@ class DifficultyCalculator {
60
60
  flashlight: skills[3].peaks,
61
61
  };
62
62
  }
63
+ /**
64
+ * Calculates the difficulty of a `Beatmap` with specific `Mod`s and returns a set of
65
+ * `TimedDifficultyAttributes` representing the difficulty at every relevant time value in the `Beatmap`.
66
+ *
67
+ * @param beatmap The `Beatmap` whose difficulty is to be calculated.
68
+ * @param mods The `Mod`s to apply to the `Beatmap`.
69
+ * @return The set of `TimedDifficultyAttributes`.
70
+ */
71
+ calculateTimed(beatmap, mods) {
72
+ if (beatmap.hitObjects.objects.length === 0) {
73
+ return [];
74
+ }
75
+ const playableBeatmap = this.createPlayableBeatmap(beatmap, mods);
76
+ const attributes = osuBase.Utils.initializeArray(beatmap.hitObjects.objects.length);
77
+ const skills = this.createSkills(playableBeatmap);
78
+ const progressiveBeatmap = new ProgressiveCalculationBeatmap(playableBeatmap);
79
+ const { objects } = playableBeatmap.hitObjects;
80
+ const difficultyObjects = this.createDifficultyHitObjects(playableBeatmap);
81
+ let currentIndex = 0;
82
+ for (let i = 0; i < objects.length; ++i) {
83
+ const obj = objects[i];
84
+ progressiveBeatmap.hitObjects.add(obj);
85
+ while (currentIndex < difficultyObjects.length && difficultyObjects[currentIndex].object.endTime <= obj.endTime) {
86
+ for (const skill of skills) {
87
+ skill.process(difficultyObjects[currentIndex]);
88
+ }
89
+ ++currentIndex;
90
+ }
91
+ attributes[i] = {
92
+ time: obj.endTime,
93
+ attributes: this.createDifficultyAttributes(beatmap, playableBeatmap, skills, difficultyObjects.slice(0, currentIndex)),
94
+ sliderCount: progressiveBeatmap.hitObjects.sliders,
95
+ sliderTickCount: progressiveBeatmap.hitObjects.sliderTicks,
96
+ sliderRepeatCount: progressiveBeatmap.hitObjects.sliderRepeatPoints
97
+ };
98
+ }
99
+ return attributes;
100
+ }
101
+ }
102
+ /**
103
+ * A {@link PlayableBeatmap} for timed difficulty calculation.
104
+ */
105
+ class ProgressiveCalculationBeatmap extends osuBase.PlayableBeatmap {
106
+ get maxCombo() {
107
+ return this.hitObjects.maxCombo;
108
+ }
109
+ constructor(baseBeatmap) {
110
+ super(baseBeatmap, baseBeatmap.mods);
111
+ this.hitObjects = new ProgressiveCalculationHitObjects();
112
+ this.baseHitWindow = baseBeatmap.hitWindow;
113
+ }
114
+ createHitWindow() {
115
+ return this.baseHitWindow;
116
+ }
117
+ }
118
+ class ProgressiveCalculationHitObjects extends osuBase.BeatmapHitObjects {
119
+ constructor() {
120
+ super(...arguments);
121
+ this.maxCombo = 0;
122
+ // We store these locally since the super class's getters iterate through all objects, which is inefficient for progressive calculation.
123
+ this.sliderTickCount = 0;
124
+ this.sliderRepeatCount = 0;
125
+ }
126
+ get sliderTicks() {
127
+ return this.sliderTickCount;
128
+ }
129
+ get sliderRepeatPoints() {
130
+ return this.sliderRepeatCount;
131
+ }
132
+ add(...objects) {
133
+ super.add(...objects);
134
+ for (const obj of objects) {
135
+ if (obj instanceof osuBase.Slider) {
136
+ this.maxCombo += obj.nestedHitObjects.length;
137
+ // Similarly, we loop through the nested hit objects since `Slider.ticks` also loops
138
+ // through the nested hit objects, which is inefficient for progressive calculation.
139
+ for (const nestedObj of obj.nestedHitObjects) {
140
+ if (nestedObj instanceof osuBase.SliderTick) {
141
+ ++this.sliderTickCount;
142
+ }
143
+ else if (nestedObj instanceof osuBase.SliderRepeat) {
144
+ ++this.sliderRepeatCount;
145
+ }
146
+ }
147
+ }
148
+ else {
149
+ ++this.maxCombo;
150
+ }
151
+ }
152
+ }
153
+ removeAt(index) {
154
+ const obj = super.removeAt(index);
155
+ if (obj !== null) {
156
+ if (obj instanceof osuBase.Slider) {
157
+ this.maxCombo -= obj.nestedHitObjects.length;
158
+ // Similarly, we loop through the nested hit objects since `Slider.ticks` also loops
159
+ // through the nested hit objects, which is inefficient for progressive calculation.
160
+ for (const nestedObj of obj.nestedHitObjects) {
161
+ if (nestedObj instanceof osuBase.SliderTick) {
162
+ --this.sliderTickCount;
163
+ }
164
+ else if (nestedObj instanceof osuBase.SliderRepeat) {
165
+ --this.sliderRepeatCount;
166
+ }
167
+ }
168
+ }
169
+ else {
170
+ --this.maxCombo;
171
+ }
172
+ }
173
+ return obj;
174
+ }
63
175
  }
64
176
 
65
177
  /**
@@ -151,11 +263,11 @@ class PerformanceCalculator {
151
263
  * @param options Options for performance calculation.
152
264
  */
153
265
  handleOptions(options) {
154
- var _a, _b;
266
+ var _a, _b, _c;
155
267
  if ((options === null || options === void 0 ? void 0 : options.accPercent) instanceof osuBase.Accuracy) {
156
268
  // Copy into new instance to not modify the original
157
269
  this.computedAccuracy = new osuBase.Accuracy(options.accPercent);
158
- if (this.computedAccuracy.n300 <= 0) {
270
+ if (!this.computedAccuracy.isN300Resolved) {
159
271
  this.computedAccuracy.n300 = Math.max(0, this.totalHits -
160
272
  this.computedAccuracy.n100 -
161
273
  this.computedAccuracy.n50 -
@@ -165,16 +277,15 @@ class PerformanceCalculator {
165
277
  this.computedAccuracy.nmiss = Math.max(0, this.totalHits - this.totalSuccessfulHits);
166
278
  }
167
279
  }
280
+ else if ((options === null || options === void 0 ? void 0 : options.accPercent) !== undefined) {
281
+ this.computedAccuracy = osuBase.Accuracy.fromPercent(options.accPercent, this.totalHits, (_a = options.miss) !== null && _a !== void 0 ? _a : 0);
282
+ }
168
283
  else {
169
- this.computedAccuracy = new osuBase.Accuracy({
170
- percent: options === null || options === void 0 ? void 0 : options.accPercent,
171
- nobjects: this.totalHits,
172
- nmiss: (_a = options === null || options === void 0 ? void 0 : options.miss) !== null && _a !== void 0 ? _a : 0,
173
- });
284
+ this.computedAccuracy = osuBase.Accuracy.fromHitCounts({ nmiss: (_b = options === null || options === void 0 ? void 0 : options.miss) !== null && _b !== void 0 ? _b : 0 }, this.totalHits);
174
285
  }
175
286
  const maxCombo = this.difficultyAttributes.maxCombo;
176
287
  const miss = this.computedAccuracy.nmiss;
177
- this.combo = (_b = options === null || options === void 0 ? void 0 : options.combo) !== null && _b !== void 0 ? _b : maxCombo - miss;
288
+ this.combo = (_c = options === null || options === void 0 ? void 0 : options.combo) !== null && _c !== void 0 ? _c : maxCombo - miss;
178
289
  if ((options === null || options === void 0 ? void 0 : options.sliderEndsDropped) !== undefined &&
179
290
  options.sliderTicksMissed !== undefined) {
180
291
  this._usingClassicSliderAccuracy = false;
@@ -3389,15 +3500,16 @@ class DroidScoreUtils {
3389
3500
  /**
3390
3501
  * Calculates the maximum possible spinner bonus for a given beatmap.
3391
3502
  *
3392
- * @param beatmap The beatmap to calculate the maximum spinner bonus for.
3503
+ * @param beatmap The `Beatmap` to calculate the maximum spinner bonus for.
3504
+ * @param playableBeatmap The `DroidPlayableBeatmap` to calculate the maximum spinner bonus for.
3393
3505
  * @returns The maximum spinner bonus.
3394
3506
  */
3395
- static calculateMaximumSpinnerBonus(beatmap) {
3396
- const { hitObjects } = beatmap;
3507
+ static calculateMaximumSpinnerBonus(beatmap, playableBeatmap) {
3508
+ const { hitObjects } = playableBeatmap;
3397
3509
  if (hitObjects.spinners === 0) {
3398
3510
  return 0;
3399
3511
  }
3400
- const scoreMultiplier = osuBase.ModUtil.calculateScoreMultiplier(beatmap.mods.values(), osuBase.Modes.Droid);
3512
+ const scoreMultiplier = new osuBase.DroidScoreMultiplierCalculator(beatmap.difficulty).calculateFor(playableBeatmap.mods.values());
3401
3513
  let bonus = 0;
3402
3514
  // In osu!droid, there is no time-based limit to spinner RPM, since the limit is
3403
3515
  // π/2 rad/*frame* and not rad/second.
@@ -3406,7 +3518,7 @@ class DroidScoreUtils {
3406
3518
  // the actual maximum spinner bonus.
3407
3519
  const maximumRotationsPerSecond = (Math.PI / 2) * 120;
3408
3520
  // Taken from https://github.com/osudroid/osu-droid/blob/45ae4d66ce275382c5de037245ceca8704b3ae75/src/ru/nsu/ccfit/zuev/osu/game/GameScene.java#L1640.
3409
- const minimumRotationsPerSecond = 2 + (2 * beatmap.difficulty.od) / 10;
3521
+ const minimumRotationsPerSecond = 2 + (2 * playableBeatmap.difficulty.od) / 10;
3410
3522
  for (const obj of hitObjects.objects) {
3411
3523
  if (!(obj instanceof osuBase.Spinner)) {
3412
3524
  continue;
@@ -3449,8 +3561,9 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
3449
3561
  attributes.overallDifficulty = playableBeatmap.difficulty.od;
3450
3562
  // Cap at 32-bit signed integer since that's the maximum score that can be submitted
3451
3563
  // to the game's leaderboards.
3452
- attributes.maximumScore = Math.min(beatmap.maxDroidScore(playableBeatmap.mods) +
3453
- DroidScoreUtils.calculateMaximumSpinnerBonus(playableBeatmap), 2147483647);
3564
+ attributes.maximumScore =
3565
+ beatmap.maxDroidScore(playableBeatmap.mods) +
3566
+ DroidScoreUtils.calculateMaximumSpinnerBonus(beatmap, playableBeatmap);
3454
3567
  this.populateAimAttributes(attributes, skills, objects);
3455
3568
  this.populateTapAttributes(attributes, skills, objects);
3456
3569
  this.populateRhythmAttributes(attributes, skills);
@@ -5025,7 +5138,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
5025
5138
  1.3 +
5026
5139
  this.totalHits *
5027
5140
  (0.0016 / (1 + 2 * this._effectiveMissCount)) *
5028
- Math.pow(this.computedAccuracy.value(), 16) *
5141
+ Math.pow(this.computedAccuracy.value, 16) *
5029
5142
  (1 -
5030
5143
  0.003 *
5031
5144
  Math.pow(this.difficultyAttributes.drainRate, 2));
@@ -5036,7 +5149,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
5036
5149
  this.calculateTraceableBonus(this.difficultyAttributes.sliderFactor);
5037
5150
  }
5038
5151
  // Scale the aim value with accuracy.
5039
- aimValue *= this.computedAccuracy.value();
5152
+ aimValue *= this.computedAccuracy.value;
5040
5153
  return aimValue;
5041
5154
  }
5042
5155
  /**
@@ -5085,7 +5198,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
5085
5198
  // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
5086
5199
  let accuracyValue = Math.pow(1.52163, this.overallDifficulty) *
5087
5200
  // It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points.
5088
- Math.pow(realAccuracy.n300 < 0 ? 0 : realAccuracy.value(), 24) *
5201
+ Math.pow(realAccuracy.n300 < 0 ? 0 : realAccuracy.value, 24) *
5089
5202
  2.83;
5090
5203
  // Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
5091
5204
  accuracyValue *= Math.pow(ncircles / 1000, ncircles < 1000 ? 0.3 : 0.1);
@@ -5119,7 +5232,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
5119
5232
  Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
5120
5233
  }
5121
5234
  // Scale the flashlight value with accuracy slightly.
5122
- flashlightValue *= 0.5 + this.computedAccuracy.value() / 2;
5235
+ flashlightValue *= 0.5 + this.computedAccuracy.value / 2;
5123
5236
  return flashlightValue;
5124
5237
  }
5125
5238
  /**
@@ -5132,7 +5245,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
5132
5245
  readingValue *= this.calculateMissPenalty(this.effectiveMissCount + aimEstimatedSliderBreaks, this.difficultyAttributes.readingDifficultNoteCount);
5133
5246
  }
5134
5247
  // Scale the reading value with accuracy _harshly_.
5135
- readingValue *= Math.pow(this.computedAccuracy.value(), 3);
5248
+ readingValue *= Math.pow(this.computedAccuracy.value, 3);
5136
5249
  return readingValue;
5137
5250
  }
5138
5251
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rian8337/osu-difficulty-calculator",
3
- "version": "4.0.0-beta.95",
3
+ "version": "4.0.0-beta.97",
4
4
  "description": "A module for calculating osu!standard beatmap difficulty and performance value with respect to the current difficulty and performance algorithm.",
5
5
  "keywords": [
6
6
  "osu",
@@ -33,10 +33,10 @@
33
33
  "url": "https://github.com/Rian8337/osu-droid-module/issues"
34
34
  },
35
35
  "dependencies": {
36
- "@rian8337/osu-base": "4.0.0-beta.95"
36
+ "@rian8337/osu-base": "4.0.0-beta.97"
37
37
  },
38
38
  "publishConfig": {
39
39
  "access": "public"
40
40
  },
41
- "gitHead": "b07790be9c9dd737170d2d576bf02298ec607ab9"
41
+ "gitHead": "0d77ee41c1b0eca8e46c118e942417483c4a9452"
42
42
  }
@@ -445,6 +445,34 @@ declare abstract class Skill {
445
445
  protected abstract processInternal(current: DifficultyHitObject): number;
446
446
  }
447
447
 
448
+ /**
449
+ * Wraps a `DifficultyAttributes` object and adds a time value for which the attribute is valid.
450
+ *
451
+ * Output by `DifficultyCalculator.calculateTimed` methods.
452
+ */
453
+ interface TimedDifficultyAttributes<TAttributes extends DifficultyAttributes> {
454
+ /**
455
+ * The non-clock-adjusted time value at which the attributes take effect.
456
+ */
457
+ readonly time: number;
458
+ /**
459
+ * The attributes.
460
+ */
461
+ readonly attributes: TAttributes;
462
+ /**
463
+ * The number of sliders in the beatmap up to this point.
464
+ */
465
+ readonly sliderCount: number;
466
+ /**
467
+ * The number of slider ticks in the beatmap up to this point.
468
+ */
469
+ readonly sliderTickCount: number;
470
+ /**
471
+ * The number of slider repeats in the beatmap up to this point.
472
+ */
473
+ readonly sliderRepeatCount: number;
474
+ }
475
+
448
476
  /**
449
477
  * The base of a difficulty calculator.
450
478
  */
@@ -483,6 +511,15 @@ declare abstract class DifficultyCalculator<TBeatmap extends PlayableBeatmap, TH
483
511
  * @returns The strain peaks of the `Beatmap`.
484
512
  */
485
513
  calculateStrainPeaks(beatmap: Beatmap, mods?: ModMap): StrainPeaks;
514
+ /**
515
+ * Calculates the difficulty of a `Beatmap` with specific `Mod`s and returns a set of
516
+ * `TimedDifficultyAttributes` representing the difficulty at every relevant time value in the `Beatmap`.
517
+ *
518
+ * @param beatmap The `Beatmap` whose difficulty is to be calculated.
519
+ * @param mods The `Mod`s to apply to the `Beatmap`.
520
+ * @return The set of `TimedDifficultyAttributes`.
521
+ */
522
+ calculateTimed(beatmap: Beatmap, mods?: ModMap): TimedDifficultyAttributes<TAttributes>[];
486
523
  /**
487
524
  * Creates the `Skill`s to calculate the difficulty of a `PlayableBeatmap`.
488
525
  *
@@ -2053,4 +2090,4 @@ declare class OsuSpeed extends HarmonicSkill {
2053
2090
  }
2054
2091
 
2055
2092
  export { DifficultyAttributes, DifficultyCalculator, DifficultyHitObject, DroidAgilityEvaluator, DroidAim, DroidDifficultyAttributes, DroidDifficultyCalculator, DroidDifficultyHitObject, DroidFlashlight, DroidFlashlightEvaluator, DroidFlowAimEvaluator, DroidPerformanceCalculator, DroidReading, DroidReadingEvaluator, DroidRhythm, DroidRhythmEvaluator, DroidSnapAimEvaluator, DroidTap, DroidTapEvaluator, ExtendedDroidDifficultyAttributes, OsuAgilityEvaluator, OsuAim, OsuDifficultyAttributes, OsuDifficultyCalculator, OsuDifficultyHitObject, OsuFlashlight, OsuFlashlightEvaluator, OsuFlowAimEvaluator, OsuPerformanceCalculator, OsuReading, OsuReadingEvaluator, OsuRhythmEvaluator, OsuSnapAimEvaluator, OsuSpeed, OsuSpeedEvaluator, PerformanceCalculator };
2056
- export type { CacheableDifficultyAttributes, DifficultSlider, HighStrainSection, IDifficultyAttributes, IDroidDifficultyAttributes, IExtendedDroidDifficultyAttributes, IOsuDifficultyAttributes, PerformanceCalculationOptions, StrainPeaks, TimedStrainPeak };
2093
+ export type { CacheableDifficultyAttributes, DifficultSlider, HighStrainSection, IDifficultyAttributes, IDroidDifficultyAttributes, IExtendedDroidDifficultyAttributes, IOsuDifficultyAttributes, PerformanceCalculationOptions, StrainPeaks, TimedDifficultyAttributes, TimedStrainPeak };