@rian8337/osu-difficulty-calculator 4.0.0-beta.30 → 4.0.0-beta.32

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
@@ -664,11 +664,19 @@ class StrainSkill extends Skill {
664
664
  * Strain peaks are stored here.
665
665
  */
666
666
  this.strainPeaks = [];
667
+ this._objectStrains = [];
668
+ this.difficulty = 0;
667
669
  this.sectionLength = 400;
668
670
  this.currentStrain = 0;
669
671
  this.currentSectionPeak = 0;
670
672
  this.currentSectionEnd = 0;
671
673
  }
674
+ /**
675
+ * The strains of hitobjects.
676
+ */
677
+ get objectStrains() {
678
+ return this._objectStrains;
679
+ }
672
680
  process(current) {
673
681
  // The first object doesn't generate a strain, so we begin with an incremented section end
674
682
  if (current.index === 0) {
@@ -694,6 +702,21 @@ class StrainSkill extends Skill {
694
702
  saveCurrentPeak() {
695
703
  this.strainPeaks.push(this.currentSectionPeak);
696
704
  }
705
+ /**
706
+ * Returns the number of strains weighed against the top strain.
707
+ *
708
+ * The result is scaled by clock rate as it affects the total number of strains.
709
+ */
710
+ countDifficultStrains() {
711
+ if (this.difficulty === 0) {
712
+ return 0;
713
+ }
714
+ // This is what the top strain is if all strain values were identical.
715
+ const consistentTopStrain = this.difficulty / 10;
716
+ // Use a weighted sum of all strains.
717
+ return this._objectStrains.reduce((total, next) => total +
718
+ 1.1 / (1 + Math.exp(-10 * (next / consistentTopStrain - 0.88))), 0);
719
+ }
697
720
  /**
698
721
  * Calculates strain decay for a specified time frame.
699
722
  *
@@ -730,32 +753,6 @@ class StrainSkill extends Skill {
730
753
  * and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
731
754
  */
732
755
  class DroidSkill extends StrainSkill {
733
- constructor() {
734
- super(...arguments);
735
- this._objectStrains = [];
736
- this.difficulty = 0;
737
- }
738
- /**
739
- * The strains of hitobjects.
740
- */
741
- get objectStrains() {
742
- return this._objectStrains;
743
- }
744
- /**
745
- * Returns the number of strains weighed against the top strain.
746
- *
747
- * The result is scaled by clock rate as it affects the total number of strains.
748
- */
749
- countDifficultStrains() {
750
- if (this.difficulty === 0) {
751
- return 0;
752
- }
753
- // This is what the top strain is if all strain values were identical.
754
- const consistentTopStrain = this.difficulty / 10;
755
- // Use a weighted sum of all strains.
756
- return this._objectStrains.reduce((total, next) => total +
757
- 1.1 / (1 + Math.exp(-10 * (next / consistentTopStrain - 0.88))), 0);
758
- }
759
756
  process(current) {
760
757
  super.process(current);
761
758
  this._objectStrains.push(this.getObjectStrain(current));
@@ -1137,6 +1134,7 @@ class Island {
1137
1134
  this.deltaCount === other.deltaCount);
1138
1135
  }
1139
1136
  }
1137
+
1140
1138
  /**
1141
1139
  * An evaluator for calculating osu!droid Rhythm skill.
1142
1140
  */
@@ -2086,10 +2084,6 @@ class PerformanceCalculator {
2086
2084
  * The calculated accuracy.
2087
2085
  */
2088
2086
  this.computedAccuracy = new osuBase.Accuracy({});
2089
- /**
2090
- * Penalty for combo breaks.
2091
- */
2092
- this.comboPenalty = 0;
2093
2087
  /**
2094
2088
  * The amount of misses that are filtered out from sliderbreaks.
2095
2089
  */
@@ -2149,7 +2143,6 @@ class PerformanceCalculator {
2149
2143
  const maxCombo = this.difficultyAttributes.maxCombo;
2150
2144
  const miss = this.computedAccuracy.nmiss;
2151
2145
  const combo = (_a = options === null || options === void 0 ? void 0 : options.combo) !== null && _a !== void 0 ? _a : maxCombo - miss;
2152
- this.comboPenalty = Math.min(Math.pow(combo / maxCombo, 0.8), 1);
2153
2146
  if ((options === null || options === void 0 ? void 0 : options.accPercent) instanceof osuBase.Accuracy) {
2154
2147
  // Copy into new instance to not modify the original
2155
2148
  this.computedAccuracy = new osuBase.Accuracy(options.accPercent);
@@ -2187,7 +2180,7 @@ class PerformanceCalculator {
2187
2180
  Math.pow(this.difficultyAttributes.overallDifficulty /
2188
2181
  13.33, 1.8)
2189
2182
  : 1);
2190
- const n50Multiplier = Math.max(0, this.difficultyAttributes.overallDifficulty > 0.0
2183
+ const n50Multiplier = Math.max(0, this.difficultyAttributes.overallDifficulty > 0
2191
2184
  ? 1 -
2192
2185
  Math.pow(this.difficultyAttributes.overallDifficulty /
2193
2186
  13.33, 5)
@@ -2212,6 +2205,22 @@ class PerformanceCalculator {
2212
2205
  this.difficultyAttributes.sliderFactor;
2213
2206
  }
2214
2207
  }
2208
+ /**
2209
+ * Calculates a strain-based miss penalty.
2210
+ *
2211
+ * Strain-based miss penalty assumes that a player will miss on the hardest parts of a map,
2212
+ * so we use the amount of relatively difficult sections to adjust miss penalty
2213
+ * to make it more punishing on maps with lower amount of hard sections.
2214
+ */
2215
+ calculateStrainBasedMissPenalty(difficultStrainCount) {
2216
+ if (this.effectiveMissCount === 0) {
2217
+ return 1;
2218
+ }
2219
+ return (0.96 /
2220
+ (this.effectiveMissCount /
2221
+ (4 * Math.pow(Math.log(difficultStrainCount), 0.94)) +
2222
+ 1));
2223
+ }
2215
2224
  /**
2216
2225
  * Calculates the amount of misses + sliderbreaks from combo.
2217
2226
  */
@@ -2555,22 +2564,6 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2555
2564
  visualValue *= 0.98 + Math.pow(5, 2) / 2500;
2556
2565
  return visualValue;
2557
2566
  }
2558
- /**
2559
- * Calculates a strain-based miss penalty.
2560
- *
2561
- * Strain-based miss penalty assumes that a player will miss on the hardest parts of a map,
2562
- * so we use the amount of relatively difficult sections to adjust miss penalty
2563
- * to make it more punishing on maps with lower amount of hard sections.
2564
- */
2565
- calculateStrainBasedMissPenalty(difficultStrainCount) {
2566
- if (this.effectiveMissCount === 0) {
2567
- return 1;
2568
- }
2569
- return (0.96 /
2570
- (this.effectiveMissCount /
2571
- (4 * Math.pow(Math.log(difficultStrainCount), 0.94)) +
2572
- 1));
2573
- }
2574
2567
  /**
2575
2568
  * The object-based proportional miss penalty.
2576
2569
  */
@@ -2781,17 +2774,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
2781
2774
  * and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
2782
2775
  */
2783
2776
  class OsuSkill extends StrainSkill {
2784
- constructor() {
2785
- super(...arguments);
2786
- /**
2787
- * The final multiplier to be applied to the final difficulty value after all other calculations.
2788
- */
2789
- this.difficultyMultiplier = OsuSkill.defaultDifficultyMultiplier;
2790
- }
2791
2777
  difficultyValue() {
2792
- const strains = this.strainPeaks
2793
- .slice()
2794
- .sort((a, b) => b - a);
2778
+ const strains = this.strainPeaks.slice().sort((a, b) => b - a);
2795
2779
  if (this.reducedSectionCount > 0) {
2796
2780
  // We are reducing the highest strains first to account for extreme difficulty spikes.
2797
2781
  for (let i = 0; i < Math.min(strains.length, this.reducedSectionCount); ++i) {
@@ -2802,25 +2786,19 @@ class OsuSkill extends StrainSkill {
2802
2786
  }
2803
2787
  // Difficulty is the weighted sum of the highest strains from every section.
2804
2788
  // We're sorting from highest to lowest strain.
2805
- let difficulty = 0;
2789
+ this.difficulty = 0;
2806
2790
  let weight = 1;
2807
2791
  for (const strain of strains) {
2808
2792
  const addition = strain * weight;
2809
- if (difficulty + addition === difficulty) {
2793
+ if (this.difficulty + addition === this.difficulty) {
2810
2794
  break;
2811
2795
  }
2812
- difficulty += addition;
2796
+ this.difficulty += addition;
2813
2797
  weight *= this.decayWeight;
2814
2798
  }
2815
- return difficulty * this.difficultyMultiplier;
2799
+ return this.difficulty;
2816
2800
  }
2817
2801
  }
2818
- /**
2819
- * The default multiplier applied to the final difficulty value after all other calculations.
2820
- *
2821
- * May be overridden via {@link difficultyMultiplier}.
2822
- */
2823
- OsuSkill.defaultDifficultyMultiplier = 1.06;
2824
2802
 
2825
2803
  /**
2826
2804
  * An evaluator for calculating osu!standard Aim skill.
@@ -2955,7 +2933,7 @@ class OsuAim extends OsuSkill {
2955
2933
  this.reducedSectionBaseline = 0.75;
2956
2934
  this.decayWeight = 0.9;
2957
2935
  this.currentAimStrain = 0;
2958
- this.skillMultiplier = 23.55;
2936
+ this.skillMultiplier = 25.18;
2959
2937
  this.withSliders = withSliders;
2960
2938
  }
2961
2939
  strainValueAt(current) {
@@ -2963,6 +2941,7 @@ class OsuAim extends OsuSkill {
2963
2941
  this.currentAimStrain +=
2964
2942
  OsuAimEvaluator.evaluateDifficultyOf(current, this.withSliders) *
2965
2943
  this.skillMultiplier;
2944
+ this._objectStrains.push(this.currentAimStrain);
2966
2945
  return this.currentAimStrain;
2967
2946
  }
2968
2947
  calculateInitialStrain(time, current) {
@@ -3008,24 +2987,32 @@ class OsuSpeedEvaluator extends SpeedEvaluator {
3008
2987
  // Cap deltatime to the OD 300 hitwindow.
3009
2988
  // 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.
3010
2989
  strainTime /= osuBase.MathUtils.clamp(strainTime / current.fullGreatWindow / 0.93, 0.92, 1);
3011
- let speedBonus = 1;
2990
+ // speedBonus will be 0.0 for BPM < 200
2991
+ let speedBonus = 0;
2992
+ // Add additional scaling bonus for streams/bursts higher than 200bpm
3012
2993
  if (strainTime < this.minSpeedBonus) {
3013
- speedBonus +=
2994
+ speedBonus =
3014
2995
  0.75 * Math.pow((this.minSpeedBonus - strainTime) / 40, 2);
3015
2996
  }
3016
2997
  const travelDistance = (_a = prev === null || prev === void 0 ? void 0 : prev.travelDistance) !== null && _a !== void 0 ? _a : 0;
2998
+ // Cap distance at spacing threshold
3017
2999
  const distance = Math.min(this.SINGLE_SPACING_THRESHOLD, travelDistance + current.minimumJumpDistance);
3018
- return (((speedBonus +
3019
- speedBonus *
3020
- Math.pow(distance / this.SINGLE_SPACING_THRESHOLD, 3.5)) *
3021
- doubletapness) /
3022
- strainTime);
3000
+ // Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
3001
+ const distanceBonus = Math.pow(distance / this.SINGLE_SPACING_THRESHOLD, 3.95) *
3002
+ this.DISTANCE_MULTIPLIER;
3003
+ // Base difficulty with all bonuses
3004
+ const difficulty = ((1 + speedBonus + distanceBonus) * 1000) / strainTime;
3005
+ // Apply penalty if there's doubletappable doubles
3006
+ return difficulty * doubletapness;
3023
3007
  }
3024
3008
  }
3025
3009
  /**
3026
3010
  * Spacing threshold for a single hitobject spacing.
3011
+ *
3012
+ * About 1.25 circles distance between hitobject centers.
3027
3013
  */
3028
3014
  OsuSpeedEvaluator.SINGLE_SPACING_THRESHOLD = 125;
3015
+ OsuSpeedEvaluator.DISTANCE_MULTIPLIER = 0.94;
3029
3016
 
3030
3017
  /**
3031
3018
  * An evaluator for calculating osu!standard Rhythm skill.
@@ -3036,20 +3023,21 @@ class OsuRhythmEvaluator {
3036
3023
  * with historic data of the current object.
3037
3024
  *
3038
3025
  * @param current The current object.
3039
- * @param greatWindow The great hit window of the current object.
3040
3026
  */
3041
3027
  static evaluateDifficultyOf(current) {
3042
3028
  if (current.object instanceof osuBase.Spinner) {
3043
3029
  return 0;
3044
3030
  }
3045
- let previousIslandSize = 0;
3031
+ const deltaDifferenceEpsilon = current.fullGreatWindow * 0.3;
3046
3032
  let rhythmComplexitySum = 0;
3047
- let islandSize = 1;
3033
+ let island = new Island(deltaDifferenceEpsilon);
3034
+ let previousIsland = new Island(deltaDifferenceEpsilon);
3035
+ const islandCounts = new Map();
3048
3036
  // Store the ratio of the current start of an island to buff for tighter rhythms.
3049
3037
  let startRatio = 0;
3050
3038
  let firstDeltaSwitch = false;
3051
- const historicalNoteCount = Math.min(current.index, 32);
3052
3039
  let rhythmStart = 0;
3040
+ const historicalNoteCount = Math.min(current.index, this.historyObjectsMax);
3053
3041
  while (rhythmStart < historicalNoteCount - 2 &&
3054
3042
  current.startTime - current.previous(rhythmStart).startTime <
3055
3043
  this.historyTimeMax) {
@@ -3060,84 +3048,119 @@ class OsuRhythmEvaluator {
3060
3048
  const prevObject = current.previous(i);
3061
3049
  const lastObject = current.previous(i + 1);
3062
3050
  // Scale note 0 to 1 from history to now.
3063
- let currentHistoricalDecay = (this.historyTimeMax -
3051
+ const timeDecay = (this.historyTimeMax -
3064
3052
  (current.startTime - currentObject.startTime)) /
3065
3053
  this.historyTimeMax;
3054
+ const noteDecay = (historicalNoteCount - i) / historicalNoteCount;
3066
3055
  // Either we're limited by time or limited by object count.
3067
- currentHistoricalDecay = Math.min(currentHistoricalDecay, (historicalNoteCount - i) / historicalNoteCount);
3056
+ const currentHistoricalDecay = Math.min(timeDecay, noteDecay);
3068
3057
  const currentDelta = currentObject.strainTime;
3069
3058
  const prevDelta = prevObject.strainTime;
3070
3059
  const lastDelta = lastObject.strainTime;
3060
+ // Calculate how much current delta difference deserves a rhythm bonus
3061
+ // This function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e. 100 and 200)
3062
+ const deltaDifferenceRatio = Math.min(prevDelta, currentDelta) /
3063
+ Math.max(prevDelta, currentDelta);
3071
3064
  const currentRatio = 1 +
3072
- 6 *
3073
- Math.min(0.5, Math.pow(Math.sin(Math.PI /
3074
- (Math.min(prevDelta, currentDelta) /
3075
- Math.max(prevDelta, currentDelta))), 2));
3076
- const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) -
3077
- current.fullGreatWindow * 0.3) /
3078
- (current.fullGreatWindow * 0.3));
3079
- let effectiveRatio = windowPenalty * currentRatio;
3065
+ this.rhythmRatioMultiplier *
3066
+ Math.min(0.5, Math.pow(Math.sin(Math.PI / deltaDifferenceRatio), 2));
3067
+ // Reduce ratio bonus if delta difference is too big
3068
+ const fraction = Math.max(prevDelta / currentDelta, currentDelta / prevDelta);
3069
+ const fractionMultiplier = osuBase.MathUtils.clamp(2 - fraction / 8, 0, 1);
3070
+ const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon);
3071
+ let effectiveRatio = windowPenalty * currentRatio * fractionMultiplier;
3080
3072
  if (firstDeltaSwitch) {
3081
- if (prevDelta <= 1.25 * currentDelta &&
3082
- prevDelta * 1.25 >= currentDelta) {
3073
+ if (Math.abs(prevDelta - currentDelta) < deltaDifferenceEpsilon) {
3083
3074
  // Island is still progressing, count size.
3084
- if (islandSize < 7) {
3085
- ++islandSize;
3086
- }
3075
+ island.addDelta(currentDelta);
3087
3076
  }
3088
3077
  else {
3078
+ // BPM change is into slider, this is easy acc window.
3089
3079
  if (currentObject.object instanceof osuBase.Slider) {
3090
- // BPM change is into slider, this is easy acc window.
3091
3080
  effectiveRatio /= 8;
3092
3081
  }
3082
+ // BPM change was from a slider, this is easier typically than circle -> circle.
3083
+ // Unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty
3084
+ // than bursts without sliders.
3093
3085
  if (prevObject.object instanceof osuBase.Slider) {
3094
- // BPM change was from a slider, this is typically easier than circle -> circle.
3095
- effectiveRatio /= 4;
3096
- }
3097
- if (previousIslandSize === islandSize) {
3098
- // Repeated island size (ex: triplet -> triplet).
3099
- effectiveRatio /= 4;
3086
+ effectiveRatio *= 0.3;
3100
3087
  }
3101
- if (previousIslandSize % 2 === islandSize % 2) {
3102
- // Repeated island polarity (2 -> 4, 3 -> 5).
3088
+ // Repeated island polarity (2 -> 4, 3 -> 5).
3089
+ if (island.isSimilarPolarity(previousIsland)) {
3103
3090
  effectiveRatio /= 2;
3104
3091
  }
3105
- if (lastDelta > prevDelta + 10 &&
3106
- prevDelta > currentDelta + 10) {
3107
- // Previous increase happened a note ago.
3108
- // Albeit this is a 1/1 -> 1/2-1/4 type of transition, we don't want to buff this.
3092
+ // Previous increase happened a note ago.
3093
+ // Albeit this is a 1/1 -> 1/2-1/4 type of transition, we don't want to buff this.
3094
+ if (lastDelta > prevDelta + deltaDifferenceEpsilon &&
3095
+ prevDelta > currentDelta + deltaDifferenceEpsilon) {
3109
3096
  effectiveRatio /= 8;
3110
3097
  }
3098
+ // Repeated island size (ex: triplet -> triplet).
3099
+ // TODO: remove this nerf since its staying here only for balancing purposes because of the flawed ratio calculation
3100
+ if (previousIsland.deltaCount == island.deltaCount) {
3101
+ effectiveRatio /= 2;
3102
+ }
3103
+ let islandFound = false;
3104
+ for (const [currentIsland, count] of islandCounts) {
3105
+ if (!island.equals(currentIsland)) {
3106
+ continue;
3107
+ }
3108
+ islandFound = true;
3109
+ let islandCount = count;
3110
+ if (previousIsland.equals(island)) {
3111
+ // Only add island to island counts if they're going one after another.
3112
+ ++islandCount;
3113
+ islandCounts.set(currentIsland, islandCount);
3114
+ }
3115
+ // Repeated island (ex: triplet -> triplet).
3116
+ // Graph: https://www.desmos.com/calculator/pj7an56zwf
3117
+ effectiveRatio *= Math.min(3 / islandCount, Math.pow(1 / islandCount, 2.75 / (1 + Math.exp(14 - 0.24 * island.delta))));
3118
+ break;
3119
+ }
3120
+ if (!islandFound) {
3121
+ islandCounts.set(island, 1);
3122
+ }
3123
+ // Scale down the difficulty if the object is doubletappable.
3124
+ effectiveRatio *= 1 - prevObject.doubletapness * 0.75;
3111
3125
  rhythmComplexitySum +=
3112
- (((Math.sqrt(effectiveRatio * startRatio) *
3113
- currentHistoricalDecay *
3114
- Math.sqrt(4 + islandSize)) /
3115
- 2) *
3116
- Math.sqrt(4 + previousIslandSize)) /
3117
- 2;
3126
+ Math.sqrt(effectiveRatio * startRatio) *
3127
+ currentHistoricalDecay;
3118
3128
  startRatio = effectiveRatio;
3119
- previousIslandSize = islandSize;
3120
- if (prevDelta * 1.25 < currentDelta) {
3129
+ previousIsland = island;
3130
+ if (prevDelta + deltaDifferenceEpsilon < currentDelta) {
3121
3131
  // We're slowing down, stop counting.
3122
3132
  // If we're speeding up, this stays as is and we keep counting island size.
3123
3133
  firstDeltaSwitch = false;
3124
3134
  }
3125
- islandSize = 1;
3135
+ island = new Island(currentDelta, deltaDifferenceEpsilon);
3126
3136
  }
3127
3137
  }
3128
- else if (prevDelta > 1.25 * currentDelta) {
3129
- // We want to be speeding up.
3138
+ else if (prevDelta > currentDelta + deltaDifferenceEpsilon) {
3139
+ // We are speeding up.
3130
3140
  // Begin counting island until we change speed again.
3131
3141
  firstDeltaSwitch = true;
3142
+ // BPM change is into slider, this is easy acc window.
3143
+ if (currentObject.object instanceof osuBase.Slider) {
3144
+ effectiveRatio *= 0.6;
3145
+ }
3146
+ // BPM change was from a slider, this is easier typically than circle -> circle
3147
+ // Unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty
3148
+ // than bursts without sliders
3149
+ if (prevObject.object instanceof osuBase.Slider) {
3150
+ effectiveRatio *= 0.6;
3151
+ }
3132
3152
  startRatio = effectiveRatio;
3133
- islandSize = 1;
3153
+ island = new Island(currentDelta, deltaDifferenceEpsilon);
3134
3154
  }
3135
3155
  }
3136
- return Math.sqrt(4 + rhythmComplexitySum * this.rhythmMultiplier) / 2;
3156
+ return (Math.sqrt(4 + rhythmComplexitySum * this.rhythmOverallMultiplier) /
3157
+ 2);
3137
3158
  }
3138
3159
  }
3139
- OsuRhythmEvaluator.rhythmMultiplier = 0.75;
3140
3160
  OsuRhythmEvaluator.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
3161
+ OsuRhythmEvaluator.historyObjectsMax = 32;
3162
+ OsuRhythmEvaluator.rhythmOverallMultiplier = 0.95;
3163
+ OsuRhythmEvaluator.rhythmRatioMultiplier = 12;
3141
3164
 
3142
3165
  /**
3143
3166
  * Represents the skill required to press keys or tap with regards to keeping up with the speed at which objects need to be hit.
@@ -3148,11 +3171,10 @@ class OsuSpeed extends OsuSkill {
3148
3171
  this.strainDecayBase = 0.3;
3149
3172
  this.reducedSectionCount = 5;
3150
3173
  this.reducedSectionBaseline = 0.75;
3151
- this.difficultyMultiplier = 1.04;
3152
3174
  this.decayWeight = 0.9;
3153
3175
  this.currentSpeedStrain = 0;
3154
3176
  this.currentRhythm = 0;
3155
- this.skillMultiplier = 1375;
3177
+ this.skillMultiplier = 1.43;
3156
3178
  }
3157
3179
  /**
3158
3180
  * @param current The hitobject to calculate.
@@ -3163,7 +3185,9 @@ class OsuSpeed extends OsuSkill {
3163
3185
  OsuSpeedEvaluator.evaluateDifficultyOf(current) *
3164
3186
  this.skillMultiplier;
3165
3187
  this.currentRhythm = OsuRhythmEvaluator.evaluateDifficultyOf(current);
3166
- return this.currentSpeedStrain * this.currentRhythm;
3188
+ const strain = this.currentSpeedStrain * this.currentRhythm;
3189
+ this._objectStrains.push(strain);
3190
+ return strain;
3167
3191
  }
3168
3192
  calculateInitialStrain(time, current) {
3169
3193
  var _a, _b;
@@ -3211,7 +3235,7 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
3211
3235
  if (!(currentObject.object instanceof osuBase.Spinner)) {
3212
3236
  const jumpDistance = current.object
3213
3237
  .getStackedPosition(osuBase.Modes.osu)
3214
- .subtract(currentObject.object.endPosition).length;
3238
+ .subtract(currentObject.object.getStackedEndPosition(osuBase.Modes.osu)).length;
3215
3239
  cumulativeStrainTime += last.strainTime;
3216
3240
  // We want to nerf objects that can be easily seen within the Flashlight circle radius.
3217
3241
  if (i === 0) {
@@ -3273,9 +3297,12 @@ class OsuFlashlight extends OsuSkill {
3273
3297
  this.reducedSectionBaseline = 1;
3274
3298
  this.decayWeight = 1;
3275
3299
  this.currentFlashlightStrain = 0;
3276
- this.skillMultiplier = 0.052;
3300
+ this.skillMultiplier = 0.05512;
3277
3301
  this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
3278
3302
  }
3303
+ difficultyValue() {
3304
+ return this.strainPeaks.reduce((a, b) => a + b, 0);
3305
+ }
3279
3306
  strainValueAt(current) {
3280
3307
  this.currentFlashlightStrain *= this.strainDecay(current.deltaTime);
3281
3308
  this.currentFlashlightStrain +=
@@ -3343,6 +3370,8 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3343
3370
  hitCircleCount: 0,
3344
3371
  sliderCount: 0,
3345
3372
  spinnerCount: 0,
3373
+ aimDifficultStrainCount: 0,
3374
+ speedDifficultStrainCount: 0,
3346
3375
  };
3347
3376
  this.difficultyMultiplier = 0.0675;
3348
3377
  this.mode = osuBase.Modes.osu;
@@ -3411,7 +3440,7 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3411
3440
  // Document for formula derivation:
3412
3441
  // https://docs.google.com/document/d/10DZGYYSsT_yjz2Mtp6yIJld0Rqx4E-vVHupCqiM4TNI/edit
3413
3442
  this.attributes.starRating =
3414
- Math.cbrt(1.14) *
3443
+ Math.cbrt(1.15) *
3415
3444
  0.027 *
3416
3445
  (Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformanceValue) +
3417
3446
  4);
@@ -3496,6 +3525,8 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3496
3525
  if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
3497
3526
  this.attributes.aimDifficulty *= 0.9;
3498
3527
  }
3528
+ this.attributes.aimDifficultStrainCount =
3529
+ aimSkill.countDifficultStrains();
3499
3530
  }
3500
3531
  /**
3501
3532
  * Called after speed skill calculation.
@@ -3505,6 +3536,8 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
3505
3536
  postCalculateSpeed(speedSkill) {
3506
3537
  this.strainPeaks.speed = speedSkill.strainPeaks;
3507
3538
  this.attributes.speedDifficulty = this.starValue(speedSkill.difficultyValue());
3539
+ this.attributes.speedDifficultStrainCount =
3540
+ speedSkill.countDifficultStrains();
3508
3541
  }
3509
3542
  /**
3510
3543
  * Calculates speed-related attributes.
@@ -3555,8 +3588,9 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
3555
3588
  * The flashlight performance value.
3556
3589
  */
3557
3590
  this.flashlight = 0;
3558
- this.finalMultiplier = 1.14;
3591
+ this.finalMultiplier = 1.15;
3559
3592
  this.mode = osuBase.Modes.osu;
3593
+ this.comboPenalty = 1;
3560
3594
  }
3561
3595
  calculateValues() {
3562
3596
  this.aim = this.calculateAimValue();
@@ -3570,6 +3604,14 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
3570
3604
  Math.pow(this.accuracy, 1.1) +
3571
3605
  Math.pow(this.flashlight, 1.1), 1 / 1.1) * this.finalMultiplier);
3572
3606
  }
3607
+ handleOptions(options) {
3608
+ var _a;
3609
+ super.handleOptions(options);
3610
+ const maxCombo = this.difficultyAttributes.maxCombo;
3611
+ const miss = this.computedAccuracy.nmiss;
3612
+ const combo = (_a = options === null || options === void 0 ? void 0 : options.combo) !== null && _a !== void 0 ? _a : maxCombo - miss;
3613
+ this.comboPenalty = Math.min(Math.pow(combo / maxCombo, 0.8), 1);
3614
+ }
3573
3615
  /**
3574
3616
  * Calculates the aim performance value of the beatmap.
3575
3617
  */
@@ -3581,16 +3623,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
3581
3623
  lengthBonus += Math.log10(this.totalHits / 2000) * 0.5;
3582
3624
  }
3583
3625
  aimValue *= lengthBonus;
3584
- if (this.effectiveMissCount > 0) {
3585
- // Penalize misses by assessing # of misses relative to the total # of objects.
3586
- // Default a 3% reduction for any # of misses.
3587
- aimValue *=
3588
- 0.97 *
3589
- Math.pow(1 -
3590
- Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
3591
- }
3592
- // Combo scaling
3593
- aimValue *= this.comboPenalty;
3626
+ aimValue *= this.calculateStrainBasedMissPenalty(this.difficultyAttributes.aimDifficultStrainCount);
3594
3627
  const calculatedAR = this.difficultyAttributes.approachRate;
3595
3628
  if (!this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModRelax)) {
3596
3629
  // AR scaling
@@ -3631,16 +3664,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
3631
3664
  lengthBonus += Math.log10(this.totalHits / 2000) * 0.5;
3632
3665
  }
3633
3666
  speedValue *= lengthBonus;
3634
- if (this.effectiveMissCount > 0) {
3635
- // Penalize misses by assessing # of misses relative to the total # of objects.
3636
- // Default a 3% reduction for any # of misses.
3637
- speedValue *=
3638
- 0.97 *
3639
- Math.pow(1 -
3640
- Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
3641
- }
3642
- // Combo scaling
3643
- speedValue *= this.comboPenalty;
3667
+ speedValue *= this.calculateStrainBasedMissPenalty(this.difficultyAttributes.speedDifficultStrainCount);
3644
3668
  // AR scaling
3645
3669
  const calculatedAR = this.difficultyAttributes.approachRate;
3646
3670
  if (calculatedAR > 10.33) {
@@ -3671,9 +3695,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
3671
3695
  750) *
3672
3696
  Math.pow((this.computedAccuracy.value() +
3673
3697
  relevantAccuracy.value(this.difficultyAttributes.speedNoteCount)) /
3674
- 2, (14.5 -
3675
- Math.max(this.difficultyAttributes.overallDifficulty, 8)) /
3676
- 2);
3698
+ 2, (14.5 - this.difficultyAttributes.overallDifficulty) / 2);
3677
3699
  // Scale the speed value with # of 50s to punish doubletapping.
3678
3700
  speedValue *= Math.pow(0.99, Math.max(0, this.computedAccuracy.n50 - this.totalHits / 500));
3679
3701
  return speedValue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rian8337/osu-difficulty-calculator",
3
- "version": "4.0.0-beta.30",
3
+ "version": "4.0.0-beta.32",
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.30"
36
+ "@rian8337/osu-base": "^4.0.0-beta.31"
37
37
  },
38
38
  "publishConfig": {
39
39
  "access": "public"
40
40
  },
41
- "gitHead": "99b10d7679a476cb5bc87fb91431a6d09ce26ec8"
41
+ "gitHead": "dd164dcc63fea94501959236d310eccf5a7cda6a"
42
42
  }
@@ -80,6 +80,10 @@ interface DifficultyAttributes {
80
80
  * The number of spinners in the beatmap.
81
81
  */
82
82
  spinnerCount: number;
83
+ /**
84
+ * The amount of strains that are considered difficult with respect to the aim skill.
85
+ */
86
+ aimDifficultStrainCount: number;
83
87
  }
84
88
 
85
89
  /**
@@ -471,6 +475,12 @@ declare abstract class StrainSkill extends Skill {
471
475
  * For example, a value of 0.15 indicates that strain decays to 15% of its original value in one second.
472
476
  */
473
477
  protected abstract readonly strainDecayBase: number;
478
+ protected readonly _objectStrains: number[];
479
+ protected difficulty: number;
480
+ /**
481
+ * The strains of hitobjects.
482
+ */
483
+ get objectStrains(): readonly number[];
474
484
  private readonly sectionLength;
475
485
  private currentStrain;
476
486
  private currentSectionPeak;
@@ -480,6 +490,12 @@ declare abstract class StrainSkill extends Skill {
480
490
  * Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty.
481
491
  */
482
492
  saveCurrentPeak(): void;
493
+ /**
494
+ * Returns the number of strains weighed against the top strain.
495
+ *
496
+ * The result is scaled by clock rate as it affects the total number of strains.
497
+ */
498
+ countDifficultStrains(): number;
483
499
  /**
484
500
  * Calculates strain decay for a specified time frame.
485
501
  *
@@ -529,18 +545,6 @@ declare abstract class DroidSkill extends StrainSkill {
529
545
  * The bonus multiplier that is given for a sequence of notes of equal difficulty.
530
546
  */
531
547
  protected abstract readonly starsPerDouble: number;
532
- protected readonly _objectStrains: number[];
533
- /**
534
- * The strains of hitobjects.
535
- */
536
- get objectStrains(): readonly number[];
537
- private difficulty;
538
- /**
539
- * Returns the number of strains weighed against the top strain.
540
- *
541
- * The result is scaled by clock rate as it affects the total number of strains.
542
- */
543
- countDifficultStrains(): number;
544
548
  process(current: DifficultyHitObject): void;
545
549
  difficultyValue(): number;
546
550
  /**
@@ -702,10 +706,6 @@ interface DroidDifficultyAttributes extends DifficultyAttributes {
702
706
  * The difficulty corresponding to the visual skill.
703
707
  */
704
708
  visualDifficulty: number;
705
- /**
706
- * The amount of strains that are considered difficult with respect to the aim skill.
707
- */
708
- aimDifficultStrainCount: number;
709
709
  /**
710
710
  * The amount of strains that are considered difficult with respect to the tap skill.
711
711
  */
@@ -1009,10 +1009,6 @@ declare abstract class PerformanceCalculator<T extends DifficultyAttributes> {
1009
1009
  * The difficulty attributes that is being calculated.
1010
1010
  */
1011
1011
  readonly difficultyAttributes: T;
1012
- /**
1013
- * Penalty for combo breaks.
1014
- */
1015
- protected comboPenalty: number;
1016
1012
  /**
1017
1013
  * The global multiplier to be applied to the final performance value.
1018
1014
  *
@@ -1073,6 +1069,14 @@ declare abstract class PerformanceCalculator<T extends DifficultyAttributes> {
1073
1069
  * @param options Options for performance calculation.
1074
1070
  */
1075
1071
  protected handleOptions(options?: PerformanceCalculationOptions): void;
1072
+ /**
1073
+ * Calculates a strain-based miss penalty.
1074
+ *
1075
+ * Strain-based miss penalty assumes that a player will miss on the hardest parts of a map,
1076
+ * so we use the amount of relatively difficult sections to adjust miss penalty
1077
+ * to make it more punishing on maps with lower amount of hard sections.
1078
+ */
1079
+ protected calculateStrainBasedMissPenalty(difficultStrainCount: number): number;
1076
1080
  /**
1077
1081
  * Calculates the amount of misses + sliderbreaks from combo.
1078
1082
  */
@@ -1205,14 +1209,6 @@ declare class DroidPerformanceCalculator extends PerformanceCalculator<DroidDiff
1205
1209
  * Calculates the visual performance value of the beatmap.
1206
1210
  */
1207
1211
  private calculateVisualValue;
1208
- /**
1209
- * Calculates a strain-based miss penalty.
1210
- *
1211
- * Strain-based miss penalty assumes that a player will miss on the hardest parts of a map,
1212
- * so we use the amount of relatively difficult sections to adjust miss penalty
1213
- * to make it more punishing on maps with lower amount of hard sections.
1214
- */
1215
- private calculateStrainBasedMissPenalty;
1216
1212
  /**
1217
1213
  * The object-based proportional miss penalty.
1218
1214
  */
@@ -1396,16 +1392,6 @@ declare abstract class DroidVisualEvaluator {
1396
1392
  * and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
1397
1393
  */
1398
1394
  declare abstract class OsuSkill extends StrainSkill {
1399
- /**
1400
- * The default multiplier applied to the final difficulty value after all other calculations.
1401
- *
1402
- * May be overridden via {@link difficultyMultiplier}.
1403
- */
1404
- static readonly defaultDifficultyMultiplier: number;
1405
- /**
1406
- * The final multiplier to be applied to the final difficulty value after all other calculations.
1407
- */
1408
- protected readonly difficultyMultiplier: number;
1409
1395
  /**
1410
1396
  * The weight by which each strain value decays.
1411
1397
  */
@@ -1434,10 +1420,10 @@ declare class OsuDifficultyHitObject extends DifficultyHitObject {
1434
1420
  * Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
1435
1421
  */
1436
1422
  declare class OsuAim extends OsuSkill {
1437
- protected readonly strainDecayBase: number;
1438
- protected readonly reducedSectionCount: number;
1439
- protected readonly reducedSectionBaseline: number;
1440
- protected readonly decayWeight: number;
1423
+ protected readonly strainDecayBase = 0.15;
1424
+ protected readonly reducedSectionCount = 10;
1425
+ protected readonly reducedSectionBaseline = 0.75;
1426
+ protected readonly decayWeight = 0.9;
1441
1427
  private currentAimStrain;
1442
1428
  private readonly skillMultiplier;
1443
1429
  private readonly withSliders;
@@ -1482,6 +1468,10 @@ interface OsuDifficultyAttributes extends DifficultyAttributes {
1482
1468
  * The difficulty corresponding to the speed skill.
1483
1469
  */
1484
1470
  speedDifficulty: number;
1471
+ /**
1472
+ * The amount of strains that are considered difficult with respect to the speed skill.
1473
+ */
1474
+ speedDifficultStrainCount: number;
1485
1475
  }
1486
1476
 
1487
1477
  /**
@@ -1551,14 +1541,15 @@ declare class OsuDifficultyCalculator extends DifficultyCalculator<OsuDifficulty
1551
1541
  * Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
1552
1542
  */
1553
1543
  declare class OsuFlashlight extends OsuSkill {
1554
- protected readonly strainDecayBase: number;
1555
- protected readonly reducedSectionCount: number;
1556
- protected readonly reducedSectionBaseline: number;
1557
- protected readonly decayWeight: number;
1544
+ protected readonly strainDecayBase = 0.15;
1545
+ protected readonly reducedSectionCount = 0;
1546
+ protected readonly reducedSectionBaseline = 1;
1547
+ protected readonly decayWeight = 1;
1558
1548
  private currentFlashlightStrain;
1559
1549
  private readonly skillMultiplier;
1560
1550
  private readonly isHidden;
1561
1551
  constructor(mods: Mod[]);
1552
+ difficultyValue(): number;
1562
1553
  protected strainValueAt(current: OsuDifficultyHitObject): number;
1563
1554
  protected calculateInitialStrain(time: number, current: OsuDifficultyHitObject): number;
1564
1555
  protected saveToHitObject(current: OsuDifficultyHitObject): void;
@@ -1604,9 +1595,11 @@ declare class OsuPerformanceCalculator extends PerformanceCalculator<OsuDifficul
1604
1595
  */
1605
1596
  flashlight: number;
1606
1597
  protected finalMultiplier: number;
1607
- protected readonly mode: Modes;
1598
+ protected readonly mode = Modes.osu;
1599
+ private comboPenalty;
1608
1600
  protected calculateValues(): void;
1609
1601
  protected calculateTotalValue(): number;
1602
+ protected handleOptions(options?: PerformanceCalculationOptions): void;
1610
1603
  /**
1611
1604
  * Calculates the aim performance value of the beatmap.
1612
1605
  */
@@ -1630,14 +1623,15 @@ declare class OsuPerformanceCalculator extends PerformanceCalculator<OsuDifficul
1630
1623
  * An evaluator for calculating osu!standard Rhythm skill.
1631
1624
  */
1632
1625
  declare abstract class OsuRhythmEvaluator {
1633
- private static readonly rhythmMultiplier;
1634
1626
  private static readonly historyTimeMax;
1627
+ private static readonly historyObjectsMax;
1628
+ private static readonly rhythmOverallMultiplier;
1629
+ private static readonly rhythmRatioMultiplier;
1635
1630
  /**
1636
1631
  * Calculates a rhythm multiplier for the difficulty of the tap associated
1637
1632
  * with historic data of the current object.
1638
1633
  *
1639
1634
  * @param current The current object.
1640
- * @param greatWindow The great hit window of the current object.
1641
1635
  */
1642
1636
  static evaluateDifficultyOf(current: OsuDifficultyHitObject): number;
1643
1637
  }
@@ -1649,7 +1643,6 @@ declare class OsuSpeed extends OsuSkill {
1649
1643
  protected readonly strainDecayBase = 0.3;
1650
1644
  protected readonly reducedSectionCount = 5;
1651
1645
  protected readonly reducedSectionBaseline = 0.75;
1652
- protected readonly difficultyMultiplier = 1.04;
1653
1646
  protected readonly decayWeight = 0.9;
1654
1647
  private currentSpeedStrain;
1655
1648
  private currentRhythm;
@@ -1671,8 +1664,11 @@ declare class OsuSpeed extends OsuSkill {
1671
1664
  declare abstract class OsuSpeedEvaluator extends SpeedEvaluator {
1672
1665
  /**
1673
1666
  * Spacing threshold for a single hitobject spacing.
1667
+ *
1668
+ * About 1.25 circles distance between hitobject centers.
1674
1669
  */
1675
1670
  private static readonly SINGLE_SPACING_THRESHOLD;
1671
+ private static readonly DISTANCE_MULTIPLIER;
1676
1672
  /**
1677
1673
  * Evaluates the difficulty of tapping the current object, based on:
1678
1674
  *