@rian8337/osu-difficulty-calculator 1.0.2 → 1.1.1

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.
@@ -74,18 +74,16 @@ class DroidPerformanceCalculator extends PerformanceCalculator_1.PerformanceCalc
74
74
  // Combo scaling
75
75
  this.aim *= this.comboPenalty;
76
76
  // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
77
- let hiddenBonus = 1;
78
77
  if (this.stars.mods.some((m) => m instanceof osu_base_1.ModHidden)) {
79
78
  // The bonus starts decreasing twice as fast
80
79
  // beyond AR10 and reaches 1 at AR11.
81
80
  if (calculatedAR > 10) {
82
- hiddenBonus += Math.max(0, 0.08 * (11 - calculatedAR));
81
+ this.aim *= 1 + Math.max(0, 0.08 * (11 - calculatedAR));
83
82
  }
84
83
  else {
85
- hiddenBonus += 0.04 * (12 - calculatedAR);
84
+ this.aim *= 1 + 0.04 * (12 - calculatedAR);
86
85
  }
87
86
  }
88
- this.aim *= hiddenBonus;
89
87
  // AR scaling
90
88
  let arFactor = 0;
91
89
  if (calculatedAR > 10.33) {
@@ -6,6 +6,7 @@ const DroidTap_1 = require("./skills/DroidTap");
6
6
  const StarRating_1 = require("./base/StarRating");
7
7
  const DroidFlashlight_1 = require("./skills/DroidFlashlight");
8
8
  const osu_base_1 = require("@rian8337/osu-base");
9
+ const DroidRhythm_1 = require("./skills/DroidRhythm");
9
10
  /**
10
11
  * Difficulty calculator for osu!droid gamemode.
11
12
  */
@@ -20,6 +21,10 @@ class DroidStarRating extends StarRating_1.StarRating {
20
21
  * The tap star rating of the beatmap.
21
22
  */
22
23
  this.tap = 0;
24
+ /**
25
+ * The rhythm star rating of the beatmap.
26
+ */
27
+ this.rhythm = 0;
23
28
  /**
24
29
  * The flashlight star rating of the beatmap.
25
30
  */
@@ -67,9 +72,6 @@ class DroidStarRating extends StarRating_1.StarRating {
67
72
  * Calculates the speed star rating of the beatmap and stores it in this instance.
68
73
  */
69
74
  calculateTap() {
70
- if (this.mods.some((m) => m instanceof osu_base_1.ModRelax)) {
71
- return;
72
- }
73
75
  const tapSkill = new DroidTap_1.DroidTap(this.mods, this.stats.od);
74
76
  this.calculateSkills(tapSkill);
75
77
  this.strainPeaks.speed = tapSkill.strainPeaks;
@@ -80,6 +82,14 @@ class DroidStarRating extends StarRating_1.StarRating {
80
82
  this.attributes.speedNoteCount = objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
81
83
  }
82
84
  }
85
+ /**
86
+ * Calculates the rhythm star rating of the beatmap and stores it in this instance.
87
+ */
88
+ calculateRhythm() {
89
+ const rhythmSkill = new DroidRhythm_1.DroidRhythm(this.mods, this.stats.od);
90
+ this.calculateSkills(rhythmSkill);
91
+ this.rhythm = this.starValue(rhythmSkill.difficultyValue());
92
+ }
83
93
  /**
84
94
  * Calculates the flashlight star rating of the beatmap and stores it in this instance.
85
95
  */
@@ -91,12 +101,12 @@ class DroidStarRating extends StarRating_1.StarRating {
91
101
  }
92
102
  calculateTotal() {
93
103
  const aimPerformanceValue = this.basePerformanceValue(this.aim);
94
- const speedPerformanceValue = this.basePerformanceValue(this.tap);
104
+ const tapPerformanceValue = this.basePerformanceValue(this.tap);
95
105
  const flashlightPerformanceValue = this.mods.some((m) => m instanceof osu_base_1.ModFlashlight)
96
106
  ? Math.pow(this.flashlight, 2) * 25
97
107
  : 0;
98
108
  const basePerformanceValue = Math.pow(Math.pow(aimPerformanceValue, 1.1) +
99
- Math.pow(speedPerformanceValue, 1.1) +
109
+ Math.pow(tapPerformanceValue, 1.1) +
100
110
  Math.pow(flashlightPerformanceValue, 1.1), 1 / 1.1);
101
111
  if (basePerformanceValue > 1e-5) {
102
112
  this.total =
@@ -109,22 +119,12 @@ class DroidStarRating extends StarRating_1.StarRating {
109
119
  calculateAll() {
110
120
  const skills = this.createSkills();
111
121
  const isRelax = this.mods.some((m) => m instanceof osu_base_1.ModRelax);
112
- if (isRelax) {
113
- // Remove speed skill to prevent overhead
114
- skills.splice(2, 1);
115
- }
116
122
  this.calculateSkills(...skills);
117
123
  const aimSkill = skills[0];
118
124
  const aimSkillWithoutSliders = skills[1];
119
- let tapSkill;
120
- let flashlightSkill;
121
- if (!isRelax) {
122
- tapSkill = skills[2];
123
- flashlightSkill = skills[3];
124
- }
125
- else {
126
- flashlightSkill = skills[2];
127
- }
125
+ const rhythmSkill = skills[2];
126
+ const tapSkill = skills[3];
127
+ const flashlightSkill = skills[4];
128
128
  this.strainPeaks.aimWithSliders = aimSkill.strainPeaks;
129
129
  this.strainPeaks.aimWithoutSliders = aimSkillWithoutSliders.strainPeaks;
130
130
  this.aim = this.starValue(aimSkill.difficultyValue());
@@ -133,15 +133,17 @@ class DroidStarRating extends StarRating_1.StarRating {
133
133
  this.starValue(aimSkillWithoutSliders.difficultyValue()) /
134
134
  this.aim;
135
135
  }
136
- if (tapSkill) {
136
+ if (!isRelax) {
137
137
  this.strainPeaks.speed = tapSkill.strainPeaks;
138
138
  this.tap = this.starValue(tapSkill.difficultyValue());
139
- const objectStrains = this.objects.map((v) => v.tapStrain);
140
- const maxStrain = Math.max(...objectStrains);
141
- if (maxStrain) {
142
- this.attributes.speedNoteCount = objectStrains.reduce((total, next) => total +
143
- 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
144
- }
139
+ }
140
+ const objectStrains = this.objects.map((v) => v.tapStrain);
141
+ const maxStrain = Math.max(...objectStrains);
142
+ if (maxStrain) {
143
+ this.attributes.speedNoteCount = objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
144
+ }
145
+ if (!isRelax) {
146
+ this.rhythm = this.starValue(rhythmSkill.difficultyValue());
145
147
  }
146
148
  this.strainPeaks.flashlight = flashlightSkill.strainPeaks;
147
149
  this.flashlight = this.starValue(flashlightSkill.difficultyValue());
@@ -157,6 +159,8 @@ class DroidStarRating extends StarRating_1.StarRating {
157
159
  " aim, " +
158
160
  this.tap.toFixed(2) +
159
161
  " tap, " +
162
+ this.rhythm.toFixed(2) +
163
+ " rhythm, " +
160
164
  this.flashlight.toFixed(2) +
161
165
  " flashlight)");
162
166
  }
@@ -167,6 +171,8 @@ class DroidStarRating extends StarRating_1.StarRating {
167
171
  return [
168
172
  new DroidAim_1.DroidAim(this.mods, true),
169
173
  new DroidAim_1.DroidAim(this.mods, false),
174
+ // Tap skill depends on rhythm skill, so we put it first
175
+ new DroidRhythm_1.DroidRhythm(this.mods, this.stats.od),
170
176
  new DroidTap_1.DroidTap(this.mods, this.stats.od),
171
177
  new DroidFlashlight_1.DroidFlashlight(this.mods),
172
178
  ];
@@ -77,11 +77,9 @@ class OsuPerformanceCalculator extends PerformanceCalculator_1.PerformanceCalcul
77
77
  // Buff for longer maps with high AR.
78
78
  this.aim *= 1 + arFactor * lengthBonus;
79
79
  // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
80
- let hiddenBonus = 1;
81
80
  if (this.stars.mods.some((m) => m instanceof osu_base_1.ModHidden)) {
82
- hiddenBonus += 0.04 * (12 - calculatedAR);
81
+ this.aim *= 1 + 0.04 * (12 - calculatedAR);
83
82
  }
84
- this.aim *= hiddenBonus;
85
83
  // Scale the aim value with slider factor to nerf very likely dropped sliderends.
86
84
  this.aim *= this.sliderNerfFactor;
87
85
  // Scale the aim value with accuracy.
@@ -62,8 +62,9 @@ class PerformanceCalculator {
62
62
  nmiss: params.miss || 0,
63
63
  });
64
64
  }
65
+ this.effectiveMissCount = this.calculateEffectiveMissCount(combo, maxCombo);
65
66
  if (this.stars.mods.some((m) => m instanceof osu_base_1.ModNoFail)) {
66
- this.finalMultiplier *= Math.max(0.9, 1 - 0.02 * this.computedAccuracy.nmiss);
67
+ this.finalMultiplier *= Math.max(0.9, 1 - 0.02 * this.effectiveMissCount);
67
68
  }
68
69
  if (this.stars.mods.some((m) => m instanceof osu_base_1.ModSpunOut)) {
69
70
  this.finalMultiplier *=
@@ -71,11 +72,13 @@ class PerformanceCalculator {
71
72
  Math.pow(this.stars.map.spinners / this.stars.objects.length, 0.85);
72
73
  }
73
74
  if (this.stars.mods.some((m) => m instanceof osu_base_1.ModRelax)) {
74
- this.computedAccuracy.nmiss +=
75
- this.computedAccuracy.n100 + this.computedAccuracy.n50;
75
+ // As we're adding 100s and 50s to an approximated number of combo breaks, the result can be higher
76
+ // than total hits in specific scenarios (which breaks some calculations), so we need to clamp it.
77
+ this.effectiveMissCount = Math.min(this.effectiveMissCount +
78
+ this.computedAccuracy.n100 +
79
+ this.computedAccuracy.n50, this.stars.objects.length);
76
80
  this.finalMultiplier *= 0.6;
77
81
  }
78
- this.effectiveMissCount = this.calculateEffectiveMissCount(combo, maxCombo);
79
82
  this.mapStatistics = new osu_base_1.MapStats({
80
83
  ar: baseAR,
81
84
  od: baseOD,
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ __exportStar(require("./preprocessing/DifficultyHitObjectCreator"), exports);
15
15
  __exportStar(require("./skills/DroidAim"), exports);
16
16
  __exportStar(require("./skills/DroidFlashlight"), exports);
17
17
  __exportStar(require("./DroidPerformanceCalculator"), exports);
18
+ __exportStar(require("./skills/DroidRhythm"), exports);
18
19
  __exportStar(require("./DroidStarRating"), exports);
19
20
  __exportStar(require("./skills/DroidTap"), exports);
20
21
  __exportStar(require("./MapStars"), exports);
@@ -19,6 +19,8 @@ class DifficultyHitObject {
19
19
  this.aimStrainWithoutSliders = 0;
20
20
  /**
21
21
  * The tap strain generated by the hitobject.
22
+ *
23
+ * This is also used for osu!standard as opposed to "speed strain".
22
24
  */
23
25
  this.tapStrain = 0;
24
26
  /**
@@ -27,9 +29,13 @@ class DifficultyHitObject {
27
29
  */
28
30
  this.originalTapStrain = 0;
29
31
  /**
30
- * The rhythm multiplier generated by the hitobject.
32
+ * The rhythm multiplier generated by the hitobject. This is used to alter tap strain.
31
33
  */
32
34
  this.rhythmMultiplier = 0;
35
+ /**
36
+ * The rhythm strain generated by the hitobject.
37
+ */
38
+ this.rhythmStrain = 0;
33
39
  /**
34
40
  * The flashlight strain generated by the hitobject.
35
41
  */
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DroidRhythm = void 0;
4
+ const osu_base_1 = require("@rian8337/osu-base");
5
+ const DroidSkill_1 = require("./DroidSkill");
6
+ /**
7
+ * Represents the skill required to properly follow a beatmap's rhythm.
8
+ */
9
+ class DroidRhythm extends DroidSkill_1.DroidSkill {
10
+ constructor(mods, overallDifficulty) {
11
+ super(mods);
12
+ this.historyLength = 32;
13
+ this.skillMultiplier = 1;
14
+ this.reducedSectionCount = 5;
15
+ this.reducedSectionBaseline = 0.75;
16
+ this.strainDecayBase = 0.3;
17
+ this.starsPerDouble = 2;
18
+ this.rhythmMultiplier = 0.75;
19
+ this.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
20
+ this.currentRhythm = 1;
21
+ this.hitWindow = new osu_base_1.OsuHitWindow(overallDifficulty);
22
+ }
23
+ /**
24
+ * Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current object.
25
+ */
26
+ calculateRhythmBonus(current) {
27
+ if (current.object instanceof osu_base_1.Spinner) {
28
+ return 0;
29
+ }
30
+ let previousIslandSize = 0;
31
+ let rhythmComplexitySum = 0;
32
+ let islandSize = 1;
33
+ // Store the ratio of the current start of an island to buff for tighter rhythms.
34
+ let startRatio = 0;
35
+ let firstDeltaSwitch = false;
36
+ let rhythmStart = 0;
37
+ while (rhythmStart < this.previous.length - 2 &&
38
+ current.startTime - this.previous[rhythmStart].startTime <
39
+ this.historyTimeMax) {
40
+ ++rhythmStart;
41
+ }
42
+ for (let i = rhythmStart; i > 0; --i) {
43
+ // Scale note 0 to 1 from history to now.
44
+ let currentHistoricalDecay = (this.historyTimeMax -
45
+ (current.startTime - this.previous[i - 1].startTime)) /
46
+ this.historyTimeMax;
47
+ // Either we're limited by time or limited by object count.
48
+ currentHistoricalDecay = Math.min(currentHistoricalDecay, (this.previous.length - i) / this.previous.length);
49
+ const currentDelta = this.previous[i - 1].strainTime;
50
+ const prevDelta = this.previous[i].strainTime;
51
+ const lastDelta = this.previous[i + 1].strainTime;
52
+ const currentRatio = 1 +
53
+ 6 *
54
+ Math.min(0.5, Math.pow(Math.sin(Math.PI /
55
+ (Math.min(prevDelta, currentDelta) /
56
+ Math.max(prevDelta, currentDelta))), 2));
57
+ const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) -
58
+ this.hitWindow.hitWindowFor300() * 0.6) /
59
+ (this.hitWindow.hitWindowFor300() * 0.6));
60
+ let effectiveRatio = windowPenalty * currentRatio;
61
+ if (firstDeltaSwitch) {
62
+ if (prevDelta <= 1.25 * currentDelta &&
63
+ prevDelta * 1.25 >= currentDelta) {
64
+ // Island is still progressing, count size.
65
+ if (islandSize < 7) {
66
+ ++islandSize;
67
+ }
68
+ }
69
+ else {
70
+ if (this.previous[i - 1].object instanceof osu_base_1.Slider) {
71
+ // BPM change is into slider, this is easy acc window.
72
+ effectiveRatio /= 8;
73
+ }
74
+ if (this.previous[i].object instanceof osu_base_1.Slider) {
75
+ // BPM change was from a slider, this is typically easier than circle -> circle.
76
+ effectiveRatio /= 4;
77
+ }
78
+ if (previousIslandSize === islandSize) {
79
+ // Repeated island size (ex: triplet -> triplet).
80
+ effectiveRatio /= 4;
81
+ }
82
+ if (previousIslandSize % 2 === islandSize % 2) {
83
+ // Repeated island polarity (2 -> 4, 3 -> 5).
84
+ effectiveRatio /= 2;
85
+ }
86
+ if (lastDelta > prevDelta + 10 &&
87
+ prevDelta > currentDelta + 10) {
88
+ // Previous increase happened a note ago.
89
+ // Albeit this is a 1/1 -> 1/2-1/4 type of transition, we don't want to buff this.
90
+ effectiveRatio /= 8;
91
+ }
92
+ rhythmComplexitySum +=
93
+ (((Math.sqrt(effectiveRatio * startRatio) *
94
+ currentHistoricalDecay *
95
+ Math.sqrt(4 + islandSize)) /
96
+ 2) *
97
+ Math.sqrt(4 + previousIslandSize)) /
98
+ 2;
99
+ startRatio = effectiveRatio;
100
+ previousIslandSize = islandSize;
101
+ if (prevDelta * 1.25 < currentDelta) {
102
+ // We're slowing down, stop counting.
103
+ // If we're speeding up, this stays as is and we keep counting island size.
104
+ firstDeltaSwitch = false;
105
+ }
106
+ islandSize = 1;
107
+ }
108
+ }
109
+ else if (prevDelta > 1.25 * currentDelta) {
110
+ // We want to be speeding up.
111
+ // Begin counting island until we change speed again.
112
+ firstDeltaSwitch = true;
113
+ startRatio = effectiveRatio;
114
+ islandSize = 1;
115
+ }
116
+ }
117
+ return Math.sqrt(4 + rhythmComplexitySum * this.rhythmMultiplier) / 2;
118
+ }
119
+ strainValueAt(current) {
120
+ this.currentRhythm = this.calculateRhythmBonus(current);
121
+ this.currentStrain *= this.strainDecay(current.deltaTime);
122
+ this.currentStrain += this.currentRhythm - 1;
123
+ return this.currentStrain;
124
+ }
125
+ saveToHitObject(current) {
126
+ current.rhythmStrain = this.currentStrain;
127
+ current.rhythmMultiplier = this.currentRhythm;
128
+ }
129
+ }
130
+ exports.DroidRhythm = DroidRhythm;
@@ -19,11 +19,7 @@ class DroidTap extends DroidSkill_1.DroidSkill {
19
19
  this.minSpeedBonus = 75;
20
20
  this.currentTapStrain = 0;
21
21
  this.currentOriginalTapStrain = 0;
22
- this.rhythmMultiplier = 0.75;
23
- this.historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
24
- this.currentRhythm = 1;
25
- this.overallDifficulty = overallDifficulty;
26
- this.hitWindow = new osu_base_1.OsuHitWindow(this.overallDifficulty);
22
+ this.hitWindow = new osu_base_1.OsuHitWindow(overallDifficulty);
27
23
  }
28
24
  /**
29
25
  * @param current The hitobject to calculate.
@@ -43,7 +39,7 @@ class DroidTap extends DroidSkill_1.DroidSkill {
43
39
  // Cap deltatime to the OD 300 hitwindow.
44
40
  // This equation is derived from making sure 260 BPM 1/4 OD7 streams aren't nerfed harshly.
45
41
  strainTime /= osu_base_1.MathUtils.clamp(strainTime /
46
- new osu_base_1.OsuHitWindow(this.overallDifficulty - 21.5).hitWindowFor300() /
42
+ new osu_base_1.OsuHitWindow(this.hitWindow.overallDifficulty - 21.5).hitWindowFor300() /
47
43
  0.35, 0.9, 1);
48
44
  let speedBonus = 1;
49
45
  if (strainTime < this.minSpeedBonus) {
@@ -57,7 +53,6 @@ class DroidTap extends DroidSkill_1.DroidSkill {
57
53
  Math.pow((this.minSpeedBonus - current.strainTime) / 40, 2);
58
54
  }
59
55
  const decay = this.strainDecay(current.deltaTime);
60
- this.currentRhythm = this.calculateRhythmBonus(current);
61
56
  this.currentTapStrain *= decay;
62
57
  this.currentTapStrain +=
63
58
  this.tapStrainOf(speedBonus, strainTime) * this.skillMultiplier;
@@ -65,103 +60,7 @@ class DroidTap extends DroidSkill_1.DroidSkill {
65
60
  this.currentOriginalTapStrain +=
66
61
  this.tapStrainOf(originalSpeedBonus, current.strainTime) *
67
62
  this.skillMultiplier;
68
- return this.currentTapStrain * this.currentRhythm;
69
- }
70
- /**
71
- * Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current object.
72
- */
73
- calculateRhythmBonus(current) {
74
- if (current.object instanceof osu_base_1.Spinner) {
75
- return 0;
76
- }
77
- let previousIslandSize = 0;
78
- let rhythmComplexitySum = 0;
79
- let islandSize = 1;
80
- // Store the ratio of the current start of an island to buff for tighter rhythms.
81
- let startRatio = 0;
82
- let firstDeltaSwitch = false;
83
- let rhythmStart = 0;
84
- while (rhythmStart < this.previous.length - 2 &&
85
- current.startTime - this.previous[rhythmStart].startTime <
86
- this.historyTimeMax) {
87
- ++rhythmStart;
88
- }
89
- for (let i = rhythmStart; i > 0; --i) {
90
- // Scale note 0 to 1 from history to now.
91
- let currentHistoricalDecay = (this.historyTimeMax -
92
- (current.startTime - this.previous[i - 1].startTime)) /
93
- this.historyTimeMax;
94
- // Either we're limited by time or limited by object count.
95
- currentHistoricalDecay = Math.min(currentHistoricalDecay, (this.previous.length - i) / this.previous.length);
96
- const currentDelta = this.previous[i - 1].strainTime;
97
- const prevDelta = this.previous[i].strainTime;
98
- const lastDelta = this.previous[i + 1].strainTime;
99
- const currentRatio = 1 +
100
- 6 *
101
- Math.min(0.5, Math.pow(Math.sin(Math.PI /
102
- (Math.min(prevDelta, currentDelta) /
103
- Math.max(prevDelta, currentDelta))), 2));
104
- const windowPenalty = Math.min(1, Math.max(0, Math.abs(prevDelta - currentDelta) -
105
- this.hitWindow.hitWindowFor300() * 0.6) /
106
- (this.hitWindow.hitWindowFor300() * 0.6));
107
- let effectiveRatio = windowPenalty * currentRatio;
108
- if (firstDeltaSwitch) {
109
- if (prevDelta <= 1.25 * currentDelta &&
110
- prevDelta * 1.25 >= currentDelta) {
111
- // Island is still progressing, count size.
112
- if (islandSize < 7) {
113
- ++islandSize;
114
- }
115
- }
116
- else {
117
- if (this.previous[i - 1].object instanceof osu_base_1.Slider) {
118
- // BPM change is into slider, this is easy acc window.
119
- effectiveRatio /= 8;
120
- }
121
- if (this.previous[i].object instanceof osu_base_1.Slider) {
122
- // BPM change was from a slider, this is typically easier than circle -> circle.
123
- effectiveRatio /= 4;
124
- }
125
- if (previousIslandSize === islandSize) {
126
- // Repeated island size (ex: triplet -> triplet).
127
- effectiveRatio /= 4;
128
- }
129
- if (previousIslandSize % 2 === islandSize % 2) {
130
- // Repeated island polarity (2 -> 4, 3 -> 5).
131
- effectiveRatio /= 2;
132
- }
133
- if (lastDelta > prevDelta + 10 &&
134
- prevDelta > currentDelta + 10) {
135
- // Previous increase happened a note ago.
136
- // Albeit this is a 1/1 -> 1/2-1/4 type of transition, we don't want to buff this.
137
- effectiveRatio /= 8;
138
- }
139
- rhythmComplexitySum +=
140
- (((Math.sqrt(effectiveRatio * startRatio) *
141
- currentHistoricalDecay *
142
- Math.sqrt(4 + islandSize)) /
143
- 2) *
144
- Math.sqrt(4 + previousIslandSize)) /
145
- 2;
146
- startRatio = effectiveRatio;
147
- previousIslandSize = islandSize;
148
- if (prevDelta * 1.25 < currentDelta) {
149
- // We're slowing down, stop counting.
150
- // If we're speeding up, this stays as is and we keep counting island size.
151
- firstDeltaSwitch = false;
152
- }
153
- islandSize = 1;
154
- }
155
- }
156
- else if (prevDelta > 1.25 * currentDelta) {
157
- // We want to be speeding up.
158
- // Begin counting island until we change speed again.
159
- firstDeltaSwitch = true;
160
- startRatio = effectiveRatio;
161
- islandSize = 1;
162
- }
163
- }
164
- return Math.sqrt(4 + rhythmComplexitySum * this.rhythmMultiplier) / 2;
63
+ return this.currentTapStrain * current.rhythmMultiplier;
165
64
  }
166
65
  /**
167
66
  * Calculates the tap strain of a hitobject given a specific speed bonus and strain time.
@@ -180,9 +79,8 @@ class DroidTap extends DroidSkill_1.DroidSkill {
180
79
  */
181
80
  saveToHitObject(current) {
182
81
  current.tapStrain = this.currentStrain;
183
- current.rhythmMultiplier = this.currentRhythm;
184
82
  current.originalTapStrain =
185
- this.currentOriginalTapStrain * this.currentRhythm;
83
+ this.currentOriginalTapStrain * current.rhythmMultiplier;
186
84
  }
187
85
  }
188
86
  exports.DroidTap = DroidTap;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rian8337/osu-difficulty-calculator",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
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",
@@ -30,10 +30,10 @@
30
30
  "url": "https://github.com/Rian8337/osu-droid-module/issues"
31
31
  },
32
32
  "dependencies": {
33
- "@rian8337/osu-base": "^1.0.2"
33
+ "@rian8337/osu-base": "^1.0.5"
34
34
  },
35
35
  "publishConfig": {
36
36
  "access": "public"
37
37
  },
38
- "gitHead": "34e161583bc0e9beb122e91f2b51ea6706e38736"
38
+ "gitHead": "595c6aac626f4346807f6da868c86324fed52422"
39
39
  }
@@ -1,5 +1,15 @@
1
1
  declare module "@rian8337/osu-difficulty-calculator" {
2
- import { Accuracy, Beatmap, HitObject, MapStats, Mod, modes, OsuHitWindow, Slider, Vector2 } from "@rian8337/osu-base";
2
+ import {
3
+ Accuracy,
4
+ Beatmap,
5
+ HitObject,
6
+ MapStats,
7
+ Mod,
8
+ modes,
9
+ OsuHitWindow,
10
+ Slider,
11
+ Vector2,
12
+ } from "@rian8337/osu-base";
3
13
 
4
14
  //#region Classes
5
15
 
@@ -15,13 +25,14 @@ declare module "@rian8337/osu-difficulty-calculator" {
15
25
  * The aim strain generated by the hitobject if sliders are considered.
16
26
  */
17
27
  aimStrainWithSliders: number;
18
-
19
28
  /**
20
29
  * The aim strain generated by the hitobject if sliders are not considered.
21
30
  */
22
31
  aimStrainWithoutSliders: number;
23
32
  /**
24
33
  * The tap strain generated by the hitobject.
34
+ *
35
+ * This is also used for osu!standard as opposed to "speed strain".
25
36
  */
26
37
  tapStrain: number;
27
38
  /**
@@ -30,16 +41,20 @@ declare module "@rian8337/osu-difficulty-calculator" {
30
41
  */
31
42
  originalTapStrain: number;
32
43
  /**
33
- * The rhythm multiplier generated by the hitobject.
44
+ * The rhythm multiplier generated by the hitobject. This is used to alter tap strain.
34
45
  */
35
46
  rhythmMultiplier: number;
47
+ /**
48
+ * The rhythm strain generated by the hitobject.
49
+ */
50
+ rhythmStrain: number;
36
51
  /**
37
52
  * The flashlight strain generated by the hitobject.
38
53
  */
39
54
  flashlightStrain: number;
40
55
  /**
41
56
  * The normalized distance from the "lazy" end position of the previous hitobject to the start position of this hitobject.
42
- *
57
+ *
43
58
  * The "lazy" end position is the position at which the cursor ends up if the previous hitobject is followed with as minimal movement as possible (i.e. on the edge of slider follow circles).
44
59
  */
45
60
  lazyJumpDistance: number;
@@ -267,6 +282,29 @@ declare module "@rian8337/osu-difficulty-calculator" {
267
282
  override toString(): string;
268
283
  }
269
284
 
285
+ /**
286
+ * Represents the skill required to properly follow a beatmap's rhythm.
287
+ */
288
+ export class DroidRhythm extends DroidSkill {
289
+ protected override readonly historyLength: number;
290
+ protected override readonly skillMultiplier: number;
291
+ protected override readonly reducedSectionCount: number;
292
+ protected override readonly reducedSectionBaseline: number;
293
+ protected override readonly strainDecayBase: number;
294
+ protected override readonly starsPerDouble: number;
295
+ private readonly rhythmMultiplier: number;
296
+ private readonly historyTimeMax: number;
297
+ private currentRhythm: number;
298
+ private readonly hitWindow: OsuHitWindow;
299
+ constructor(mods: Mod[], overallDifficulty: number);
300
+ /**
301
+ * Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current object.
302
+ */
303
+ private calculateRhythmBonus(current: DifficultyHitObject): number;
304
+ protected override strainValueAt(current: DifficultyHitObject): number;
305
+ protected override saveToHitObject(current: DifficultyHitObject): void;
306
+ }
307
+
270
308
  /**
271
309
  * Difficulty calculator for osu!droid gamemode.
272
310
  */
@@ -393,6 +431,41 @@ declare module "@rian8337/osu-difficulty-calculator" {
393
431
  protected override saveToHitObject(current: DifficultyHitObject): void;
394
432
  }
395
433
 
434
+ /**
435
+ * A star rating calculator that configures which mode to calculate difficulty for and what mods are applied.
436
+ */
437
+ export class MapStars {
438
+ /**
439
+ * The osu!droid star rating of the beatmap.
440
+ */
441
+ readonly droidStars: DroidStarRating;
442
+ /**
443
+ * The osu!standard star rating of the beatmap.
444
+ */
445
+ readonly pcStars: OsuStarRating;
446
+ /**
447
+ * Calculates the star rating of a beatmap.
448
+ */
449
+ calculate(params: {
450
+ /**
451
+ * The beatmap to calculate.
452
+ */
453
+ map: Beatmap;
454
+ /**
455
+ * Applied modifications.
456
+ */
457
+ mods?: Mod[];
458
+ /**
459
+ * Custom map statistics to apply speed multiplier and force AR values as well as old statistics.
460
+ */
461
+ stats?: MapStats;
462
+ }): MapStars;
463
+ /**
464
+ * Returns a string representative of the class.
465
+ */
466
+ toString(): string;
467
+ }
468
+
396
469
  /**
397
470
  * Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
398
471
  */
@@ -822,7 +895,10 @@ declare module "@rian8337/osu-difficulty-calculator" {
822
895
  /**
823
896
  * Calculates the amount of misses + sliderbreaks from combo.
824
897
  */
825
- private calculateEffectiveMissCount(combo: number, maxCombo: number): number;
898
+ private calculateEffectiveMissCount(
899
+ combo: number,
900
+ maxCombo: number
901
+ ): number;
826
902
  }
827
903
 
828
904
  /**