@rian8337/osu-difficulty-calculator 4.0.0-beta.0 → 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 +244 -213
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/typings/index.d.ts +46 -29
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
|
/**
|
|
@@ -1273,6 +1253,7 @@ class DroidFlashlight extends DroidSkill {
|
|
|
1273
1253
|
* This class should be considered an "evaluating" class and not persisted.
|
|
1274
1254
|
*/
|
|
1275
1255
|
class RhythmEvaluator {
|
|
1256
|
+
static rhythmMultiplier = 0.75;
|
|
1276
1257
|
static historyTimeMax = 5000; // 5 seconds of calculateRhythmBonus max.
|
|
1277
1258
|
}
|
|
1278
1259
|
|
|
@@ -1290,7 +1271,7 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
|
|
|
1290
1271
|
static evaluateDifficultyOf(current, greatWindow) {
|
|
1291
1272
|
if (current.object instanceof osuBase.Spinner ||
|
|
1292
1273
|
// Exclude overlapping objects that can be tapped at once.
|
|
1293
|
-
current.
|
|
1274
|
+
current.isOverlapping(false)) {
|
|
1294
1275
|
return 1;
|
|
1295
1276
|
}
|
|
1296
1277
|
let previousIslandSize = 0;
|
|
@@ -1308,7 +1289,7 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
|
|
|
1308
1289
|
if (!object) {
|
|
1309
1290
|
break;
|
|
1310
1291
|
}
|
|
1311
|
-
if (object.
|
|
1292
|
+
if (!object.isOverlapping(false)) {
|
|
1312
1293
|
validPrevious.push(object);
|
|
1313
1294
|
}
|
|
1314
1295
|
}
|
|
@@ -1402,20 +1383,7 @@ class DroidRhythmEvaluator extends RhythmEvaluator {
|
|
|
1402
1383
|
const windowRatio = Math.pow(Math.min(1, currentDeltaTime / (greatWindow * 2)), 2);
|
|
1403
1384
|
doubletapness = Math.pow(speedRatio, 1 - windowRatio);
|
|
1404
1385
|
}
|
|
1405
|
-
return (Math.sqrt(4 +
|
|
1406
|
-
rhythmComplexitySum *
|
|
1407
|
-
this.calculateRhythmMultiplier(greatWindow) *
|
|
1408
|
-
doubletapness) / 2);
|
|
1409
|
-
}
|
|
1410
|
-
/**
|
|
1411
|
-
* Calculates the rhythm multiplier of a given hit window.
|
|
1412
|
-
*
|
|
1413
|
-
* @param greatWindow The great hit window.
|
|
1414
|
-
*/
|
|
1415
|
-
static calculateRhythmMultiplier(greatWindow) {
|
|
1416
|
-
const od = osuBase.OsuHitWindow.hitWindow300ToOD(greatWindow);
|
|
1417
|
-
const odScaling = Math.pow(od, 2) / 400;
|
|
1418
|
-
return 0.75 + (od >= 0 ? odScaling : -odScaling);
|
|
1386
|
+
return (Math.sqrt(4 + rhythmComplexitySum * this.rhythmMultiplier * doubletapness) / 2);
|
|
1419
1387
|
}
|
|
1420
1388
|
}
|
|
1421
1389
|
|
|
@@ -1468,7 +1436,8 @@ class DroidVisualEvaluator {
|
|
|
1468
1436
|
static evaluateDifficultyOf(current, isHiddenMod, withSliders) {
|
|
1469
1437
|
if (current.object instanceof osuBase.Spinner ||
|
|
1470
1438
|
// Exclude overlapping objects that can be tapped at once.
|
|
1471
|
-
current.isOverlapping(true)
|
|
1439
|
+
current.isOverlapping(true) ||
|
|
1440
|
+
current.index === 0) {
|
|
1472
1441
|
return 0;
|
|
1473
1442
|
}
|
|
1474
1443
|
// Start with base density and give global bonus for Hidden.
|
|
@@ -1495,7 +1464,7 @@ class DroidVisualEvaluator {
|
|
|
1495
1464
|
}
|
|
1496
1465
|
strain +=
|
|
1497
1466
|
(1 -
|
|
1498
|
-
current.opacityAt(previous.object.startTime, isHiddenMod
|
|
1467
|
+
current.opacityAt(previous.object.startTime, isHiddenMod)) /
|
|
1499
1468
|
4;
|
|
1500
1469
|
}
|
|
1501
1470
|
// Scale the value with overlapping factor.
|
|
@@ -1531,44 +1500,6 @@ class DroidVisualEvaluator {
|
|
|
1531
1500
|
Math.min(1, 300 / cumulativeStrainTime);
|
|
1532
1501
|
}
|
|
1533
1502
|
}
|
|
1534
|
-
// Reward for rhythm changes.
|
|
1535
|
-
if (current.rhythmMultiplier > 1) {
|
|
1536
|
-
let rhythmBonus = (current.rhythmMultiplier - 1) / 20;
|
|
1537
|
-
// Rhythm changes are harder to read in Hidden.
|
|
1538
|
-
// Add additional bonus for Hidden.
|
|
1539
|
-
if (isHiddenMod) {
|
|
1540
|
-
rhythmBonus += (current.rhythmMultiplier - 1) / 25;
|
|
1541
|
-
}
|
|
1542
|
-
// Rhythm changes are harder to read when objects are stacked together.
|
|
1543
|
-
// Scale rhythm bonus based on the stack of past objects.
|
|
1544
|
-
const diameter = 2 * current.object.getRadius(osuBase.Modes.droid);
|
|
1545
|
-
let cumulativeStrainTime = 0;
|
|
1546
|
-
for (let i = 0; i < Math.min(current.index, 5); ++i) {
|
|
1547
|
-
const previous = current.previous(i);
|
|
1548
|
-
if (previous.object instanceof osuBase.Spinner ||
|
|
1549
|
-
// Exclude overlapping objects that can be tapped at once.
|
|
1550
|
-
previous.isOverlapping(true)) {
|
|
1551
|
-
continue;
|
|
1552
|
-
}
|
|
1553
|
-
const jumpDistance = current.object
|
|
1554
|
-
.getStackedPosition(osuBase.Modes.droid)
|
|
1555
|
-
.getDistance(previous.object.getStackedEndPosition(osuBase.Modes.droid));
|
|
1556
|
-
cumulativeStrainTime += previous.strainTime;
|
|
1557
|
-
rhythmBonus +=
|
|
1558
|
-
// Scale the bonus with diameter.
|
|
1559
|
-
osuBase.MathUtils.clamp((0.5 - jumpDistance / diameter) / 10, 0, 0.05) *
|
|
1560
|
-
// Scale with cumulative strain time to avoid overbuffing past objects.
|
|
1561
|
-
Math.min(1, 300 / cumulativeStrainTime);
|
|
1562
|
-
// Give a larger bonus for Hidden.
|
|
1563
|
-
if (isHiddenMod) {
|
|
1564
|
-
rhythmBonus +=
|
|
1565
|
-
(1 -
|
|
1566
|
-
current.opacityAt(previous.object.startTime, isHiddenMod, osuBase.Modes.droid)) /
|
|
1567
|
-
20;
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
strain += rhythmBonus;
|
|
1571
|
-
}
|
|
1572
1503
|
return strain;
|
|
1573
1504
|
}
|
|
1574
1505
|
}
|
|
@@ -1583,20 +1514,20 @@ class DroidVisual extends DroidSkill {
|
|
|
1583
1514
|
skillMultiplier = 10;
|
|
1584
1515
|
strainDecayBase = 0.1;
|
|
1585
1516
|
isHidden;
|
|
1586
|
-
|
|
1517
|
+
withsliders;
|
|
1587
1518
|
constructor(mods, withSliders) {
|
|
1588
1519
|
super(mods);
|
|
1589
1520
|
this.isHidden = mods.some((m) => m instanceof osuBase.ModHidden);
|
|
1590
|
-
this.
|
|
1521
|
+
this.withsliders = withSliders;
|
|
1591
1522
|
}
|
|
1592
1523
|
strainValueAt(current) {
|
|
1593
1524
|
this.currentStrain *= this.strainDecay(current.deltaTime);
|
|
1594
1525
|
this.currentStrain +=
|
|
1595
|
-
DroidVisualEvaluator.evaluateDifficultyOf(current, this.isHidden, this.
|
|
1596
|
-
return this.currentStrain;
|
|
1526
|
+
DroidVisualEvaluator.evaluateDifficultyOf(current, this.isHidden, this.withsliders) * this.skillMultiplier;
|
|
1527
|
+
return this.currentStrain * (1 + (current.rhythmMultiplier - 1) / 5);
|
|
1597
1528
|
}
|
|
1598
1529
|
saveToHitObject(current) {
|
|
1599
|
-
if (this.
|
|
1530
|
+
if (this.withsliders) {
|
|
1600
1531
|
current.visualStrainWithSliders = this.currentStrain;
|
|
1601
1532
|
}
|
|
1602
1533
|
else {
|
|
@@ -1647,15 +1578,21 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1647
1578
|
flashlightDifficulty: 0,
|
|
1648
1579
|
speedNoteCount: 0,
|
|
1649
1580
|
sliderFactor: 0,
|
|
1581
|
+
clockRate: 1,
|
|
1650
1582
|
approachRate: 0,
|
|
1651
1583
|
overallDifficulty: 0,
|
|
1652
1584
|
hitCircleCount: 0,
|
|
1653
1585
|
sliderCount: 0,
|
|
1654
1586
|
spinnerCount: 0,
|
|
1587
|
+
aimDifficultStrainCount: 0,
|
|
1588
|
+
tapDifficultStrainCount: 0,
|
|
1589
|
+
flashlightDifficultStrainCount: 0,
|
|
1590
|
+
visualDifficultStrainCount: 0,
|
|
1655
1591
|
flashlightSliderFactor: 0,
|
|
1656
1592
|
visualSliderFactor: 0,
|
|
1657
1593
|
possibleThreeFingeredSections: [],
|
|
1658
1594
|
difficultSliders: [],
|
|
1595
|
+
averageSpeedDeltaTime: 0,
|
|
1659
1596
|
};
|
|
1660
1597
|
difficultyMultiplier = 0.18;
|
|
1661
1598
|
mode = osuBase.Modes.droid;
|
|
@@ -1777,9 +1714,6 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1777
1714
|
}
|
|
1778
1715
|
this.calculateTotal();
|
|
1779
1716
|
}
|
|
1780
|
-
/**
|
|
1781
|
-
* Returns a string representative of the class.
|
|
1782
|
-
*/
|
|
1783
1717
|
toString() {
|
|
1784
1718
|
return (this.total.toFixed(2) +
|
|
1785
1719
|
" stars (" +
|
|
@@ -1794,9 +1728,13 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1794
1728
|
this.visual.toFixed(2) +
|
|
1795
1729
|
" visual)");
|
|
1796
1730
|
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
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
|
+
}
|
|
1800
1738
|
createSkills() {
|
|
1801
1739
|
return [
|
|
1802
1740
|
new DroidAim(this.mods, true),
|
|
@@ -1837,33 +1775,39 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1837
1775
|
calculateAimAttributes() {
|
|
1838
1776
|
const objectStrains = [];
|
|
1839
1777
|
let maxStrain = 0;
|
|
1840
|
-
// Take the top 15% most difficult sliders based on velocity.
|
|
1841
1778
|
const topDifficultSliders = [];
|
|
1842
1779
|
for (let i = 0; i < this.objects.length; ++i) {
|
|
1843
1780
|
const object = this.objects[i];
|
|
1844
1781
|
objectStrains.push(object.aimStrainWithSliders);
|
|
1845
1782
|
maxStrain = Math.max(maxStrain, object.aimStrainWithSliders);
|
|
1846
|
-
|
|
1783
|
+
const velocity = object.travelDistance / object.travelTime;
|
|
1784
|
+
if (velocity > 0) {
|
|
1847
1785
|
topDifficultSliders.push({
|
|
1848
1786
|
index: i,
|
|
1849
|
-
velocity:
|
|
1787
|
+
velocity: velocity,
|
|
1850
1788
|
});
|
|
1851
|
-
topDifficultSliders.sort((a, b) => b.velocity - a.velocity);
|
|
1852
|
-
while (topDifficultSliders.length >
|
|
1853
|
-
Math.floor(0.15 * this.objects.length)) {
|
|
1854
|
-
topDifficultSliders.pop();
|
|
1855
|
-
}
|
|
1856
1789
|
}
|
|
1857
1790
|
}
|
|
1858
1791
|
if (maxStrain) {
|
|
1859
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);
|
|
1860
1794
|
}
|
|
1861
1795
|
const velocitySum = topDifficultSliders.reduce((a, v) => a + v.velocity, 0);
|
|
1862
1796
|
for (const slider of topDifficultSliders) {
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
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();
|
|
1867
1811
|
}
|
|
1868
1812
|
}
|
|
1869
1813
|
/**
|
|
@@ -1882,6 +1826,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1882
1826
|
this.attributes.possibleThreeFingeredSections = [];
|
|
1883
1827
|
const tempSections = [];
|
|
1884
1828
|
const objectStrains = [];
|
|
1829
|
+
const objectDeltaTimes = [];
|
|
1885
1830
|
let maxStrain = 0;
|
|
1886
1831
|
const maxSectionDeltaTime = 2000;
|
|
1887
1832
|
const minSectionObjectCount = 5;
|
|
@@ -1891,8 +1836,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1891
1836
|
const next = this.objects[i + 1];
|
|
1892
1837
|
if (i === 0) {
|
|
1893
1838
|
objectStrains.push(current.tapStrain);
|
|
1839
|
+
objectDeltaTimes.push(current.deltaTime);
|
|
1894
1840
|
}
|
|
1895
1841
|
objectStrains.push(next.tapStrain);
|
|
1842
|
+
objectDeltaTimes.push(next.deltaTime);
|
|
1896
1843
|
maxStrain = Math.max(current.tapStrain, maxStrain);
|
|
1897
1844
|
const realDeltaTime = next.object.startTime - current.object.endTime;
|
|
1898
1845
|
if (realDeltaTime >= maxSectionDeltaTime) {
|
|
@@ -1931,6 +1878,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1931
1878
|
if (inSpeedSection &&
|
|
1932
1879
|
current.originalTapStrain < threeFingerStrainThreshold) {
|
|
1933
1880
|
inSpeedSection = false;
|
|
1881
|
+
// Ignore sections that don't meet object count requirement.
|
|
1882
|
+
if (i - newFirstObjectIndex < minSectionObjectCount) {
|
|
1883
|
+
continue;
|
|
1884
|
+
}
|
|
1934
1885
|
this.attributes.possibleThreeFingeredSections.push({
|
|
1935
1886
|
firstObjectIndex: newFirstObjectIndex,
|
|
1936
1887
|
lastObjectIndex: i,
|
|
@@ -1939,7 +1890,10 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1939
1890
|
}
|
|
1940
1891
|
}
|
|
1941
1892
|
// Don't forget to manually add the last beatmap section, which would otherwise be ignored.
|
|
1942
|
-
|
|
1893
|
+
// Ignore sections that don't meet object count requirement.
|
|
1894
|
+
if (inSpeedSection &&
|
|
1895
|
+
section.lastObjectIndex - newFirstObjectIndex >=
|
|
1896
|
+
minSectionObjectCount) {
|
|
1943
1897
|
this.attributes.possibleThreeFingeredSections.push({
|
|
1944
1898
|
firstObjectIndex: newFirstObjectIndex,
|
|
1945
1899
|
lastObjectIndex: section.lastObjectIndex,
|
|
@@ -1949,6 +1903,16 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1949
1903
|
}
|
|
1950
1904
|
if (maxStrain) {
|
|
1951
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);
|
|
1952
1916
|
}
|
|
1953
1917
|
}
|
|
1954
1918
|
/**
|
|
@@ -1991,6 +1955,12 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1991
1955
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
1992
1956
|
this.flashlight *= 0.7;
|
|
1993
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
|
+
}
|
|
1994
1964
|
this.attributes.flashlightDifficulty = this.flashlight;
|
|
1995
1965
|
}
|
|
1996
1966
|
/**
|
|
@@ -2008,6 +1978,11 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
2008
1978
|
this.starValue(visualSkillWithoutSliders.difficultyValue()) /
|
|
2009
1979
|
this.visual;
|
|
2010
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
|
+
}
|
|
2011
1986
|
}
|
|
2012
1987
|
}
|
|
2013
1988
|
|
|
@@ -2130,7 +2105,7 @@ class PerformanceCalculator {
|
|
|
2130
2105
|
if (this.difficultyAttributes.sliderCount > 0) {
|
|
2131
2106
|
// We assume 15% of sliders in a beatmap are difficult since there's no way to tell from the performance calculator.
|
|
2132
2107
|
const estimateDifficultSliders = this.difficultyAttributes.sliderCount * 0.15;
|
|
2133
|
-
const estimateSliderEndsDropped = osuBase.MathUtils.clamp(Math.min(this.computedAccuracy.
|
|
2108
|
+
const estimateSliderEndsDropped = osuBase.MathUtils.clamp(Math.min(this.computedAccuracy.n100 +
|
|
2134
2109
|
this.computedAccuracy.n50 +
|
|
2135
2110
|
this.computedAccuracy.nmiss, maxCombo - combo), 0, estimateDifficultSliders);
|
|
2136
2111
|
this.sliderNerfFactor =
|
|
@@ -2145,6 +2120,7 @@ class PerformanceCalculator {
|
|
|
2145
2120
|
* Calculates the amount of misses + sliderbreaks from combo.
|
|
2146
2121
|
*/
|
|
2147
2122
|
calculateEffectiveMissCount(combo, maxCombo) {
|
|
2123
|
+
// Guess the number of misses + slider breaks from combo.
|
|
2148
2124
|
let comboBasedMissCount = 0;
|
|
2149
2125
|
if (this.difficultyAttributes.sliderCount > 0) {
|
|
2150
2126
|
const fullComboThreshold = maxCombo - 0.1 * this.difficultyAttributes.sliderCount;
|
|
@@ -2266,20 +2242,20 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2266
2242
|
*
|
|
2267
2243
|
* The aim and total performance value will be recalculated afterwards.
|
|
2268
2244
|
*
|
|
2269
|
-
* @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.
|
|
2270
2246
|
*/
|
|
2271
2247
|
applyAimSliderCheesePenalty(value) {
|
|
2272
|
-
if (value
|
|
2273
|
-
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.");
|
|
2274
2250
|
}
|
|
2275
2251
|
if (value > 1) {
|
|
2276
|
-
throw new RangeError("New
|
|
2252
|
+
throw new RangeError("New aim slider cheese penalty must be less than or equal to one.");
|
|
2277
2253
|
}
|
|
2278
2254
|
if (value === this._aimSliderCheesePenalty) {
|
|
2279
2255
|
return;
|
|
2280
2256
|
}
|
|
2281
|
-
this.aim *= this._aimSliderCheesePenalty / value;
|
|
2282
2257
|
this._aimSliderCheesePenalty = value;
|
|
2258
|
+
this.calculateAimValue();
|
|
2283
2259
|
this.calculateTotalValue();
|
|
2284
2260
|
}
|
|
2285
2261
|
/**
|
|
@@ -2287,11 +2263,11 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2287
2263
|
*
|
|
2288
2264
|
* The flashlight and total performance value will be recalculated afterwards.
|
|
2289
2265
|
*
|
|
2290
|
-
* @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.
|
|
2291
2267
|
*/
|
|
2292
2268
|
applyFlashlightSliderCheesePenalty(value) {
|
|
2293
|
-
if (value
|
|
2294
|
-
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.");
|
|
2295
2271
|
}
|
|
2296
2272
|
if (value > 1) {
|
|
2297
2273
|
throw new RangeError("New flashlight slider cheese penalty must be less than or equal to one.");
|
|
@@ -2299,8 +2275,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2299
2275
|
if (value === this._flashlightSliderCheesePenalty) {
|
|
2300
2276
|
return;
|
|
2301
2277
|
}
|
|
2302
|
-
this.flashlight *= this._flashlightSliderCheesePenalty / value;
|
|
2303
2278
|
this._flashlightSliderCheesePenalty = value;
|
|
2279
|
+
this.calculateFlashlightValue();
|
|
2304
2280
|
this.calculateTotalValue();
|
|
2305
2281
|
}
|
|
2306
2282
|
/**
|
|
@@ -2308,11 +2284,11 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2308
2284
|
*
|
|
2309
2285
|
* The visual and total performance value will be recalculated afterwards.
|
|
2310
2286
|
*
|
|
2311
|
-
* @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.
|
|
2312
2288
|
*/
|
|
2313
2289
|
applyVisualSliderCheesePenalty(value) {
|
|
2314
|
-
if (value
|
|
2315
|
-
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.");
|
|
2316
2292
|
}
|
|
2317
2293
|
if (value > 1) {
|
|
2318
2294
|
throw new RangeError("New visual slider cheese penalty must be less than or equal to one.");
|
|
@@ -2320,8 +2296,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2320
2296
|
if (value === this._visualSliderCheesePenalty) {
|
|
2321
2297
|
return;
|
|
2322
2298
|
}
|
|
2323
|
-
this.visual *= this._visualSliderCheesePenalty / value;
|
|
2324
2299
|
this._visualSliderCheesePenalty = value;
|
|
2300
|
+
this.calculateVisualValue();
|
|
2325
2301
|
this.calculateTotalValue();
|
|
2326
2302
|
}
|
|
2327
2303
|
calculateValues() {
|
|
@@ -2355,16 +2331,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2355
2331
|
*/
|
|
2356
2332
|
calculateAimValue() {
|
|
2357
2333
|
this.aim = this.baseValue(Math.pow(this.difficultyAttributes.aimDifficulty, 0.8));
|
|
2358
|
-
|
|
2359
|
-
// Penalize misses by assessing # of misses relative to the total # of objects.
|
|
2360
|
-
// Default a 3% reduction for any # of misses.
|
|
2361
|
-
this.aim *=
|
|
2362
|
-
0.97 *
|
|
2363
|
-
Math.pow(1 -
|
|
2364
|
-
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
|
|
2365
|
-
}
|
|
2366
|
-
// Combo scaling
|
|
2367
|
-
this.aim *= this.comboPenalty;
|
|
2334
|
+
this.aim *= this.calculateMissPenalty(this.difficultyAttributes.aimDifficultStrainCount);
|
|
2368
2335
|
// Scale the aim value with slider factor to nerf very likely dropped sliderends.
|
|
2369
2336
|
this.aim *= this.sliderNerfFactor;
|
|
2370
2337
|
// Scale the aim value with slider cheese penalty.
|
|
@@ -2379,20 +2346,23 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2379
2346
|
*/
|
|
2380
2347
|
calculateTapValue() {
|
|
2381
2348
|
this.tap = this.baseValue(this.difficultyAttributes.tapDifficulty);
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
this.
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
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))));
|
|
2392
2362
|
// Scale the tap value with tap deviation.
|
|
2393
2363
|
this.tap *=
|
|
2394
2364
|
1.1 *
|
|
2395
|
-
Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 *
|
|
2365
|
+
Math.pow(osuBase.ErrorFunction.erf(25 / (Math.SQRT2 * adjustedDeviation)), 1.25);
|
|
2396
2366
|
// Scale the tap value with three-fingered penalty.
|
|
2397
2367
|
this.tap /= this._tapPenalty;
|
|
2398
2368
|
}
|
|
@@ -2407,9 +2377,14 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2407
2377
|
}
|
|
2408
2378
|
this.accuracy =
|
|
2409
2379
|
650 *
|
|
2410
|
-
Math.exp(-0.
|
|
2380
|
+
Math.exp(-0.1 * this._deviation) *
|
|
2411
2381
|
// The following function is to give higher reward for deviations lower than 25 (250 UR).
|
|
2412
2382
|
(15 / (this._deviation + 15) + 0.65);
|
|
2383
|
+
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
|
|
2384
|
+
const ncircles = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModScoreV2)
|
|
2385
|
+
? this.totalHits - this.difficultyAttributes.spinnerCount
|
|
2386
|
+
: this.difficultyAttributes.hitCircleCount;
|
|
2387
|
+
this.accuracy *= Math.min(1.15, Math.sqrt(Math.log(1 + ((Math.E - 1) * ncircles) / 1000)));
|
|
2413
2388
|
// Scale the accuracy value with rhythm complexity.
|
|
2414
2389
|
this.accuracy *=
|
|
2415
2390
|
1.5 /
|
|
@@ -2429,15 +2404,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2429
2404
|
}
|
|
2430
2405
|
this.flashlight =
|
|
2431
2406
|
Math.pow(this.difficultyAttributes.flashlightDifficulty, 1.6) * 25;
|
|
2432
|
-
|
|
2433
|
-
this.flashlight *= this.comboPenalty;
|
|
2434
|
-
if (this.effectiveMissCount > 0) {
|
|
2435
|
-
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
2436
|
-
this.flashlight *=
|
|
2437
|
-
0.97 *
|
|
2438
|
-
Math.pow(1 -
|
|
2439
|
-
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), Math.pow(this.effectiveMissCount, 0.875));
|
|
2440
|
-
}
|
|
2407
|
+
this.flashlight *= this.calculateMissPenalty(this.difficultyAttributes.flashlightDifficultStrainCount);
|
|
2441
2408
|
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
2442
2409
|
this.flashlight *=
|
|
2443
2410
|
0.7 +
|
|
@@ -2447,9 +2414,8 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2447
2414
|
: 0);
|
|
2448
2415
|
// Scale the flashlight value with slider cheese penalty.
|
|
2449
2416
|
this.flashlight *= this._flashlightSliderCheesePenalty;
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
this.flashlight *= osuBase.ErrorFunction.erf(50 / (Math.SQRT2 * this._deviation));
|
|
2417
|
+
// Scale the flashlight value with deviation.
|
|
2418
|
+
this.flashlight *= osuBase.ErrorFunction.erf(50 / (Math.SQRT2 * this._deviation));
|
|
2453
2419
|
}
|
|
2454
2420
|
/**
|
|
2455
2421
|
* Calculates the visual performance value of the beatmap.
|
|
@@ -2457,15 +2423,7 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2457
2423
|
calculateVisualValue() {
|
|
2458
2424
|
this.visual =
|
|
2459
2425
|
Math.pow(this.difficultyAttributes.visualDifficulty, 1.6) * 22.5;
|
|
2460
|
-
|
|
2461
|
-
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
2462
|
-
this.visual *=
|
|
2463
|
-
0.97 *
|
|
2464
|
-
Math.pow(1 -
|
|
2465
|
-
Math.pow(this.effectiveMissCount / this.totalHits, 0.775), this.effectiveMissCount);
|
|
2466
|
-
}
|
|
2467
|
-
// Combo scaling
|
|
2468
|
-
this.visual *= this.comboPenalty;
|
|
2426
|
+
this.visual *= this.calculateMissPenalty(this.difficultyAttributes.visualDifficultStrainCount);
|
|
2469
2427
|
// Scale the visual value with object count to penalize short maps.
|
|
2470
2428
|
this.visual *= Math.min(1, 1.650668 +
|
|
2471
2429
|
(0.4845796 - 1.650668) /
|
|
@@ -2477,6 +2435,21 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2477
2435
|
1.065 *
|
|
2478
2436
|
Math.pow(osuBase.ErrorFunction.erf(30 / (Math.SQRT2 * this._deviation)), 1.75);
|
|
2479
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
|
+
}
|
|
2480
2453
|
/**
|
|
2481
2454
|
* Estimates the player's tap deviation based on the OD, number of circles and sliders,
|
|
2482
2455
|
* and number of 300s, 100s, 50s, and misses, assuming the player's mean hit error is 0.
|
|
@@ -2496,17 +2469,16 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2496
2469
|
return Number.POSITIVE_INFINITY;
|
|
2497
2470
|
}
|
|
2498
2471
|
const hitWindow300 = new osuBase.OsuHitWindow(this.difficultyAttributes.overallDifficulty).hitWindowFor300();
|
|
2499
|
-
// Obtain the 50 hit window for droid.
|
|
2500
|
-
const
|
|
2501
|
-
mods: this.difficultyAttributes.mods,
|
|
2502
|
-
}).calculate().speedMultiplier;
|
|
2503
|
-
const realHitWindow300 = hitWindow300 * clockRate;
|
|
2472
|
+
// Obtain the 50 and 100 hit window for droid.
|
|
2473
|
+
const realHitWindow300 = hitWindow300 * this.difficultyAttributes.clockRate;
|
|
2504
2474
|
const droidHitWindow = new osuBase.DroidHitWindow(osuBase.OsuHitWindow.hitWindow300ToOD(realHitWindow300));
|
|
2505
|
-
const
|
|
2506
|
-
const
|
|
2507
|
-
this.
|
|
2508
|
-
|
|
2509
|
-
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;
|
|
2510
2482
|
// The probability that a player hits a circle is unknown, but we can estimate it to be
|
|
2511
2483
|
// the number of greats on circles divided by the number of circles, and then add one
|
|
2512
2484
|
// to the number of circles as a bias correction / bayesian prior.
|
|
@@ -2525,10 +2497,32 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2525
2497
|
if (greatProbabilityCircle === 0 && greatProbabilitySlider === 0) {
|
|
2526
2498
|
return Number.POSITIVE_INFINITY;
|
|
2527
2499
|
}
|
|
2528
|
-
const
|
|
2529
|
-
(
|
|
2530
|
-
|
|
2531
|
-
|
|
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);
|
|
2532
2526
|
return Math.min(deviationOnCircles, deviationOnSliders);
|
|
2533
2527
|
}
|
|
2534
2528
|
/**
|
|
@@ -2542,13 +2536,48 @@ class DroidPerformanceCalculator extends PerformanceCalculator {
|
|
|
2542
2536
|
return Number.POSITIVE_INFINITY;
|
|
2543
2537
|
}
|
|
2544
2538
|
const hitWindow300 = new osuBase.OsuHitWindow(this.difficultyAttributes.overallDifficulty).hitWindowFor300();
|
|
2545
|
-
|
|
2546
|
-
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);
|
|
2547
2558
|
if (relevantCountGreat === 0) {
|
|
2548
2559
|
return Number.POSITIVE_INFINITY;
|
|
2549
2560
|
}
|
|
2550
2561
|
const greatProbability = relevantCountGreat / (this.difficultyAttributes.speedNoteCount + 1);
|
|
2551
|
-
|
|
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));
|
|
2552
2581
|
}
|
|
2553
2582
|
toString() {
|
|
2554
2583
|
return (this.total.toFixed(2) +
|
|
@@ -2827,7 +2856,6 @@ class OsuSpeedEvaluator extends SpeedEvaluator {
|
|
|
2827
2856
|
* An evaluator for calculating osu!standard Rhythm skill.
|
|
2828
2857
|
*/
|
|
2829
2858
|
class OsuRhythmEvaluator extends RhythmEvaluator {
|
|
2830
|
-
static rhythmMultiplier = 0.75;
|
|
2831
2859
|
/**
|
|
2832
2860
|
* Calculates a rhythm multiplier for the difficulty of the tap associated
|
|
2833
2861
|
* with historic data of the current object.
|
|
@@ -2945,8 +2973,6 @@ class OsuSpeed extends OsuSkill {
|
|
|
2945
2973
|
decayWeight = 0.9;
|
|
2946
2974
|
currentSpeedStrain = 0;
|
|
2947
2975
|
currentRhythm = 0;
|
|
2948
|
-
// ~200 1/4 BPM streams
|
|
2949
|
-
minSpeedBonus = 75;
|
|
2950
2976
|
greatWindow;
|
|
2951
2977
|
constructor(mods, greatWindow) {
|
|
2952
2978
|
super(mods);
|
|
@@ -3015,7 +3041,7 @@ class OsuFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
3015
3041
|
const opacityBonus = 1 +
|
|
3016
3042
|
this.maxOpacityBonus *
|
|
3017
3043
|
(1 -
|
|
3018
|
-
current.opacityAt(currentObject.object.startTime, isHiddenMod
|
|
3044
|
+
current.opacityAt(currentObject.object.startTime, isHiddenMod));
|
|
3019
3045
|
result +=
|
|
3020
3046
|
(stackNerf * opacityBonus * scalingFactor * jumpDistance) /
|
|
3021
3047
|
cumulativeStrainTime;
|
|
@@ -3107,6 +3133,7 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3107
3133
|
flashlightDifficulty: 0,
|
|
3108
3134
|
speedNoteCount: 0,
|
|
3109
3135
|
sliderFactor: 0,
|
|
3136
|
+
clockRate: 1,
|
|
3110
3137
|
approachRate: 0,
|
|
3111
3138
|
overallDifficulty: 0,
|
|
3112
3139
|
hitCircleCount: 0,
|
|
@@ -3187,9 +3214,6 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3187
3214
|
this.postCalculateFlashlight(flashlightSkill);
|
|
3188
3215
|
this.calculateTotal();
|
|
3189
3216
|
}
|
|
3190
|
-
/**
|
|
3191
|
-
* Returns a string representative of the class.
|
|
3192
|
-
*/
|
|
3193
3217
|
toString() {
|
|
3194
3218
|
return (this.total.toFixed(2) +
|
|
3195
3219
|
" stars (" +
|
|
@@ -3200,9 +3224,17 @@ class OsuDifficultyCalculator extends DifficultyCalculator {
|
|
|
3200
3224
|
this.flashlight.toFixed(2) +
|
|
3201
3225
|
" flashlight)");
|
|
3202
3226
|
}
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
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
|
+
}
|
|
3206
3238
|
createSkills() {
|
|
3207
3239
|
return [
|
|
3208
3240
|
new OsuAim(this.mods, true),
|
|
@@ -3395,7 +3427,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3395
3427
|
// Scale the aim value with slider factor to nerf very likely dropped sliderends.
|
|
3396
3428
|
this.aim *= this.sliderNerfFactor;
|
|
3397
3429
|
// Scale the aim value with accuracy.
|
|
3398
|
-
this.aim *= this.computedAccuracy.value(
|
|
3430
|
+
this.aim *= this.computedAccuracy.value();
|
|
3399
3431
|
// It is also important to consider accuracy difficulty when doing that.
|
|
3400
3432
|
const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
|
|
3401
3433
|
this.aim *= 0.98 + odScaling;
|
|
@@ -3443,15 +3475,14 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3443
3475
|
n300: Math.max(0, countGreat - relevantTotalDiff),
|
|
3444
3476
|
n100: Math.max(0, countOk - Math.max(0, relevantTotalDiff - countGreat)),
|
|
3445
3477
|
n50: Math.max(0, countMeh - Math.max(0, relevantTotalDiff - countGreat - countOk)),
|
|
3446
|
-
nmiss: this.effectiveMissCount,
|
|
3447
3478
|
});
|
|
3448
3479
|
// Scale the speed value with accuracy and OD.
|
|
3449
3480
|
this.speed *=
|
|
3450
3481
|
(0.95 +
|
|
3451
3482
|
Math.pow(this.difficultyAttributes.overallDifficulty, 2) /
|
|
3452
3483
|
750) *
|
|
3453
|
-
Math.pow((this.computedAccuracy.value(
|
|
3454
|
-
relevantAccuracy.value()) /
|
|
3484
|
+
Math.pow((this.computedAccuracy.value() +
|
|
3485
|
+
relevantAccuracy.value(this.difficultyAttributes.speedNoteCount)) /
|
|
3455
3486
|
2, (14.5 -
|
|
3456
3487
|
Math.max(this.difficultyAttributes.overallDifficulty, 8)) /
|
|
3457
3488
|
2);
|
|
@@ -3481,7 +3512,8 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3481
3512
|
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
|
|
3482
3513
|
this.accuracy =
|
|
3483
3514
|
Math.pow(1.52163, this.difficultyAttributes.overallDifficulty) *
|
|
3484
|
-
|
|
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) *
|
|
3485
3517
|
2.83;
|
|
3486
3518
|
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
|
|
3487
3519
|
this.accuracy *= Math.min(1.15, Math.pow(ncircles / 1000, 0.3));
|
|
@@ -3520,8 +3552,7 @@ class OsuPerformanceCalculator extends PerformanceCalculator {
|
|
|
3520
3552
|
? 0.2 * Math.min(1, (this.totalHits - 200) / 200)
|
|
3521
3553
|
: 0);
|
|
3522
3554
|
// Scale the flashlight value with accuracy slightly.
|
|
3523
|
-
this.flashlight *=
|
|
3524
|
-
0.5 + this.computedAccuracy.value(this.totalHits) / 2;
|
|
3555
|
+
this.flashlight *= 0.5 + this.computedAccuracy.value() / 2;
|
|
3525
3556
|
// It is also important to consider accuracy difficulty when doing that.
|
|
3526
3557
|
const odScaling = Math.pow(this.difficultyAttributes.overallDifficulty, 2) / 2500;
|
|
3527
3558
|
this.flashlight *= 0.98 + odScaling;
|