mirror of
https://github.com/ppy/osu.git
synced 2024-12-05 09:42:54 +08:00
ported my stat acc branch
This commit is contained in:
parent
76a1f19233
commit
198f35dc63
@ -24,6 +24,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("effective_miss_count")]
|
||||
public double EffectiveMissCount { get; set; }
|
||||
|
||||
[JsonProperty("deviation")]
|
||||
public double Deviation { get; set; }
|
||||
|
||||
[JsonProperty("speed_deviation")]
|
||||
public double SpeedDeviation { get; set; }
|
||||
|
||||
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
||||
{
|
||||
foreach (var attribute in base.GetAttributesForDisplay())
|
||||
@ -33,6 +39,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
yield return new PerformanceDisplayAttribute(nameof(Speed), "Speed", Speed);
|
||||
yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy);
|
||||
yield return new PerformanceDisplayAttribute(nameof(Flashlight), "Flashlight Bonus", Flashlight);
|
||||
yield return new PerformanceDisplayAttribute(nameof(Deviation), "Estimated UR", Deviation * 10);
|
||||
yield return new PerformanceDisplayAttribute(nameof(SpeedDeviation), "Estimated UR (speed)", SpeedDeviation * 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MathNet.Numerics;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
@ -17,13 +21,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
private double accuracy;
|
||||
private int scoreMaxCombo;
|
||||
|
||||
private int countGreat;
|
||||
private int countOk;
|
||||
private int countMeh;
|
||||
private int countMiss;
|
||||
private int countSliderBreaks;
|
||||
private int countSliderEndsDropped;
|
||||
|
||||
private double effectiveMissCount;
|
||||
|
||||
private bool usingSliderAccuracy;
|
||||
private double hitWindow300, hitWindow100, hitWindow50;
|
||||
private double deviation, speedDeviation;
|
||||
private double deviationARadjust;
|
||||
|
||||
public OsuPerformanceCalculator()
|
||||
: base(new OsuRuleset())
|
||||
{
|
||||
@ -39,7 +51,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
|
||||
countSliderBreaks = score.Statistics.GetValueOrDefault(HitResult.LargeTickMiss);
|
||||
|
||||
usingSliderAccuracy = !score.Mods.Any(h => h is OsuModClassic cl && cl.NoSliderHeadAccuracy.Value);
|
||||
|
||||
if (usingSliderAccuracy)
|
||||
{
|
||||
effectiveMissCount = countMiss;
|
||||
countSliderEndsDropped = osuAttributes.SliderCount - score.Statistics.GetValueOrDefault(HitResult.SliderTailHit);
|
||||
}
|
||||
else
|
||||
{
|
||||
effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
|
||||
countSliderEndsDropped = Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo);
|
||||
}
|
||||
|
||||
double multiplier = PERFORMANCE_BASE_MULTIPLIER;
|
||||
|
||||
@ -61,6 +86,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
|
||||
}
|
||||
|
||||
var track = new TrackVirtual(1);
|
||||
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
double clockRate = track.Rate;
|
||||
|
||||
hitWindow300 = 80 - 6 * osuAttributes.OverallDifficulty;
|
||||
hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate;
|
||||
hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate;
|
||||
|
||||
deviation = calculateDeviation(score, osuAttributes);
|
||||
speedDeviation = calculateSpeedDeviation(score, osuAttributes);
|
||||
|
||||
// Bonus for low AR to account for the fact that it's more difficult to get low UR on low AR
|
||||
deviationARadjust = 0.475 + 0.7 / (1.0 + Math.Pow(1.73, 7.9 - osuAttributes.ApproachRate));
|
||||
|
||||
double aimValue = computeAimValue(score, osuAttributes);
|
||||
double speedValue = computeSpeedValue(score, osuAttributes);
|
||||
double accuracyValue = computeAccuracyValue(score, osuAttributes);
|
||||
@ -80,14 +119,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
Accuracy = accuracyValue,
|
||||
Flashlight = flashlightValue,
|
||||
EffectiveMissCount = effectiveMissCount,
|
||||
Deviation = deviation,
|
||||
SpeedDeviation = speedDeviation,
|
||||
Total = totalValue
|
||||
};
|
||||
}
|
||||
|
||||
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (deviation == double.PositiveInfinity)
|
||||
return 0.0;
|
||||
|
||||
double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||
|
||||
// Rake tapping nerf
|
||||
aimValue = adjustPerformanceWithUR(aimValue, deviation);
|
||||
|
||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||
aimValue *= lengthBonus;
|
||||
@ -122,25 +169,28 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
if (attributes.SliderCount > 0)
|
||||
{
|
||||
double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
|
||||
double estimateSliderEndsDropped = Math.Clamp(countSliderEndsDropped + countSliderBreaks * 2, 0, estimateDifficultSliders);
|
||||
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + attributes.SliderFactor;
|
||||
aimValue *= sliderNerfFactor;
|
||||
}
|
||||
|
||||
aimValue *= accuracy;
|
||||
// It is important to consider accuracy difficulty when scaling with accuracy.
|
||||
aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
|
||||
// Scale the aim value with deviation
|
||||
aimValue *= SpecialFunctions.Erf(30 / (Math.Sqrt(2) * deviation * deviationARadjust));
|
||||
aimValue *= 0.98 + Math.Pow(100.0 / 9, 2) / 2500; // OD 11 SS stays the same.
|
||||
|
||||
return aimValue;
|
||||
}
|
||||
|
||||
private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (score.Mods.Any(h => h is OsuModRelax))
|
||||
if (score.Mods.Any(h => h is OsuModRelax) || speedDeviation == double.PositiveInfinity)
|
||||
return 0.0;
|
||||
|
||||
double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||
|
||||
// Rake tapping nerf
|
||||
speedValue = adjustSpeedWithUR(speedValue, speedDeviation);
|
||||
|
||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||
speedValue *= lengthBonus;
|
||||
@ -168,46 +218,33 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
|
||||
}
|
||||
|
||||
// Calculate accuracy assuming the worst case scenario
|
||||
double relevantTotalDiff = totalHits - attributes.SpeedNoteCount;
|
||||
double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
|
||||
double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat));
|
||||
double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk));
|
||||
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
|
||||
|
||||
// Scale the speed value with accuracy and OD.
|
||||
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
|
||||
|
||||
// Scale the speed value with # of 50s to punish doubletapping.
|
||||
speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
||||
// Scale the speed value with speed deviation
|
||||
speedValue *= SpecialFunctions.Erf(20 / (Math.Sqrt(2) * speedDeviation * deviationARadjust));
|
||||
speedValue *= 0.95 + Math.Pow(100.0 / 9, 2) / 750; // OD 11 SS stays the same.
|
||||
|
||||
return speedValue;
|
||||
}
|
||||
|
||||
private double computeAccuracyValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (score.Mods.Any(h => h is OsuModRelax))
|
||||
if (deviation == double.PositiveInfinity || score.Mods.Any(h => h is OsuModRelax) || deviation == double.PositiveInfinity)
|
||||
return 0.0;
|
||||
|
||||
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
|
||||
double betterAccuracyPercentage;
|
||||
int amountHitObjectsWithAccuracy = attributes.HitCircleCount;
|
||||
if (usingSliderAccuracy) amountHitObjectsWithAccuracy += attributes.SliderCount;
|
||||
|
||||
if (amountHitObjectsWithAccuracy > 0)
|
||||
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
|
||||
else
|
||||
betterAccuracyPercentage = 0;
|
||||
double liveLengthBonus = Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
|
||||
double threshold = 1000 * Math.Pow(1.15, 1 / 0.3); // Number of objects until length bonus caps.
|
||||
|
||||
// It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points.
|
||||
if (betterAccuracyPercentage < 0)
|
||||
betterAccuracyPercentage = 0;
|
||||
// Some fancy stuff to make curve similar to live
|
||||
double scaling = Math.Sqrt(2) * Math.Log(1.52163) * SpecialFunctions.ErfInv(1 / (1 + 1 / Math.Min(amountHitObjectsWithAccuracy, threshold))) / 6;
|
||||
|
||||
// Lots of arbitrary values from testing.
|
||||
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution.
|
||||
double accuracyValue = Math.Pow(1.52163, attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
|
||||
// Accuracy pp formula that's roughly the same as live.
|
||||
double accuracyValue = 2.83 * Math.Pow(1.52163, 40.0 / 3) * liveLengthBonus * Math.Exp(-scaling * deviation * deviationARadjust);
|
||||
|
||||
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
|
||||
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
|
||||
// Punish very low amount of hits additionally to prevent big pp values right at the start of the map
|
||||
if (amountHitObjectsWithAccuracy < 30)
|
||||
accuracyValue *= Math.Sqrt((double)amountHitObjectsWithAccuracy / 30);
|
||||
|
||||
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
|
||||
if (score.Mods.Any(m => m is OsuModBlinds))
|
||||
@ -223,11 +260,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
private double computeFlashlightValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (!score.Mods.Any(h => h is OsuModFlashlight))
|
||||
if (!score.Mods.Any(h => h is OsuModFlashlight) || deviation == double.PositiveInfinity)
|
||||
return 0.0;
|
||||
|
||||
double flashlightValue = Math.Pow(attributes.FlashlightDifficulty, 2.0) * 25.0;
|
||||
|
||||
// Rake tapping nerf
|
||||
flashlightValue = adjustPerformanceWithUR(flashlightValue, deviation);
|
||||
|
||||
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
||||
if (effectiveMissCount > 0)
|
||||
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
|
||||
@ -238,10 +278,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
|
||||
(totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0);
|
||||
|
||||
// Scale the flashlight value with accuracy _slightly_.
|
||||
flashlightValue *= 0.5 + accuracy / 2.0;
|
||||
// It is important to also consider accuracy difficulty when doing that.
|
||||
flashlightValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
|
||||
// Scale the flashlight value with deviation
|
||||
flashlightValue *= SpecialFunctions.Erf(50 / (Math.Sqrt(2) * deviation * deviationARadjust));
|
||||
flashlightValue *= 0.98 + Math.Pow(100.0 / 9, 2) / 2500; // OD 11 SS stays the same.
|
||||
|
||||
return flashlightValue;
|
||||
}
|
||||
@ -264,7 +303,194 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return Math.Max(countMiss, comboBasedMissCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estimates the player's tap deviation based on the OD, number of circles and sliders, and number of 300s, 100s, 50s, and misses,
|
||||
/// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings
|
||||
/// will always return the same deviation. Sliders are treated as circles with a 50 hit window. Misses are ignored because they are usually due to misaiming.
|
||||
/// 300s and 100s are assumed to follow a normal distribution, whereas 50s are assumed to follow a uniform distribution.
|
||||
/// </summary>
|
||||
private double calculateDeviation(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (totalSuccessfulHits == 0)
|
||||
return double.PositiveInfinity;
|
||||
|
||||
int accuracyObjectCount = attributes.HitCircleCount;
|
||||
if (usingSliderAccuracy) accuracyObjectCount += attributes.SliderCount;
|
||||
|
||||
int missCountCircles = Math.Min(countMiss, accuracyObjectCount);
|
||||
int mehCountCircles = Math.Min(countMeh, accuracyObjectCount - missCountCircles);
|
||||
int okCountCircles = Math.Min(countOk, accuracyObjectCount - missCountCircles - mehCountCircles);
|
||||
int greatCountCircles = Math.Max(0, accuracyObjectCount - missCountCircles - mehCountCircles - okCountCircles);
|
||||
|
||||
// Assume 100s, 50s, and misses happen on circles. If there are less non-300s on circles than 300s,
|
||||
// compute the deviation on circles.
|
||||
if (usingSliderAccuracy || greatCountCircles > 0)
|
||||
{
|
||||
//// The probability that a player hits a circle is unknown, but we can estimate it to be
|
||||
//// the number of greats on circles divided by the number of circles, and then add one
|
||||
//// to the number of circles as a bias correction.
|
||||
double n = Math.Max(1, accuracyObjectCount - missCountCircles - mehCountCircles);
|
||||
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
|
||||
|
||||
// Proportion of greats hit on circles, ignoring misses and 50s.
|
||||
double p = greatCountCircles / n;
|
||||
|
||||
// We can be 99% confident that p is at least this value.
|
||||
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||
|
||||
// Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed.
|
||||
// Begin with 300s and 100s first. Ignoring 50s, we can be 99% confident that the deviation is not higher than:
|
||||
double estimatedDeviation = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
|
||||
|
||||
double randomValue = Math.Sqrt(2 / Math.PI) * hitWindow100 * Math.Exp(-0.5 * Math.Pow(hitWindow100 / estimatedDeviation, 2))
|
||||
/ (estimatedDeviation * SpecialFunctions.Erf(hitWindow100 / (Math.Sqrt(2) * estimatedDeviation)));
|
||||
|
||||
estimatedDeviation *= Math.Sqrt(1 - randomValue);
|
||||
|
||||
// If precision is not enough - use limit value
|
||||
if (pLowerBound == 0 || randomValue >= 1)
|
||||
estimatedDeviation = hitWindow100 / Math.Sqrt(3);
|
||||
|
||||
// Then compute the variance for 50s.
|
||||
double mehVariance = (hitWindow50 * hitWindow50 + hitWindow100 * hitWindow50 + hitWindow100 * hitWindow100) / 3;
|
||||
|
||||
// Find the total deviation.
|
||||
estimatedDeviation = Math.Sqrt(((greatCountCircles + okCountCircles) * Math.Pow(estimatedDeviation, 2) + mehCountCircles * mehVariance) / (greatCountCircles + okCountCircles + mehCountCircles));
|
||||
|
||||
// Adjust by 0.9 to account for the fact that it's higher bound UR value
|
||||
return estimatedDeviation * 0.9;
|
||||
}
|
||||
|
||||
// If there are more non-300s than there are circles, compute the deviation on sliders instead.
|
||||
// Here, all that matters is whether or not the slider was missed, since it is impossible
|
||||
// to get a 100 or 50 on a slider by mis-tapping it.
|
||||
int sliderCount = attributes.SliderCount;
|
||||
int missCountSliders = Math.Min(sliderCount, countMiss - missCountCircles);
|
||||
int greatCountSliders = sliderCount - missCountSliders;
|
||||
|
||||
// We only get here if nothing was hit. In this case, there is no estimate for deviation.
|
||||
// Note that this is never negative, so checking if this is only equal to 0 makes sense.
|
||||
if (greatCountSliders == 0)
|
||||
{
|
||||
return double.PositiveInfinity;
|
||||
}
|
||||
|
||||
double greatProbabilitySlider = greatCountSliders / (sliderCount + 1.0);
|
||||
double deviationOnSliders = hitWindow50 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbabilitySlider));
|
||||
|
||||
return deviationOnSliders;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the same as <see cref="calculateDeviation"/>, but only for notes and inaccuracies that are relevant to speed difficulty.
|
||||
/// Treats all difficult speed notes as circles, so this method can sometimes return a lower deviation than <see cref="calculateDeviation"/>.
|
||||
/// This is fine though, since this method is only used to scale speed pp.
|
||||
/// </summary>
|
||||
private double calculateSpeedDeviation(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (totalSuccessfulHits == 0)
|
||||
return double.PositiveInfinity;
|
||||
|
||||
// Calculate accuracy assuming the worst case scenario
|
||||
double speedNoteCount = attributes.SpeedNoteCount;
|
||||
|
||||
double relevantTotalDiff = totalHits - speedNoteCount;
|
||||
|
||||
double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
|
||||
double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat));
|
||||
double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk));
|
||||
double relevantCountMiss = Math.Max(0, countMiss - Math.Max(0, relevantTotalDiff - countGreat - countOk - countMeh));
|
||||
|
||||
if (usingSliderAccuracy)
|
||||
{
|
||||
double greatsRatio = relevantCountGreat / speedNoteCount;
|
||||
double mistapRatio = 1 - greatsRatio;
|
||||
|
||||
// Assume sliders are 2 times easier to acc than streams
|
||||
double hitcircleRatio = attributes.HitCircleCount / (attributes.HitCircleCount + attributes.SliderCount / 2.0);
|
||||
mistapRatio *= hitcircleRatio;
|
||||
|
||||
// This can't get higher than total value
|
||||
double adjustedGreatsRatio = Math.Min(1 - mistapRatio, (double)countGreat / totalHits);
|
||||
|
||||
double mistapsMultiplier = (greatsRatio == 1) ? 0 : (1 - adjustedGreatsRatio) / (1 - greatsRatio);
|
||||
|
||||
relevantCountGreat = speedNoteCount * adjustedGreatsRatio;
|
||||
relevantCountOk *= mistapsMultiplier;
|
||||
relevantCountMeh *= mistapsMultiplier;
|
||||
relevantCountMiss *= mistapsMultiplier;
|
||||
}
|
||||
|
||||
if (relevantCountMiss < speedNoteCount)
|
||||
{
|
||||
double n = Math.Max(1, speedNoteCount - relevantCountMiss - relevantCountMeh);
|
||||
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
|
||||
|
||||
// Proportion of greats hit on circles, ignoring misses and 50s.
|
||||
double p = relevantCountGreat / n;
|
||||
|
||||
// We can be 99% confident that p is at least this value.
|
||||
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||
|
||||
// Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed.
|
||||
// Begin with 300s and 100s first. Ignoring 50s, we can be 99% confident that the deviation is not higher than:
|
||||
double estimatedDeviation = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
|
||||
|
||||
double randomValue = Math.Sqrt(2 / Math.PI) * hitWindow100 * Math.Exp(-0.5 * Math.Pow(hitWindow100 / estimatedDeviation, 2))
|
||||
/ (estimatedDeviation * SpecialFunctions.Erf(hitWindow100 / (Math.Sqrt(2) * estimatedDeviation)));
|
||||
|
||||
estimatedDeviation *= Math.Sqrt(1 - randomValue);
|
||||
|
||||
// If precision is not enough - use limit value
|
||||
if (pLowerBound == 0 || randomValue >= 1)
|
||||
estimatedDeviation = hitWindow100 / Math.Sqrt(3);
|
||||
|
||||
// Then compute the variance for 50s.
|
||||
double mehVariance = (hitWindow50 * hitWindow50 + hitWindow100 * hitWindow50 + hitWindow100 * hitWindow100) / 3;
|
||||
|
||||
// Find the total deviation.
|
||||
estimatedDeviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(estimatedDeviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh));
|
||||
|
||||
// Adjust by 0.9 to account for the fact that it's higher bound UR value
|
||||
return estimatedDeviation * 0.9;
|
||||
}
|
||||
|
||||
return double.PositiveInfinity;
|
||||
}
|
||||
|
||||
// https://www.desmos.com/calculator/no1pv5gv5g
|
||||
private double adjustSpeedWithUR(double speedValue, double UR)
|
||||
{
|
||||
// Starting from this pp amount - penalty will be applied
|
||||
double abusePoint = Math.Pow(400 / UR, 2);
|
||||
|
||||
if (speedValue <= abusePoint)
|
||||
return speedValue;
|
||||
|
||||
// Descale values to make log curve look correctly
|
||||
const double scale = 50;
|
||||
speedValue = scale * (Math.Log(speedValue / scale + 1 - abusePoint / scale) + abusePoint / scale);
|
||||
|
||||
return speedValue;
|
||||
}
|
||||
|
||||
private double adjustPerformanceWithUR(double performanceValue, double UR)
|
||||
{
|
||||
// Starting from this pp amount - penalty will be applied
|
||||
double abusePoint = Math.Pow(600 / UR, 2);
|
||||
|
||||
if (performanceValue <= abusePoint)
|
||||
return performanceValue;
|
||||
|
||||
// Descale value to make log curve look correctly
|
||||
const double scale = 50;
|
||||
performanceValue = scale * (Math.Log(performanceValue / scale + 1 - abusePoint / scale) + abusePoint / scale);
|
||||
|
||||
return performanceValue;
|
||||
}
|
||||
|
||||
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
|
||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,10 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
@ -20,12 +22,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
private double strainDecayBase => 0.3;
|
||||
|
||||
private double currentStrain;
|
||||
private double currentStrainNoDistance;
|
||||
private double currentRhythm;
|
||||
|
||||
protected override int ReducedSectionCount => 5;
|
||||
protected override double DifficultyMultiplier => 1.04;
|
||||
|
||||
private readonly List<double> objectStrains = new List<double>();
|
||||
// Used to calculate acc punishment
|
||||
private readonly List<double> objectStrainsNoDistance = new List<double>();
|
||||
|
||||
public Speed(Mod[] mods)
|
||||
: base(mods)
|
||||
@ -41,26 +45,34 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime);
|
||||
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier;
|
||||
|
||||
// Disregard distance when computing the number of speed notes.
|
||||
double travelDistance = current.Index > 0 ? ((OsuDifficultyHitObject)current.Previous(0)).TravelDistance : 0;
|
||||
double distance = Math.Min(125, travelDistance + ((OsuDifficultyHitObject)current).MinimumJumpDistance);
|
||||
|
||||
currentStrainNoDistance *= strainDecay(((OsuDifficultyHitObject)current).StrainTime);
|
||||
currentStrainNoDistance += SpeedEvaluator.EvaluateDifficultyOf(current) / (1 + Math.Pow(distance / 125, 3.5)) * skillMultiplier;
|
||||
|
||||
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
|
||||
|
||||
double totalStrain = currentStrain * currentRhythm;
|
||||
double totalStrainNoDistance = currentStrainNoDistance * currentRhythm;
|
||||
|
||||
objectStrains.Add(totalStrain);
|
||||
objectStrainsNoDistance.Add(totalStrainNoDistance);
|
||||
|
||||
return totalStrain;
|
||||
}
|
||||
|
||||
public double RelevantNoteCount()
|
||||
{
|
||||
if (objectStrains.Count == 0)
|
||||
if (objectStrainsNoDistance.Count == 0)
|
||||
return 0;
|
||||
|
||||
double maxStrain = objectStrains.Max();
|
||||
double maxStrain = objectStrainsNoDistance.Max();
|
||||
|
||||
if (maxStrain == 0)
|
||||
return 0;
|
||||
|
||||
return objectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0))));
|
||||
return objectStrainsNoDistance.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,10 @@
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Label="Project References">
|
||||
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
|
||||
</ItemGroup>
|
||||
|
Loading…
Reference in New Issue
Block a user