@rian8337/osu-difficulty-calculator 4.0.0-beta.48 → 4.0.0-beta.49
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 +282 -237
- package/package.json +2 -2
- package/typings/index.d.ts +24 -4
package/dist/index.js
CHANGED
|
@@ -27,6 +27,7 @@ AimEvaluator.wideAngleMultiplier = 1.5;
|
|
|
27
27
|
AimEvaluator.acuteAngleMultiplier = 1.95;
|
|
28
28
|
AimEvaluator.sliderMultiplier = 1.35;
|
|
29
29
|
AimEvaluator.velocityChangeMultiplier = 0.75;
|
|
30
|
+
AimEvaluator.wiggleMultiplier = 1.02;
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* The base of a difficulty calculator.
|
|
@@ -175,6 +176,12 @@ class DifficultyCalculator {
|
|
|
175
176
|
* Represents a hit object with difficulty calculation values.
|
|
176
177
|
*/
|
|
177
178
|
class DifficultyHitObject {
|
|
179
|
+
/**
|
|
180
|
+
* The normalized diameter of the hitobject.
|
|
181
|
+
*/
|
|
182
|
+
static get normalizedDiameter() {
|
|
183
|
+
return this.normalizedRadius * 2;
|
|
184
|
+
}
|
|
178
185
|
/**
|
|
179
186
|
* Note: You **must** call `computeProperties` at some point due to how TypeScript handles
|
|
180
187
|
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue).
|
|
@@ -236,9 +243,8 @@ class DifficultyHitObject {
|
|
|
236
243
|
* Calculated as the angle between the circles (current-2, current-1, current).
|
|
237
244
|
*/
|
|
238
245
|
this.angle = null;
|
|
239
|
-
this.
|
|
240
|
-
this.
|
|
241
|
-
this.assumedSliderRadius = this.normalizedRadius * 1.8;
|
|
246
|
+
this.maximumSliderRadius = DifficultyHitObject.normalizedRadius * 2.4;
|
|
247
|
+
this.assumedSliderRadius = DifficultyHitObject.normalizedRadius * 1.8;
|
|
242
248
|
this.object = object;
|
|
243
249
|
this.lastObject = lastObject;
|
|
244
250
|
this.lastLastObject = lastLastObject;
|
|
@@ -442,7 +448,7 @@ class DifficultyHitObject {
|
|
|
442
448
|
.getStackedPosition(this.mode)
|
|
443
449
|
.add(slider.path.positionAt(endTimeMin));
|
|
444
450
|
let currentCursorPosition = slider.getStackedPosition(this.mode);
|
|
445
|
-
const scalingFactor =
|
|
451
|
+
const scalingFactor = DifficultyHitObject.normalizedRadius / slider.radius;
|
|
446
452
|
for (let i = 1; i < slider.nestedHitObjects.length; ++i) {
|
|
447
453
|
const currentMovementObject = slider.nestedHitObjects[i];
|
|
448
454
|
let currentMovement = currentMovementObject
|
|
@@ -464,7 +470,7 @@ class DifficultyHitObject {
|
|
|
464
470
|
}
|
|
465
471
|
else if (currentMovementObject instanceof osuBase.SliderRepeat) {
|
|
466
472
|
// For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.
|
|
467
|
-
requiredMovement =
|
|
473
|
+
requiredMovement = DifficultyHitObject.normalizedRadius;
|
|
468
474
|
}
|
|
469
475
|
if (currentMovementLength > requiredMovement) {
|
|
470
476
|
// This finds the positional delta from the required radius and the current position,
|
|
@@ -491,11 +497,211 @@ class DifficultyHitObject {
|
|
|
491
497
|
return pos;
|
|
492
498
|
}
|
|
493
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* The normalized radius of the hitobject.
|
|
502
|
+
*/
|
|
503
|
+
DifficultyHitObject.normalizedRadius = 50;
|
|
494
504
|
/**
|
|
495
505
|
* The lowest possible delta time value.
|
|
496
506
|
*/
|
|
497
507
|
DifficultyHitObject.minDeltaTime = 25;
|
|
498
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Represents an osu!droid hit object with difficulty calculation values.
|
|
511
|
+
*/
|
|
512
|
+
class DroidDifficultyHitObject extends DifficultyHitObject {
|
|
513
|
+
get scalingFactor() {
|
|
514
|
+
const radius = this.object.radius;
|
|
515
|
+
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
|
516
|
+
let scalingFactor = DifficultyHitObject.normalizedRadius / radius;
|
|
517
|
+
// High circle size (small CS) bonus
|
|
518
|
+
if (radius < this.radiusBuffThreshold) {
|
|
519
|
+
scalingFactor *=
|
|
520
|
+
1 + Math.pow((this.radiusBuffThreshold - radius) / 50, 2);
|
|
521
|
+
}
|
|
522
|
+
return scalingFactor;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Note: You **must** call `computeProperties` at some point due to how TypeScript handles
|
|
526
|
+
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue).
|
|
527
|
+
*
|
|
528
|
+
* @param object The underlying hitobject.
|
|
529
|
+
* @param lastObject The hitobject before this hitobject.
|
|
530
|
+
* @param lastLastObject The hitobject before the last hitobject.
|
|
531
|
+
* @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
|
|
532
|
+
* @param clockRate The clock rate of the beatmap.
|
|
533
|
+
*/
|
|
534
|
+
constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate) {
|
|
535
|
+
super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate);
|
|
536
|
+
/**
|
|
537
|
+
* The tap strain generated by the hitobject.
|
|
538
|
+
*/
|
|
539
|
+
this.tapStrain = 0;
|
|
540
|
+
/**
|
|
541
|
+
* The tap strain generated by the hitobject if `strainTime` isn't modified by
|
|
542
|
+
* OD. This is used in three-finger detection.
|
|
543
|
+
*/
|
|
544
|
+
this.originalTapStrain = 0;
|
|
545
|
+
/**
|
|
546
|
+
* The rhythm strain generated by the hitobject.
|
|
547
|
+
*/
|
|
548
|
+
this.rhythmStrain = 0;
|
|
549
|
+
/**
|
|
550
|
+
* The flashlight strain generated by the hitobject if sliders are considered.
|
|
551
|
+
*/
|
|
552
|
+
this.flashlightStrainWithSliders = 0;
|
|
553
|
+
/**
|
|
554
|
+
* The flashlight strain generated by the hitobject if sliders are not considered.
|
|
555
|
+
*/
|
|
556
|
+
this.flashlightStrainWithoutSliders = 0;
|
|
557
|
+
/**
|
|
558
|
+
* The visual strain generated by the hitobject if sliders are considered.
|
|
559
|
+
*/
|
|
560
|
+
this.visualStrainWithSliders = 0;
|
|
561
|
+
/**
|
|
562
|
+
* The visual strain generated by the hitobject if sliders are not considered.
|
|
563
|
+
*/
|
|
564
|
+
this.visualStrainWithoutSliders = 0;
|
|
565
|
+
/**
|
|
566
|
+
* The note density of the hitobject.
|
|
567
|
+
*/
|
|
568
|
+
this.noteDensity = 1;
|
|
569
|
+
/**
|
|
570
|
+
* The overlapping factor of the hitobject.
|
|
571
|
+
*
|
|
572
|
+
* This is used to scale visual skill.
|
|
573
|
+
*/
|
|
574
|
+
this.overlappingFactor = 0;
|
|
575
|
+
this.radiusBuffThreshold = 70;
|
|
576
|
+
this.mode = osuBase.Modes.droid;
|
|
577
|
+
this.maximumSliderRadius = DifficultyHitObject.normalizedRadius * 2;
|
|
578
|
+
this.timePreempt = object.timePreempt / clockRate;
|
|
579
|
+
}
|
|
580
|
+
computeProperties(clockRate, hitObjects) {
|
|
581
|
+
super.computeProperties(clockRate, hitObjects);
|
|
582
|
+
this.setVisuals(clockRate, hitObjects);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Determines whether this hitobject is considered overlapping with the hitobject before it.
|
|
586
|
+
*
|
|
587
|
+
* Keep in mind that "overlapping" in this case is overlapping to the point where both hitobjects
|
|
588
|
+
* can be hit with just a single tap in osu!droid.
|
|
589
|
+
*
|
|
590
|
+
* In the case of sliders, it is considered overlapping if all nested hitobjects can be hit with
|
|
591
|
+
* one aim motion.
|
|
592
|
+
*
|
|
593
|
+
* @param considerDistance Whether to consider the distance between both hitobjects.
|
|
594
|
+
* @returns Whether the hitobject is considered overlapping.
|
|
595
|
+
*/
|
|
596
|
+
isOverlapping(considerDistance) {
|
|
597
|
+
if (this.object instanceof osuBase.Spinner) {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
const prev = this.previous(0);
|
|
601
|
+
if (!prev || prev.object instanceof osuBase.Spinner) {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
if (this.object.startTime !== prev.object.startTime) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
if (!considerDistance) {
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
const distanceThreshold = 2 * this.object.radius;
|
|
611
|
+
const startPosition = this.object.getStackedPosition(osuBase.Modes.droid);
|
|
612
|
+
const prevStartPosition = prev.object.getStackedPosition(osuBase.Modes.droid);
|
|
613
|
+
// We need to consider two cases:
|
|
614
|
+
//
|
|
615
|
+
// Case 1: Current object is a circle, or previous object is a circle.
|
|
616
|
+
// In this case, we only need to check if their positions are close enough to be tapped together.
|
|
617
|
+
//
|
|
618
|
+
// Case 2: Both objects are sliders.
|
|
619
|
+
// In this case, we need to check if all nested hitobjects can be hit together.
|
|
620
|
+
// To start with, check if the starting positions can be tapped together.
|
|
621
|
+
if (startPosition.getDistance(prevStartPosition) > distanceThreshold) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
if (this.object instanceof osuBase.Circle || prev.object instanceof osuBase.Circle) {
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
// Check if all nested hitobjects can be hit together.
|
|
628
|
+
for (let i = 1; i < this.object.nestedHitObjects.length; ++i) {
|
|
629
|
+
const position = this.object.nestedHitObjects[i].getStackedPosition(osuBase.Modes.droid);
|
|
630
|
+
const prevPosition = prevStartPosition.add(prev.object.curvePositionAt(i / (this.object.nestedHitObjects.length - 1)));
|
|
631
|
+
if (position.getDistance(prevPosition) > distanceThreshold) {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
// Do the same for the previous slider as well.
|
|
636
|
+
for (let i = 1; i < prev.object.nestedHitObjects.length; ++i) {
|
|
637
|
+
const prevPosition = prev.object.nestedHitObjects[i].getStackedPosition(osuBase.Modes.droid);
|
|
638
|
+
const position = startPosition.add(this.object.curvePositionAt(i / (prev.object.nestedHitObjects.length - 1)));
|
|
639
|
+
if (prevPosition.getDistance(position) > distanceThreshold) {
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
setVisuals(clockRate, hitObjects) {
|
|
646
|
+
// We'll have two visible object arrays. The first array contains objects before the current object starts in a reversed order,
|
|
647
|
+
// while the second array contains objects after the current object ends.
|
|
648
|
+
// For overlapping factor, we also need to consider previous visible objects.
|
|
649
|
+
const prevVisibleObjects = [];
|
|
650
|
+
const nextVisibleObjects = [];
|
|
651
|
+
for (let j = this.index + 2; j < hitObjects.length; ++j) {
|
|
652
|
+
const o = hitObjects[j];
|
|
653
|
+
if (o instanceof osuBase.Spinner) {
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (o.startTime / clockRate > this.endTime + this.timePreempt) {
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
nextVisibleObjects.push(o);
|
|
660
|
+
}
|
|
661
|
+
for (let j = 0; j < this.index; ++j) {
|
|
662
|
+
const prev = this.previous(j);
|
|
663
|
+
if (prev.object instanceof osuBase.Spinner) {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
if (prev.startTime >= this.startTime) {
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (prev.startTime < this.startTime - this.timePreempt) {
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
prevVisibleObjects.push(prev.object);
|
|
673
|
+
}
|
|
674
|
+
for (const hitObject of prevVisibleObjects) {
|
|
675
|
+
const distance = this.object
|
|
676
|
+
.getStackedPosition(this.mode)
|
|
677
|
+
.getDistance(hitObject.getStackedEndPosition(this.mode));
|
|
678
|
+
const deltaTime = this.startTime - hitObject.endTime / clockRate;
|
|
679
|
+
this.applyToOverlappingFactor(distance, deltaTime);
|
|
680
|
+
}
|
|
681
|
+
for (const hitObject of nextVisibleObjects) {
|
|
682
|
+
const distance = hitObject
|
|
683
|
+
.getStackedPosition(this.mode)
|
|
684
|
+
.getDistance(this.object.getStackedEndPosition(this.mode));
|
|
685
|
+
const deltaTime = hitObject.startTime / clockRate - this.endTime;
|
|
686
|
+
if (deltaTime >= 0) {
|
|
687
|
+
this.noteDensity += 1 - deltaTime / this.timePreempt;
|
|
688
|
+
}
|
|
689
|
+
this.applyToOverlappingFactor(distance, deltaTime);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
applyToOverlappingFactor(distance, deltaTime) {
|
|
693
|
+
// Penalize objects that are too close to the object in both distance
|
|
694
|
+
// and delta time to prevent stream maps from being overweighted.
|
|
695
|
+
this.overlappingFactor +=
|
|
696
|
+
Math.max(0, 1 - distance / (2.5 * this.object.radius)) *
|
|
697
|
+
(7.5 /
|
|
698
|
+
(1 +
|
|
699
|
+
Math.exp(0.15 *
|
|
700
|
+
(Math.max(deltaTime, DifficultyHitObject.minDeltaTime) -
|
|
701
|
+
75))));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
499
705
|
/**
|
|
500
706
|
* An evaluator for calculating osu!droid Aim skill.
|
|
501
707
|
*/
|
|
@@ -531,6 +737,8 @@ class DroidAimEvaluator extends AimEvaluator {
|
|
|
531
737
|
}
|
|
532
738
|
const last = current.previous(0);
|
|
533
739
|
const lastLast = current.previous(1);
|
|
740
|
+
const radius = DroidDifficultyHitObject.normalizedRadius;
|
|
741
|
+
const diameter = DroidDifficultyHitObject.normalizedDiameter;
|
|
534
742
|
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
|
|
535
743
|
let currentVelocity = current.lazyJumpDistance / current.strainTime;
|
|
536
744
|
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
|
|
@@ -557,6 +765,7 @@ class DroidAimEvaluator extends AimEvaluator {
|
|
|
557
765
|
let acuteAngleBonus = 0;
|
|
558
766
|
let sliderBonus = 0;
|
|
559
767
|
let velocityChangeBonus = 0;
|
|
768
|
+
let wiggleBonus = 0;
|
|
560
769
|
// Start strain with regular velocity.
|
|
561
770
|
let strain = currentVelocity;
|
|
562
771
|
if (
|
|
@@ -566,40 +775,40 @@ class DroidAimEvaluator extends AimEvaluator {
|
|
|
566
775
|
current.angle !== null &&
|
|
567
776
|
last.angle !== null &&
|
|
568
777
|
lastLast.angle !== null) {
|
|
778
|
+
const currentAngle = current.angle;
|
|
779
|
+
const lastAngle = last.angle;
|
|
569
780
|
// Rewarding angles, take the smaller velocity as base.
|
|
570
781
|
const angleBonus = Math.min(currentVelocity, prevVelocity);
|
|
571
782
|
wideAngleBonus = this.calculateWideAngleBonus(current.angle);
|
|
572
783
|
acuteAngleBonus = this.calculateAcuteAngleBonus(current.angle);
|
|
573
|
-
//
|
|
574
|
-
if (current.strainTime > 100) {
|
|
575
|
-
acuteAngleBonus = 0;
|
|
576
|
-
}
|
|
577
|
-
else {
|
|
578
|
-
acuteAngleBonus *=
|
|
579
|
-
// Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
|
|
580
|
-
this.calculateAcuteAngleBonus(last.angle) *
|
|
581
|
-
// The maximum velocity we buff is equal to 125 / strainTime.
|
|
582
|
-
Math.min(angleBonus, 125 / current.strainTime) *
|
|
583
|
-
// Scale buff from 300 BPM 1/2 to 400 BPM 1/2.
|
|
584
|
-
Math.pow(Math.sin((Math.PI / 2) *
|
|
585
|
-
Math.min(1, (100 - current.strainTime) / 25)), 2) *
|
|
586
|
-
// Buff distance exceeding 50 (radius) up to 100 (diameter).
|
|
587
|
-
Math.pow(Math.sin(((Math.PI / 2) *
|
|
588
|
-
(osuBase.MathUtils.clamp(current.lazyJumpDistance, 50, 100) -
|
|
589
|
-
50)) /
|
|
590
|
-
50), 2);
|
|
591
|
-
}
|
|
592
|
-
// Penalize wide angles if they're repeated, reducing the penalty as last.angle gets more acute.
|
|
784
|
+
// Penalize angle repetition.
|
|
593
785
|
wideAngleBonus *=
|
|
594
|
-
|
|
595
|
-
(
|
|
596
|
-
Math.min(wideAngleBonus, Math.pow(this.calculateWideAngleBonus(last.angle), 3)));
|
|
597
|
-
// Penalize acute angles if they're repeated, reducing the penalty as lastLast.angle gets more obtuse.
|
|
786
|
+
1 -
|
|
787
|
+
Math.min(wideAngleBonus, Math.pow(this.calculateWideAngleBonus(lastAngle), 3));
|
|
598
788
|
acuteAngleBonus *=
|
|
599
|
-
0.
|
|
600
|
-
0.
|
|
789
|
+
0.08 +
|
|
790
|
+
0.92 *
|
|
601
791
|
(1 -
|
|
602
|
-
Math.min(acuteAngleBonus, Math.pow(this.calculateAcuteAngleBonus(
|
|
792
|
+
Math.min(acuteAngleBonus, Math.pow(this.calculateAcuteAngleBonus(lastAngle), 3)));
|
|
793
|
+
// Apply full wide angle bonus for distance more than one diameter
|
|
794
|
+
wideAngleBonus *=
|
|
795
|
+
angleBonus *
|
|
796
|
+
osuBase.MathUtils.smootherstep(current.lazyJumpDistance, 0, diameter);
|
|
797
|
+
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
|
798
|
+
acuteAngleBonus *=
|
|
799
|
+
angleBonus *
|
|
800
|
+
osuBase.MathUtils.smootherstep(osuBase.MathUtils.millisecondsToBPM(current.strainTime, 2), 300, 400) *
|
|
801
|
+
osuBase.MathUtils.smootherstep(current.lazyJumpDistance, diameter, diameter * 2);
|
|
802
|
+
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
|
803
|
+
// https://www.desmos.com/calculator/dp0v0nvowc
|
|
804
|
+
wiggleBonus =
|
|
805
|
+
angleBonus *
|
|
806
|
+
osuBase.MathUtils.smootherstep(current.lazyJumpDistance, radius, diameter) *
|
|
807
|
+
Math.pow(osuBase.MathUtils.reverseLerp(current.lazyJumpDistance, diameter * 3, diameter), 1.8) *
|
|
808
|
+
osuBase.MathUtils.smootherstep(currentAngle, osuBase.MathUtils.degreesToRadians(110), osuBase.MathUtils.degreesToRadians(60)) *
|
|
809
|
+
osuBase.MathUtils.smootherstep(last.lazyJumpDistance, radius, diameter) *
|
|
810
|
+
Math.pow(osuBase.MathUtils.reverseLerp(last.lazyJumpDistance, diameter * 3, diameter), 1.8) *
|
|
811
|
+
osuBase.MathUtils.smootherstep(lastAngle, osuBase.MathUtils.degreesToRadians(110), osuBase.MathUtils.degreesToRadians(60));
|
|
603
812
|
}
|
|
604
813
|
if (Math.max(prevVelocity, currentVelocity)) {
|
|
605
814
|
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
|
|
@@ -623,6 +832,7 @@ class DroidAimEvaluator extends AimEvaluator {
|
|
|
623
832
|
// Reward sliders based on velocity.
|
|
624
833
|
sliderBonus = last.travelDistance / last.travelTime;
|
|
625
834
|
}
|
|
835
|
+
strain += wiggleBonus * this.wiggleMultiplier;
|
|
626
836
|
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
|
|
627
837
|
strain += Math.max(acuteAngleBonus * this.acuteAngleMultiplier, wideAngleBonus * this.wideAngleMultiplier +
|
|
628
838
|
velocityChangeBonus * this.velocityChangeMultiplier);
|
|
@@ -648,10 +858,17 @@ class DroidAimEvaluator extends AimEvaluator {
|
|
|
648
858
|
const shortDistancePenalty = Math.pow(Math.min(this.singleSpacingThreshold, travelDistance + current.minimumJumpDistance) / this.singleSpacingThreshold, 3.5);
|
|
649
859
|
return (200 * speedBonus * shortDistancePenalty) / current.strainTime;
|
|
650
860
|
}
|
|
861
|
+
static calculateWideAngleBonus(angle) {
|
|
862
|
+
return osuBase.MathUtils.smoothstep(angle, osuBase.MathUtils.degreesToRadians(40), osuBase.MathUtils.degreesToRadians(140));
|
|
863
|
+
}
|
|
864
|
+
static calculateAcuteAngleBonus(angle) {
|
|
865
|
+
return osuBase.MathUtils.smoothstep(angle, osuBase.MathUtils.degreesToRadians(140), osuBase.MathUtils.degreesToRadians(40));
|
|
866
|
+
}
|
|
651
867
|
}
|
|
652
|
-
DroidAimEvaluator.wideAngleMultiplier = 1.
|
|
653
|
-
DroidAimEvaluator.
|
|
654
|
-
DroidAimEvaluator.
|
|
868
|
+
DroidAimEvaluator.wideAngleMultiplier = 1.5;
|
|
869
|
+
DroidAimEvaluator.acuteAngleMultiplier = 2.6;
|
|
870
|
+
DroidAimEvaluator.sliderMultiplier = 1.35;
|
|
871
|
+
DroidAimEvaluator.velocityChangeMultiplier = 0.75;
|
|
655
872
|
DroidAimEvaluator.singleSpacingThreshold = 100;
|
|
656
873
|
// 200 1/4 BPM delta time
|
|
657
874
|
DroidAimEvaluator.minSpeedBonus = 75;
|
|
@@ -805,15 +1022,33 @@ class DroidAim extends DroidSkill {
|
|
|
805
1022
|
this.reducedSectionCount = 10;
|
|
806
1023
|
this.reducedSectionBaseline = 0.75;
|
|
807
1024
|
this.starsPerDouble = 1.05;
|
|
808
|
-
this.skillMultiplier =
|
|
1025
|
+
this.skillMultiplier = 25.6;
|
|
809
1026
|
this.currentAimStrain = 0;
|
|
1027
|
+
this.sliderStrains = [];
|
|
810
1028
|
this.withSliders = withSliders;
|
|
811
1029
|
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Obtains the amount of sliders that are considered difficult in terms of relative strain.
|
|
1032
|
+
*/
|
|
1033
|
+
countDifficultSliders() {
|
|
1034
|
+
if (this.sliderStrains.length === 0) {
|
|
1035
|
+
return 0;
|
|
1036
|
+
}
|
|
1037
|
+
const maxSliderStrain = Math.max(...this.sliderStrains);
|
|
1038
|
+
if (maxSliderStrain === 0) {
|
|
1039
|
+
return 0;
|
|
1040
|
+
}
|
|
1041
|
+
return this.sliderStrains.reduce((total, strain) => total +
|
|
1042
|
+
1 / (1 + Math.exp(-((strain / maxSliderStrain) * 12 - 6))), 0);
|
|
1043
|
+
}
|
|
812
1044
|
strainValueAt(current) {
|
|
813
1045
|
this.currentAimStrain *= this.strainDecay(current.deltaTime);
|
|
814
1046
|
this.currentAimStrain +=
|
|
815
1047
|
DroidAimEvaluator.evaluateDifficultyOf(current, this.withSliders) *
|
|
816
1048
|
this.skillMultiplier;
|
|
1049
|
+
if (current.object instanceof osuBase.Slider) {
|
|
1050
|
+
this.sliderStrains.push(this.currentAimStrain);
|
|
1051
|
+
}
|
|
817
1052
|
return this.currentAimStrain;
|
|
818
1053
|
}
|
|
819
1054
|
calculateInitialStrain(time, current) {
|
|
@@ -884,7 +1119,7 @@ class DroidTapEvaluator extends SpeedEvaluator {
|
|
|
884
1119
|
0.75 *
|
|
885
1120
|
Math.pow(osuBase.ErrorFunction.erf((this.minSpeedBonus - strainTime) / 40), 2);
|
|
886
1121
|
}
|
|
887
|
-
return (speedBonus * Math.pow(doubletapness, 1.5)) / strainTime;
|
|
1122
|
+
return (speedBonus * Math.pow(doubletapness, 1.5) * 1000) / strainTime;
|
|
888
1123
|
}
|
|
889
1124
|
}
|
|
890
1125
|
|
|
@@ -906,7 +1141,7 @@ class DroidTap extends DroidSkill {
|
|
|
906
1141
|
this.starsPerDouble = 1.1;
|
|
907
1142
|
this.currentTapStrain = 0;
|
|
908
1143
|
this.currentRhythmMultiplier = 0;
|
|
909
|
-
this.skillMultiplier =
|
|
1144
|
+
this.skillMultiplier = 1.375;
|
|
910
1145
|
this._objectDeltaTimes = [];
|
|
911
1146
|
this.considerCheesability = considerCheesability;
|
|
912
1147
|
this.strainTimeCap = strainTimeCap;
|
|
@@ -1021,13 +1256,13 @@ class DroidFlashlightEvaluator extends FlashlightEvaluator {
|
|
|
1021
1256
|
let angleRepeatCount = 0;
|
|
1022
1257
|
for (let i = 0; i < Math.min(current.index, 10); ++i) {
|
|
1023
1258
|
const currentObject = current.previous(i);
|
|
1259
|
+
cumulativeStrainTime += last.strainTime;
|
|
1024
1260
|
if (!(currentObject.object instanceof osuBase.Spinner) &&
|
|
1025
1261
|
// Exclude overlapping objects that can be tapped at once.
|
|
1026
1262
|
!currentObject.isOverlapping(false)) {
|
|
1027
1263
|
const jumpDistance = current.object
|
|
1028
1264
|
.getStackedPosition(osuBase.Modes.droid)
|
|
1029
1265
|
.subtract(currentObject.object.getStackedEndPosition(osuBase.Modes.droid)).length;
|
|
1030
|
-
cumulativeStrainTime += last.strainTime;
|
|
1031
1266
|
// We want to nerf objects that can be easily seen within the Flashlight circle radius.
|
|
1032
1267
|
if (i === 0) {
|
|
1033
1268
|
smallDistNerf = Math.min(1, jumpDistance / 75);
|
|
@@ -1263,7 +1498,7 @@ class DroidRhythmEvaluator {
|
|
|
1263
1498
|
}
|
|
1264
1499
|
// Repeated island (ex: triplet -> triplet).
|
|
1265
1500
|
// Graph: https://www.desmos.com/calculator/pj7an56zwf
|
|
1266
|
-
effectiveRatio *= Math.min(3 / islandCount, Math.pow(1 / islandCount,
|
|
1501
|
+
effectiveRatio *= Math.min(3 / islandCount, Math.pow(1 / islandCount, osuBase.MathUtils.offsetLogistic(island.delta, 58.33, 0.24, 2.75)));
|
|
1267
1502
|
break;
|
|
1268
1503
|
}
|
|
1269
1504
|
if (!islandFound) {
|
|
@@ -1487,202 +1722,6 @@ class DroidVisual extends DroidSkill {
|
|
|
1487
1722
|
}
|
|
1488
1723
|
}
|
|
1489
1724
|
|
|
1490
|
-
/**
|
|
1491
|
-
* Represents an osu!droid hit object with difficulty calculation values.
|
|
1492
|
-
*/
|
|
1493
|
-
class DroidDifficultyHitObject extends DifficultyHitObject {
|
|
1494
|
-
get scalingFactor() {
|
|
1495
|
-
const radius = this.object.radius;
|
|
1496
|
-
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
|
1497
|
-
let scalingFactor = this.normalizedRadius / radius;
|
|
1498
|
-
// High circle size (small CS) bonus
|
|
1499
|
-
if (radius < this.radiusBuffThreshold) {
|
|
1500
|
-
scalingFactor *=
|
|
1501
|
-
1 + Math.pow((this.radiusBuffThreshold - radius) / 50, 2);
|
|
1502
|
-
}
|
|
1503
|
-
return scalingFactor;
|
|
1504
|
-
}
|
|
1505
|
-
/**
|
|
1506
|
-
* Note: You **must** call `computeProperties` at some point due to how TypeScript handles
|
|
1507
|
-
* overridden properties (see [this](https://github.com/microsoft/TypeScript/issues/1617) GitHub issue).
|
|
1508
|
-
*
|
|
1509
|
-
* @param object The underlying hitobject.
|
|
1510
|
-
* @param lastObject The hitobject before this hitobject.
|
|
1511
|
-
* @param lastLastObject The hitobject before the last hitobject.
|
|
1512
|
-
* @param difficultyHitObjects All difficulty hitobjects in the processed beatmap.
|
|
1513
|
-
* @param clockRate The clock rate of the beatmap.
|
|
1514
|
-
*/
|
|
1515
|
-
constructor(object, lastObject, lastLastObject, difficultyHitObjects, clockRate) {
|
|
1516
|
-
super(object, lastObject, lastLastObject, difficultyHitObjects, clockRate);
|
|
1517
|
-
/**
|
|
1518
|
-
* The tap strain generated by the hitobject.
|
|
1519
|
-
*/
|
|
1520
|
-
this.tapStrain = 0;
|
|
1521
|
-
/**
|
|
1522
|
-
* The tap strain generated by the hitobject if `strainTime` isn't modified by
|
|
1523
|
-
* OD. This is used in three-finger detection.
|
|
1524
|
-
*/
|
|
1525
|
-
this.originalTapStrain = 0;
|
|
1526
|
-
/**
|
|
1527
|
-
* The rhythm strain generated by the hitobject.
|
|
1528
|
-
*/
|
|
1529
|
-
this.rhythmStrain = 0;
|
|
1530
|
-
/**
|
|
1531
|
-
* The flashlight strain generated by the hitobject if sliders are considered.
|
|
1532
|
-
*/
|
|
1533
|
-
this.flashlightStrainWithSliders = 0;
|
|
1534
|
-
/**
|
|
1535
|
-
* The flashlight strain generated by the hitobject if sliders are not considered.
|
|
1536
|
-
*/
|
|
1537
|
-
this.flashlightStrainWithoutSliders = 0;
|
|
1538
|
-
/**
|
|
1539
|
-
* The visual strain generated by the hitobject if sliders are considered.
|
|
1540
|
-
*/
|
|
1541
|
-
this.visualStrainWithSliders = 0;
|
|
1542
|
-
/**
|
|
1543
|
-
* The visual strain generated by the hitobject if sliders are not considered.
|
|
1544
|
-
*/
|
|
1545
|
-
this.visualStrainWithoutSliders = 0;
|
|
1546
|
-
/**
|
|
1547
|
-
* The note density of the hitobject.
|
|
1548
|
-
*/
|
|
1549
|
-
this.noteDensity = 1;
|
|
1550
|
-
/**
|
|
1551
|
-
* The overlapping factor of the hitobject.
|
|
1552
|
-
*
|
|
1553
|
-
* This is used to scale visual skill.
|
|
1554
|
-
*/
|
|
1555
|
-
this.overlappingFactor = 0;
|
|
1556
|
-
this.radiusBuffThreshold = 70;
|
|
1557
|
-
this.mode = osuBase.Modes.droid;
|
|
1558
|
-
this.maximumSliderRadius = this.normalizedRadius * 2;
|
|
1559
|
-
this.timePreempt = object.timePreempt / clockRate;
|
|
1560
|
-
}
|
|
1561
|
-
computeProperties(clockRate, hitObjects) {
|
|
1562
|
-
super.computeProperties(clockRate, hitObjects);
|
|
1563
|
-
this.setVisuals(clockRate, hitObjects);
|
|
1564
|
-
}
|
|
1565
|
-
/**
|
|
1566
|
-
* Determines whether this hitobject is considered overlapping with the hitobject before it.
|
|
1567
|
-
*
|
|
1568
|
-
* Keep in mind that "overlapping" in this case is overlapping to the point where both hitobjects
|
|
1569
|
-
* can be hit with just a single tap in osu!droid.
|
|
1570
|
-
*
|
|
1571
|
-
* In the case of sliders, it is considered overlapping if all nested hitobjects can be hit with
|
|
1572
|
-
* one aim motion.
|
|
1573
|
-
*
|
|
1574
|
-
* @param considerDistance Whether to consider the distance between both hitobjects.
|
|
1575
|
-
* @returns Whether the hitobject is considered overlapping.
|
|
1576
|
-
*/
|
|
1577
|
-
isOverlapping(considerDistance) {
|
|
1578
|
-
if (this.object instanceof osuBase.Spinner) {
|
|
1579
|
-
return false;
|
|
1580
|
-
}
|
|
1581
|
-
const prev = this.previous(0);
|
|
1582
|
-
if (!prev || prev.object instanceof osuBase.Spinner) {
|
|
1583
|
-
return false;
|
|
1584
|
-
}
|
|
1585
|
-
if (this.object.startTime !== prev.object.startTime) {
|
|
1586
|
-
return false;
|
|
1587
|
-
}
|
|
1588
|
-
if (!considerDistance) {
|
|
1589
|
-
return true;
|
|
1590
|
-
}
|
|
1591
|
-
const distanceThreshold = 2 * this.object.radius;
|
|
1592
|
-
const startPosition = this.object.getStackedPosition(osuBase.Modes.droid);
|
|
1593
|
-
const prevStartPosition = prev.object.getStackedPosition(osuBase.Modes.droid);
|
|
1594
|
-
// We need to consider two cases:
|
|
1595
|
-
//
|
|
1596
|
-
// Case 1: Current object is a circle, or previous object is a circle.
|
|
1597
|
-
// In this case, we only need to check if their positions are close enough to be tapped together.
|
|
1598
|
-
//
|
|
1599
|
-
// Case 2: Both objects are sliders.
|
|
1600
|
-
// In this case, we need to check if all nested hitobjects can be hit together.
|
|
1601
|
-
// To start with, check if the starting positions can be tapped together.
|
|
1602
|
-
if (startPosition.getDistance(prevStartPosition) > distanceThreshold) {
|
|
1603
|
-
return false;
|
|
1604
|
-
}
|
|
1605
|
-
if (this.object instanceof osuBase.Circle || prev.object instanceof osuBase.Circle) {
|
|
1606
|
-
return true;
|
|
1607
|
-
}
|
|
1608
|
-
// Check if all nested hitobjects can be hit together.
|
|
1609
|
-
for (let i = 1; i < this.object.nestedHitObjects.length; ++i) {
|
|
1610
|
-
const position = this.object.nestedHitObjects[i].getStackedPosition(osuBase.Modes.droid);
|
|
1611
|
-
const prevPosition = prevStartPosition.add(prev.object.curvePositionAt(i / (this.object.nestedHitObjects.length - 1)));
|
|
1612
|
-
if (position.getDistance(prevPosition) > distanceThreshold) {
|
|
1613
|
-
return false;
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
// Do the same for the previous slider as well.
|
|
1617
|
-
for (let i = 1; i < prev.object.nestedHitObjects.length; ++i) {
|
|
1618
|
-
const prevPosition = prev.object.nestedHitObjects[i].getStackedPosition(osuBase.Modes.droid);
|
|
1619
|
-
const position = startPosition.add(this.object.curvePositionAt(i / (prev.object.nestedHitObjects.length - 1)));
|
|
1620
|
-
if (prevPosition.getDistance(position) > distanceThreshold) {
|
|
1621
|
-
return false;
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
return true;
|
|
1625
|
-
}
|
|
1626
|
-
setVisuals(clockRate, hitObjects) {
|
|
1627
|
-
// We'll have two visible object arrays. The first array contains objects before the current object starts in a reversed order,
|
|
1628
|
-
// while the second array contains objects after the current object ends.
|
|
1629
|
-
// For overlapping factor, we also need to consider previous visible objects.
|
|
1630
|
-
const prevVisibleObjects = [];
|
|
1631
|
-
const nextVisibleObjects = [];
|
|
1632
|
-
for (let j = this.index + 2; j < hitObjects.length; ++j) {
|
|
1633
|
-
const o = hitObjects[j];
|
|
1634
|
-
if (o instanceof osuBase.Spinner) {
|
|
1635
|
-
continue;
|
|
1636
|
-
}
|
|
1637
|
-
if (o.startTime / clockRate > this.endTime + this.timePreempt) {
|
|
1638
|
-
break;
|
|
1639
|
-
}
|
|
1640
|
-
nextVisibleObjects.push(o);
|
|
1641
|
-
}
|
|
1642
|
-
for (let j = 0; j < this.index; ++j) {
|
|
1643
|
-
const prev = this.previous(j);
|
|
1644
|
-
if (prev.object instanceof osuBase.Spinner) {
|
|
1645
|
-
continue;
|
|
1646
|
-
}
|
|
1647
|
-
if (prev.startTime >= this.startTime) {
|
|
1648
|
-
continue;
|
|
1649
|
-
}
|
|
1650
|
-
if (prev.startTime < this.startTime - this.timePreempt) {
|
|
1651
|
-
break;
|
|
1652
|
-
}
|
|
1653
|
-
prevVisibleObjects.push(prev.object);
|
|
1654
|
-
}
|
|
1655
|
-
for (const hitObject of prevVisibleObjects) {
|
|
1656
|
-
const distance = this.object
|
|
1657
|
-
.getStackedPosition(this.mode)
|
|
1658
|
-
.getDistance(hitObject.getStackedEndPosition(this.mode));
|
|
1659
|
-
const deltaTime = this.startTime - hitObject.endTime / clockRate;
|
|
1660
|
-
this.applyToOverlappingFactor(distance, deltaTime);
|
|
1661
|
-
}
|
|
1662
|
-
for (const hitObject of nextVisibleObjects) {
|
|
1663
|
-
const distance = hitObject
|
|
1664
|
-
.getStackedPosition(this.mode)
|
|
1665
|
-
.getDistance(this.object.getStackedEndPosition(this.mode));
|
|
1666
|
-
const deltaTime = hitObject.startTime / clockRate - this.endTime;
|
|
1667
|
-
if (deltaTime >= 0) {
|
|
1668
|
-
this.noteDensity += 1 - deltaTime / this.timePreempt;
|
|
1669
|
-
}
|
|
1670
|
-
this.applyToOverlappingFactor(distance, deltaTime);
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
applyToOverlappingFactor(distance, deltaTime) {
|
|
1674
|
-
// Penalize objects that are too close to the object in both distance
|
|
1675
|
-
// and delta time to prevent stream maps from being overweighted.
|
|
1676
|
-
this.overlappingFactor +=
|
|
1677
|
-
Math.max(0, 1 - distance / (2.5 * this.object.radius)) *
|
|
1678
|
-
(7.5 /
|
|
1679
|
-
(1 +
|
|
1680
|
-
Math.exp(0.15 *
|
|
1681
|
-
(Math.max(deltaTime, DifficultyHitObject.minDeltaTime) -
|
|
1682
|
-
75))));
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
1725
|
/**
|
|
1687
1726
|
* A difficulty calculator for osu!droid gamemode.
|
|
1688
1727
|
*/
|
|
@@ -1691,6 +1730,7 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1691
1730
|
super(...arguments);
|
|
1692
1731
|
this.attributes = {
|
|
1693
1732
|
mode: "live",
|
|
1733
|
+
aimDifficultSliderCount: 0,
|
|
1694
1734
|
tapDifficulty: 0,
|
|
1695
1735
|
rhythmDifficulty: 0,
|
|
1696
1736
|
visualDifficulty: 0,
|
|
@@ -1918,6 +1958,8 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
1918
1958
|
}
|
|
1919
1959
|
this.attributes.aimDifficultStrainCount =
|
|
1920
1960
|
aimSkill.countDifficultStrains();
|
|
1961
|
+
this.attributes.aimDifficultSliderCount =
|
|
1962
|
+
aimSkill.countDifficultSliders();
|
|
1921
1963
|
this.calculateAimAttributes();
|
|
1922
1964
|
}
|
|
1923
1965
|
/**
|
|
@@ -2051,6 +2093,9 @@ class DroidDifficultyCalculator extends DifficultyCalculator {
|
|
|
2051
2093
|
if (this.mods.some((m) => m instanceof osuBase.ModRelax)) {
|
|
2052
2094
|
this.attributes.flashlightDifficulty *= 0.7;
|
|
2053
2095
|
}
|
|
2096
|
+
else if (this.mods.some((m) => m instanceof osuBase.ModAutopilot)) {
|
|
2097
|
+
this.attributes.flashlightDifficulty *= 0.4;
|
|
2098
|
+
}
|
|
2054
2099
|
this.attributes.flashlightDifficultStrainCount =
|
|
2055
2100
|
flashlightSkill.countDifficultStrains();
|
|
2056
2101
|
}
|
|
@@ -3356,7 +3401,7 @@ class OsuDifficultyHitObject extends DifficultyHitObject {
|
|
|
3356
3401
|
get scalingFactor() {
|
|
3357
3402
|
const radius = this.object.radius;
|
|
3358
3403
|
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
|
3359
|
-
let scalingFactor =
|
|
3404
|
+
let scalingFactor = DifficultyHitObject.normalizedRadius / radius;
|
|
3360
3405
|
// High circle size (small CS) bonus
|
|
3361
3406
|
if (radius < this.radiusBuffThreshold) {
|
|
3362
3407
|
scalingFactor *=
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rian8337/osu-difficulty-calculator",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.49",
|
|
4
4
|
"description": "A module for calculating osu!standard beatmap difficulty and performance value with respect to the current difficulty and performance algorithm.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"osu",
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "a352562e37332f9eea56f47628d1d2280aea4437"
|
|
42
42
|
}
|
package/typings/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ declare abstract class AimEvaluator {
|
|
|
10
10
|
protected static readonly acuteAngleMultiplier: number;
|
|
11
11
|
protected static readonly sliderMultiplier: number;
|
|
12
12
|
protected static readonly velocityChangeMultiplier: number;
|
|
13
|
+
protected static readonly wiggleMultiplier: number;
|
|
13
14
|
/**
|
|
14
15
|
* Calculates the bonus of wide angles.
|
|
15
16
|
*/
|
|
@@ -197,8 +198,15 @@ declare abstract class DifficultyHitObject {
|
|
|
197
198
|
* Other hitobjects in the beatmap, including this hitobject.
|
|
198
199
|
*/
|
|
199
200
|
protected readonly hitObjects: readonly DifficultyHitObject[];
|
|
201
|
+
/**
|
|
202
|
+
* The normalized radius of the hitobject.
|
|
203
|
+
*/
|
|
204
|
+
static readonly normalizedRadius: number;
|
|
205
|
+
/**
|
|
206
|
+
* The normalized diameter of the hitobject.
|
|
207
|
+
*/
|
|
208
|
+
static get normalizedDiameter(): number;
|
|
200
209
|
protected abstract readonly mode: Modes;
|
|
201
|
-
protected readonly normalizedRadius = 50;
|
|
202
210
|
protected readonly maximumSliderRadius: number;
|
|
203
211
|
protected readonly assumedSliderRadius: number;
|
|
204
212
|
/**
|
|
@@ -647,7 +655,12 @@ declare class DroidAim extends DroidSkill {
|
|
|
647
655
|
private readonly skillMultiplier;
|
|
648
656
|
private readonly withSliders;
|
|
649
657
|
private currentAimStrain;
|
|
658
|
+
private readonly sliderStrains;
|
|
650
659
|
constructor(mods: Mod[], withSliders: boolean);
|
|
660
|
+
/**
|
|
661
|
+
* Obtains the amount of sliders that are considered difficult in terms of relative strain.
|
|
662
|
+
*/
|
|
663
|
+
countDifficultSliders(): number;
|
|
651
664
|
protected strainValueAt(current: DroidDifficultyHitObject): number;
|
|
652
665
|
protected calculateInitialStrain(time: number, current: DroidDifficultyHitObject): number;
|
|
653
666
|
protected getObjectStrain(): number;
|
|
@@ -661,9 +674,10 @@ declare class DroidAim extends DroidSkill {
|
|
|
661
674
|
* An evaluator for calculating osu!droid Aim skill.
|
|
662
675
|
*/
|
|
663
676
|
declare abstract class DroidAimEvaluator extends AimEvaluator {
|
|
664
|
-
protected static readonly wideAngleMultiplier = 1.
|
|
665
|
-
protected static readonly
|
|
666
|
-
protected static readonly
|
|
677
|
+
protected static readonly wideAngleMultiplier = 1.5;
|
|
678
|
+
protected static readonly acuteAngleMultiplier = 2.6;
|
|
679
|
+
protected static readonly sliderMultiplier = 1.35;
|
|
680
|
+
protected static readonly velocityChangeMultiplier = 0.75;
|
|
667
681
|
private static readonly singleSpacingThreshold;
|
|
668
682
|
private static readonly minSpeedBonus;
|
|
669
683
|
/**
|
|
@@ -686,12 +700,18 @@ declare abstract class DroidAimEvaluator extends AimEvaluator {
|
|
|
686
700
|
* Calculates the flow aim strain of a hitobject.
|
|
687
701
|
*/
|
|
688
702
|
private static flowAimStrainOf;
|
|
703
|
+
protected static calculateWideAngleBonus(angle: number): number;
|
|
704
|
+
protected static calculateAcuteAngleBonus(angle: number): number;
|
|
689
705
|
}
|
|
690
706
|
|
|
691
707
|
/**
|
|
692
708
|
* Holds data that can be used to calculate osu!droid performance points.
|
|
693
709
|
*/
|
|
694
710
|
interface DroidDifficultyAttributes extends DifficultyAttributes {
|
|
711
|
+
/**
|
|
712
|
+
* The number of sliders weighted by difficulty.
|
|
713
|
+
*/
|
|
714
|
+
aimDifficultSliderCount: number;
|
|
695
715
|
/**
|
|
696
716
|
* The difficulty corresponding to the tap skill.
|
|
697
717
|
*/
|