mirror of
https://github.com/ppy/osu.git
synced 2025-02-06 23:53:00 +08:00
Merge branch 'pp-dev' into match-my-freak-sliders
This commit is contained in:
commit
7742bb2eb0
@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
|
||||||
|
|
||||||
[TestCase(6.718709884850683d, 239, "diffcalc-test")]
|
[TestCase(6.6860329680488437d, 239, "diffcalc-test")]
|
||||||
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
|
[TestCase(1.4485740324170036d, 54, "zero-length-sliders")]
|
||||||
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
|
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
|
||||||
[TestCase(0.14143808967817237d, 2, "nan-slider")]
|
[TestCase(0.14143808967817237d, 2, "nan-slider")]
|
||||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||||
|
|
||||||
[TestCase(9.6343245007055653d, 239, "diffcalc-test")]
|
[TestCase(9.6300773538770041d, 239, "diffcalc-test")]
|
||||||
[TestCase(1.7550169162648608d, 54, "zero-length-sliders")]
|
[TestCase(1.7550155729445993d, 54, "zero-length-sliders")]
|
||||||
[TestCase(0.55231632896800109d, 4, "very-fast-slider")]
|
[TestCase(0.55785578988249407d, 4, "very-fast-slider")]
|
||||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
||||||
|
|
||||||
[TestCase(6.718709884850683d, 239, "diffcalc-test")]
|
[TestCase(6.6860329680488437d, 239, "diffcalc-test")]
|
||||||
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
|
[TestCase(1.4485740324170036d, 54, "zero-length-sliders")]
|
||||||
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
|
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
|
||||||
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
public static class AimEvaluator
|
public static class AimEvaluator
|
||||||
{
|
{
|
||||||
private const double wide_angle_multiplier = 1.5;
|
private const double wide_angle_multiplier = 1.5;
|
||||||
private const double acute_angle_multiplier = 2.7;
|
private const double acute_angle_multiplier = 2.6;
|
||||||
private const double slider_multiplier = 1.35;
|
private const double slider_multiplier = 1.35;
|
||||||
private const double velocity_change_multiplier = 0.75;
|
private const double velocity_change_multiplier = 0.75;
|
||||||
private const double wiggle_multiplier = 1.02;
|
private const double wiggle_multiplier = 1.02;
|
||||||
@ -80,17 +80,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
double angleBonus = Math.Min(currVelocity, prevVelocity);
|
double angleBonus = Math.Min(currVelocity, prevVelocity);
|
||||||
|
|
||||||
wideAngleBonus = calcWideAngleBonus(currAngle);
|
wideAngleBonus = calcWideAngleBonus(currAngle);
|
||||||
|
acuteAngleBonus = calcAcuteAngleBonus(currAngle);
|
||||||
|
|
||||||
|
// Penalize angle repetition.
|
||||||
|
wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3));
|
||||||
|
acuteAngleBonus *= 0.1 + 0.9 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
|
||||||
|
|
||||||
|
// Apply full wide angle bonus for distance more than one diameter
|
||||||
|
wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter);
|
||||||
|
|
||||||
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
||||||
acuteAngleBonus = calcAcuteAngleBonus(currAngle) *
|
acuteAngleBonus *= angleBonus *
|
||||||
angleBonus *
|
DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) *
|
||||||
DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) *
|
DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2);
|
||||||
DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2);
|
|
||||||
|
|
||||||
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
|
|
||||||
wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)));
|
|
||||||
// Penalize acute angles if they're repeated, reducing the penalty as the lastAngle gets more obtuse.
|
|
||||||
acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
|
|
||||||
|
|
||||||
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
||||||
// https://www.desmos.com/calculator/dp0v0nvowc
|
// https://www.desmos.com/calculator/dp0v0nvowc
|
||||||
@ -140,8 +142,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
return aimStrain;
|
return aimStrain;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double calcWideAngleBonus(double angle) => Math.Pow(Math.Sin(3.0 / 4 * (Math.Min(5.0 / 6 * Math.PI, Math.Max(Math.PI / 6, angle)) - Math.PI / 6)), 2);
|
private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(40), double.DegreesToRadians(140));
|
||||||
|
|
||||||
private static double calcAcuteAngleBonus(double angle) => 1 - calcWideAngleBonus(angle);
|
private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(140), double.DegreesToRadians(40));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
|
private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
|
||||||
private const double min_speed_bonus = 200; // 200 BPM 1/4th
|
private const double min_speed_bonus = 200; // 200 BPM 1/4th
|
||||||
private const double speed_balancing_factor = 40;
|
private const double speed_balancing_factor = 40;
|
||||||
private const double distance_multiplier = 0.94;
|
private const double distance_multiplier = 0.9;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Evaluates the difficulty of tapping the current object, based on:
|
/// Evaluates the difficulty of tapping the current object, based on:
|
||||||
|
@ -78,21 +78,33 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
|
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
|
|
||||||
/// </remarks>
|
|
||||||
[JsonProperty("approach_rate")]
|
[JsonProperty("approach_rate")]
|
||||||
public double ApproachRate { get; set; }
|
public double ApproachRate { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc).
|
/// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// Rate-adjusting mods don't directly affect the overall difficulty value, but have a perceived effect as a result of adjusting audio timing.
|
|
||||||
/// </remarks>
|
|
||||||
[JsonProperty("overall_difficulty")]
|
[JsonProperty("overall_difficulty")]
|
||||||
public double OverallDifficulty { get; set; }
|
public double OverallDifficulty { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("great_hit_window")]
|
||||||
|
public double GreatHitWindow { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc).
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("ok_hit_window")]
|
||||||
|
public double OkHitWindow { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The perceived hit window for a MEH hit inclusive of rate-adjusting mods (DT/HT/etc).
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("meh_hit_window")]
|
||||||
|
public double MehHitWindow { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The beatmap's drain rate. This doesn't scale with rate-adjusting mods.
|
/// The beatmap's drain rate. This doesn't scale with rate-adjusting mods.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -123,6 +135,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
|
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
|
||||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||||
|
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||||
|
|
||||||
if (ShouldSerializeFlashlightDifficulty())
|
if (ShouldSerializeFlashlightDifficulty())
|
||||||
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
|
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
|
||||||
@ -133,6 +146,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
|
yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
|
||||||
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
|
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
|
||||||
yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount);
|
yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount);
|
||||||
|
|
||||||
|
yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
|
||||||
|
yield return (ATTRIB_ID_MEH_HIT_WINDOW, MehHitWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||||
@ -144,12 +160,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
|
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
|
||||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
|
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||||
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||||
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
||||||
AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT];
|
AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT];
|
||||||
SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
|
SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
|
||||||
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
|
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
|
||||||
AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT];
|
AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT];
|
||||||
|
OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW];
|
||||||
|
MehHitWindow = values[ATTRIB_ID_MEH_HIT_WINDOW];
|
||||||
DrainRate = onlineInfo.DrainRate;
|
DrainRate = onlineInfo.DrainRate;
|
||||||
HitCircleCount = onlineInfo.CircleCount;
|
HitCircleCount = onlineInfo.CircleCount;
|
||||||
SliderCount = onlineInfo.SliderCount;
|
SliderCount = onlineInfo.SliderCount;
|
||||||
|
@ -105,6 +105,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||||
|
|
||||||
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||||
|
double hitWindowOk = hitWindows.WindowFor(HitResult.Ok) / clockRate;
|
||||||
|
double hitWindowMeh = hitWindows.WindowFor(HitResult.Meh) / clockRate;
|
||||||
|
|
||||||
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
|
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
|
||||||
{
|
{
|
||||||
@ -122,6 +124,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
SpeedTopWeightedSliderFactor = speedTopWeightedSliderFactor,
|
SpeedTopWeightedSliderFactor = speedTopWeightedSliderFactor,
|
||||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||||
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
||||||
|
GreatHitWindow = hitWindowGreat,
|
||||||
|
OkHitWindow = hitWindowOk,
|
||||||
|
MehHitWindow = hitWindowMeh,
|
||||||
DrainRate = drainRate,
|
DrainRate = drainRate,
|
||||||
MaxCombo = beatmap.GetMaxCombo(),
|
MaxCombo = beatmap.GetMaxCombo(),
|
||||||
HitCircleCount = hitCirclesCount,
|
HitCircleCount = hitCirclesCount,
|
||||||
|
@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
[JsonProperty("effective_miss_count")]
|
[JsonProperty("effective_miss_count")]
|
||||||
public double EffectiveMissCount { get; set; }
|
public double EffectiveMissCount { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("speed_deviation")]
|
||||||
|
public double? SpeedDeviation { get; set; }
|
||||||
|
|
||||||
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
||||||
{
|
{
|
||||||
foreach (var attribute in base.GetAttributesForDisplay())
|
foreach (var attribute in base.GetAttributesForDisplay())
|
||||||
|
@ -10,6 +10,7 @@ using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
|||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||||
{
|
{
|
||||||
@ -41,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private double effectiveMissCount;
|
private double effectiveMissCount;
|
||||||
|
|
||||||
|
private double? speedDeviation;
|
||||||
|
|
||||||
public OsuPerformanceCalculator()
|
public OsuPerformanceCalculator()
|
||||||
: base(new OsuRuleset())
|
: base(new OsuRuleset())
|
||||||
{
|
{
|
||||||
@ -111,10 +114,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
|
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
speedDeviation = calculateSpeedDeviation(osuAttributes);
|
||||||
|
|
||||||
double aimValue = computeAimValue(score, osuAttributes);
|
double aimValue = computeAimValue(score, osuAttributes);
|
||||||
double speedValue = computeSpeedValue(score, osuAttributes);
|
double speedValue = computeSpeedValue(score, osuAttributes);
|
||||||
double accuracyValue = computeAccuracyValue(score, osuAttributes);
|
double accuracyValue = computeAccuracyValue(score, osuAttributes);
|
||||||
double flashlightValue = computeFlashlightValue(score, osuAttributes);
|
double flashlightValue = computeFlashlightValue(score, osuAttributes);
|
||||||
|
|
||||||
double totalValue =
|
double totalValue =
|
||||||
Math.Pow(
|
Math.Pow(
|
||||||
Math.Pow(aimValue, 1.1) +
|
Math.Pow(aimValue, 1.1) +
|
||||||
@ -130,6 +136,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
Accuracy = accuracyValue,
|
Accuracy = accuracyValue,
|
||||||
Flashlight = flashlightValue,
|
Flashlight = flashlightValue,
|
||||||
EffectiveMissCount = effectiveMissCount,
|
EffectiveMissCount = effectiveMissCount,
|
||||||
|
SpeedDeviation = speedDeviation,
|
||||||
Total = totalValue
|
Total = totalValue
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -195,14 +202,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
aimValue *= accuracy;
|
aimValue *= accuracy;
|
||||||
// It is important to consider accuracy difficulty when scaling with accuracy.
|
// It is important to consider accuracy difficulty when scaling with accuracy.
|
||||||
aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
|
aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500;
|
||||||
|
|
||||||
return aimValue;
|
return aimValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||||
{
|
{
|
||||||
if (score.Mods.Any(h => h is OsuModRelax))
|
if (score.Mods.Any(h => h is OsuModRelax) || speedDeviation == null)
|
||||||
return 0.0;
|
return 0.0;
|
||||||
|
|
||||||
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||||
@ -237,6 +244,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
|
speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes);
|
||||||
|
speedValue *= speedHighDeviationMultiplier;
|
||||||
|
|
||||||
// Calculate accuracy assuming the worst case scenario
|
// Calculate accuracy assuming the worst case scenario
|
||||||
double relevantTotalDiff = totalHits - attributes.SpeedNoteCount;
|
double relevantTotalDiff = totalHits - attributes.SpeedNoteCount;
|
||||||
double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
|
double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
|
||||||
@ -245,10 +255,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
|
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.
|
// 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 - attributes.OverallDifficulty) / 2);
|
speedValue *= (0.95 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 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);
|
|
||||||
|
|
||||||
return speedValue;
|
return speedValue;
|
||||||
}
|
}
|
||||||
@ -312,11 +319,113 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
// Scale the flashlight value with accuracy _slightly_.
|
// Scale the flashlight value with accuracy _slightly_.
|
||||||
flashlightValue *= 0.5 + accuracy / 2.0;
|
flashlightValue *= 0.5 + accuracy / 2.0;
|
||||||
// It is important to also consider accuracy difficulty when doing that.
|
// It is important to also consider accuracy difficulty when doing that.
|
||||||
flashlightValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
|
flashlightValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500;
|
||||||
|
|
||||||
return flashlightValue;
|
return flashlightValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Estimates player's deviation on speed notes using <see cref="calculateDeviation"/>, assuming worst-case.
|
||||||
|
/// Treats all speed notes as hit circles.
|
||||||
|
/// </summary>
|
||||||
|
private double? calculateSpeedDeviation(OsuDifficultyAttributes attributes)
|
||||||
|
{
|
||||||
|
if (totalSuccessfulHits == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Calculate accuracy assuming the worst case scenario
|
||||||
|
double speedNoteCount = attributes.SpeedNoteCount;
|
||||||
|
speedNoteCount += (totalHits - attributes.SpeedNoteCount) * 0.1;
|
||||||
|
|
||||||
|
// Assume worst case: all mistakes were on speed notes
|
||||||
|
double relevantCountMiss = Math.Min(countMiss, speedNoteCount);
|
||||||
|
double relevantCountMeh = Math.Min(countMeh, speedNoteCount - relevantCountMiss);
|
||||||
|
double relevantCountOk = Math.Min(countOk, speedNoteCount - relevantCountMiss - relevantCountMeh);
|
||||||
|
double relevantCountGreat = Math.Max(0, speedNoteCount - relevantCountMiss - relevantCountMeh - relevantCountOk);
|
||||||
|
|
||||||
|
return calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Estimates the player's tap deviation based on the OD, given number of greats, oks, mehs 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. Misses are ignored because they are usually due to misaiming.
|
||||||
|
/// Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution.
|
||||||
|
/// </summary>
|
||||||
|
private double? calculateDeviation(OsuDifficultyAttributes attributes, double relevantCountGreat, double relevantCountOk, double relevantCountMeh, double relevantCountMiss)
|
||||||
|
{
|
||||||
|
if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss;
|
||||||
|
|
||||||
|
double hitWindowGreat = attributes.GreatHitWindow;
|
||||||
|
double hitWindowOk = attributes.OkHitWindow;
|
||||||
|
double hitWindowMeh = attributes.MehHitWindow;
|
||||||
|
|
||||||
|
// 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, objectCount - 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 greats and oks are normally distributed, and mehs are uniformly distributed.
|
||||||
|
// Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than:
|
||||||
|
double deviation = hitWindowGreat / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
|
||||||
|
|
||||||
|
double randomValue = Math.Sqrt(2 / Math.PI) * hitWindowOk * Math.Exp(-0.5 * Math.Pow(hitWindowOk / deviation, 2))
|
||||||
|
/ (deviation * SpecialFunctions.Erf(hitWindowOk / (Math.Sqrt(2) * deviation)));
|
||||||
|
|
||||||
|
deviation *= Math.Sqrt(1 - randomValue);
|
||||||
|
|
||||||
|
// Value deviation approach as greatCount approaches 0
|
||||||
|
double limitValue = hitWindowOk / Math.Sqrt(3);
|
||||||
|
|
||||||
|
// If precision is not enough to compute true deviation - use limit value
|
||||||
|
if (pLowerBound == 0 || randomValue >= 1 || deviation > limitValue)
|
||||||
|
deviation = limitValue;
|
||||||
|
|
||||||
|
// Then compute the variance for mehs.
|
||||||
|
double mehVariance = (hitWindowMeh * hitWindowMeh + hitWindowOk * hitWindowMeh + hitWindowOk * hitWindowOk) / 3;
|
||||||
|
|
||||||
|
// Find the total deviation.
|
||||||
|
deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh));
|
||||||
|
|
||||||
|
return deviation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates multiplier for speed to account for improper tapping based on the deviation and speed difficulty
|
||||||
|
// https://www.desmos.com/calculator/dmogdhzofn
|
||||||
|
private double calculateSpeedHighDeviationNerf(OsuDifficultyAttributes attributes)
|
||||||
|
{
|
||||||
|
if (speedDeviation == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||||
|
|
||||||
|
// Decides a point where the PP value achieved compared to the speed deviation is assumed to be tapped improperly. Any PP above this point is considered "excess" speed difficulty.
|
||||||
|
// This is used to cause PP above the cutoff to scale logarithmically towards the original speed value thus nerfing the value.
|
||||||
|
double excessSpeedDifficultyCutoff = 100 + 220 * Math.Pow(22 / speedDeviation.Value, 6.5);
|
||||||
|
|
||||||
|
if (speedValue <= excessSpeedDifficultyCutoff)
|
||||||
|
return 1.0;
|
||||||
|
|
||||||
|
const double scale = 50;
|
||||||
|
double adjustedSpeedValue = scale * (Math.Log((speedValue - excessSpeedDifficultyCutoff) / scale + 1) + excessSpeedDifficultyCutoff / scale);
|
||||||
|
|
||||||
|
// 200 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible
|
||||||
|
double lerp = 1 - Math.Clamp((speedDeviation.Value - 20) / (24 - 20), 0, 1);
|
||||||
|
adjustedSpeedValue = double.Lerp(adjustedSpeedValue, speedValue, lerp);
|
||||||
|
|
||||||
|
return adjustedSpeedValue / speedValue;
|
||||||
|
}
|
||||||
|
|
||||||
// Miss penalty assumes that a player will miss on the hardest parts of a map,
|
// Miss penalty assumes that a player will miss on the hardest parts of a map,
|
||||||
// so we use the amount of relatively difficult sections to adjust miss penalty
|
// so we use the amount of relatively difficult sections to adjust miss penalty
|
||||||
// to make it more punishing on maps with lower amount of hard sections.
|
// to make it more punishing on maps with lower amount of hard sections.
|
||||||
@ -334,6 +443,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||||
|
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
||||||
private int totalImperfectHits => countOk + countMeh + countMiss;
|
private int totalImperfectHits => countOk + countMeh + countMiss;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Speed : OsuStrainSkill
|
public class Speed : OsuStrainSkill
|
||||||
{
|
{
|
||||||
private double skillMultiplier => 1.430;
|
private double skillMultiplier => 1.46;
|
||||||
private double strainDecayBase => 0.3;
|
private double strainDecayBase => 0.3;
|
||||||
|
|
||||||
private double currentStrain;
|
private double currentStrain;
|
||||||
|
@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
||||||
|
|
||||||
[TestCase(3.0950934814938953d, 200, "diffcalc-test")]
|
[TestCase(2.837609165845338d, 200, "diffcalc-test")]
|
||||||
[TestCase(3.0950934814938953d, 200, "diffcalc-test-strong")]
|
[TestCase(2.837609165845338d, 200, "diffcalc-test-strong")]
|
||||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||||
|
|
||||||
[TestCase(4.0839365008715403d, 200, "diffcalc-test")]
|
[TestCase(3.8005218640444949, 200, "diffcalc-test")]
|
||||||
[TestCase(4.0839365008715403d, 200, "diffcalc-test-strong")]
|
[TestCase(3.8005218640444949, 200, "diffcalc-test-strong")]
|
||||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
|
||||||
|
|
||||||
|
@ -36,18 +36,70 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
|||||||
return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
|
return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates a consistency penalty based on the number of consecutive consistent intervals,
|
||||||
|
/// considering the delta time between each colour sequence.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hitObject">The current hitObject to consider.</param>
|
||||||
|
/// <param name="threshold"> The allowable margin of error for determining whether ratios are consistent.</param>
|
||||||
|
/// <param name="maxObjectsToCheck">The maximum objects to check per count of consistent ratio.</param>
|
||||||
|
private static double consistentRatioPenalty(TaikoDifficultyHitObject hitObject, double threshold = 0.01, int maxObjectsToCheck = 64)
|
||||||
|
{
|
||||||
|
int consistentRatioCount = 0;
|
||||||
|
double totalRatioCount = 0.0;
|
||||||
|
|
||||||
|
TaikoDifficultyHitObject current = hitObject;
|
||||||
|
|
||||||
|
for (int i = 0; i < maxObjectsToCheck; i++)
|
||||||
|
{
|
||||||
|
// Break if there is no valid previous object
|
||||||
|
if (current.Index <= 1)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var previousHitObject = (TaikoDifficultyHitObject)current.Previous(1);
|
||||||
|
|
||||||
|
double currentRatio = current.Rhythm.Ratio;
|
||||||
|
double previousRatio = previousHitObject.Rhythm.Ratio;
|
||||||
|
|
||||||
|
// A consistent interval is defined as the percentage difference between the two rhythmic ratios with the margin of error.
|
||||||
|
if (Math.Abs(1 - currentRatio / previousRatio) <= threshold)
|
||||||
|
{
|
||||||
|
consistentRatioCount++;
|
||||||
|
totalRatioCount += currentRatio;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to the previous object
|
||||||
|
current = previousHitObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure no division by zero
|
||||||
|
double ratioPenalty = 1 - totalRatioCount / (consistentRatioCount + 1) * 0.80;
|
||||||
|
|
||||||
|
return ratioPenalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Evaluate the difficulty of the first hitobject within a colour streak.
|
||||||
|
/// </summary>
|
||||||
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject)
|
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject)
|
||||||
{
|
{
|
||||||
TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour;
|
var taikoObject = (TaikoDifficultyHitObject)hitObject;
|
||||||
|
TaikoDifficultyHitObjectColour colour = taikoObject.Colour;
|
||||||
double difficulty = 0.0d;
|
double difficulty = 0.0d;
|
||||||
|
|
||||||
if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak
|
if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak
|
||||||
difficulty += EvaluateDifficultyOf(colour.MonoStreak);
|
difficulty += EvaluateDifficultyOf(colour.MonoStreak);
|
||||||
|
|
||||||
if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern
|
if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern
|
||||||
difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern);
|
difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern);
|
||||||
|
|
||||||
if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
|
if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
|
||||||
difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern);
|
difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern);
|
||||||
|
|
||||||
|
double consistencyPenalty = consistentRatioPenalty(taikoObject);
|
||||||
|
difficulty *= consistencyPenalty;
|
||||||
|
|
||||||
return difficulty;
|
return difficulty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
protected const int ATTRIB_ID_OK_HIT_WINDOW = 27;
|
protected const int ATTRIB_ID_OK_HIT_WINDOW = 27;
|
||||||
protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29;
|
protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29;
|
||||||
protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31;
|
protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31;
|
||||||
|
protected const int ATTRIB_ID_MEH_HIT_WINDOW = 33;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The mods which were applied to the beatmap.
|
/// The mods which were applied to the beatmap.
|
||||||
|
@ -66,6 +66,19 @@ namespace osu.Game.Rulesets.Difficulty.Utils
|
|||||||
/// <returns>The output of the bell curve function of <paramref name="x"/></returns>
|
/// <returns>The output of the bell curve function of <paramref name="x"/></returns>
|
||||||
public static double BellCurve(double x, double mean, double width, double multiplier = 1.0) => multiplier * Math.Exp(Math.E * -(Math.Pow(x - mean, 2) / Math.Pow(width, 2)));
|
public static double BellCurve(double x, double mean, double width, double multiplier = 1.0) => multiplier * Math.Exp(Math.E * -(Math.Pow(x - mean, 2) / Math.Pow(width, 2)));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Smoothstep function (https://en.wikipedia.org/wiki/Smoothstep)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="x">Value to calculate the function for</param>
|
||||||
|
/// <param name="start">Value at which function returns 0</param>
|
||||||
|
/// <param name="end">Value at which function returns 1</param>
|
||||||
|
public static double Smoothstep(double x, double start, double end)
|
||||||
|
{
|
||||||
|
x = Math.Clamp((x - start) / (end - start), 0.0, 1.0);
|
||||||
|
|
||||||
|
return x * x * (3.0 - 2.0 * x);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Smootherstep function (https://en.wikipedia.org/wiki/Smoothstep#Variations)
|
/// Smootherstep function (https://en.wikipedia.org/wiki/Smoothstep#Variations)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
Loading…
Reference in New Issue
Block a user