@rian8337/osu-difficulty-calculator 4.0.0-beta.23 → 4.0.0-beta.25

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
@@ -44,7 +44,6 @@ class DifficultyCalculator {
44
44
  * @param beatmap The beatmap to calculate. This beatmap will be deep-cloned to prevent reference changes.
45
45
  */
46
46
  constructor(beatmap) {
47
- var _a;
48
47
  /**
49
48
  * The difficulty objects of the beatmap.
50
49
  */
@@ -65,7 +64,7 @@ class DifficultyCalculator {
65
64
  this.beatmap = beatmap;
66
65
  this.difficultyStatistics = {
67
66
  circleSize: beatmap.difficulty.cs,
68
- approachRate: (_a = beatmap.difficulty.ar) !== null && _a !== void 0 ? _a : beatmap.difficulty.od,
67
+ approachRate: beatmap.difficulty.ar,
69
68
  overallDifficulty: beatmap.difficulty.od,
70
69
  healthDrain: beatmap.difficulty.hp,
71
70
  overallSpeedMultiplier: 1,
@@ -89,14 +88,14 @@ class DifficultyCalculator {
89
88
  calculate(options) {
90
89
  var _a;
91
90
  this.mods = (_a = options === null || options === void 0 ? void 0 : options.mods) !== null && _a !== void 0 ? _a : [];
92
- const converted = new osuBase.BeatmapConverter(this.beatmap).convert({
91
+ const playableBeatmap = this.beatmap.createPlayableBeatmap({
93
92
  mode: this.mode,
94
93
  mods: this.mods,
95
94
  customSpeedMultiplier: options === null || options === void 0 ? void 0 : options.customSpeedMultiplier,
96
95
  });
97
96
  this.difficultyStatistics = Object.seal(this.computeDifficultyStatistics(options));
98
97
  this.populateDifficultyAttributes();
99
- this.objects.push(...this.generateDifficultyHitObjects(converted));
98
+ this.objects.push(...this.generateDifficultyHitObjects(playableBeatmap));
100
99
  this.calculateAll();
101
100
  return this;
102
101
  }
@@ -159,8 +158,9 @@ class DifficultyHitObject {
159
158
  * @param lastLastObject The hitobject before the last hitobject.
160
159
  * @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
161
160
  * @param clockRate The clock rate of the beatmap.
161
+ * @param greatWindow The great window of the hitobject.
162
162
  */
163
- constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate) {
163
+ constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, greatWindow) {
164
164
  /**
165
165
  * The aim strain generated by the hitobject if sliders are considered.
166
166
  */
@@ -213,18 +213,18 @@ class DifficultyHitObject {
213
213
  this.normalizedRadius = 50;
214
214
  this.maximumSliderRadius = this.normalizedRadius * 2.4;
215
215
  this.assumedSliderRadius = this.normalizedRadius * 1.8;
216
- this.minDeltaTime = 25;
217
216
  this.object = object;
218
217
  this.lastObject = lastObject;
219
218
  this.lastLastObject = lastLastObject;
220
219
  this.hitObjects = difficultyHitObjects;
220
+ this.fullGreatWindow = greatWindow * 2;
221
221
  this.index = difficultyHitObjects.length - 1;
222
222
  // Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
223
223
  this.startTime = object.startTime / clockRate;
224
224
  this.endTime = object.endTime / clockRate;
225
225
  if (lastObject) {
226
226
  this.deltaTime = this.startTime - lastObject.startTime / clockRate;
227
- this.strainTime = Math.max(this.deltaTime, this.minDeltaTime);
227
+ this.strainTime = Math.max(this.deltaTime, DifficultyHitObject.minDeltaTime);
228
228
  }
229
229
  else {
230
230
  this.deltaTime = 0;
@@ -295,6 +295,24 @@ class DifficultyHitObject {
295
295
  }
296
296
  return osuBase.MathUtils.clamp((time - fadeInStartTime) / fadeInDuration, 0, 1);
297
297
  }
298
+ /**
299
+ * How possible is it to doubletap this object together with the next one and get perfect
300
+ * judgement in range from 0 to 1.
301
+ *
302
+ * A value closer to 1 indicates a higher possibility.
303
+ */
304
+ get doubletapness() {
305
+ const next = this.next(0);
306
+ if (!next) {
307
+ return 0;
308
+ }
309
+ const currentDeltaTime = Math.max(1, this.deltaTime);
310
+ const nextDeltaTime = Math.max(1, next.deltaTime);
311
+ const deltaDifference = Math.abs(nextDeltaTime - currentDeltaTime);
312
+ const speedRatio = currentDeltaTime / Math.max(currentDeltaTime, deltaDifference);
313
+ const windowRatio = Math.pow(Math.min(1, currentDeltaTime / this.fullGreatWindow), 2);
314
+ return 1 - Math.pow(speedRatio, 1 - windowRatio);
315
+ }
298
316
  setDistances(clockRate) {
299
317
  if (this.object instanceof osuBase.Slider) {
300
318
  this.calculateSliderCursorPosition(this.object);
@@ -306,7 +324,7 @@ class DifficultyHitObject {
306
324
  else {
307
325
  this.travelDistance *= Math.pow(1 + this.object.repeatCount / 2.5, 1 / 2.5);
308
326
  }
309
- this.travelTime = Math.max(this.object.lazyTravelTime / clockRate, this.minDeltaTime);
327
+ this.travelTime = Math.max(this.object.lazyTravelTime / clockRate, DifficultyHitObject.minDeltaTime);
310
328
  }
311
329
  // We don't need to calculate either angle or distance when one of the last->curr objects is a spinner.
312
330
  if (!this.lastObject ||
@@ -324,8 +342,8 @@ class DifficultyHitObject {
324
342
  this.minimumJumpTime = this.strainTime;
325
343
  this.minimumJumpDistance = this.lazyJumpDistance;
326
344
  if (this.lastObject instanceof osuBase.Slider) {
327
- const lastTravelTime = Math.max(this.lastObject.lazyTravelTime / clockRate, this.minDeltaTime);
328
- this.minimumJumpTime = Math.max(this.strainTime - lastTravelTime, this.minDeltaTime);
345
+ const lastTravelTime = Math.max(this.lastObject.lazyTravelTime / clockRate, DifficultyHitObject.minDeltaTime);
346
+ this.minimumJumpTime = Math.max(this.strainTime - lastTravelTime, DifficultyHitObject.minDeltaTime);
329
347
  // There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects.
330
348
  //
331
349
  // 1. The anti-flow pattern, where players cut the slider short in order to move to the next hitobject.
@@ -440,6 +458,10 @@ class DifficultyHitObject {
440
458
  return pos;
441
459
  }
442
460
  }
461
+ /**
462
+ * The lowest possible delta time value.
463
+ */
464
+ DifficultyHitObject.minDeltaTime = 25;
443
465
 
444
466
  /**
445
467
  * An evaluator for calculating osu!droid Aim skill.
@@ -692,6 +714,7 @@ class DroidSkill extends StrainSkill {
692
714
  constructor() {
693
715
  super(...arguments);
694
716
  this._objectStrains = [];
717
+ this.difficulty = 0;
695
718
  }
696
719
  /**
697
720
  * The strains of hitobjects.
@@ -705,14 +728,14 @@ class DroidSkill extends StrainSkill {
705
728
  * The result is scaled by clock rate as it affects the total number of strains.
706
729
  */
707
730
  countDifficultStrains() {
708
- if (this._objectStrains.length === 0) {
709
- return 0;
710
- }
711
- const maxStrain = Math.max(...this._objectStrains);
712
- if (maxStrain === 0) {
731
+ if (this.difficulty === 0) {
713
732
  return 0;
714
733
  }
715
- return this._objectStrains.reduce((total, next) => total + Math.pow(next / maxStrain, 4), 0);
734
+ // This is what the top strain is if all strain values were identical.
735
+ const consistentTopStrain = this.difficulty / 10;
736
+ // Use a weighted sum of all strains.
737
+ return this._objectStrains.reduce((total, next) => total +
738
+ 1.1 / (1 + Math.exp(-10 * (next / consistentTopStrain - 0.88))), 0);
716
739
  }
717
740
  process(current) {
718
741
  super.process(current);
@@ -730,12 +753,12 @@ class DroidSkill extends StrainSkill {
730
753
  }
731
754
  // Math here preserves the property that two notes of equal difficulty x, we have their summed difficulty = x * starsPerDouble.
732
755
  // This also applies to two sets of notes with equal difficulty.
733
- return Math.pow(strains.reduce((a, v) => {
734
- if (v <= 0) {
735
- return a;
736
- }
737
- return a + Math.pow(v, 1 / Math.log2(this.starsPerDouble));
738
- }, 0), Math.log2(this.starsPerDouble));
756
+ this.difficulty = 0;
757
+ for (const strain of strains) {
758
+ this.difficulty += Math.pow(strain, 1 / Math.log2(this.starsPerDouble));
759
+ }
760
+ this.difficulty = Math.pow(this.difficulty, Math.log2(this.starsPerDouble));
761
+ return this.difficulty;
739
762
  }
740
763
  calculateCurrentSectionStart(current) {
741
764
  return current.startTime;
@@ -811,27 +834,16 @@ class DroidTapEvaluator extends SpeedEvaluator {
811
834
  * @param considerCheesability Whether to consider cheesability.
812
835
  * @param strainTimeCap The strain time to cap the object's strain time to.
813
836
  */
814
- static evaluateDifficultyOf(current, greatWindow, considerCheesability, strainTimeCap) {
837
+ static evaluateDifficultyOf(current, considerCheesability, strainTimeCap) {
815
838
  if (current.object instanceof osuBase.Spinner ||
816
839
  // Exclude overlapping objects that can be tapped at once.
817
840
  current.isOverlapping(false)) {
818
841
  return 0;
819
842
  }
820
- let doubletapness = 1;
821
- if (considerCheesability) {
822
- // Nerf doubletappable doubles.
823
- const next = current.next(0);
824
- if (next) {
825
- const greatWindowFull = greatWindow * 2;
826
- const currentDeltaTime = Math.max(1, current.deltaTime);
827
- const nextDeltaTime = Math.max(1, next.deltaTime);
828
- const deltaDifference = Math.abs(nextDeltaTime - currentDeltaTime);
829
- const speedRatio = currentDeltaTime /
830
- Math.max(currentDeltaTime, deltaDifference);
831
- const windowRatio = Math.pow(Math.min(1, currentDeltaTime / greatWindowFull), 2);
832
- doubletapness = Math.pow(speedRatio, 1 - windowRatio);
833
- }
834
- }
843
+ // Nerf doubletappable doubles.
844
+ const doubletapness = considerCheesability
845
+ ? 1 - current.doubletapness
846
+ : 1;
835
847
  const strainTime = strainTimeCap !== undefined
836
848
  ? // We cap the strain time to 50 here as the chance of vibro is higher in any BPM higher than 300.
837
849
  Math.max(50, strainTimeCap, current.strainTime)
@@ -856,7 +868,7 @@ class DroidTap extends DroidSkill {
856
868
  get objectDeltaTimes() {
857
869
  return this._objectDeltaTimes;
858
870
  }
859
- constructor(mods, overallDifficulty, considerCheesability, strainTimeCap) {
871
+ constructor(mods, considerCheesability, strainTimeCap) {
860
872
  super(mods);
861
873
  this.reducedSectionCount = 10;
862
874
  this.reducedSectionBaseline = 0.75;
@@ -866,7 +878,6 @@ class DroidTap extends DroidSkill {
866
878
  this.currentRhythmMultiplier = 0;
867
879
  this.skillMultiplier = 1375;
868
880
  this._objectDeltaTimes = [];
869
- this.greatWindow = new osuBase.OsuHitWindow(overallDifficulty).hitWindowFor300();
870
881
  this.considerCheesability = considerCheesability;
871
882
  this.strainTimeCap = strainTimeCap;
872
883
  }
@@ -905,7 +916,7 @@ class DroidTap extends DroidSkill {
905
916
  strainValueAt(current) {
906
917
  this.currentTapStrain *= this.strainDecay(current.strainTime);
907
918
  this.currentTapStrain +=
908
- DroidTapEvaluator.evaluateDifficultyOf(current, this.greatWindow, this.considerCheesability, this.strainTimeCap) * this.skillMultiplier;
919
+ DroidTapEvaluator.evaluateDifficultyOf(current, this.considerCheesability, this.strainTimeCap) * this.skillMultiplier;
909
920
  this.currentRhythmMultiplier = current.rhythmMultiplier;
910
921
  this._objectDeltaTimes.push(current.deltaTime);
911
922
  return this.currentTapStrain * current.rhythmMultiplier;
@@ -1089,6 +1100,46 @@ class RhythmEvaluator {
1089
1100
  RhythmEvaluator.rhythmMultiplier = 0.75;
1090
1101
  RhythmEvaluator.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
1091
1102
 
1103
+ class Island {
1104
+ constructor(firstDelta, epsilon) {
1105
+ this.deltas = [];
1106
+ if (epsilon === undefined) {
1107
+ this.deltaDifferenceEpsilon = firstDelta;
1108
+ return;
1109
+ }
1110
+ this.deltaDifferenceEpsilon = epsilon;
1111
+ this.addDelta(firstDelta);
1112
+ }
1113
+ addDelta(delta) {
1114
+ // Convert to integer
1115
+ delta = Math.trunc(delta);
1116
+ const existingDelta = this.deltas.find((v) => Math.abs(v - delta) >= this.deltaDifferenceEpsilon);
1117
+ this.deltas.push(existingDelta !== null && existingDelta !== void 0 ? existingDelta : delta);
1118
+ }
1119
+ get averageDelta() {
1120
+ return this.deltas.length > 0
1121
+ ? Math.max(this.deltas.reduce((a, b) => a + b) / this.deltas.length, DifficultyHitObject.minDeltaTime)
1122
+ : 0;
1123
+ }
1124
+ isSimilarPolarity(other) {
1125
+ // Consider islands to be of similar polarity only if they're having the same
1126
+ // average delta (we don't want to consider 3 singletaps similar to a triple)
1127
+ return (Math.abs(this.averageDelta - other.averageDelta) <
1128
+ this.deltaDifferenceEpsilon &&
1129
+ this.deltas.length % 2 === other.deltas.length % 2);
1130
+ }
1131
+ equals(other) {
1132
+ if (this.deltas.length !== other.deltas.length) {
1133
+ return false;
1134
+ }
1135
+ for (let i = 0; i < this.deltas.length; ++i) {
1136
+ if (this.deltas[i] !== other.deltas[i]) {
1137
+ return false;
1138
+ }
1139
+ }
1140
+ return true;
1141
+ }
1142
+ }
1092
1143
  /**
1093
1144
  * An evaluator for calculating osu!droid Rhythm skill.
1094
1145
  */
@@ -1098,22 +1149,23 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
1098
1149
  * with historic data of the current object.
1099
1150
  *
1100
1151
  * @param current The current object.
1101
- * @param greatWindow The great hit window of the current object.
1102
1152
  */
1103
- static evaluateDifficultyOf(current, greatWindow) {
1153
+ static evaluateDifficultyOf(current) {
1104
1154
  if (current.object instanceof osuBase.Spinner ||
1105
1155
  // Exclude overlapping objects that can be tapped at once.
1106
1156
  current.isOverlapping(false)) {
1107
1157
  return 1;
1108
1158
  }
1109
- let previousIslandSize = 0;
1159
+ const deltaDifferenceEpsilon = current.fullGreatWindow * 0.3;
1110
1160
  let rhythmComplexitySum = 0;
1111
- let islandSize = 1;
1161
+ let island = new Island(deltaDifferenceEpsilon);
1162
+ let previousIsland = new Island(deltaDifferenceEpsilon);
1163
+ const islandCounts = new Map();
1112
1164
  // Store the ratio of the current start of an island to buff for tighter rhythms.
1113
1165
  let startRatio = 0;
1114
1166
  let firstDeltaSwitch = false;
1115
1167
  let rhythmStart = 0;
1116
- const historicalNoteCount = Math.min(current.index, 32);
1168
+ const historicalNoteCount = Math.min(current.index, this.historyObjectsMax);
1117
1169
  // Exclude overlapping objects that can be tapped at once.
1118
1170
  const validPrevious = [];
1119
1171
  for (let i = 0; i < historicalNoteCount; ++i) {
@@ -1137,105 +1189,125 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
1137
1189
  this.historyTimeMax;
1138
1190
  // Either we're limited by time or limited by object count.
1139
1191
  currentHistoricalDecay = Math.min(currentHistoricalDecay, (validPrevious.length - i) / validPrevious.length);
1140
- const currentDelta = validPrevious[i - 1].strainTime;
1141
- const prevDelta = validPrevious[i].strainTime;
1142
- const lastDelta = validPrevious[i + 1].strainTime;
1192
+ const currentObject = validPrevious[i - 1];
1193
+ const prevObject = validPrevious[i];
1194
+ const lastObject = validPrevious[i + 1];
1195
+ const currentDelta = currentObject.strainTime;
1196
+ const prevDelta = prevObject.strainTime;
1197
+ const lastDelta = lastObject.strainTime;
1143
1198
  const currentRatio = 1 +
1144
- 6 *
1199
+ 10 *
1145
1200
  Math.min(0.5, Math.pow(Math.sin(Math.PI /
1146
1201
  (Math.min(prevDelta, currentDelta) /
1147
1202
  Math.max(prevDelta, currentDelta))), 2));
1148
- const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) - greatWindow * 0.6) /
1149
- (greatWindow * 0.6));
1203
+ const windowPenalty = osuBase.MathUtils.clamp((Math.abs(prevDelta - currentDelta) - deltaDifferenceEpsilon) /
1204
+ deltaDifferenceEpsilon, 0, 1);
1150
1205
  let effectiveRatio = windowPenalty * currentRatio;
1151
1206
  if (firstDeltaSwitch) {
1152
- if (prevDelta <= 1.25 * currentDelta &&
1153
- prevDelta * 1.25 >= currentDelta) {
1154
- // Island is still progressing, count size.
1155
- if (islandSize < 7) {
1156
- ++islandSize;
1207
+ if (Math.abs(prevDelta - currentDelta) <= deltaDifferenceEpsilon) {
1208
+ if (island.deltas.length < this.maxIslandSize) {
1209
+ // Island is still progressing.
1210
+ island.addDelta(currentDelta);
1157
1211
  }
1158
1212
  }
1159
1213
  else {
1160
- if (validPrevious[i - 1].object instanceof osuBase.Slider) {
1161
- // BPM change is into slider, this is easy acc window.
1214
+ // BPM change is into slider, this is easy acc window.
1215
+ if (currentObject.object instanceof osuBase.Slider) {
1162
1216
  effectiveRatio /= 8;
1163
1217
  }
1164
- if (validPrevious[i].object instanceof osuBase.Slider) {
1165
- // BPM change was from a slider, this is typically easier than circle -> circle.
1218
+ // BPM change was from a slider, this is typically easier than circle -> circle.
1219
+ // Unintentional side effect is that bursts with kicksliders at the ends might
1220
+ // have lower difficulty than bursts without sliders.
1221
+ if (prevObject.object instanceof osuBase.Slider) {
1166
1222
  effectiveRatio /= 4;
1167
1223
  }
1168
- if (previousIslandSize === islandSize) {
1169
- // Repeated island size (ex: triplet -> triplet).
1170
- effectiveRatio /= 4;
1224
+ // Repeated island polarity (2 -> 4, 3 -> 5).
1225
+ if (island.isSimilarPolarity(previousIsland)) {
1226
+ effectiveRatio *= 0.3;
1171
1227
  }
1172
- if (previousIslandSize % 2 === islandSize % 2) {
1173
- // Repeated island polarity (2 -> 4, 3 -> 5).
1228
+ // Previous increase happened a note ago.
1229
+ // Albeit this is a 1/1 -> 1/2-1/4 type of transition, we don't want to buff this.
1230
+ if (lastDelta > prevDelta + deltaDifferenceEpsilon &&
1231
+ prevDelta > currentDelta + deltaDifferenceEpsilon) {
1232
+ effectiveRatio /= 8;
1233
+ }
1234
+ // Singletaps are easier to control.
1235
+ if (island.deltas.length === 1) {
1174
1236
  effectiveRatio /= 2;
1175
1237
  }
1176
- if (lastDelta > prevDelta + 10 &&
1177
- prevDelta > currentDelta + 10) {
1178
- // Previous increase happened a note ago.
1179
- // Albeit this is a 1/1 -> 1/2-1/4 type of transition, we don't want to buff this.
1180
- effectiveRatio /= 8;
1238
+ let islandFound = false;
1239
+ for (const [currentIsland, count] of islandCounts) {
1240
+ if (!island.equals(currentIsland)) {
1241
+ continue;
1242
+ }
1243
+ islandFound = true;
1244
+ let islandCount = count;
1245
+ if (previousIsland.equals(island)) {
1246
+ // Only add island to island counts if they're going one after another.
1247
+ ++islandCount;
1248
+ islandCounts.set(currentIsland, islandCount);
1249
+ }
1250
+ // Repeated island (ex: triplet -> triplet).
1251
+ // Graph: https://www.desmos.com/calculator/pj7an56zwf
1252
+ effectiveRatio *= Math.min(1 / islandCount, Math.pow(1 / islandCount, 4 /
1253
+ (1 +
1254
+ Math.exp(10 - 0.165 * island.averageDelta))));
1255
+ break;
1181
1256
  }
1257
+ if (!islandFound) {
1258
+ islandCounts.set(island, 1);
1259
+ }
1260
+ // Scale down the difficulty if the object is doubletappable.
1261
+ effectiveRatio *= 1 - prevObject.doubletapness * 0.75;
1182
1262
  rhythmComplexitySum +=
1183
- (((Math.sqrt(effectiveRatio * startRatio) *
1184
- currentHistoricalDecay *
1185
- Math.sqrt(4 + islandSize)) /
1186
- 2) *
1187
- Math.sqrt(4 + previousIslandSize)) /
1188
- 2;
1263
+ Math.sqrt(effectiveRatio * startRatio) *
1264
+ currentHistoricalDecay;
1189
1265
  startRatio = effectiveRatio;
1190
- previousIslandSize = islandSize;
1191
- if (prevDelta * 1.25 < currentDelta) {
1266
+ previousIsland = island;
1267
+ if (prevDelta + deltaDifferenceEpsilon < currentDelta) {
1192
1268
  // We're slowing down, stop counting.
1193
1269
  // If we're speeding up, this stays as is and we keep counting island size.
1194
1270
  firstDeltaSwitch = false;
1195
1271
  }
1196
- islandSize = 1;
1272
+ island = new Island(Math.trunc(currentDelta), deltaDifferenceEpsilon);
1197
1273
  }
1198
1274
  }
1199
- else if (prevDelta > 1.25 * currentDelta) {
1200
- // We want to be speeding up.
1275
+ else if (prevDelta > deltaDifferenceEpsilon + currentDelta) {
1276
+ // We're speeding up.
1201
1277
  // Begin counting island until we change speed again.
1202
1278
  firstDeltaSwitch = true;
1279
+ // Reduce ratio if we're starting after a slider.
1280
+ if (prevObject.object instanceof osuBase.Slider) {
1281
+ effectiveRatio *= 0.3;
1282
+ }
1203
1283
  startRatio = effectiveRatio;
1204
- islandSize = 1;
1284
+ island = new Island(Math.trunc(currentDelta), deltaDifferenceEpsilon);
1205
1285
  }
1206
1286
  }
1207
- // Nerf doubles that can be tapped at the same time to get Great hit results.
1208
- const next = current.next(0);
1209
- let doubletapness = 1;
1210
- if (next) {
1211
- const currentDeltaTime = Math.max(1, current.deltaTime);
1212
- const nextDeltaTime = Math.max(1, next.deltaTime);
1213
- const deltaDifference = Math.abs(nextDeltaTime - currentDeltaTime);
1214
- const speedRatio = currentDeltaTime / Math.max(currentDeltaTime, deltaDifference);
1215
- const windowRatio = Math.pow(Math.min(1, currentDeltaTime / (greatWindow * 2)), 2);
1216
- doubletapness = Math.pow(speedRatio, 1 - windowRatio);
1217
- }
1218
- return (Math.sqrt(4 + rhythmComplexitySum * this.rhythmMultiplier * doubletapness) / 2);
1287
+ return Math.sqrt(4 + rhythmComplexitySum * this.rhythmMultiplier) / 2;
1219
1288
  }
1220
1289
  }
1290
+ DroidRhythmEvaluator.rhythmMultiplier = 1.2;
1291
+ DroidRhythmEvaluator.historyTimeMax = 4000;
1292
+ DroidRhythmEvaluator.maxIslandSize = 7;
1293
+ DroidRhythmEvaluator.historyObjectsMax = 24;
1221
1294
 
1222
1295
  /**
1223
1296
  * Represents the skill required to properly follow a beatmap's rhythm.
1224
1297
  */
1225
1298
  class DroidRhythm extends DroidSkill {
1226
- constructor(mods, overallDifficulty) {
1227
- super(mods);
1299
+ constructor() {
1300
+ super(...arguments);
1228
1301
  this.reducedSectionCount = 5;
1229
1302
  this.reducedSectionBaseline = 0.75;
1230
1303
  this.strainDecayBase = 0.3;
1231
1304
  this.starsPerDouble = 1.75;
1232
1305
  this.currentRhythmStrain = 0;
1233
1306
  this.currentRhythmMultiplier = 1;
1234
- this.hitWindow = new osuBase.OsuHitWindow(overallDifficulty);
1235
1307
  }
1236
1308
  strainValueAt(current) {
1237
1309
  this.currentRhythmMultiplier =
1238
- DroidRhythmEvaluator.evaluateDifficultyOf(current, this.hitWindow.hitWindowFor300());
1310
+ DroidRhythmEvaluator.evaluateDifficultyOf(current);
1239
1311
  this.currentRhythmStrain *= this.strainDecay(current.deltaTime);
1240
1312
  this.currentRhythmStrain += this.currentRhythmMultiplier - 1;
1241
1313
  return this.currentRhythmStrain;
@@ -1318,11 +1390,12 @@ class DroidVisualEvaluator {
1318
1390
  // Invert the scaling factor to determine the true travel distance independent of circle size.
1319
1391
  const pixelTravelDistance = current.object.lazyTravelDistance / scalingFactor;
1320
1392
  const currentVelocity = pixelTravelDistance / current.travelTime;
1393
+ const spanTravelDistance = pixelTravelDistance / current.object.spanCount;
1321
1394
  strain +=
1322
1395
  // Reward sliders based on velocity, while also avoiding overbuffing extremely fast sliders.
1323
1396
  Math.min(6, currentVelocity * 1.5) *
1324
1397
  // Longer sliders require more reading.
1325
- (pixelTravelDistance / 100);
1398
+ (spanTravelDistance / 100);
1326
1399
  let cumulativeStrainTime = 0;
1327
1400
  // Reward for velocity changes based on last few sliders.
1328
1401
  for (let i = 0; i < Math.min(current.index, 4); ++i) {
@@ -1334,14 +1407,15 @@ class DroidVisualEvaluator {
1334
1407
  continue;
1335
1408
  }
1336
1409
  // Invert the scaling factor to determine the true travel distance independent of circle size.
1337
- const pixelTravelDistance = last.object.lazyTravelDistance / scalingFactor;
1338
- const lastVelocity = pixelTravelDistance / last.travelTime;
1410
+ const lastPixelTravelDistance = last.object.lazyTravelDistance / scalingFactor;
1411
+ const lastVelocity = lastPixelTravelDistance / last.travelTime;
1412
+ const lastSpanTravelDistance = lastPixelTravelDistance / last.object.spanCount;
1339
1413
  strain +=
1340
1414
  // Reward past sliders based on velocity changes, while also
1341
1415
  // avoiding overbuffing extremely fast velocity changes.
1342
1416
  Math.min(10, 2.5 * Math.abs(currentVelocity - lastVelocity)) *
1343
1417
  // Longer sliders require more reading.
1344
- (pixelTravelDistance / 125) *
1418
+ (lastSpanTravelDistance / 125) *
1345
1419
  // Avoid overbuffing past sliders.
1346
1420
  Math.min(1, 300 / cumulativeStrainTime);
1347
1421
  }
@@ -1417,10 +1491,11 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
1417
1491
  * @param lastLastObject The hitobject before the last hitobject.
1418
1492
  * @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
1419
1493
  * @param clockRate The clock rate of the beatmap.
1494
+ * @param greatWindow The great window of the hitobject.
1420
1495
  * @param isForceAR Whether force AR is enabled.
1421
1496
  */
1422
- constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, isForceAR) {
1423
- super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate);
1497
+ constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, greatWindow, isForceAR) {
1498
+ super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, greatWindow);
1424
1499
  /**
1425
1500
  * The tap strain generated by the hitobject.
1426
1501
  */
@@ -1559,7 +1634,9 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
1559
1634
  Math.max(0, 1 - distance / (2.5 * this.object.radius)) *
1560
1635
  (7.5 /
1561
1636
  (1 +
1562
- Math.exp(0.15 * (Math.max(deltaTime, this.minDeltaTime) - 75))));
1637
+ Math.exp(0.15 *
1638
+ (Math.max(deltaTime, DifficultyHitObject.minDeltaTime) -
1639
+ 75))));
1563
1640
  }
1564
1641
  }
1565
1642
 
@@ -1671,11 +1748,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1671
1748
  * Calculates the tap star rating of the beatmap and stores it in this instance.
1672
1749
  */
1673
1750
  calculateTap() {
1674
- const od = this.difficultyStatistics.overallDifficulty;
1675
- const tapSkillCheese = new DroidTap(this.mods, od, true);
1676
- const tapSkillNoCheese = new DroidTap(this.mods, od, false);
1751
+ const tapSkillCheese = new DroidTap(this.mods, true);
1752
+ const tapSkillNoCheese = new DroidTap(this.mods, false);
1677
1753
  this.calculateSkills(tapSkillCheese, tapSkillNoCheese);
1678
- const tapSkillVibro = new DroidTap(this.mods, od, true, tapSkillCheese.relevantDeltaTime());
1754
+ const tapSkillVibro = new DroidTap(this.mods, true, tapSkillCheese.relevantDeltaTime());
1679
1755
  this.calculateSkills(tapSkillVibro);
1680
1756
  this.postCalculateTap(tapSkillCheese, tapSkillVibro);
1681
1757
  }
@@ -1683,7 +1759,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1683
1759
  * Calculates the rhythm star rating of the beatmap and stores it in this instance.
1684
1760
  */
1685
1761
  calculateRhythm() {
1686
- const rhythmSkill = new DroidRhythm(this.mods, this.difficultyStatistics.overallDifficulty);
1762
+ const rhythmSkill = new DroidRhythm(this.mods);
1687
1763
  this.calculateSkills(rhythmSkill);
1688
1764
  this.postCalculateRhythm(rhythmSkill);
1689
1765
  }
@@ -1743,7 +1819,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1743
1819
  const flashlightSkillWithoutSliders = skills[6];
1744
1820
  const visualSkill = skills[7];
1745
1821
  const visualSkillWithoutSliders = skills[8];
1746
- const tapSkillVibro = new DroidTap(this.mods, this.difficultyStatistics.overallDifficulty, true, tapSkillCheese.relevantDeltaTime());
1822
+ const tapSkillVibro = new DroidTap(this.mods, true, tapSkillCheese.relevantDeltaTime());
1747
1823
  this.calculateSkills(tapSkillVibro);
1748
1824
  this.postCalculateAim(aimSkill, aimSkillWithoutSliders);
1749
1825
  this.postCalculateTap(tapSkillCheese, tapSkillVibro);
@@ -1771,19 +1847,19 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1771
1847
  const difficultyObjects = [];
1772
1848
  const { objects } = convertedBeatmap.hitObjects;
1773
1849
  const difficultyAdjustMod = this.mods.find((m) => m instanceof osuBase.ModDifficultyAdjust);
1850
+ const greatWindow = new osuBase.OsuHitWindow(this.difficultyStatistics.overallDifficulty).hitWindowFor300();
1774
1851
  for (let i = 0; i < objects.length; ++i) {
1775
- const difficultyObject = new DroidDifficultyHitObject(objects[i], (_a = objects[i - 1]) !== null && _a !== void 0 ? _a : null, (_b = objects[i - 2]) !== null && _b !== void 0 ? _b : null, difficultyObjects, this.difficultyStatistics.overallSpeedMultiplier, (difficultyAdjustMod === null || difficultyAdjustMod === void 0 ? void 0 : difficultyAdjustMod.ar) !== undefined);
1852
+ const difficultyObject = new DroidDifficultyHitObject(objects[i], (_a = objects[i - 1]) !== null && _a !== void 0 ? _a : null, (_b = objects[i - 2]) !== null && _b !== void 0 ? _b : null, difficultyObjects, this.difficultyStatistics.overallSpeedMultiplier, greatWindow, (difficultyAdjustMod === null || difficultyAdjustMod === void 0 ? void 0 : difficultyAdjustMod.ar) !== undefined);
1776
1853
  difficultyObject.computeProperties(this.difficultyStatistics.overallSpeedMultiplier, objects);
1777
1854
  difficultyObjects.push(difficultyObject);
1778
1855
  }
1779
1856
  return difficultyObjects;
1780
1857
  }
1781
1858
  computeDifficultyStatistics(options) {
1782
- var _a;
1783
1859
  const { difficulty } = this.beatmap;
1784
1860
  return osuBase.calculateDroidDifficultyStatistics({
1785
1861
  circleSize: difficulty.cs,
1786
- approachRate: (_a = difficulty.ar) !== null && _a !== void 0 ? _a : difficulty.od,
1862
+ approachRate: difficulty.ar,
1787
1863
  overallDifficulty: difficulty.od,
1788
1864
  healthDrain: difficulty.hp,
1789
1865
  mods: this.mods,
@@ -1792,16 +1868,15 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1792
1868
  });
1793
1869
  }
1794
1870
  createSkills() {
1795
- const od = this.difficultyStatistics.overallDifficulty;
1796
1871
  return [
1797
1872
  new DroidAim(this.mods, true),
1798
1873
  new DroidAim(this.mods, false),
1799
1874
  // Tap skill depends on rhythm skill, so we put it first
1800
- new DroidRhythm(this.mods, od),
1875
+ new DroidRhythm(this.mods),
1801
1876
  // Cheesability tap
1802
- new DroidTap(this.mods, od, true),
1877
+ new DroidTap(this.mods, true),
1803
1878
  // Non-cheesability tap
1804
- new DroidTap(this.mods, od, false),
1879
+ new DroidTap(this.mods, false),
1805
1880
  new DroidFlashlight(this.mods, true),
1806
1881
  new DroidFlashlight(this.mods, false),
1807
1882
  new DroidVisual(this.mods, true),
@@ -1817,7 +1892,9 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1817
1892
  postCalculateAim(aimSkill, aimSkillWithoutSliders) {
1818
1893
  this.strainPeaks.aimWithSliders = aimSkill.strainPeaks;
1819
1894
  this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
1820
- this.attributes.aimDifficulty = this.starValue(aimSkill.difficultyValue());
1895
+ this.attributes.aimDifficulty = this.mods.some((m) => m instanceof osuBase.ModAutopilot)
1896
+ ? 0
1897
+ : this.starValue(aimSkill.difficultyValue());
1821
1898
  if (this.aim) {
1822
1899
  this.attributes.sliderFactor =
1823
1900
  this.starValue(aimSkillWithoutSliders.difficultyValue()) /
@@ -1834,6 +1911,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1834
1911
  * Calculates aim-related attributes.
1835
1912
  */
1836
1913
  calculateAimAttributes() {
1914
+ this.attributes.difficultSliders = [];
1837
1915
  const topDifficultSliders = [];
1838
1916
  for (let i = 0; i < this.objects.length; ++i) {
1839
1917
  const object = this.objects[i];
@@ -1954,10 +2032,12 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1954
2032
  this.attributes.flashlightSliderFactor =
1955
2033
  this.starValue(flashlightSkillWithoutSliders.difficultyValue()) / this.flashlight;
1956
2034
  }
2035
+ if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
2036
+ this.attributes.flashlightDifficulty *= 0.3;
2037
+ }
1957
2038
  if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
1958
2039
  this.attributes.flashlightDifficulty *= 0.7;
1959
2040
  }
1960
- this.attributes.flashlightDifficulty = this.flashlight;
1961
2041
  this.attributes.flashlightDifficultStrainCount =
1962
2042
  flashlightSkill.countDifficultStrains();
1963
2043
  }
@@ -1976,6 +2056,9 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1976
2056
  this.starValue(visualSkillWithoutSliders.difficultyValue()) /
1977
2057
  this.visual;
1978
2058
  }
2059
+ if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
2060
+ this.attributes.visualDifficulty *= 0.8;
2061
+ }
1979
2062
  this.attributes.visualDifficultStrainCount =
1980
2063
  visualSkillWithSliders.countDifficultStrains();
1981
2064
  }
@@ -2357,8 +2440,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2357
2440
  aimValue *= this._aimSliderCheesePenalty;
2358
2441
  // Scale the aim value with deviation.
2359
2442
  aimValue *=
2360
- 1.05 *
2361
- Math.sqrt(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * this._deviation)));
2443
+ 1.025 *
2444
+ Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * this._deviation)), 0.475);
2362
2445
  // OD 7 SS stays the same.
2363
2446
  aimValue *= 0.98 + Math.pow(7, 2) / 2500;
2364
2447
  return aimValue;
@@ -2370,8 +2453,9 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2370
2453
  let tapValue = this.baseValue(this.difficultyAttributes.tapDifficulty);
2371
2454
  tapValue *= this.calculateStrainBasedMissPenalty(this.difficultyAttributes.tapDifficultStrainCount);
2372
2455
  // Scale the tap value with estimated full combo deviation.
2373
- // Require more objects to be present as object count can rack up easily in tap-oriented beatmaps.
2374
- tapValue *= this.calculateDeviationBasedLengthScaling(this.totalHits / 1.45);
2456
+ // Consider notes that are difficult to tap with respect to other notes, but
2457
+ // also cap the note count to prevent buffing filler patterns.
2458
+ tapValue *= this.calculateDeviationBasedLengthScaling(Math.min(this.difficultyAttributes.speedNoteCount, this.totalHits / 1.45));
2375
2459
  // Normalize the deviation to 300 BPM.
2376
2460
  const normalizedDeviation = this.tapDeviation *
2377
2461
  Math.max(1, 50 / this.difficultyAttributes.averageSpeedDeltaTime);
@@ -2386,8 +2470,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2386
2470
  ((2 * 300) / averageBPM))));
2387
2471
  // Scale the tap value with tap deviation.
2388
2472
  tapValue *=
2389
- 1.1 *
2390
- Math.pow(osuBase.ErrorFunction.erf(20 / (Math.SQRT2 * adjustedDeviation)), 0.625);
2473
+ 1.05 *
2474
+ Math.pow(osuBase.ErrorFunction.erf(20 / (Math.SQRT2 * adjustedDeviation)), 0.6);
2391
2475
  // Additional scaling for tap value based on average BPM and how "vibroable" the beatmap is.
2392
2476
  // Higher BPMs require more precise tapping. When the deviation is too high,
2393
2477
  // it can be assumed that the player taps invariant to rhythm.
@@ -2412,7 +2496,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2412
2496
  this.totalSuccessfulHits === 0) {
2413
2497
  return 0;
2414
2498
  }
2415
- let accuracyValue = 800 * Math.exp(-0.1 * this._deviation);
2499
+ let accuracyValue = 650 * Math.exp(-0.1 * this._deviation);
2416
2500
  const ncircles = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModScoreV2)
2417
2501
  ? this.totalHits - this.difficultyAttributes.spinnerCount
2418
2502
  : this.difficultyAttributes.hitCircleCount;
@@ -2465,8 +2549,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2465
2549
  visualValue *= this._visualSliderCheesePenalty;
2466
2550
  // Scale the visual value with deviation.
2467
2551
  visualValue *=
2468
- 1.065 *
2469
- Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * this._deviation)), 0.8);
2552
+ 1.05 *
2553
+ Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * this._deviation)), 0.775);
2470
2554
  // OD 5 SS stays the same.
2471
2555
  visualValue *= 0.98 + Math.pow(5, 2) / 2500;
2472
2556
  return visualValue;
@@ -2482,8 +2566,9 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2482
2566
  if (this.effectiveMissCount === 0) {
2483
2567
  return 1;
2484
2568
  }
2485
- return (0.94 /
2486
- (this.effectiveMissCount / (2 * Math.sqrt(difficultStrainCount)) +
2569
+ return (0.96 /
2570
+ (this.effectiveMissCount /
2571
+ (4 * Math.pow(Math.log(difficultStrainCount), 0.94)) +
2487
2572
  1));
2488
2573
  }
2489
2574
  /**
@@ -2910,30 +2995,19 @@ class OsuSpeedEvaluator extends SpeedEvaluator {
2910
2995
  * - and how easily they can be cheesed.
2911
2996
  *
2912
2997
  * @param current The current object.
2913
- * @param greatWindow The great hit window of the current object.
2914
2998
  */
2915
- static evaluateDifficultyOf(current, greatWindow) {
2999
+ static evaluateDifficultyOf(current) {
2916
3000
  var _a;
2917
3001
  if (current.object instanceof osuBase.Spinner) {
2918
3002
  return 0;
2919
3003
  }
2920
3004
  const prev = current.previous(0);
2921
3005
  let strainTime = current.strainTime;
2922
- const greatWindowFull = greatWindow * 2;
2923
3006
  // Nerf doubletappable doubles.
2924
- const next = current.next(0);
2925
- let doubletapness = 1;
2926
- if (next) {
2927
- const currentDeltaTime = Math.max(1, current.deltaTime);
2928
- const nextDeltaTime = Math.max(1, next.deltaTime);
2929
- const deltaDifference = Math.abs(nextDeltaTime - currentDeltaTime);
2930
- const speedRatio = currentDeltaTime / Math.max(currentDeltaTime, deltaDifference);
2931
- const windowRatio = Math.pow(Math.min(1, currentDeltaTime / greatWindowFull), 2);
2932
- doubletapness = Math.pow(speedRatio, 1 - windowRatio);
2933
- }
3007
+ const doubletapness = 1 - current.doubletapness;
2934
3008
  // Cap deltatime to the OD 300 hitwindow.
2935
3009
  // 0.93 is derived from making sure 260 BPM 1/4 OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
2936
- strainTime /= osuBase.MathUtils.clamp(strainTime / greatWindowFull / 0.93, 0.92, 1);
3010
+ strainTime /= osuBase.MathUtils.clamp(strainTime / current.fullGreatWindow / 0.93, 0.92, 1);
2937
3011
  let speedBonus = 1;
2938
3012
  if (strainTime < this.minSpeedBonus) {
2939
3013
  speedBonus +=
@@ -2964,7 +3038,7 @@ class OsuRhythmEvaluator extends RhythmEvaluator {
2964
3038
  * @param current The current object.
2965
3039
  * @param greatWindow The great hit window of the current object.
2966
3040
  */
2967
- static evaluateDifficultyOf(current, greatWindow) {
3041
+ static evaluateDifficultyOf(current) {
2968
3042
  if (current.object instanceof osuBase.Spinner) {
2969
3043
  return 0;
2970
3044
  }
@@ -2999,8 +3073,9 @@ class OsuRhythmEvaluator extends RhythmEvaluator {
2999
3073
  Math.min(0.5, Math.pow(Math.sin(Math.PI /
3000
3074
  (Math.min(prevDelta, currentDelta) /
3001
3075
  Math.max(prevDelta, currentDelta))), 2));
3002
- const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) - greatWindow * 0.6) /
3003
- (greatWindow * 0.6));
3076
+ const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) -
3077
+ current.fullGreatWindow * 0.3) /
3078
+ (current.fullGreatWindow * 0.3));
3004
3079
  let effectiveRatio = windowPenalty * currentRatio;
3005
3080
  if (firstDeltaSwitch) {
3006
3081
  if (prevDelta <= 1.25 * currentDelta &&
@@ -3066,8 +3141,8 @@ class OsuRhythmEvaluator extends RhythmEvaluator {
3066
3141
  * Represents the skill required to press keys or tap with regards to keeping up with the speed at which objects need to be hit.
3067
3142
  */
3068
3143
  class OsuSpeed extends OsuSkill {
3069
- constructor(mods, overallDifficulty) {
3070
- super(mods);
3144
+ constructor() {
3145
+ super(...arguments);
3071
3146
  this.strainDecayBase = 0.3;
3072
3147
  this.reducedSectionCount = 5;
3073
3148
  this.reducedSectionBaseline = 0.75;
@@ -3076,7 +3151,6 @@ class OsuSpeed extends OsuSkill {
3076
3151
  this.currentSpeedStrain = 0;
3077
3152
  this.currentRhythm = 0;
3078
3153
  this.skillMultiplier = 1375;
3079
- this.greatWindow = new osuBase.OsuHitWindow(overallDifficulty).hitWindowFor300();
3080
3154
  }
3081
3155
  /**
3082
3156
  * @param current The hitobject to calculate.
@@ -3084,9 +3158,9 @@ class OsuSpeed extends OsuSkill {
3084
3158
  strainValueAt(current) {
3085
3159
  this.currentSpeedStrain *= this.strainDecay(current.strainTime);
3086
3160
  this.currentSpeedStrain +=
3087
- OsuSpeedEvaluator.evaluateDifficultyOf(current, this.greatWindow) *
3161
+ OsuSpeedEvaluator.evaluateDifficultyOf(current) *
3088
3162
  this.skillMultiplier;
3089
- this.currentRhythm = OsuRhythmEvaluator.evaluateDifficultyOf(current, this.greatWindow);
3163
+ this.currentRhythm = OsuRhythmEvaluator.evaluateDifficultyOf(current);
3090
3164
  return this.currentSpeedStrain * this.currentRhythm;
3091
3165
  }
3092
3166
  calculateInitialStrain(time, current) {
@@ -3220,29 +3294,8 @@ class OsuFlashlight extends OsuSkill {
3220
3294
  * Represents an osu!standard hit object with difficulty calculation values.
3221
3295
  */
3222
3296
  class OsuDifficultyHitObject extends DifficultyHitObject {
3223
- get scalingFactor() {
3224
- const radius = this.object.radius;
3225
- // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
3226
- let scalingFactor = this.normalizedRadius / radius;
3227
- // High circle size (small CS) bonus
3228
- if (radius < this.radiusBuffThreshold) {
3229
- scalingFactor *=
3230
- 1 + Math.min(this.radiusBuffThreshold - radius, 5) / 50;
3231
- }
3232
- return scalingFactor;
3233
- }
3234
- /**
3235
- * Note: You **must** call `computeProperties` at some point due to how TypeScript handles
3236
- * overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue).
3237
- *
3238
- * @param object The underlying hitobject.
3239
- * @param lastObject The hitobject before this hitobject.
3240
- * @param lastLastObject The hitobject before the last hitobject.
3241
- * @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
3242
- * @param clockRate The clock rate of the beatmap.
3243
- */
3244
- constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate) {
3245
- super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate);
3297
+ constructor() {
3298
+ super(...arguments);
3246
3299
  /**
3247
3300
  * The speed strain generated by the hitobject.
3248
3301
  */
@@ -3254,6 +3307,17 @@ class OsuDifficultyHitObject extends DifficultyHitObject {
3254
3307
  this.radiusBuffThreshold = 30;
3255
3308
  this.mode = osuBase.Modes.osu;
3256
3309
  }
3310
+ get scalingFactor() {
3311
+ const radius = this.object.radius;
3312
+ // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
3313
+ let scalingFactor = this.normalizedRadius / radius;
3314
+ // High circle size (small CS) bonus
3315
+ if (radius < this.radiusBuffThreshold) {
3316
+ scalingFactor *=
3317
+ 1 + Math.min(this.radiusBuffThreshold - radius, 5) / 50;
3318
+ }
3319
+ return scalingFactor;
3320
+ }
3257
3321
  }
3258
3322
 
3259
3323
  /**
@@ -3319,7 +3383,7 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3319
3383
  this.attributes.speedDifficulty = 0;
3320
3384
  return;
3321
3385
  }
3322
- const speedSkill = new OsuSpeed(this.mods, this.difficultyStatistics.overallDifficulty);
3386
+ const speedSkill = new OsuSpeed(this.mods);
3323
3387
  this.calculateSkills(speedSkill);
3324
3388
  this.postCalculateSpeed(speedSkill);
3325
3389
  }
@@ -3387,19 +3451,19 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3387
3451
  var _a, _b;
3388
3452
  const difficultyObjects = [];
3389
3453
  const { objects } = convertedBeatmap.hitObjects;
3454
+ const greatWindow = new osuBase.OsuHitWindow(this.difficultyStatistics.overallDifficulty).hitWindowFor300();
3390
3455
  for (let i = 0; i < objects.length; ++i) {
3391
- const difficultyObject = new OsuDifficultyHitObject(objects[i], (_a = objects[i - 1]) !== null && _a !== void 0 ? _a : null, (_b = objects[i - 2]) !== null && _b !== void 0 ? _b : null, difficultyObjects, this.difficultyStatistics.overallSpeedMultiplier);
3456
+ const difficultyObject = new OsuDifficultyHitObject(objects[i], (_a = objects[i - 1]) !== null && _a !== void 0 ? _a : null, (_b = objects[i - 2]) !== null && _b !== void 0 ? _b : null, difficultyObjects, this.difficultyStatistics.overallSpeedMultiplier, greatWindow);
3392
3457
  difficultyObject.computeProperties(this.difficultyStatistics.overallSpeedMultiplier, objects);
3393
3458
  difficultyObjects.push(difficultyObject);
3394
3459
  }
3395
3460
  return difficultyObjects;
3396
3461
  }
3397
3462
  computeDifficultyStatistics(options) {
3398
- var _a;
3399
3463
  const { difficulty } = this.beatmap;
3400
3464
  return osuBase.calculateOsuDifficultyStatistics({
3401
3465
  circleSize: difficulty.cs,
3402
- approachRate: (_a = difficulty.ar) !== null && _a !== void 0 ? _a : difficulty.od,
3466
+ approachRate: difficulty.ar,
3403
3467
  overallDifficulty: difficulty.od,
3404
3468
  healthDrain: difficulty.hp,
3405
3469
  mods: options === null || options === void 0 ? void 0 : options.mods,
@@ -3410,7 +3474,7 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3410
3474
  return [
3411
3475
  new OsuAim(this.mods, true),
3412
3476
  new OsuAim(this.mods, false),
3413
- new OsuSpeed(this.mods, this.difficultyStatistics.overallDifficulty),
3477
+ new OsuSpeed(this.mods),
3414
3478
  new OsuFlashlight(this.mods),
3415
3479
  ];
3416
3480
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rian8337/osu-difficulty-calculator",
3
- "version": "4.0.0-beta.23",
3
+ "version": "4.0.0-beta.25",
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.23"
36
+ "@rian8337/osu-base": "^4.0.0-beta.25"
37
37
  },
38
38
  "publishConfig": {
39
39
  "access": "public"
40
40
  },
41
- "gitHead": "62063ddac57c3a60426c7206268a0573cc6db6dd"
41
+ "gitHead": "2d6271ea24c787caad1d24c3d20dbeeaf450359e"
42
42
  }
@@ -191,6 +191,10 @@ declare abstract class DifficultyHitObject {
191
191
  * Adjusted end time of the hitobject, taking speed multiplier into account.
192
192
  */
193
193
  readonly endTime: number;
194
+ /**
195
+ * The full great window of the hitobject.
196
+ */
197
+ readonly fullGreatWindow: number;
194
198
  /**
195
199
  * Other hitobjects in the beatmap, including this hitobject.
196
200
  */
@@ -199,7 +203,10 @@ declare abstract class DifficultyHitObject {
199
203
  protected readonly normalizedRadius = 50;
200
204
  protected readonly maximumSliderRadius: number;
201
205
  protected readonly assumedSliderRadius: number;
202
- protected readonly minDeltaTime = 25;
206
+ /**
207
+ * The lowest possible delta time value.
208
+ */
209
+ static readonly minDeltaTime = 25;
203
210
  private readonly lastObject;
204
211
  private readonly lastLastObject;
205
212
  /**
@@ -211,8 +218,9 @@ declare abstract class DifficultyHitObject {
211
218
  * @param lastLastObject The hitobject before the last hitobject.
212
219
  * @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
213
220
  * @param clockRate The clock rate of the beatmap.
221
+ * @param greatWindow The great window of the hitobject.
214
222
  */
215
- protected constructor(object: PlaceableHitObject, lastObject: PlaceableHitObject | null, lastLastObject: PlaceableHitObject | null, difficultyHitObjects: readonly DifficultyHitObject[], clockRate: number);
223
+ constructor(object: PlaceableHitObject, lastObject: PlaceableHitObject | null, lastLastObject: PlaceableHitObject | null, difficultyHitObjects: readonly DifficultyHitObject[], clockRate: number, greatWindow: number);
216
224
  /**
217
225
  * Computes the properties of this hitobject.
218
226
  *
@@ -250,6 +258,13 @@ declare abstract class DifficultyHitObject {
250
258
  * @returns The opacity of the hitobject at the given time.
251
259
  */
252
260
  opacityAt(time: number, isHidden: boolean): number;
261
+ /**
262
+ * How possible is it to doubletap this object together with the next one and get perfect
263
+ * judgement in range from 0 to 1.
264
+ *
265
+ * A value closer to 1 indicates a higher possibility.
266
+ */
267
+ get doubletapness(): number;
253
268
  protected abstract get scalingFactor(): number;
254
269
  protected setDistances(clockRate: number): void;
255
270
  private calculateSliderCursorPosition;
@@ -521,6 +536,7 @@ declare abstract class DroidSkill extends StrainSkill {
521
536
  * The strains of hitobjects.
522
537
  */
523
538
  get objectStrains(): readonly number[];
539
+ private difficulty;
524
540
  /**
525
541
  * Returns the number of strains weighed against the top strain.
526
542
  *
@@ -599,9 +615,10 @@ declare class DroidDifficultyHitObject extends DifficultyHitObject {
599
615
  * @param lastLastObject The hitobject before the last hitobject.
600
616
  * @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
601
617
  * @param clockRate The clock rate of the beatmap.
618
+ * @param greatWindow The great window of the hitobject.
602
619
  * @param isForceAR Whether force AR is enabled.
603
620
  */
604
- constructor(object: PlaceableHitObject, lastObject: PlaceableHitObject | null, lastLastObject: PlaceableHitObject | null, difficultyHitObjects: readonly DifficultyHitObject[], clockRate: number, isForceAR: boolean);
621
+ constructor(object: PlaceableHitObject, lastObject: PlaceableHitObject | null, lastLastObject: PlaceableHitObject | null, difficultyHitObjects: readonly DifficultyHitObject[], clockRate: number, greatWindow: number, isForceAR: boolean);
605
622
  computeProperties(clockRate: number, hitObjects: readonly PlaceableHitObject[]): void;
606
623
  /**
607
624
  * Determines whether this hitobject is considered overlapping with the hitobject before it.
@@ -1243,8 +1260,6 @@ declare class DroidRhythm extends DroidSkill {
1243
1260
  protected readonly starsPerDouble = 1.75;
1244
1261
  private currentRhythmStrain;
1245
1262
  private currentRhythmMultiplier;
1246
- private readonly hitWindow;
1247
- constructor(mods: Mod[], overallDifficulty: number);
1248
1263
  protected strainValueAt(current: DroidDifficultyHitObject): number;
1249
1264
  protected calculateInitialStrain(time: number, current: DroidDifficultyHitObject): number;
1250
1265
  protected getObjectStrain(): number;
@@ -1265,14 +1280,17 @@ declare abstract class RhythmEvaluator {
1265
1280
  * An evaluator for calculating osu!droid Rhythm skill.
1266
1281
  */
1267
1282
  declare abstract class DroidRhythmEvaluator extends RhythmEvaluator {
1283
+ protected static readonly rhythmMultiplier = 1.2;
1284
+ protected static readonly historyTimeMax = 4000;
1285
+ private static readonly maxIslandSize;
1286
+ private static readonly historyObjectsMax;
1268
1287
  /**
1269
1288
  * Calculates a rhythm multiplier for the difficulty of the tap associated
1270
1289
  * with historic data of the current object.
1271
1290
  *
1272
1291
  * @param current The current object.
1273
- * @param greatWindow The great hit window of the current object.
1274
1292
  */
1275
- static evaluateDifficultyOf(current: DroidDifficultyHitObject, greatWindow: number): number;
1293
+ static evaluateDifficultyOf(current: DroidDifficultyHitObject): number;
1276
1294
  }
1277
1295
 
1278
1296
  /**
@@ -1286,7 +1304,6 @@ declare class DroidTap extends DroidSkill {
1286
1304
  private currentTapStrain;
1287
1305
  private currentRhythmMultiplier;
1288
1306
  private readonly skillMultiplier;
1289
- private readonly greatWindow;
1290
1307
  private readonly considerCheesability;
1291
1308
  private readonly strainTimeCap?;
1292
1309
  private readonly _objectDeltaTimes;
@@ -1294,7 +1311,7 @@ declare class DroidTap extends DroidSkill {
1294
1311
  * The delta time of hitobjects.
1295
1312
  */
1296
1313
  get objectDeltaTimes(): readonly number[];
1297
- constructor(mods: Mod[], overallDifficulty: number, considerCheesability: boolean, strainTimeCap?: number);
1314
+ constructor(mods: Mod[], considerCheesability: boolean, strainTimeCap?: number);
1298
1315
  /**
1299
1316
  * The amount of notes that are relevant to the difficulty.
1300
1317
  */
@@ -1338,7 +1355,7 @@ declare abstract class DroidTapEvaluator extends SpeedEvaluator {
1338
1355
  * @param considerCheesability Whether to consider cheesability.
1339
1356
  * @param strainTimeCap The strain time to cap the object's strain time to.
1340
1357
  */
1341
- static evaluateDifficultyOf(current: DroidDifficultyHitObject, greatWindow: number, considerCheesability: boolean, strainTimeCap?: number): number;
1358
+ static evaluateDifficultyOf(current: DroidDifficultyHitObject, considerCheesability: boolean, strainTimeCap?: number): number;
1342
1359
  }
1343
1360
 
1344
1361
  /**
@@ -1420,17 +1437,6 @@ declare class OsuDifficultyHitObject extends DifficultyHitObject {
1420
1437
  private readonly radiusBuffThreshold;
1421
1438
  protected readonly mode = Modes.osu;
1422
1439
  protected get scalingFactor(): number;
1423
- /**
1424
- * Note: You **must** call `computeProperties` at some point due to how TypeScript handles
1425
- * overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue).
1426
- *
1427
- * @param object The underlying hitobject.
1428
- * @param lastObject The hitobject before this hitobject.
1429
- * @param lastLastObject The hitobject before the last hitobject.
1430
- * @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
1431
- * @param clockRate The clock rate of the beatmap.
1432
- */
1433
- constructor(object: PlaceableHitObject, lastObject: PlaceableHitObject | null, lastLastObject: PlaceableHitObject | null, difficultyHitObjects: readonly DifficultyHitObject[], clockRate: number);
1434
1440
  }
1435
1441
 
1436
1442
  /**
@@ -1634,7 +1640,7 @@ declare abstract class OsuRhythmEvaluator extends RhythmEvaluator {
1634
1640
  * @param current The current object.
1635
1641
  * @param greatWindow The great hit window of the current object.
1636
1642
  */
1637
- static evaluateDifficultyOf(current: OsuDifficultyHitObject, greatWindow: number): number;
1643
+ static evaluateDifficultyOf(current: OsuDifficultyHitObject): number;
1638
1644
  }
1639
1645
 
1640
1646
  /**
@@ -1649,8 +1655,6 @@ declare class OsuSpeed extends OsuSkill {
1649
1655
  private currentSpeedStrain;
1650
1656
  private currentRhythm;
1651
1657
  private readonly skillMultiplier;
1652
- private readonly greatWindow;
1653
- constructor(mods: Mod[], overallDifficulty: number);
1654
1658
  /**
1655
1659
  * @param current The hitobject to calculate.
1656
1660
  */
@@ -1678,9 +1682,8 @@ declare abstract class OsuSpeedEvaluator extends SpeedEvaluator {
1678
1682
  * - and how easily they can be cheesed.
1679
1683
  *
1680
1684
  * @param current The current object.
1681
- * @param greatWindow The great hit window of the current object.
1682
1685
  */
1683
- static evaluateDifficultyOf(current: OsuDifficultyHitObject, greatWindow: number): number;
1686
+ static evaluateDifficultyOf(current: OsuDifficultyHitObject): number;
1684
1687
  }
1685
1688
 
1686
1689
  export { AimEvaluator, type CacheableDifficultyAttributes, type DifficultSlider, type DifficultyAttributes, type DifficultyCalculationOptions, DifficultyCalculator, DifficultyHitObject, DroidAim, DroidAimEvaluator, type DroidDifficultyAttributes, type DroidDifficultyCalculationOptions, DroidDifficultyCalculator, DroidDifficultyHitObject, DroidFlashlight, DroidFlashlightEvaluator, DroidPerformanceCalculator, DroidRhythm, DroidRhythmEvaluator, DroidTap, DroidTapEvaluator, DroidVisual, DroidVisualEvaluator, type ExtendedDroidDifficultyAttributes, FlashlightEvaluator, type HighStrainSection, OsuAim, OsuAimEvaluator, type OsuDifficultyAttributes, OsuDifficultyCalculator, OsuDifficultyHitObject, OsuFlashlight, OsuFlashlightEvaluator, OsuPerformanceCalculator, OsuRhythmEvaluator, OsuSpeed, OsuSpeedEvaluator, type PerformanceCalculationOptions, PerformanceCalculator, RhythmEvaluator, SpeedEvaluator, type StrainPeaks };