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

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
@@ -32,6 +32,12 @@ AimEvaluator.velocityChangeMultiplier = 0.75;
32
32
  * The base of a difficulty calculator.
33
33
  */
34
34
  class DifficultyCalculator {
35
+ /**
36
+ * The difficulty objects of the beatmap.
37
+ */
38
+ get objects() {
39
+ return this._objects;
40
+ }
35
41
  /**
36
42
  * The total star rating of the beatmap.
37
43
  */
@@ -41,13 +47,13 @@ class DifficultyCalculator {
41
47
  /**
42
48
  * Constructs a new instance of the calculator.
43
49
  *
44
- * @param beatmap The beatmap to calculate. This beatmap will be deep-cloned to prevent reference changes.
50
+ * @param beatmap The beatmap to calculate.
45
51
  */
46
52
  constructor(beatmap) {
47
53
  /**
48
54
  * The difficulty objects of the beatmap.
49
55
  */
50
- this.objects = [];
56
+ this._objects = [];
51
57
  /**
52
58
  * The modifications applied.
53
59
  */
@@ -62,13 +68,6 @@ class DifficultyCalculator {
62
68
  flashlight: [],
63
69
  };
64
70
  this.beatmap = beatmap;
65
- this.difficultyStatistics = {
66
- circleSize: beatmap.difficulty.cs,
67
- approachRate: beatmap.difficulty.ar,
68
- overallDifficulty: beatmap.difficulty.od,
69
- healthDrain: beatmap.difficulty.hp,
70
- overallSpeedMultiplier: 1,
71
- };
72
71
  }
73
72
  /**
74
73
  * Calculates the star rating of the specified beatmap.
@@ -86,16 +85,17 @@ class DifficultyCalculator {
86
85
  * @returns The current instance.
87
86
  */
88
87
  calculate(options) {
89
- var _a;
88
+ var _a, _b;
90
89
  this.mods = (_a = options === null || options === void 0 ? void 0 : options.mods) !== null && _a !== void 0 ? _a : [];
91
90
  const playableBeatmap = this.beatmap.createPlayableBeatmap({
92
91
  mode: this.mode,
93
92
  mods: this.mods,
94
93
  customSpeedMultiplier: options === null || options === void 0 ? void 0 : options.customSpeedMultiplier,
95
94
  });
96
- this.difficultyStatistics = Object.seal(this.computeDifficultyStatistics(options));
97
- this.populateDifficultyAttributes();
98
- this.objects.push(...this.generateDifficultyHitObjects(playableBeatmap));
95
+ const clockRate = osuBase.ModUtil.calculateRateWithMods(this.mods) *
96
+ ((_b = options === null || options === void 0 ? void 0 : options.customSpeedMultiplier) !== null && _b !== void 0 ? _b : 1);
97
+ this.populateDifficultyAttributes(playableBeatmap, clockRate);
98
+ this._objects = this.generateDifficultyHitObjects(playableBeatmap, clockRate);
99
99
  this.calculateAll();
100
100
  return this;
101
101
  }
@@ -114,18 +114,27 @@ class DifficultyCalculator {
114
114
  }
115
115
  /**
116
116
  * Populates the stored difficulty attributes with necessary data.
117
+ *
118
+ * @param beatmap The beatmap to populate the attributes with.
119
+ * @param clockRate The clock rate of the beatmap.
117
120
  */
118
- populateDifficultyAttributes() {
119
- this.attributes.approachRate = this.difficultyStatistics.approachRate;
121
+ populateDifficultyAttributes(beatmap, clockRate) {
120
122
  this.attributes.hitCircleCount = this.beatmap.hitObjects.circles;
121
123
  this.attributes.maxCombo = this.beatmap.maxCombo;
122
124
  this.attributes.mods = this.mods.slice();
123
- this.attributes.overallDifficulty =
124
- this.difficultyStatistics.overallDifficulty;
125
125
  this.attributes.sliderCount = this.beatmap.hitObjects.sliders;
126
126
  this.attributes.spinnerCount = this.beatmap.hitObjects.spinners;
127
- this.attributes.clockRate =
128
- this.difficultyStatistics.overallSpeedMultiplier;
127
+ this.attributes.clockRate = clockRate;
128
+ let greatWindow;
129
+ switch (this.mode) {
130
+ case osuBase.Modes.droid:
131
+ greatWindow = new osuBase.DroidHitWindow(beatmap.difficulty.od).hitWindowFor300(this.mods.some((m) => m instanceof osuBase.ModPrecise));
132
+ break;
133
+ case osuBase.Modes.osu:
134
+ greatWindow = new osuBase.OsuHitWindow(beatmap.difficulty.od).hitWindowFor300();
135
+ break;
136
+ }
137
+ this.attributes.overallDifficulty = osuBase.OsuHitWindow.hitWindow300ToOD(greatWindow / clockRate);
129
138
  }
130
139
  /**
131
140
  * Calculates the star rating value of a difficulty.
@@ -1057,7 +1066,7 @@ class DroidFlashlight extends DroidSkill {
1057
1066
  this.reducedSectionCount = 0;
1058
1067
  this.reducedSectionBaseline = 1;
1059
1068
  this.starsPerDouble = 1.06;
1060
- this.skillMultiplier = 0.052;
1069
+ this.skillMultiplier = 0.02;
1061
1070
  this.currentFlashlightStrain = 0;
1062
1071
  this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
1063
1072
  this.withSliders = withSliders;
@@ -1086,64 +1095,42 @@ class DroidFlashlight extends DroidSkill {
1086
1095
  }
1087
1096
  }
1088
1097
  difficultyValue() {
1089
- return Math.pow(this.strainPeaks.reduce((a, v) => a + v, 0) * this.starsPerDouble, 0.8);
1098
+ return (this.strainPeaks.reduce((a, v) => a + v, 0) * this.starsPerDouble);
1090
1099
  }
1091
1100
  }
1092
1101
 
1093
- /**
1094
- * An evaluator for calculating rhythm skill.
1095
- *
1096
- * This class should be considered an "evaluating" class and not persisted.
1097
- */
1098
- class RhythmEvaluator {
1099
- }
1100
- RhythmEvaluator.rhythmMultiplier = 0.75;
1101
- RhythmEvaluator.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
1102
-
1103
1102
  class Island {
1104
- constructor(firstDelta, epsilon) {
1105
- this.deltas = [];
1106
- if (epsilon === undefined) {
1107
- this.deltaDifferenceEpsilon = firstDelta;
1108
- return;
1103
+ constructor(delta, deltaDifferenceEpsilon) {
1104
+ this.delta = Number.MAX_SAFE_INTEGER;
1105
+ this.deltaCount = 0;
1106
+ if (deltaDifferenceEpsilon === undefined) {
1107
+ this.deltaDifferenceEpsilon = delta;
1108
+ }
1109
+ else {
1110
+ this.deltaDifferenceEpsilon = deltaDifferenceEpsilon;
1111
+ this.addDelta(delta);
1109
1112
  }
1110
- this.deltaDifferenceEpsilon = epsilon;
1111
- this.addDelta(firstDelta);
1112
1113
  }
1113
1114
  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;
1115
+ if (this.delta === Number.MAX_SAFE_INTEGER) {
1116
+ this.delta = Math.max(Math.trunc(delta), DifficultyHitObject.minDeltaTime);
1117
+ }
1118
+ ++this.deltaCount;
1123
1119
  }
1124
1120
  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);
1121
+ // TODO: consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple)
1122
+ // naively adding delta check here breaks _a lot_ of maps because of the flawed ratio calculation
1123
+ return this.deltaCount % 2 == other.deltaCount % 2;
1130
1124
  }
1131
1125
  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;
1126
+ return (Math.abs(this.delta - other.delta) < this.deltaDifferenceEpsilon &&
1127
+ this.deltaCount === other.deltaCount);
1141
1128
  }
1142
1129
  }
1143
1130
  /**
1144
1131
  * An evaluator for calculating osu!droid Rhythm skill.
1145
1132
  */
1146
- class DroidRhythmEvaluator extends RhythmEvaluator {
1133
+ class DroidRhythmEvaluator {
1147
1134
  /**
1148
1135
  * Calculates a rhythm multiplier for the difficulty of the tap associated
1149
1136
  * with historic data of the current object.
@@ -1151,9 +1138,7 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
1151
1138
  * @param current The current object.
1152
1139
  */
1153
1140
  static evaluateDifficultyOf(current) {
1154
- if (current.object instanceof osuBase.Spinner ||
1155
- // Exclude overlapping objects that can be tapped at once.
1156
- current.isOverlapping(false)) {
1141
+ if (current.object instanceof osuBase.Spinner) {
1157
1142
  return 1;
1158
1143
  }
1159
1144
  const deltaDifferenceEpsilon = current.fullGreatWindow * 0.3;
@@ -1183,47 +1168,50 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
1183
1168
  ++rhythmStart;
1184
1169
  }
1185
1170
  for (let i = rhythmStart; i > 0; --i) {
1186
- // Scale note 0 to 1 from history to now.
1187
- let currentHistoricalDecay = (this.historyTimeMax -
1188
- (current.startTime - validPrevious[i - 1].startTime)) /
1189
- this.historyTimeMax;
1190
- // Either we're limited by time or limited by object count.
1191
- currentHistoricalDecay = Math.min(currentHistoricalDecay, (validPrevious.length - i) / validPrevious.length);
1192
1171
  const currentObject = validPrevious[i - 1];
1193
1172
  const prevObject = validPrevious[i];
1194
1173
  const lastObject = validPrevious[i + 1];
1174
+ // Scale note 0 to 1 from history to now.
1175
+ const timeDecay = (this.historyTimeMax -
1176
+ (current.startTime - currentObject.startTime)) /
1177
+ this.historyTimeMax;
1178
+ const noteDecay = (validPrevious.length - i) / validPrevious.length;
1179
+ // Either we're limited by time or limited by object count.
1180
+ const currentHistoricalDecay = Math.min(timeDecay, noteDecay);
1195
1181
  const currentDelta = currentObject.strainTime;
1196
1182
  const prevDelta = prevObject.strainTime;
1197
1183
  const lastDelta = lastObject.strainTime;
1184
+ // Calculate how much current delta difference deserves a rhythm bonus
1185
+ // This function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e. 100 and 200)
1186
+ const deltaDifferenceRatio = Math.min(prevDelta, currentDelta) /
1187
+ Math.max(prevDelta, currentDelta);
1198
1188
  const currentRatio = 1 +
1199
- 10 *
1200
- Math.min(0.5, Math.pow(Math.sin(Math.PI /
1201
- (Math.min(prevDelta, currentDelta) /
1202
- Math.max(prevDelta, currentDelta))), 2));
1203
- const windowPenalty = osuBase.MathUtils.clamp((Math.abs(prevDelta - currentDelta) - deltaDifferenceEpsilon) /
1204
- deltaDifferenceEpsilon, 0, 1);
1205
- let effectiveRatio = windowPenalty * currentRatio;
1189
+ this.rhythmRatioMultiplier *
1190
+ Math.min(0.5, Math.pow(Math.sin(Math.PI / deltaDifferenceRatio), 2));
1191
+ // Reduce ratio bonus if delta difference is too big
1192
+ const fraction = Math.max(prevDelta / currentDelta, currentDelta / prevDelta);
1193
+ const fractionMultiplier = osuBase.MathUtils.clamp(2 - fraction / 8, 0, 1);
1194
+ const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon);
1195
+ let effectiveRatio = windowPenalty * currentRatio * fractionMultiplier;
1206
1196
  if (firstDeltaSwitch) {
1207
- if (Math.abs(prevDelta - currentDelta) <= deltaDifferenceEpsilon) {
1208
- if (island.deltas.length < this.maxIslandSize) {
1209
- // Island is still progressing.
1210
- island.addDelta(currentDelta);
1211
- }
1197
+ if (Math.abs(prevDelta - currentDelta) < deltaDifferenceEpsilon) {
1198
+ // Island is still progressing, count size.
1199
+ island.addDelta(currentDelta);
1212
1200
  }
1213
1201
  else {
1214
1202
  // BPM change is into slider, this is easy acc window.
1215
1203
  if (currentObject.object instanceof osuBase.Slider) {
1216
1204
  effectiveRatio /= 8;
1217
1205
  }
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.
1206
+ // BPM change was from a slider, this is easier typically than circle -> circle.
1207
+ // Unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty
1208
+ // than bursts without sliders.
1221
1209
  if (prevObject.object instanceof osuBase.Slider) {
1222
- effectiveRatio /= 4;
1210
+ effectiveRatio *= 0.3;
1223
1211
  }
1224
1212
  // Repeated island polarity (2 -> 4, 3 -> 5).
1225
1213
  if (island.isSimilarPolarity(previousIsland)) {
1226
- effectiveRatio *= 0.3;
1214
+ effectiveRatio /= 2;
1227
1215
  }
1228
1216
  // Previous increase happened a note ago.
1229
1217
  // Albeit this is a 1/1 -> 1/2-1/4 type of transition, we don't want to buff this.
@@ -1231,8 +1219,9 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
1231
1219
  prevDelta > currentDelta + deltaDifferenceEpsilon) {
1232
1220
  effectiveRatio /= 8;
1233
1221
  }
1234
- // Singletaps are easier to control.
1235
- if (island.deltas.length === 1) {
1222
+ // Repeated island size (ex: triplet -> triplet).
1223
+ // TODO: remove this nerf since its staying here only for balancing purposes because of the flawed ratio calculation
1224
+ if (previousIsland.deltaCount == island.deltaCount) {
1236
1225
  effectiveRatio /= 2;
1237
1226
  }
1238
1227
  let islandFound = false;
@@ -1249,9 +1238,7 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
1249
1238
  }
1250
1239
  // Repeated island (ex: triplet -> triplet).
1251
1240
  // 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))));
1241
+ effectiveRatio *= Math.min(3 / islandCount, Math.pow(1 / islandCount, 2.75 / (1 + Math.exp(14 - 0.24 * island.delta))));
1255
1242
  break;
1256
1243
  }
1257
1244
  if (!islandFound) {
@@ -1269,28 +1256,35 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
1269
1256
  // If we're speeding up, this stays as is and we keep counting island size.
1270
1257
  firstDeltaSwitch = false;
1271
1258
  }
1272
- island = new Island(Math.trunc(currentDelta), deltaDifferenceEpsilon);
1259
+ island = new Island(currentDelta, deltaDifferenceEpsilon);
1273
1260
  }
1274
1261
  }
1275
- else if (prevDelta > deltaDifferenceEpsilon + currentDelta) {
1276
- // We're speeding up.
1262
+ else if (prevDelta > currentDelta + deltaDifferenceEpsilon) {
1263
+ // We are speeding up.
1277
1264
  // Begin counting island until we change speed again.
1278
1265
  firstDeltaSwitch = true;
1279
- // Reduce ratio if we're starting after a slider.
1266
+ // BPM change is into slider, this is easy acc window.
1267
+ if (currentObject.object instanceof osuBase.Slider) {
1268
+ effectiveRatio *= 0.6;
1269
+ }
1270
+ // BPM change was from a slider, this is easier typically than circle -> circle
1271
+ // Unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty
1272
+ // than bursts without sliders
1280
1273
  if (prevObject.object instanceof osuBase.Slider) {
1281
- effectiveRatio *= 0.3;
1274
+ effectiveRatio *= 0.6;
1282
1275
  }
1283
1276
  startRatio = effectiveRatio;
1284
- island = new Island(Math.trunc(currentDelta), deltaDifferenceEpsilon);
1277
+ island = new Island(currentDelta, deltaDifferenceEpsilon);
1285
1278
  }
1286
1279
  }
1287
- return Math.sqrt(4 + rhythmComplexitySum * this.rhythmMultiplier) / 2;
1280
+ return (Math.sqrt(4 + rhythmComplexitySum * this.rhythmOverallMultiplier) /
1281
+ 2);
1288
1282
  }
1289
1283
  }
1290
- DroidRhythmEvaluator.rhythmMultiplier = 1.2;
1291
- DroidRhythmEvaluator.historyTimeMax = 4000;
1292
- DroidRhythmEvaluator.maxIslandSize = 7;
1293
- DroidRhythmEvaluator.historyObjectsMax = 24;
1284
+ DroidRhythmEvaluator.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
1285
+ DroidRhythmEvaluator.historyObjectsMax = 32;
1286
+ DroidRhythmEvaluator.rhythmOverallMultiplier = 0.95;
1287
+ DroidRhythmEvaluator.rhythmRatioMultiplier = 12;
1294
1288
 
1295
1289
  /**
1296
1290
  * Represents the skill required to properly follow a beatmap's rhythm.
@@ -1492,9 +1486,8 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
1492
1486
  * @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
1493
1487
  * @param clockRate The clock rate of the beatmap.
1494
1488
  * @param greatWindow The great window of the hitobject.
1495
- * @param isForceAR Whether force AR is enabled.
1496
1489
  */
1497
- constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, greatWindow, isForceAR) {
1490
+ constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, greatWindow) {
1498
1491
  super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate, greatWindow);
