@rian8337/osu-difficulty-calculator 4.0.0-beta.1 → 4.0.0-beta.10
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 +228 -188
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/typings/index.d.ts +45 -21
package/dist/index.js
CHANGED
|
@@ -207,10 +207,9 @@ class DifficultyHitObject {
|
|
|
207
207
|
*
|
|
208
208
|
* @param time The time to calculate the hitobject's opacity at.
|
|
209
209
|
* @param isHidden Whether Hidden mod is used.
|
|
210
|
-
* @param mode The gamemode to calculate the opacity for.
|
|
211
210
|
* @returns The opacity of the hitobject at the given time.
|
|
212
211
|
*/
|
|
213
|
-
opacityAt(time, isHidden
|
|
212
|
+
opacityAt(time, isHidden) {
|
|
214
213
|
if (time > this.object.startTime) {
|
|
215
214
|
// Consider a hitobject as being invisible when its start time is passed.
|
|
216
215
|
// In reality the hitobject will be visible beyond its start time up until its hittable window has passed,
|
|
@@ -221,10 +220,7 @@ class DifficultyHitObject {
|
|
|
221
220
|
const fadeInDuration = this.timeFadeIn;
|
|
222
221
|
if (isHidden) {
|
|
223
222
|
const fadeOutStartTime = fadeInStartTime + fadeInDuration;
|
|
224
|
-
const fadeOutDuration = this.baseTimePreempt *
|
|
225
|
-
(mode === osuBase.Modes.droid
|
|
226
|
-
? 0.35
|
|
227
|
-
: osuBase.ModHidden.fadeOutDurationMultiplier);
|
|
223
|
+
const fadeOutDuration = this.baseTimePreempt * osuBase.ModHidden.fadeOutDurationMultiplier;
|
|
228
224
|
return Math.min(osuBase.MathUtils.clamp((time - fadeInStartTime) / fadeInDuration, 0, 1), 1 -
|
|
229
225
|
osuBase.MathUtils.clamp((time - fadeOutStartTime) / fadeOutDuration, 0, 1));
|
|
230
226
|
}
|
|
@@ -297,34 +293,16 @@ class DifficultyHitObjectCreator {
|
|
|
297
293
|
if (this.mode === osuBase.Modes.droid) {
|
|
298
294
|
this.maximumSliderRadius = this.normalizedRadius * 2;
|
|
299
295
|
}
|
|
300
|
-
const droidCircleSize = new osuBase.MapStats({
|
|
301
|
-
cs: params.circleSize,
|
|
302
|
-
mods: params.mods,
|
|
303
|
-
}).calculate({ mode: osuBase.Modes.droid }).cs;
|
|
304
|
-
const droidScale = (1 - (0.7 * (droidCircleSize - 5)) / 5) / 2;
|
|
305
|
-
const osuCircleSize = new osuBase.MapStats({
|
|
306
|
-
cs: params.circleSize,
|
|
307
|
-
mods: params.mods,
|
|
308
|
-
}).calculate({ mode: osuBase.Modes.osu }).cs;
|
|
309
|
-
const osuScale = (1 - (0.7 * (osuCircleSize - 5)) / 5) / 2;
|
|
310
|
-
params.objects[0].droidScale = droidScale;
|
|
311
|
-
params.objects[0].osuScale = osuScale;
|
|
312
296
|
const scalingFactor = this.getScalingFactor(params.objects[0].getRadius(this.mode));
|
|
313
297
|
const difficultyObjects = [];
|
|
314
298
|
for (let i = 0; i < params.objects.length; ++i) {
|
|
315
299
|
const object = new DifficultyHitObject(params.objects[i], difficultyObjects);
|
|
316
300
|
object.index = difficultyObjects.length - 1;
|
|
317
|
-
object.object.droidScale = droidScale;
|
|
318
|
-
object.object.osuScale = osuScale;
|
|
319
301
|
object.timePreempt = params.preempt;
|
|
320
302
|
object.baseTimePreempt = params.preempt * params.speedMultiplier;
|
|
321
303
|
if (object.object instanceof osuBase.Slider) {
|
|
322
304
|
object.velocity =
|
|
323
305
|
object.object.velocity * params.speedMultiplier;
|
|
324
|
-
object.object.nestedHitObjects.forEach((o) => {
|
|
325
|
-
o.droidScale = droidScale;
|
|
326
|
-
o.osuScale = osuScale;
|
|
327
|
-
});
|
|
328
306
|
this.calculateSliderCursorPosition(object.object);
|
|
329
307
|
object.travelDistance = object.object.lazyTravelDistance;
|
|
330
308
|
// Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
|
@@ -367,9 +345,6 @@ class DifficultyHitObjectCreator {
|
|
|
367
345
|
object.endTime + object.timePreempt) {
|
|
368
346
|
break;
|
|
369
347
|
}
|
|
370
|
-
// Future objects do not have their scales set, so we set them here.
|
|
371
|
-
o.droidScale = droidScale;
|
|
372
|
-
o.osuScale = osuScale;
|
|
373
348
|
nextVisibleObjects.push(o);
|
|
374
349
|
}
|
|
375
350
|
for (let j = 0; j < object.index; ++j) {
|
|
@@ -620,7 +595,6 @@ class DifficultyCalculator {
|
|
|
620
595
|
speed: [],
|
|
621
596
|
flashlight: [],
|
|
622
597
|
};
|
|
623
|
-
sectionLength = 400;
|
|
624
598
|
/**
|
|
625
599
|
* Constructs a new instance of the calculator.
|
|
626
600
|
*
|
|
@@ -655,6 +629,7 @@ class DifficultyCalculator {
|
|
|
655
629
|
speedMultiplier: options?.stats?.speedMultiplier,
|
|
656
630
|
oldStatistics: options?.stats?.oldStatistics,
|
|
657
631
|
}).calculate({ mode: this.mode });
|
|
632
|
+
this.preProcess();
|
|
658
633
|
this.populateDifficultyAttributes();
|
|
659
634
|
this.generateDifficultyHitObjects();
|
|
660
635
|
this.calculateAll();
|
|
@@ -674,6 +649,11 @@ class DifficultyCalculator {
|
|
|
674
649
|
preempt: osuBase.MapStats.arToMS(this.stats.ar),
|
|
675
650
|
}));
|
|
676
651
|
}
|
|
652
|
+
/**
|
|
653
|
+
* Performs some pre-processing before proceeding with difficulty calculation.
|
|
654
|
+
*/
|
|
655
|
+
preProcess() {
|
|
656
|
+
}
|
|
677
657
|
/**
|
|
678
658
|
* Calculates the skills provided.
|
|
679
659
|
*
|
|
@@ -702,6 +682,7 @@ class DifficultyCalculator {
|
|
|
702
682
|
this.attributes.overallDifficulty = this.stats.od;
|
|
703
683
|
this.attributes.sliderCount = this.beatmap.hitObjects.sliders;
|
|
704
684
|
this.attributes.spinnerCount = this.beatmap.hitObjects.spinners;
|
|
685
|
+
this.attributes.clockRate = this.stats.speedMultiplier;
|
|
705
686
|
}
|
|
706
687
|
/**
|
|
707
688
|
* Calculates the star rating value of a difficulty.
|
|
@@ -861,7 +842,8 @@ class DroidAimEvaluator extends AimEvaluator {
|
|
|
861
842
|
velocityChangeBonus * this.velocityChangeMultiplier);
|
|
862
843
|
// Add in additional slider velocity bonus.
|
|
863
844
|
if (withSliders) {
|
|
864
|
-
strain +=
|
|
845
|
+
strain +=
|
|
846
|
+
Math.pow(1 + sliderBonus * this.sliderMultiplier, 1.25) - 1;
|
|
865
847
|
}
|
|
866
848
|
return strain;
|
|
867
849
|
}
|
|
@@ -918,7 +900,6 @@ class StrainSkill extends Skill {
|
|
|
918
900
|
strainPeaks = [];
|
|
919
901
|
sectionLength = 400;
|
|
920
902
|
currentSectionEnd = 0;
|
|
921
|
-
isFirstObject = true;
|
|
922
903
|
/**
|
|
923
904
|
* Calculates the strain value of a hitobject and stores the value in it. This value is affected by previously processed objects.
|
|
924
905
|
*
|
|
@@ -926,11 +907,10 @@ class StrainSkill extends Skill {
|
|
|
926
907
|
*/
|
|
927
908
|
process(current) {
|
|
928
909
|
// The first object doesn't generate a strain, so we begin with an incremented section end
|
|
929
|
-
if (
|
|
910
|
+
if (current.index === 0) {
|
|
930
911
|
this.currentSectionEnd =
|
|
931
912
|
Math.ceil(current.startTime / this.sectionLength) *
|
|
932
913
|
this.sectionLength;
|
|
933
|
-
this.isFirstObject = false;
|
|
934
914
|
}
|
|
935
915
|
while (current.startTime > this.currentSectionEnd) {
|
|
936
916
|
this.saveCurrentPeak();
|
|
@@ -1065,7 +1045,6 @@ class DroidTapEvaluator extends SpeedEvaluator {
|
|
|
1065
1045
|
current.isOverlapping(false)) {
|
|
1066
1046
|
return 0;
|
|
1067
1047
|
}
|
|
1068
|
-
let strainTime = current.strainTime;
|
|
1069
1048
|
let doubletapness = 1;
|
|
1070
1049
|
if (considerCheesability) {
|
|
1071
1050
|
const greatWindowFull = greatWindow * 2;
|
|
@@ -1080,16 +1059,14 @@ class DroidTapEvaluator extends SpeedEvaluator {
|
|
|
1080
1059
|
const windowRatio = Math.pow(Math.min(1, currentDeltaTime / greatWindowFull), 2);
|
|
1081
1060
|
doubletapness = Math.pow(speedRatio, 1 - windowRatio);
|
|
1082
1061
|
}
|
|
1083
|
-
// Cap deltatime to the OD 300 hitwindow.
|
|
1084
|
-
// 0.58 is derived from making sure 260 BPM 1/4 OD5 streams aren't nerfed harshly, whilst 0.91 limits the effect of the cap.
|
|
1085
|
-
strainTime /= osuBase.MathUtils.clamp(strainTime / greatWindowFull / 0.58, 0.91, 1);
|
|
1086
1062
|
}
|
|
1087
1063
|
let speedBonus = 1;
|
|
1088
|
-
if (strainTime < this.minSpeedBonus) {
|
|
1064
|
+
if (current.strainTime < this.minSpeedBonus) {
|
|
1089
1065
|
speedBonus +=
|
|
1090
|
-
0.75 *
|
|
1066
|
+
0.75 *
|
|
1067
|
+
Math.pow(osuBase.ErrorFunction.erf((this.minSpeedBonus - current.strainTime) / 40), 2);
|
|
1091
1068
|
}
|
|
1092
|
-
return (speedBonus * doubletapness) / strainTime;
|
|
1069
|
+
return (speedBonus * Math.pow(doubletapness, 1.5)) / current.strainTime;
|
|
1093
1070
|
}
|
|
1094
1071
|
}
|
|
1095
1072
|
|
|
@@ -1193,7 +1170,7 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
1193
1170
|
const opacityBonus = 1 +
|
|
1194
1171
|
this.maxOpacityBonus *
|
|
1195
1172
|
(1 -
|
|
1196
|
-
current.opacityAt(currentObject.object.startTime, isHiddenMod
|
|
1173
|
+
current.opacityAt(currentObject.object.startTime, isHiddenMod));
|
|
1197
1174
|
result +=
|
|
1198
1175
|
(stackNerf * opacityBonus * scalingFactor * jumpDistance) /
|
|
1199
1176
|
cumulativeStrainTime;
|
|
@@ -1236,11 +1213,11 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
1236
1213
|
* Represents the skill required to memorize and hit every object in a beatmap with the Flashlight mod enabled.
|
|
1237
1214
|
*/
|
|
1238
1215
|
class DroidFlashlight extends DroidSkill {
|
|
1239
|
-
skillMultiplier = 0.
|
|
1216
|
+
skillMultiplier = 0.052;
|
|
1240
1217
|
strainDecayBase = 0.15;
|
|
1241
1218
|
reducedSectionCount = 0;
|
|
1242
1219
|
reducedSectionBaseline = 1;
|
|
1243
|
-
starsPerDouble = 1.
|
|
1220
|
+
starsPerDouble = 1.06;
|
|
1244
1221
|
isHidden;
|
|
1245
1222
|
withSliders;
|
|
1246
1223
|
constructor(mods, withSliders) {
|
|
@@ -1265,6 +1242,9 @@ class DroidFlashlight extends DroidSkill {
|
|
|
1265
1242
|
current.flashlightStrainWithoutSliders = this.currentStrain;
|
|
1266
1243
|
}
|
|
1267
1244
|
}
|
|
1245
|
+
difficultyValue() {
|
|
1246
|
+
return Math.pow(this.strainPeaks.reduce((a, v) => a + v, 0) * this.starsPerDouble, 0.8);
|
|
1247
|
+
}
|
|
1268
1248
|
}
|
|
1269
1249
|
|
|
1270
1250
|
/**
|
|
@@ -1456,7 +1436,8 @@ class DroidVisualEvaluator {
|
|
|
1456
1436
|
static evaluateDifficultyOf(current, isHiddenMod, withSliders) {
|
|
1457
1437
|
if (current.object instanceof osuBase.Spinner ||
|
|
1458
1438
|
// Exclude overlapping objects that can be tapped at once.
|
|
1459
|
-
current.isOverlapping(true)
|
|
1439
|
+
current.isOverlapping(true) ||
|
|
1440
|
+
current.index === 0) {
|
|
1460
1441
|
return 0;
|
|
1461
1442
|
}
|
|
1462
1443
|
// Start with base density and give global bonus for Hidden.
|
|
@@ -1483,7 +1464,7 @@ class DroidVisualEvaluator {
|
|
|
1483
1464
|
}
|
|
1484
1465
|
strain +=
|
|
1485
1466
|
(1 -
|
|
1486
|
-
current.opacityAt(previous.object.startTime, isHiddenMod
|
|
1467
|
+
current.opacityAt(previous.object.startTime, isHiddenMod)) /
|
|
1487
1468
|
4;
|
|
1488
1469
|
}
|
|
1489
1470
|
// Scale the value with overlapping factor.
|
|
@@ -1519,44 +1500,6 @@ class DroidVisualEvaluator {
|
|
|
1519
1500
|
Math.min(1, 300 / cumulativeStrainTime);
|
|
1520
1501
|
}
|
|
1521
1502
|
}
|
|
1522
|
-
// Reward for rhythm changes.
|
|
1523
|
-
if (current.rhythmMultiplier > 1) {
|
|
1524
|
-
let rhythmBonus = (current.rhythmMultiplier - 1) / 20;
|
|
1525
|
-
// Rhythm changes are harder to read in Hidden.
|
|
1526
|
-
// Add additional bonus for Hidden.
|
|
1527
|
-
if (isHiddenMod) {
|
|
1528
|
-
rhythmBonus += (current.rhythmMultiplier - 1) / 25;
|
|
1529
|
-
}
|
|
1530
|
-
// Rhythm changes are harder to read when objects are stacked together.
|
|
1531
|
-
// Scale rhythm bonus based on the stack of past objects.
|
|
1532
|
-
const diameter = 2 * current.object.getRadius(osuBase.Modes.droid);
|
|
1533
|
-
let cumulativeStrainTime = 0;
|
|
1534
|
-
for (let i = 0; i < Math.min(current.index, 5); ++i) {
|
|
1535
|
-
const previous = current.previous(i);
|
|
1536
|
-
if (previous.object instanceof osuBase.Spinner ||
|
|
1537
|
-
// Exclude overlapping objects that can be tapped at once.
|
|
1538
|
-
previous.isOverlapping(true)) {
|
|
1539
|
-
continue;
|
|
1540
|
-
}
|
|
1541
|
-
const jumpDistance = current.object
|
|
1542
|
-
.getStackedPosition(osuBase.Modes.droid)
|
|
1543
|
-
.getDistance(previous.object.getStackedEndPosition(osuBase.Modes.droid));
|
|
1544
|
-
cumulativeStrainTime += previous.strainTime;
|
|
1545
|
-
rhythmBonus +=
|
|
1546
|
-
// Scale the bonus with diameter.
|
|
1547
|
-
osuBase.MathUtils.clamp((0.5 - jumpDistance / diameter) / 10, 0, 0.05) *
|
|
1548
|
-
// Scale with cumulative strain time to avoid overbuffing past objects.
|
|
1549
|
-
Math.min(1, 300 / cumulativeStrainTime);
|
|
1550
|
-
// Give a larger bonus for Hidden.
|
|
1551
|
-
if (isHiddenMod) {
|
|
1552
|
-
rhythmBonus +=
|
|
1553
|
-
(1 -
|
|
1554
|
-
current.opacityAt(previous.object.startTime, isHiddenMod, osuBase.Modes.droid)) /
|
|
1555
|
-
20;
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
strain += rhythmBonus;
|
|
1559
|
-
}
|
|
1560
1503
|
return strain;
|
|
1561
1504
|
}
|
|
1562
1505
|
}
|
|
@@ -1571,20 +1514,20 @@ class DroidVisual extends DroidSkill {
|
|
|
1571
1514
|
skillMultiplier = 10;
|
|
1572
1515
|
strainDecayBase = 0.1;
|
|
1573
1516
|
isHidden;
|
|
1574
|
-
|
|
1517
|
+
withsliders;
|
|
1575
1518
|
constructor(mods, withSliders) {
|
|
1576
1519
|
super(mods);
|
|
1577
1520
|
this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
|
|
1578
|
-
this.
|
|
1521
|
+
this.withsliders = withSliders;
|
|
1579
1522
|
}
|
|
1580
1523
|
strainValueAt(current) {
|
|
1581
1524
|
this.currentStrain *= this.strainDecay(current.deltaTime);
|
|
1582
1525
|
this.currentStrain +=
|
|
1583
|
-
DroidVisualEvaluator.evaluateDifficultyOf(current, this.isHidden, this.
|
|
1584
|
-
return this.currentStrain;
|
|
1526
|
+
DroidVisualEvaluator.evaluateDifficultyOf(current, this.isHidden, this.withsliders) * this.skillMultiplier;
|
|
1527
|
+
return this.currentStrain * (1 + (current.rhythmMultiplier - 1) / 5);
|
|
1585
1528
|
}
|
|
1586
1529
|
saveToHitObject(current) {
|
|
1587
|
-
if (this.
|
|
1530
|
+
if (this.withsliders) {
|
|
1588
1531
|
current.visualStrainWithSliders = this.currentStrain;
|
|
1589
1532
|
}
|
|
1590
1533
|
else {
|
|
@@ -1635,15 +1578,21 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1635
1578
|
flashlightDifficulty: 0,
|
|
1636
1579
|
speedNoteCount: 0,
|
|
1637
1580
|
sliderFactor: 0,
|
|
1581
|
+
clockRate: 1,
|
|
1638
1582
|
approachRate: 0,
|
|
1639
1583
|
overallDifficulty: 0,
|
|
1640
1584
|
hitCircleCount: 0,
|
|
1641
1585
|
sliderCount: 0,
|
|
1642
1586
|
spinnerCount: 0,
|
|
1587
|
+
aimDifficultStrainCount: 0,
|
|
1588
|
+
tapDifficultStrainCount: 0,
|
|
1589
|
+
flashlightDifficultStrainCount: 0,
|
|
1590
|
+
visualDifficultStrainCount: 0,
|
|
1643
1591
|
flashlightSliderFactor: 0,
|
|
1644
1592
|
visualSliderFactor: 0,
|
|
1645
1593
|
possibleThreeFingeredSections: [],
|
|
1646
1594
|
difficultSliders: [],
|
|
1595
|
+
averageSpeedDeltaTime: 0,
|
|
1647
1596
|
};
|
|
1648
1597
|
difficultyMultiplier = 0.18;
|
|
1649
1598
|
mode = osuBase.Modes.droid;
|
|
@@ -1765,9 +1714,6 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1765
1714
|
}
|
|
1766
1715
|
this.calculateTotal();
|
|
1767
1716
|
}
|
|
1768
|
-
/**
|
|
1769
|
-
* Returns a string representative of the class.
|
|
1770
|
-
*/
|
|
1771
1717
|
toString() {
|
|
1772
1718
|
return (this.total.toFixed(2) +
|
|
1773
1719
|
" stars (" +
|
|
@@ -1782,9 +1728,13 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1782
1728
|
this.visual.toFixed(2) +
|
|
1783
1729
|
" visual)");
|
|
1784
1730
|
}
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1731
|
+
preProcess() {
|
|
1732
|
+
const scale = osuBase.CircleSizeCalculator.standardCSToStandardScale(this.stats.cs);
|
|
1733
|
+
for (const object of this.beatmap.hitObjects.objects) {
|
|
1734
|
+
object.droidScale = scale;
|
|
1735
|
+
}
|
|
1736
|
+
osuBase.HitObjectStackEvaluator.applyDroidStacking(this.beatmap.hitObjects.objects, this.beatmap.general.stackLeniency);
|
|
1737
|
+
}
|
|
1788
1738
|
createSkills() {
|
|
1789
1739
|
return [
|
|
1790
1740
|
new DroidAim(this.mods, true),
|
|
@@ -1825,7 +1775,6 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1825
1775
|
calculateAimAttributes() {
|
|
1826
1776
|
const objectStrains = [];
|
|
1827
1777
|
let maxStrain = 0;
|
|
1828
|
-
// Take the top 15% most difficult sliders based on velocity.
|
|
1829
1778
|
const topDifficultSliders = [];
|
|
1830
1779
|
for (let i = 0; i < this.objects.length; ++i) {
|
|
1831
1780
|
const object = this.objects[i];
|
|
@@ -1837,22 +1786,28 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1837
1786
|
index: i,
|
|
1838
1787
|
velocity: velocity,
|
|
1839
1788
|
});
|
|
1840
|
-
topDifficultSliders.sort((a, b) => b.velocity - a.velocity);
|
|
1841
|
-
while (topDifficultSliders.length >
|
|
1842
|
-
Math.ceil(0.15 * this.beatmap.hitObjects.sliders)) {
|
|
1843
|
-
topDifficultSliders.pop();
|
|
1844
|
-
}
|
|
1845
1789
|
}
|
|
1846
1790
|
}
|
|
1847
1791
|
if (maxStrain) {
|
|
1848
1792
|
this.attributes.aimNoteCount = objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
|
|
1793
|
+
this.attributes.aimDifficultStrainCount = objectStrains.reduce((total, next) => total + Math.pow(next / maxStrain, 4), 0);
|
|
1849
1794
|
}
|
|
1850
1795
|
const velocitySum = topDifficultSliders.reduce((a, v) => a + v.velocity, 0);
|
|
1851
1796
|
for (const slider of topDifficultSliders) {
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1797
|
+
const difficultyRating = slider.velocity / velocitySum;
|
|
1798
|
+
// Only consider sliders that are fast enough.
|
|
1799
|
+
if (difficultyRating > 0.02) {
|
|
1800
|
+
this.attributes.difficultSliders.push({
|
|
1801
|
+
index: slider.index,
|
|
1802
|
+
difficultyRating: slider.velocity / velocitySum,
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
this.attributes.difficultSliders.sort((a, b) => b.difficultyRating - a.difficultyRating);
|
|
1807
|
+
// Take the top 15% most difficult sliders.
|
|
1808
|
+
while (this.attributes.difficultSliders.length >
|
|
1809
|
+
Math.ceil(0.15 * this.beatmap.hitObjects.sliders)) {
|
|
1810
|
+
this.attributes.difficultSliders.pop();
|
|
1856
1811
|
}
|
|
1857
1812
|
}
|
|
1858
1813
|
/**
|
|
@@ -1871,6 +1826,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1871
1826
|
this.attributes.possibleThreeFingeredSections = [];
|
|
1872
1827
|
const tempSections = [];
|
|
1873
1828
|
const objectStrains = [];
|
|
1829
|
+
const objectDeltaTimes = [];
|
|
1874
1830
|
let maxStrain = 0;
|
|
1875
1831
|
const maxSectionDeltaTime = 2000;
|
|
1876
1832
|
const minSectionObjectCount = 5;
|
|
@@ -1880,8 +1836,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1880
1836
|
const next = this.objects[i + 1];
|
|
1881
1837
|
if (i === 0) {
|
|
1882
1838
|
objectStrains.push(current.tapStrain);
|
|
1839
|
+
objectDeltaTimes.push(current.deltaTime);
|
|
1883
1840
|
}
|
|
1884
1841
|
objectStrains.push(next.tapStrain);
|
|
1842
|
+
objectDeltaTimes.push(next.deltaTime);
|
|
1885
1843
|
maxStrain = Math.max(current.tapStrain, maxStrain);
|
|
1886
1844
|
const realDeltaTime = next.object.startTime - current.object.endTime;
|
|
1887
1845
|
if (realDeltaTime >= maxSectionDeltaTime) {
|
|
@@ -1920,6 +1878,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1920
1878
|
if (inSpeedSection &&
|
|
1921
1879
|
current.originalTapStrain < threeFingerStrainThreshold) {
|
|
1922
1880
|
inSpeedSection = false;
|
|
1881
|
+
// Ignore sections that don't meet object count requirement.
|
|
1882
|
+
if (i - newFirstObjectIndex < minSectionObjectCount) {
|
|
1883
|
+
continue;
|
|
1884
|
+
}
|
|
1923
1885
|
this.attributes.possibleThreeFingeredSections.push({
|
|
1924
1886
|
firstObjectIndex: newFirstObjectIndex,
|
|
1925
1887
|
lastObjectIndex: i,
|
|
@@ -1928,7 +1890,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1928
1890
|
}
|
|
1929
1891
|
}
|
|
1930
1892
|
// Don't forget to manually add the last beatmap section, which would otherwise be ignored.
|
|
1931
|
-
|
|
1893
|
+
// Ignore sections that don't meet object count requirement.
|
|
1894
|
+
if (inSpeedSection &&
|
|
1895
|
+
section.lastObjectIndex - newFirstObjectIndex >=
|
|
1896
|
+
minSectionObjectCount) {
|
|
1932
1897
|
this.attributes.possibleThreeFingeredSections.push({
|
|
1933
1898
|
firstObjectIndex: newFirstObjectIndex,
|
|
1934
1899
|
lastObjectIndex: section.lastObjectIndex,
|
|
@@ -1938,6 +1903,16 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1938
1903
|
}
|
|
1939
1904
|
if (maxStrain) {
|
|
1940
1905
|
this.attributes.speedNoteCount = objectStrains.reduce((total, next) => total + 1 / (1 + Math.exp(-((next / maxStrain) * 12 - 6))), 0);
|
|
1906
|
+
this.attributes.averageSpeedDeltaTime =
|
|
1907
|
+
objectDeltaTimes.reduce((total, next, index) => total +
|
|
1908
|
+
(next * 1) /
|
|
1909
|
+
(1 +
|
|
1910
|
+
Math.exp(-((objectStrains[index] / maxStrain) *
|
|
1911
|
+
25 -
|
|
1912
|
+
20))), 0) /
|
|
1913
|
+
objectStrains.reduce((total, next) => total +
|
|
1914
|
+
1 / (1 + Math.exp(-((next / maxStrain) * 25 - 20))), 0);
|
|
1915
|
+
this.attributes.tapDifficultStrainCount = objectStrains.reduce((total, next) => total + Math.pow(next / maxStrain, 4), 0);
|
|
1941
1916
|
}
|
|
1942
1917
|
}
|
|
1943
1918
|
/**
|
|
@@ -1980,6 +1955,12 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1980
1955
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
1981
1956
|
this.flashlight *= 0.7;
|
|
1982
1957
|
}
|
|
1958
|
+
const objectStrains = this.objects.map((v) => v.flashlightStrainWithSliders);
|
|
1959
|
+
const maxStrain = Math.max(...objectStrains);
|
|
1960
|
+
if (maxStrain) {
|
|
1961
|
+
this.attributes.flashlightDifficultStrainCount =
|
|
1962
|
+
objectStrains.reduce((total, next) => total + Math.pow(next / maxStrain, 4), 0);
|
|
1963
|
+
}
|
|
1983
1964
|
this.attributes.flashlightDifficulty = this.flashlight;
|
|
1984
1965
|
}
|
|
1985
1966
|
/**
|
|
@@ -1997,6 +1978,11 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1997
1978
|
this.starValue(visualSkillWithoutSliders.difficultyValue()) /
|
|
1998
1979
|
this.visual;
|
|
1999
1980
|
}
|
|
1981
|
+
const objectStrains = this.objects.map((v) => v.flashlightStrainWithSliders);
|
|
1982
|
+
const maxStrain = Math.max(...objectStrains);
|
|
1983
|
+
if (maxStrain) {
|
|
1984
|
+
this.attributes.visualDifficultStrainCount = objectStrains.reduce((total, next) => total + Math.pow(next / maxStrain, 4), 0);
|
|
1985
|
+
}
|
|
2000
1986
|
}
|
|
2001
1987
|
}
|
|
2002
1988
|
|
|
@@ -2119,7 +2105,7 @@ class PerformanceCalculator {
|
|
|
2119
2105
|
if (this.difficultyAttributes.sliderCount > 0) {
|
|
2120
2106
|
// We assume 15% of sliders in a beatmap are difficult since there's no way to tell from the performance calculator.
|
|
2121
2107
|
const estimateDifficultSliders = this.difficultyAttributes.sliderCount * 0.15;
|
|
2122
|
-
const estimateSliderEndsDropped = osuBase.MathUtils.clamp(Math.min(this.computedAccuracy.
|
|
2108
|
+
const estimateSliderEndsDropped = osuBase.MathUtils.clamp(Math.min(this.computedAccuracy.n100 +
|
|
2123
2109
|
this.computedAccuracy.n50 +
|
|
2124
2110
|
this.computedAccuracy.nmiss, maxCombo - combo), 0, estimateDifficultSliders);
|
|
2125
2111
|
this.sliderNerfFactor =
|
|
@@ -2256,11 +2242,11 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2256
2242
|
*
|
|
2257
2243
|
* The aim and total performance value will be recalculated afterwards.
|
|
2258
2244
|
*
|
|
2259
|
-
* @param value The slider cheese penalty value. Must be between than 0
|
|
2245
|
+
* @param value The slider cheese penalty value. Must be between than 0 and 1.
|
|
2260
2246
|
*/
|
|
2261
2247
|
applyAimSliderCheesePenalty(value) {
|
|
2262
|
-
if (value
|
|
2263
|
-
throw new RangeError("New aim slider cheese penalty must be greater than zero.");
|
|
2248
|
+
if (value < 0) {
|
|
2249
|
+
throw new RangeError("New aim slider cheese penalty must be greater than or equal to zero.");
|
|
2264
2250
|
}
|
|
2265
2251
|
if (value > 1) {
|
|
2266
2252
|
throw new RangeError("New aim slider cheese penalty must be less than or equal to one.");
|
|
@@ -2268,8 +2254,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2268
2254
|
if (value === this._aimSliderCheesePenalty) {
|
|
2269
2255
|
return;
|
|
2270
2256
|
}
|
|
2271
|
-
this.aim *= value / this._aimSliderCheesePenalty;
|
|
2272
2257
|
this._aimSliderCheesePenalty = value;
|
|
2258
|
+
this.calculateAimValue();
|
|
2273
2259
|
this.calculateTotalValue();
|
|
2274
2260
|
}
|
|
2275
2261
|
/**
|
|
@@ -2277,11 +2263,11 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2277
2263
|
*
|
|
2278
2264
|
* The flashlight and total performance value will be recalculated afterwards.
|
|
2279
2265
|
*
|
|
2280
|
-
* @param value The slider cheese penalty value. Must be between 0
|
|
2266
|
+
* @param value The slider cheese penalty value. Must be between 0 and 1.
|
|
2281
2267
|
*/
|
|
2282
2268
|
applyFlashlightSliderCheesePenalty(value) {
|
|
2283
|
-
if (value
|
|
2284
|
-
throw new RangeError("New flashlight slider cheese penalty must be greater than zero.");
|
|
2269
|
+
if (value < 0) {
|
|
2270
|
+
throw new RangeError("New flashlight slider cheese penalty must be greater than or equal to zero.");
|
|
2285
2271
|
}
|
|
2286
2272
|
if (value > 1) {
|
|
2287
2273
|
throw new RangeError("New flashlight slider cheese penalty must be less than or equal to one.");
|
|
@@ -2289,8 +2275,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2289
2275
|
if (value === this._flashlightSliderCheesePenalty) {
|
|
2290
2276
|
return;
|
|
2291
2277
|
}
|
|
2292
|
-
this.flashlight *= value / this._flashlightSliderCheesePenalty;
|
|
2293
2278
|
this._flashlightSliderCheesePenalty = value;
|
|
2279
|
+
this.calculateFlashlightValue();
|
|
2294
2280
|
this.calculateTotalValue();
|
|
2295
2281
|
}
|
|
2296
2282
|
/**
|
|
@@ -2298,11 +2284,11 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2298
2284
|
*
|
|
2299
2285
|
* The visual and total performance value will be recalculated afterwards.
|
|
2300
2286
|
*
|
|
2301
|
-
* @param value The slider cheese penalty value. Must be between 0
|
|
2287
|
+
* @param value The slider cheese penalty value. Must be between 0 and 1.
|
|
2302
2288
|
*/
|
|
2303
2289
|
applyVisualSliderCheesePenalty(value) {
|
|
2304
|
-
if (value
|
|
2305
|
-
throw new RangeError("New visual slider cheese penalty must be greater than zero.");
|
|
2290
|
+
if (value < 0) {
|
|
2291
|
+
throw new RangeError("New visual slider cheese penalty must be greater than or equal to zero.");
|
|
2306
2292
|
}
|
|
2307
2293
|
if (value > 1) {
|
|
2308
2294
|
throw new RangeError("New visual slider cheese penalty must be less than or equal to one.");
|
|
@@ -2310,8 +2296,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2310
2296
|
if (value === this._visualSliderCheesePenalty) {
|
|
2311
2297
|
return;
|
|
2312
2298
|
}
|
|
2313
|
-
this.visual *= value / this._visualSliderCheesePenalty;
|
|
2314
2299
|
this._visualSliderCheesePenalty = value;
|
|
2300
|
+
this.calculateVisualValue();
|
|
2315
2301
|
this.calculateTotalValue();
|
|
2316
2302
|
}
|
|
2317
2303
|
calculateValues() {
|
|
@@ -2345,16 +2331,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2345
2331
|
*/
|
|
2346
2332
|
calculateAimValue() {
|
|
2347
2333
|
this.aim = this.baseValue(Math.pow(this.difficultyAttributes.aimDifficulty, 0.8));
|
|
2348
|
-
|
|
2349
|
-
// Penalize misses by assessing # of misses relative to the total # of objects.
|
|
2350
|
-
// Default a 3% reduction for any # of misses.
|
|
2351
|
-
this.aim *=
|
|
2352
|
-
0.97 *
|
|
2353
|
-
Math.pow(1 -
|
|
2354
|
-
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
|
|
2355
|
-
}
|
|
2356
|
-
// Combo scaling
|
|
2357
|
-
this.aim *= this.comboPenalty;
|
|
2334
|
+
this.aim *= this.calculateMissPenalty(this.difficultyAttributes.aimDifficultStrainCount);
|
|
2358
2335
|
// Scale the aim value with slider factor to nerf very likely dropped sliderends.
|
|
2359
2336
|
this.aim *= this.sliderNerfFactor;
|
|
2360
2337
|
// Scale the aim value with slider cheese penalty.
|
|
@@ -2369,20 +2346,23 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2369
2346
|
*/
|
|
2370
2347
|
calculateTapValue() {
|
|
2371
2348
|
this.tap = this.baseValue(this.difficultyAttributes.tapDifficulty);
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
this.
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2349
|
+
this.tap *= this.calculateMissPenalty(this.difficultyAttributes.tapDifficultStrainCount);
|
|
2350
|
+
// Normalize the deviation to 300 BPM.
|
|
2351
|
+
const normalizedDeviation = this.tapDeviation *
|
|
2352
|
+
Math.max(1, 50 / this.difficultyAttributes.averageSpeedDeltaTime);
|
|
2353
|
+
// We expect the player to get 7500/x deviation when doubletapping x BPM.
|
|
2354
|
+
// Using this expectation, we penalize scores with deviation above 25.
|
|
2355
|
+
const averageBPM = 60000 / 4 / this.difficultyAttributes.averageSpeedDeltaTime;
|
|
2356
|
+
const adjustedDeviation = normalizedDeviation *
|
|
2357
|
+
(1 +
|
|
2358
|
+
1 /
|
|
2359
|
+
(1 +
|
|
2360
|
+
Math.exp(-(normalizedDeviation - 7500 / averageBPM) /
|
|
2361
|
+
((2 * 300) / averageBPM))));
|
|
2382
2362
|
// Scale the tap value with tap deviation.
|
|
2383
2363
|
this.tap *=
|
|
2384
2364
|
1.1 *
|
|
2385
|
-
Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 *
|
|
2365
|
+
Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * adjustedDeviation)), 1.25);
|
|
2386
2366
|
// Scale the tap value with three-fingered penalty.
|
|
2387
2367
|
this.tap /= this._tapPenalty;
|
|
2388
2368
|
}
|
|
@@ -2397,7 +2377,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2397
2377
|
}
|
|
2398
2378
|
this.accuracy =
|
|
2399
2379
|
650 *
|
|
2400
|
-
Math.exp(-0.
|
|
2380
|
+
Math.exp(-0.1 * this._deviation) *
|
|
2401
2381
|
// The following function is to give higher reward for deviations lower than 25 (250 UR).
|
|
2402
2382
|
(15 / (this._deviation + 15) + 0.65);
|
|
2403
2383
|
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
|
|
@@ -2424,15 +2404,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2424
2404
|
}
|
|
2425
2405
|
this.flashlight =
|
|
2426
2406
|
Math.pow(this.difficultyAttributes.flashlightDifficulty, 1.6) * 25;
|
|
2427
|
-
|
|
2428
|
-
this.flashlight *= this.comboPenalty;
|
|
2429
|
-
if (this.effectiveMissCount > 0) {
|
|
2430
|
-
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
2431
|
-
this.flashlight *=
|
|
2432
|
-
0.97 *
|
|
2433
|
-
Math.pow(1 -
|
|
2434
|
-
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
|
|
2435
|
-
}
|
|
2407
|
+
this.flashlight *= this.calculateMissPenalty(this.difficultyAttributes.flashlightDifficultStrainCount);
|
|
2436
2408
|
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
2437
2409
|
this.flashlight *=
|
|
2438
2410
|
0.7 +
|
|
@@ -2451,15 +2423,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2451
2423
|
calculateVisualValue() {
|
|
2452
2424
|
this.visual =
|
|
2453
2425
|
Math.pow(this.difficultyAttributes.visualDifficulty, 1.6) * 22.5;
|
|
2454
|
-
|
|
2455
|
-
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
2456
|
-
this.visual *=
|
|
2457
|
-
0.97 *
|
|
2458
|
-
Math.pow(1 -
|
|
2459
|
-
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
|
|
2460
|
-
}
|
|
2461
|
-
// Combo scaling
|
|
2462
|
-
this.visual *= this.comboPenalty;
|
|
2426
|
+
this.visual *= this.calculateMissPenalty(this.difficultyAttributes.visualDifficultStrainCount);
|
|
2463
2427
|
// Scale the visual value with object count to penalize short maps.
|
|
2464
2428
|
this.visual *= Math.min(1, 1.650668 +
|
|
2465
2429
|
(0.4845796 - 1.650668) /
|
|
@@ -2471,6 +2435,21 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2471
2435
|
1.065 *
|
|
2472
2436
|
Math.pow(osuBase.ErrorFunction.erf(30 / (Math.SQRT2 * this._deviation)), 1.75);
|
|
2473
2437
|
}
|
|
2438
|
+
/**
|
|
2439
|
+
* Calculates miss penalty.
|
|
2440
|
+
*
|
|
2441
|
+
* Miss penalty assumes that a player will miss on the hardest parts of a map,
|
|
2442
|
+
* so we use the amount of relatively difficult sections to adjust miss penalty
|
|
2443
|
+
* to make it more punishing on maps with lower amount of hard sections.
|
|
2444
|
+
*/
|
|
2445
|
+
calculateMissPenalty(difficultStrainCount) {
|
|
2446
|
+
if (this.effectiveMissCount === 0) {
|
|
2447
|
+
return 1;
|
|
2448
|
+
}
|
|
2449
|
+
return (0.94 /
|
|
2450
|
+
(this.effectiveMissCount / (2 * Math.sqrt(difficultStrainCount)) +
|
|
2451
|
+
1));
|
|
2452
|
+
}
|
|
2474
2453
|
/**
|
|
2475
2454
|
* Estimates the player's tap deviation based on the OD, number of circles and sliders,
|
|
2476
2455
|
* and number of 300s, 100s, 50s, and misses, assuming the player's mean hit error is 0.
|
|
@@ -2490,17 +2469,16 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2490
2469
|
return Number.POSITIVE_INFINITY;
|
|
2491
2470
|
}
|
|
2492
2471
|
const hitWindow300 = new osuBase.OsuHitWindow(this.difficultyAttributes.overallDifficulty).hitWindowFor300();
|
|
2493
|
-
// Obtain the 50 hit window for droid.
|
|
2494
|
-
const
|
|
2495
|
-
mods: this.difficultyAttributes.mods,
|
|
2496
|
-
}).calculate().speedMultiplier;
|
|
2497
|
-
const realHitWindow300 = hitWindow300 * clockRate;
|
|
2472
|
+
// Obtain the 50 and 100 hit window for droid.
|
|
2473
|
+
const realHitWindow300 = hitWindow300 * this.difficultyAttributes.clockRate;
|
|
2498
2474
|
const droidHitWindow = new osuBase.DroidHitWindow(osuBase.OsuHitWindow.hitWindow300ToOD(realHitWindow300));
|
|
2499
|
-
const
|
|
2500
|
-
const
|
|
2501
|
-
this.
|
|
2502
|
-
|
|
2503
|
-
this.
|
|
2475
|
+
const isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
2476
|
+
const hitWindow50 = droidHitWindow.hitWindowFor50(isPrecise) /
|
|
2477
|
+
this.difficultyAttributes.clockRate;
|
|
2478
|
+
const hitWindow100 = droidHitWindow.hitWindowFor100(isPrecise) /
|
|
2479
|
+
this.difficultyAttributes.clockRate;
|
|
2480
|
+
const { n300, n100, n50, nmiss } = this.computedAccuracy;
|
|
2481
|
+
const greatCountOnCircles = this.difficultyAttributes.hitCircleCount - n100 - n50 - nmiss;
|
|
2504
2482
|
// The probability that a player hits a circle is unknown, but we can estimate it to be
|
|
2505
2483
|
// the number of greats on circles divided by the number of circles, and then add one
|
|
2506
2484
|
// to the number of circles as a bias correction / bayesian prior.
|
|
@@ -2519,10 +2497,32 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2519
2497
|
if (greatProbabilityCircle === 0 && greatProbabilitySlider === 0) {
|
|
2520
2498
|
return Number.POSITIVE_INFINITY;
|
|
2521
2499
|
}
|
|
2522
|
-
const
|
|
2523
|
-
(
|
|
2524
|
-
|
|
2525
|
-
|
|
2500
|
+
const calculateDeviation = (mainHitWindow, probability) => {
|
|
2501
|
+
if (probability === 0) {
|
|
2502
|
+
return Number.POSITIVE_INFINITY;
|
|
2503
|
+
}
|
|
2504
|
+
// Start with normal deviation.
|
|
2505
|
+
const normalDeviation = mainHitWindow /
|
|
2506
|
+
(Math.SQRT2 * osuBase.ErrorFunction.erfInv(probability));
|
|
2507
|
+
// Get the variance of the truncated variable.
|
|
2508
|
+
const truncatedVariance = Math.pow(normalDeviation, 2) -
|
|
2509
|
+
(Math.SQRT2 *
|
|
2510
|
+
hitWindow100 *
|
|
2511
|
+
normalDeviation *
|
|
2512
|
+
Math.exp(-0.5 * Math.pow(hitWindow100 / normalDeviation, 2))) /
|
|
2513
|
+
(Math.sqrt(Math.PI) *
|
|
2514
|
+
osuBase.ErrorFunction.erf(hitWindow100 / (Math.SQRT2 * normalDeviation)));
|
|
2515
|
+
// Add 50s by assuming they are uniformly distributed.
|
|
2516
|
+
return Math.sqrt((1 / (n300 + n100 + n50)) *
|
|
2517
|
+
((n300 + n100) * truncatedVariance +
|
|
2518
|
+
(n50 *
|
|
2519
|
+
(Math.pow(hitWindow50, 2) +
|
|
2520
|
+
hitWindow100 * hitWindow50 +
|
|
2521
|
+
Math.pow(hitWindow100, 2))) /
|
|
2522
|
+
3));
|
|
2523
|
+
};
|
|
2524
|
+
const deviationOnCircles = calculateDeviation(hitWindow300, greatProbabilityCircle);
|
|
2525
|
+
const deviationOnSliders = calculateDeviation(hitWindow50, greatProbabilitySlider);
|
|
2526
2526
|
return Math.min(deviationOnCircles, deviationOnSliders);
|
|
2527
2527
|
}
|
|
2528
2528
|
/**
|
|
@@ -2536,13 +2536,48 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2536
2536
|
return Number.POSITIVE_INFINITY;
|
|
2537
2537
|
}
|
|
2538
2538
|
const hitWindow300 = new osuBase.OsuHitWindow(this.difficultyAttributes.overallDifficulty).hitWindowFor300();
|
|
2539
|
-
|
|
2540
|
-
const
|
|
2539
|
+
// Obtain the 50 and 100 hit window for droid.
|
|
2540
|
+
const realHitWindow300 = hitWindow300 * this.difficultyAttributes.clockRate;
|
|
2541
|
+
const droidHitWindow = new osuBase.DroidHitWindow(osuBase.OsuHitWindow.hitWindow300ToOD(realHitWindow300));
|
|
2542
|
+
const isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
2543
|
+
const hitWindow50 = droidHitWindow.hitWindowFor50(isPrecise) /
|
|
2544
|
+
this.difficultyAttributes.clockRate;
|
|
2545
|
+
const hitWindow100 = droidHitWindow.hitWindowFor100(isPrecise) /
|
|
2546
|
+
this.difficultyAttributes.clockRate;
|
|
2547
|
+
const { n300, n100, n50, nmiss } = this.computedAccuracy;
|
|
2548
|
+
// Assume a fixed ratio of non-300s hit in speed notes based on speed note count ratio and OD.
|
|
2549
|
+
// Graph: https://www.desmos.com/calculator/31argjcxqc
|
|
2550
|
+
const speedNoteRatio = this.difficultyAttributes.speedNoteCount / this.totalHits;
|
|
2551
|
+
const nonGreatCount = n100 + n50 + nmiss;
|
|
2552
|
+
const nonGreatRatio = 1 -
|
|
2553
|
+
(Math.pow(Math.exp(Math.sqrt(hitWindow300)) + 1, 1 - speedNoteRatio) -
|
|
2554
|
+
1) /
|
|
2555
|
+
Math.exp(Math.sqrt(hitWindow300));
|
|
2556
|
+
const relevantCountGreat = Math.max(0, this.difficultyAttributes.speedNoteCount -
|
|
2557
|
+
nonGreatCount * nonGreatRatio);
|
|
2541
2558
|
if (relevantCountGreat === 0) {
|
|
2542
2559
|
return Number.POSITIVE_INFINITY;
|
|
2543
2560
|
}
|
|
2544
2561
|
const greatProbability = relevantCountGreat / (this.difficultyAttributes.speedNoteCount + 1);
|
|
2545
|
-
|
|
2562
|
+
// Start with normal deviation.
|
|
2563
|
+
const normalDeviation = hitWindow300 /
|
|
2564
|
+
(Math.SQRT2 * osuBase.ErrorFunction.erfInv(greatProbability));
|
|
2565
|
+
// Get the variance of the truncated variable.
|
|
2566
|
+
const truncatedVariance = Math.pow(normalDeviation, 2) -
|
|
2567
|
+
(Math.SQRT2 *
|
|
2568
|
+
hitWindow100 *
|
|
2569
|
+
normalDeviation *
|
|
2570
|
+
Math.exp(-0.5 * Math.pow(hitWindow100 / normalDeviation, 2))) /
|
|
2571
|
+
(Math.sqrt(Math.PI) *
|
|
2572
|
+
osuBase.ErrorFunction.erf(hitWindow100 / (Math.SQRT2 * normalDeviation)));
|
|
2573
|
+
// Add 50s by assuming they are uniformly distributed.
|
|
2574
|
+
return Math.sqrt((1 / (n300 + n100 + n50)) *
|
|
2575
|
+
((n300 + n100) * truncatedVariance +
|
|
2576
|
+
(n50 *
|
|
2577
|
+
(Math.pow(hitWindow50, 2) +
|
|
2578
|
+
hitWindow100 * hitWindow50 +
|
|
2579
|
+
Math.pow(hitWindow100, 2))) /
|
|
2580
|
+
3));
|
|
2546
2581
|
}
|
|
2547
2582
|
toString() {
|
|
2548
2583
|
return (this.total.toFixed(2) +
|
|
@@ -3006,7 +3041,7 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
3006
3041
|
const opacityBonus = 1 +
|
|
3007
3042
|
this.maxOpacityBonus *
|
|
3008
3043
|
(1 -
|
|
3009
|
-
current.opacityAt(currentObject.object.startTime, isHiddenMod
|
|
3044
|
+
current.opacityAt(currentObject.object.startTime, isHiddenMod));
|
|
3010
3045
|
result +=
|
|
3011
3046
|
(stackNerf * opacityBonus * scalingFactor * jumpDistance) /
|
|
3012
3047
|
cumulativeStrainTime;
|
|
@@ -3098,6 +3133,7 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3098
3133
|
flashlightDifficulty: 0,
|
|
3099
3134
|
speedNoteCount: 0,
|
|
3100
3135
|
sliderFactor: 0,
|
|
3136
|
+
clockRate: 1,
|
|
3101
3137
|
approachRate: 0,
|
|
3102
3138
|
overallDifficulty: 0,
|
|
3103
3139
|
hitCircleCount: 0,
|
|
@@ -3178,9 +3214,6 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3178
3214
|
this.postCalculateFlashlight(flashlightSkill);
|
|
3179
3215
|
this.calculateTotal();
|
|
3180
3216
|
}
|
|
3181
|
-
/**
|
|
3182
|
-
* Returns a string representative of the class.
|
|
3183
|
-
*/
|
|
3184
3217
|
toString() {
|
|
3185
3218
|
return (this.total.toFixed(2) +
|
|
3186
3219
|
" stars (" +
|
|
@@ -3191,9 +3224,17 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3191
3224
|
this.flashlight.toFixed(2) +
|
|
3192
3225
|
" flashlight)");
|
|
3193
3226
|
}
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3227
|
+
preProcess() {
|
|
3228
|
+
const scale = osuBase.CircleSizeCalculator.standardCSToStandardScale(this.stats.cs);
|
|
3229
|
+
for (const object of this.beatmap.hitObjects.objects) {
|
|
3230
|
+
object.osuScale = scale;
|
|
3231
|
+
}
|
|
3232
|
+
const ar = new osuBase.MapStats({
|
|
3233
|
+
ar: this.beatmap.difficulty.ar,
|
|
3234
|
+
mods: osuBase.ModUtil.removeSpeedChangingMods(this.mods),
|
|
3235
|
+
}).calculate().ar;
|
|
3236
|
+
osuBase.HitObjectStackEvaluator.applyStandardStacking(this.beatmap.formatVersion, this.beatmap.hitObjects.objects, ar, this.beatmap.general.stackLeniency);
|
|
3237
|
+
}
|
|
3197
3238
|
createSkills() {
|
|
3198
3239
|
return [
|
|
3199
3240
|
new OsuAim(this.mods, true),
|
|
@@ -3386,7 +3427,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3386
3427
|
// Scale the aim value with slider factor to nerf very likely dropped sliderends.
|
|
3387
3428
|
this.aim *= this.sliderNerfFactor;
|
|
3388
3429
|
// Scale the aim value with accuracy.
|
|
3389
|
-
this.aim *= this.computedAccuracy.value(
|
|
3430
|
+
this.aim *= this.computedAccuracy.value();
|
|
3390
3431
|
// It is also important to consider accuracy difficulty when doing that.
|
|
3391
3432
|
const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
|
|
3392
3433
|
this.aim *= 0.98 + odScaling;
|
|
@@ -3434,15 +3475,14 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3434
3475
|
n300: Math.max(0, countGreat - relevantTotalDiff),
|
|
3435
3476
|
n100: Math.max(0, countOk - Math.max(0, relevantTotalDiff - countGreat)),
|
|
3436
3477
|
n50: Math.max(0, countMeh - Math.max(0, relevantTotalDiff - countGreat - countOk)),
|
|
3437
|
-
nmiss: this.effectiveMissCount,
|
|
3438
3478
|
});
|
|
3439
3479
|
// Scale the speed value with accuracy and OD.
|
|
3440
3480
|
this.speed *=
|
|
3441
3481
|
(0.95 +
|
|
3442
3482
|
Math.pow(this.difficultyAttributes.overallDifficulty, 2) /
|
|
3443
3483
|
750) *
|
|
3444
|
-
Math.pow((this.computedAccuracy.value(
|
|
3445
|
-
relevantAccuracy.value()) /
|
|
3484
|
+
Math.pow((this.computedAccuracy.value() +
|
|
3485
|
+
relevantAccuracy.value(this.difficultyAttributes.speedNoteCount)) /
|
|
3446
3486
|
2, (14.5 -
|
|
3447
3487
|
Math.max(this.difficultyAttributes.overallDifficulty, 8)) /
|
|
3448
3488
|
2);
|
|
@@ -3472,7 +3512,8 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3472
3512
|
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
|
|
3473
3513
|
this.accuracy =
|
|
3474
3514
|
Math.pow(1.52163, this.difficultyAttributes.overallDifficulty) *
|
|
3475
|
-
|
|
3515
|
+
// It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points.
|
|
3516
|
+
Math.pow(realAccuracy.n300 < 0 ? 0 : realAccuracy.value(), 24) *
|
|
3476
3517
|
2.83;
|
|
3477
3518
|
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
|
|
3478
3519
|
this.accuracy *= Math.min(1.15, Math.pow(ncircles / 1000, 0.3));
|
|
@@ -3511,8 +3552,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3511
3552
|
? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
|
|
3512
3553
|
: 0);
|
|
3513
3554
|
// Scale the flashlight value with accuracy slightly.
|
|
3514
|
-
this.flashlight *=
|
|
3515
|
-
0.5 + this.computedAccuracy.value(this.totalHits) / 2;
|
|
3555
|
+
this.flashlight *= 0.5 + this.computedAccuracy.value() / 2;
|
|
3516
3556
|
// It is also important to consider accuracy difficulty when doing that.
|
|
3517
3557
|
const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
|
|
3518
3558
|
this.flashlight *= 0.98 + odScaling;
|