1499
1492
  /**
1500
1493
  * The tap strain generated by the hitobject.
@@ -1538,10 +1531,7 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
1538
1531
  this.radiusBuffThreshold = 70;
1539
1532
  this.mode = osuBase.Modes.droid;
1540
1533
  this.maximumSliderRadius = this.normalizedRadius * 2;
1541
- this.timePreempt = object.timePreempt;
1542
- if (!isForceAR) {
1543
- this.timePreempt /= clockRate;
1544
- }
1534
+ this.timePreempt = object.timePreempt / clockRate;
1545
1535
  }
1546
1536
  computeProperties(clockRate, hitObjects) {
1547
1537
  super.computeProperties(clockRate, hitObjects);
@@ -1553,6 +1543,9 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
1553
1543
  * Keep in mind that "overlapping" in this case is overlapping to the point where both hitobjects
1554
1544
  * can be hit with just a single tap in osu!droid.
1555
1545
  *
1546
+ * In the case of sliders, it is considered overlapping if all nested hitobjects can be hit with
1547
+ * one aim motion.
1548
+ *
1556
1549
  * @param considerDistance Whether to consider the distance between both hitobjects.
1557
1550
  * @returns Whether the hitobject is considered overlapping.
1558
1551
  */
@@ -1560,23 +1553,48 @@ class DroidDifficultyHitObject extends DifficultyHitObject {
1560
1553
  if (this.object instanceof osuBase.Spinner) {
1561
1554
  return false;
1562
1555
  }
1563
- const previous = this.previous(0);
1564
- if (!previous || previous.object instanceof osuBase.Spinner) {
1556
+ const prev = this.previous(0);
1557
+ if (!prev || prev.object instanceof osuBase.Spinner) {
1565
1558
  return false;
1566
1559
  }
1567
- if (this.deltaTime >= 5) {
1560
+ if (this.object.startTime !== prev.object.startTime) {
1568
1561
  return false;
1569
1562
  }
1570
- if (considerDistance) {
1571
- const endPosition = this.object.getStackedPosition(osuBase.Modes.droid);
1572
- let distance = previous.object
1573
- .getStackedEndPosition(osuBase.Modes.droid)
1574
- .getDistance(endPosition);
1575
- if (previous.object instanceof osuBase.Slider &&
1576
- previous.object.lazyEndPosition) {
1577
- distance = Math.min(distance, previous.object.lazyEndPosition.getDistance(endPosition));
1563
+ if (!considerDistance) {
1564
+ return true;
1565
+ }
1566
+ const distanceThreshold = 2 * this.object.radius;
1567
+ const startPosition = this.object.getStackedPosition(osuBase.Modes.droid);
1568
+ const prevStartPosition = prev.object.getStackedPosition(osuBase.Modes.droid);
1569
+ // We need to consider two cases:
1570
+ //
1571
+ // Case 1: Current object is a circle, or previous object is a circle.
1572
+ // In this case, we only need to check if their positions are close enough to be tapped together.
1573
+ //
1574
+ // Case 2: Both objects are sliders.
1575
+ // In this case, we need to check if all nested hitobjects can be hit together.
1576
+ // To start with, check if the starting positions can be tapped together.
1577
+ if (startPosition.getDistance(prevStartPosition) > distanceThreshold) {
1578
+ return false;
1579
+ }
1580
+ if (this.object instanceof osuBase.Circle || prev.object instanceof osuBase.Circle) {
1581
+ return true;
1582
+ }
1583
+ // Check if all nested hitobjects can be hit together.
1584
+ for (let i = 1; i < this.object.nestedHitObjects.length; ++i) {
1585
+ const position = this.object.nestedHitObjects[i].getStackedPosition(osuBase.Modes.droid);
1586
+ const prevPosition = prevStartPosition.add(prev.object.curvePositionAt(i / (this.object.nestedHitObjects.length - 1)));
1587
+ if (position.getDistance(prevPosition) > distanceThreshold) {
1588
+ return false;
1589
+ }
1590
+ }
1591
+ // Do the same for the previous slider as well.
1592
+ for (let i = 1; i < prev.object.nestedHitObjects.length; ++i) {
1593
+ const prevPosition = prev.object.nestedHitObjects[i].getStackedPosition(osuBase.Modes.droid);
1594
+ const position = startPosition.add(this.object.curvePositionAt(i / (prev.object.nestedHitObjects.length - 1)));
1595
+ if (prevPosition.getDistance(position) > distanceThreshold) {
1596
+ return false;
1578
1597
  }
1579
- return distance <= 2 * this.object.radius;
1580
1598
  }
1581
1599
  return true;
1582
1600
  }
@@ -1660,7 +1678,6 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1660
1678
  speedNoteCount: 0,
1661
1679
  sliderFactor: 0,
1662
1680
  clockRate: 1,
1663
- approachRate: 0,
1664
1681
  overallDifficulty: 0,
1665
1682
  hitCircleCount: 0,
1666
1683
  sliderCount: 0,
@@ -1710,30 +1727,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1710
1727
  return this.attributes.visualDifficulty;
1711
1728
  }
1712
1729
  get cacheableAttributes() {
1713
- return {
1714
- tapDifficulty: this.tap,
1715
- rhythmDifficulty: this.rhythm,
1716
- visualDifficulty: this.visual,
1717
- mods: osuBase.ModUtil.modsToOsuString(this.attributes.mods),
1718
- starRating: this.total,
1719
- maxCombo: this.attributes.maxCombo,
1720
- aimDifficulty: this.aim,
1721
- flashlightDifficulty: this.flashlight,
1722
- speedNoteCount: this.attributes.speedNoteCount,
1723
- sliderFactor: this.attributes.sliderFactor,
1724
- clockRate: this.attributes.clockRate,
1725
- approachRate: this.attributes.approachRate,
1726
- overallDifficulty: this.attributes.overallDifficulty,
1727
- hitCircleCount: this.attributes.hitCircleCount,
1728
- sliderCount: this.attributes.sliderCount,
1729
- spinnerCount: this.attributes.spinnerCount,
1730
- aimDifficultStrainCount: this.attributes.aimDifficultStrainCount,
1731
- tapDifficultStrainCount: this.attributes.tapDifficultStrainCount,
1732
- flashlightDifficultStrainCount: this.attributes.flashlightDifficultStrainCount,
1733
- visualDifficultStrainCount: this.attributes.visualDifficultStrainCount,
1734
- averageSpeedDeltaTime: this.attributes.averageSpeedDeltaTime,
1735
- vibroFactor: this.attributes.vibroFactor,
1736
- };
1730
+ return Object.assign(Object.assign({}, this.attributes), { mods: osuBase.ModUtil.modsToOsuString(this.attributes.mods) });
1737
1731
  }
1738
1732
  /**
1739
1733
  * Calculates the aim star rating of the beatmap and stores it in this instance.
@@ -1842,31 +1836,19 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
1842
1836
  this.visual.toFixed(2) +
1843
1837
  " visual)");
1844
1838
  }
1845
- generateDifficultyHitObjects(convertedBeatmap) {
1839
+ generateDifficultyHitObjects(beatmap, clockRate) {
1846
1840
  var _a, _b;
1847
1841
  const difficultyObjects = [];
1848
- const { objects } = convertedBeatmap.hitObjects;
1849
- const difficultyAdjustMod = this.mods.find((m) => m instanceof osuBase.ModDifficultyAdjust);
1850
- const greatWindow = new osuBase.OsuHitWindow(this.difficultyStatistics.overallDifficulty).hitWindowFor300();
1842
+ const { objects } = beatmap.hitObjects;
1843
+ const isPrecise = this.mods.some((m) => m instanceof osuBase.ModPrecise);
1844
+ const greatWindow = new osuBase.DroidHitWindow(beatmap.difficulty.od).hitWindowFor300(isPrecise) / clockRate;
1851
1845
  for (let i = 0; i < objects.length; ++i) {
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);
1853
- difficultyObject.computeProperties(this.difficultyStatistics.overallSpeedMultiplier, objects);
1846
+ 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, clockRate, greatWindow);
1847
+ difficultyObject.computeProperties(clockRate, objects);
1854
1848
  difficultyObjects.push(difficultyObject);
1855
1849
  }
1856
1850
  return difficultyObjects;
1857
1851
  }
1858
- computeDifficultyStatistics(options) {
1859
- const { difficulty } = this.beatmap;
1860
- return osuBase.calculateDroidDifficultyStatistics({
1861
- circleSize: difficulty.cs,
1862
- approachRate: difficulty.ar,
1863
- overallDifficulty: difficulty.od,
1864
- healthDrain: difficulty.hp,
1865
- mods: this.mods,
1866
- customSpeedMultiplier: options === null || options === void 0 ? void 0 : options.customSpeedMultiplier,
1867
- oldStatistics: options === null || options === void 0 ? void 0 : options.oldStatistics,
1868
- });
1869
- }
1870
1852
  createSkills() {
1871
1853
  return [
1872
1854
  new DroidAim(this.mods, true),
@@ -3030,7 +3012,7 @@ OsuSpeedEvaluator.SINGLE_SPACING_THRESHOLD = 125;
3030
3012
  /**
3031
3013
  * An evaluator for calculating osu!standard Rhythm skill.
3032
3014
  */
3033
- class OsuRhythmEvaluator extends RhythmEvaluator {
3015
+ class OsuRhythmEvaluator {
3034
3016
  /**
3035
3017
  * Calculates a rhythm multiplier for the difficulty of the tap associated
3036
3018
  * with historic data of the current object.
@@ -3136,6 +3118,8 @@ class OsuRhythmEvaluator extends RhythmEvaluator {
3136
3118
  return Math.sqrt(4 + rhythmComplexitySum * this.rhythmMultiplier) / 2;
3137
3119
  }
3138
3120
  }
3121
+ OsuRhythmEvaluator.rhythmMultiplier = 0.75;
3122
+ OsuRhythmEvaluator.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
3139
3123
 
3140
3124
  /**
3141
3125
  * Represents the skill required to press keys or tap with regards to keeping up with the speed at which objects need to be hit.
@@ -3447,29 +3431,19 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3447
3431
  this.flashlight.toFixed(2) +
3448
3432
  " flashlight)");
3449
3433
  }
3450
- generateDifficultyHitObjects(convertedBeatmap) {
3434
+ generateDifficultyHitObjects(beatmap, clockRate) {
3451
3435
  var _a, _b;
3452
3436
  const difficultyObjects = [];
3453
- const { objects } = convertedBeatmap.hitObjects;
3454
- const greatWindow = new osuBase.OsuHitWindow(this.difficultyStatistics.overallDifficulty).hitWindowFor300();
3437
+ const { objects } = beatmap.hitObjects;
3438
+ const greatWindow = new osuBase.OsuHitWindow(beatmap.difficulty.od).hitWindowFor300() /
3439
+ clockRate;
3455
3440
  for (let i = 0; i < objects.length; ++i) {
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);
3457
- difficultyObject.computeProperties(this.difficultyStatistics.overallSpeedMultiplier, objects);
3441
+ 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, clockRate, greatWindow);
3442
+ difficultyObject.computeProperties(clockRate, objects);
3458
3443
  difficultyObjects.push(difficultyObject);
3459
3444
  }
3460
3445
  return difficultyObjects;
3461
3446
  }
3462
- computeDifficultyStatistics(options) {
3463
- const { difficulty } = this.beatmap;
3464
- return osuBase.calculateOsuDifficultyStatistics({
3465
- circleSize: difficulty.cs,
3466
- approachRate: difficulty.ar,
3467
- overallDifficulty: difficulty.od,
3468
- healthDrain: difficulty.hp,
3469
- mods: options === null || options === void 0 ? void 0 : options.mods,
3470
- customSpeedMultiplier: options === null || options === void 0 ? void 0 : options.customSpeedMultiplier,
3471
- });
3472
- }
3473
3447
  createSkills() {
3474
3448
  return [
3475
3449
  new OsuAim(this.mods, true),
@@ -3478,6 +3452,11 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3478
3452
  new OsuFlashlight(this.mods),
3479
3453
  ];
3480
3454
  }
3455
+ populateDifficultyAttributes(beatmap, clockRate) {
3456
+ super.populateDifficultyAttributes(beatmap, clockRate);
3457
+ const preempt = osuBase.BeatmapDifficulty.difficultyRange(beatmap.difficulty.ar, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin) / clockRate;
3458
+ this.attributes.approachRate = osuBase.BeatmapDifficulty.inverseDifficultyRange(preempt, osuBase.HitObject.preemptMax, osuBase.HitObject.preemptMid, osuBase.HitObject.preemptMin);
3459
+ }
3481
3460
  /**
3482
3461
  * Called after aim skill calculation.
3483
3462
  *
@@ -3784,6 +3763,5 @@ exports.OsuRhythmEvaluator = OsuRhythmEvaluator;
3784
3763
  exports.OsuSpeed = OsuSpeed;
3785
3764
  exports.OsuSpeedEvaluator = OsuSpeedEvaluator;
3786
3765
  exports.PerformanceCalculator = PerformanceCalculator;
3787
- exports.RhythmEvaluator = RhythmEvaluator;
3788
3766
  exports.SpeedEvaluator = SpeedEvaluator;
3789
3767
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rian8337/osu-difficulty-calculator",
3
- "version": "4.0.0-beta.25",
3
+ "version": "4.0.0-beta.26",
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.25"
36
+ "@rian8337/osu-base": "^4.0.0-beta.26"
37
37
  },
38
38
  "publishConfig": {
39
39
  "access": "public"
40
40
  },
41
- "gitHead": "2d6271ea24c787caad1d24c3d20dbeeaf450359e"
41
+ "gitHead": "1c353ee9455d80a5ca8358762f107899d64f1d5e"
42
42
  }
@@ -1,4 +1,4 @@
1
- import { Mod, PlaceableHitObject, Modes, Beatmap, DifficultyStatisticsCalculatorResult, Accuracy } from '@rian8337/osu-base';
1
+ import { Mod, PlaceableHitObject, Modes, Beatmap, Accuracy } from '@rian8337/osu-base';
2
2
 
3
3
  /**
4
4
  * An evaluator for calculating aim skill.
@@ -62,12 +62,6 @@ interface DifficultyAttributes {
62
62
  * The overall clock rate that was applied to the beatmap.
63
63
  */
64
64
  clockRate: number;
65
- /**
66
- * The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
67
- *
68
- * Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
69
- */
70
- approachRate: number;
71
65
  /**
72
66
  * The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc), based on osu!standard judgement.
73
67
  *
@@ -327,7 +321,11 @@ declare abstract class DifficultyCalculator<THitObject extends DifficultyHitObje
327
321
  /**
328
322
  * The difficulty objects of the beatmap.
329
323
  */
330
- readonly objects: THitObject[];
324
+ private _objects;
325
+ /**
326
+ * The difficulty objects of the beatmap.
327
+ */
328
+ get objects(): readonly THitObject[];
331
329
  /**
332
330
  * The modifications applied.
333
331
  */
@@ -336,10 +334,6 @@ declare abstract class DifficultyCalculator<THitObject extends DifficultyHitObje
336
334
  * The total star rating of the beatmap.
337
335
  */
338
336
  get total(): number;
339
- /**
340
- * The difficulty statistics of the beatmap after modifications are applied.
341
- */
342
- difficultyStatistics: DifficultyStatisticsCalculatorResult<number, number, number, number>;
343
337
  /**
344
338
  * The strain peaks of various calculated difficulties.
345
339
  */
@@ -357,7 +351,7 @@ declare abstract class DifficultyCalculator<THitObject extends DifficultyHitObje
357
351
  /**
358
352
  * Constructs a new instance of the calculator.
359
353
  *
360
- * @param beatmap The beatmap to calculate. This beatmap will be deep-cloned to prevent reference changes.
354
+ * @param beatmap The beatmap to calculate.
361
355
  */
362
356
  constructor(beatmap: Beatmap);
363
357
  /**
@@ -379,16 +373,10 @@ declare abstract class DifficultyCalculator<THitObject extends DifficultyHitObje
379
373
  /**
380
374
  * Generates difficulty hitobjects for this calculator.
381
375
  *
382
- * @param convertedBeatmap The beatmap to generate difficulty hitobjects from.
383
- */
384
- protected abstract generateDifficultyHitObjects(convertedBeatmap: Beatmap): THitObject[];
385
- /**
386
- * Computes the difficulty statistics of the original beatmap with respect to the used options.
387
- *
388
- * @param options The options to use for the difficulty statistics calculation.
389
- * @returns The computed difficulty statistics.
376
+ * @param beatmap The beatmap to generate difficulty hitobjects from.
377
+ * @param clockRate The clock rate of the beatmap.
390
378
  */
391
- protected abstract computeDifficultyStatistics(options?: DifficultyCalculationOptions): DifficultyStatisticsCalculatorResult<number, number, number, number>;
379
+ protected abstract generateDifficultyHitObjects(beatmap: Beatmap, clockRate: number): THitObject[];
392
380
  /**
393
381
  * Calculates the skills provided.
394
382
  *
@@ -413,8 +401,11 @@ declare abstract class DifficultyCalculator<THitObject extends DifficultyHitObje
413
401
  protected abstract createSkills(): Skill[];
414
402
  /**
415
403
  * Populates the stored difficulty attributes with necessary data.
404
+ *
405
+ * @param beatmap The beatmap to populate the attributes with.
406
+ * @param clockRate The clock rate of the beatmap.
416
407
  */
417
- protected populateDifficultyAttributes(): void;
408
+ protected populateDifficultyAttributes(beatmap: Beatmap, clockRate: number): void;
418
409
  /**
419
410
  * Calculates the star rating value of a difficulty.
420
411
  *
@@ -616,9 +607,8 @@ declare class DroidDifficultyHitObject extends DifficultyHitObject {
616
607
  * @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
617
608
  * @param clockRate The clock rate of the beatmap.
618
609
  * @param greatWindow The great window of the hitobject.
619
- * @param isForceAR Whether force AR is enabled.
620
610
  */
621
- constructor(object: PlaceableHitObject, lastObject: PlaceableHitObject | null, lastLastObject: PlaceableHitObject | null, difficultyHitObjects: readonly DifficultyHitObject[], clockRate: number, greatWindow: number, isForceAR: boolean);
611
+ constructor(object: PlaceableHitObject, lastObject: PlaceableHitObject | null, lastLastObject: PlaceableHitObject | null, difficultyHitObjects: readonly DifficultyHitObject[], clockRate: number, greatWindow: number);
622
612
  computeProperties(clockRate: number, hitObjects: readonly PlaceableHitObject[]): void;
623
613
  /**
624
614
  * Determines whether this hitobject is considered overlapping with the hitobject before it.
@@ -626,6 +616,9 @@ declare class DroidDifficultyHitObject extends DifficultyHitObject {
626
616
  * Keep in mind that "overlapping" in this case is overlapping to the point where both hitobjects
627
617
  * can be hit with just a single tap in osu!droid.
628
618
  *
619
+ * In the case of sliders, it is considered overlapping if all nested hitobjects can be hit with
620
+ * one aim motion.
621
+ *
629
622
  * @param considerDistance Whether to consider the distance between both hitobjects.
630
623
  * @returns Whether the hitobject is considered overlapping.
631
624
  */
@@ -858,8 +851,7 @@ declare class DroidDifficultyCalculator extends DifficultyCalculator<DroidDiffic
858
851
  calculateTotal(): void;
859
852
  calculateAll(): void;
860
853
  toString(): string;
861
- protected generateDifficultyHitObjects(convertedBeatmap: Beatmap): DroidDifficultyHitObject[];
862
- protected computeDifficultyStatistics(options?: DroidDifficultyCalculationOptions): DifficultyStatisticsCalculatorResult<number, number, number, number>;
854
+ protected generateDifficultyHitObjects(beatmap: Beatmap, clockRate: number): DroidDifficultyHitObject[];
863
855
  protected createSkills(): DroidSkill[];
864
856
  /**
865
857
  * Called after aim skill calculation.
@@ -1266,24 +1258,14 @@ declare class DroidRhythm extends DroidSkill {
1266
1258
  protected saveToHitObject(current: DroidDifficultyHitObject): void;
1267
1259
  }
1268
1260
 
1269
- /**
1270
- * An evaluator for calculating rhythm skill.
1271
- *
1272
- * This class should be considered an "evaluating" class and not persisted.
1273
- */
1274
- declare abstract class RhythmEvaluator {
1275
- protected static readonly rhythmMultiplier: number;
1276
- protected static readonly historyTimeMax: number;
1277
- }
1278
-
1279
1261
  /**
1280
1262
  * An evaluator for calculating osu!droid Rhythm skill.
1281
1263
  */
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;
1264
+ declare abstract class DroidRhythmEvaluator {
1265
+ private static readonly historyTimeMax;
1286
1266
  private static readonly historyObjectsMax;
1267
+ private static readonly rhythmOverallMultiplier;
1268
+ private static readonly rhythmRatioMultiplier;
1287
1269
  /**
1288
1270
  * Calculates a rhythm multiplier for the difficulty of the tap associated
1289
1271
  * with historic data of the current object.
@@ -1481,6 +1463,12 @@ declare abstract class OsuAimEvaluator extends AimEvaluator {
1481
1463
  * Holds data that can be used to calculate osu!standard performance points.
1482
1464
  */
1483
1465
  interface OsuDifficultyAttributes extends DifficultyAttributes {
1466
+ /**
1467
+ * The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
1468
+ *
1469
+ * Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
1470
+ */
1471
+ approachRate: number;
1484
1472
  /**
1485
1473
  * The difficulty corresponding to the speed skill.
1486
1474
  */
@@ -1522,9 +1510,9 @@ declare class OsuDifficultyCalculator extends DifficultyCalculator<OsuDifficulty
1522
1510
  calculateTotal(): void;
1523
1511
  calculateAll(): void;
1524
1512
  toString(): string;
1525
- protected generateDifficultyHitObjects(convertedBeatmap: Beatmap): OsuDifficultyHitObject[];
1526
- protected computeDifficultyStatistics(options?: DifficultyCalculationOptions): DifficultyStatisticsCalculatorResult<number, number, number, number>;
1513
+ protected generateDifficultyHitObjects(beatmap: Beatmap, clockRate: number): OsuDifficultyHitObject[];
1527
1514
  protected createSkills(): OsuSkill[];
1515
+ protected populateDifficultyAttributes(beatmap: Beatmap, clockRate: number): void;
1528
1516
  /**
1529
1517
  * Called after aim skill calculation.
1530
1518
  *
@@ -1632,7 +1620,9 @@ declare class OsuPerformanceCalculator extends PerformanceCalculator<OsuDifficul
1632
1620
  /**
1633
1621
  * An evaluator for calculating osu!standard Rhythm skill.
1634
1622
  */
1635
- declare abstract class OsuRhythmEvaluator extends RhythmEvaluator {
1623
+ declare abstract class OsuRhythmEvaluator {
1624
+ private static readonly rhythmMultiplier;
1625
+ private static readonly historyTimeMax;
1636
1626
  /**
1637
1627
  * Calculates a rhythm multiplier for the difficulty of the tap associated
1638
1628
  * with historic data of the current object.
@@ -1686,4 +1676,4 @@ declare abstract class OsuSpeedEvaluator extends SpeedEvaluator {
1686
1676
  static evaluateDifficultyOf(current: OsuDifficultyHitObject): number;
1687
1677
  }
1688
1678
 
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 };
1679
+ 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, SpeedEvaluator, type StrainPeaks };