mirror of
https://github.com/ppy/osu.git
synced 2026-05-27 23:40:45 +08:00
Move remaining osu! rating adjustments to skills (#37623)
Co-authored-by: James Wilson <tsunyoku@gmail.com>
This commit is contained in:
@@ -15,8 +15,6 @@ using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
@@ -30,22 +28,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
}
|
||||
|
||||
public static double CalculateRateAdjustedApproachRate(double approachRate, double clockRate)
|
||||
{
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(approachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN) / clockRate;
|
||||
return IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
|
||||
}
|
||||
|
||||
public static double CalculateRateAdjustedOverallDifficulty(double overallDifficulty, double clockRate)
|
||||
{
|
||||
HitWindows hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(overallDifficulty);
|
||||
|
||||
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
|
||||
return (79.5 - hitWindowGreat) / 6;
|
||||
}
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
||||
{
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
@@ -78,8 +60,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double difficultSliders = aim.GetDifficultSliders();
|
||||
|
||||
double overallDifficulty = CalculateRateAdjustedOverallDifficulty(beatmap.Difficulty.OverallDifficulty, ModUtils.CalculateRateWithMods(mods));
|
||||
|
||||
int hitCircleCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
||||
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
||||
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
|
||||
@@ -87,19 +67,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
int totalHits = beatmap.HitObjects.Count;
|
||||
|
||||
double sliderFactor = aimDifficultyValue > 0
|
||||
? OsuRatingCalculator.CalculateDifficultyRating(aimNoSlidersDifficultyValue) / OsuRatingCalculator.CalculateDifficultyRating(aimDifficultyValue)
|
||||
? calculateDifficultyRating(aimNoSlidersDifficultyValue) / calculateDifficultyRating(aimDifficultyValue) // TODO: this is intentionally left incorrect
|
||||
: 1;
|
||||
|
||||
var osuRatingCalculator = new OsuRatingCalculator(totalHits, overallDifficulty);
|
||||
|
||||
double aimRating = osuRatingCalculator.ComputeAimRating(aimDifficultyValue);
|
||||
double speedRating = osuRatingCalculator.ComputeSpeedRating(speedDifficultyValue);
|
||||
double readingRating = osuRatingCalculator.ComputeReadingRating(readingDifficultyValue);
|
||||
double aimRating = calculateAimDifficultyRating(aimDifficultyValue);
|
||||
double speedRating = calculateDifficultyRating(speedDifficultyValue);
|
||||
double readingRating = calculateDifficultyRating(readingDifficultyValue);
|
||||
|
||||
double flashlightRating = 0.0;
|
||||
|
||||
if (flashlight is not null)
|
||||
flashlightRating = osuRatingCalculator.ComputeFlashlightRating(flashlight.DifficultyValue());
|
||||
flashlightRating = calculateDifficultyRating(flashlight.DifficultyValue());
|
||||
|
||||
double sliderNestedScorePerObject = LegacyScoreUtils.CalculateNestedScorePerObject(beatmap, totalHits);
|
||||
double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(WorkingBeatmap.Beatmap);
|
||||
@@ -157,6 +135,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return DifficultyCalculationUtils.Norm(OsuPerformanceCalculator.PERFORMANCE_NORM_EXPONENT, reading, flashlight * Math.Clamp(flashlight / reading, 0.25, 1.0));
|
||||
}
|
||||
|
||||
private double calculateAimDifficultyRating(double difficultyValue) => Math.Pow(difficultyValue, 0.63) * 0.02275;
|
||||
|
||||
private double calculateDifficultyRating(double difficultyValue) => Math.Sqrt(difficultyValue) * 0.0675;
|
||||
|
||||
private double calculateStarRating(double basePerformance)
|
||||
{
|
||||
return Math.Cbrt(basePerformance * OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER);
|
||||
@@ -189,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
};
|
||||
|
||||
if (mods.Any(h => h is OsuModFlashlight))
|
||||
skills.Add(new Flashlight(mods));
|
||||
skills.Add(new Flashlight(mods, beatmap.HitObjects.Count));
|
||||
|
||||
return skills.ToArray();
|
||||
}
|
||||
|
||||
@@ -5,13 +5,15 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Utils;
|
||||
@@ -99,8 +101,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
okHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate;
|
||||
mehHitWindow = hitWindows.WindowFor(HitResult.Meh) / clockRate;
|
||||
|
||||
approachRate = OsuDifficultyCalculator.CalculateRateAdjustedApproachRate(difficulty.ApproachRate, clockRate);
|
||||
overallDifficulty = OsuDifficultyCalculator.CalculateRateAdjustedOverallDifficulty(difficulty.OverallDifficulty, clockRate);
|
||||
approachRate = calculateRateAdjustedApproachRate(difficulty.ApproachRate, clockRate);
|
||||
overallDifficulty = (79.5 - greatHitWindow) / 6;
|
||||
drainRate = difficulty.DrainRate;
|
||||
|
||||
double comboBasedEstimatedMissCount = calculateComboBasedEstimatedMissCount(osuAttributes);
|
||||
@@ -536,6 +538,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.93 / (missCount / (4 * Math.Log(difficultStrainCount)) + 1);
|
||||
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 double calculateRateAdjustedApproachRate(double approachRate, double clockRate)
|
||||
{
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(approachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN) / clockRate;
|
||||
return IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
|
||||
}
|
||||
|
||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
||||
private int totalImperfectHits => countOk + countMeh + countMiss;
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
public class OsuRatingCalculator
|
||||
{
|
||||
private const double difficulty_multiplier = 0.0675;
|
||||
|
||||
private readonly int totalHits;
|
||||
private readonly double overallDifficulty;
|
||||
|
||||
public OsuRatingCalculator(int totalHits, double overallDifficulty)
|
||||
{
|
||||
this.totalHits = totalHits;
|
||||
this.overallDifficulty = overallDifficulty;
|
||||
}
|
||||
|
||||
public double ComputeAimRating(double aimDifficultyValue)
|
||||
{
|
||||
double aimRating = Math.Pow(aimDifficultyValue, 0.63) * 0.02275;
|
||||
|
||||
double ratingMultiplier = 1.0;
|
||||
|
||||
// It is important to consider accuracy difficulty when scaling with accuracy.
|
||||
ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
|
||||
|
||||
return aimRating * Math.Cbrt(ratingMultiplier);
|
||||
}
|
||||
|
||||
public double ComputeSpeedRating(double speedDifficultyValue)
|
||||
{
|
||||
return CalculateDifficultyRating(speedDifficultyValue);
|
||||
}
|
||||
|
||||
public double ComputeReadingRating(double readingDifficultyValue)
|
||||
{
|
||||
double readingRating = CalculateDifficultyRating(readingDifficultyValue);
|
||||
|
||||
double ratingMultiplier = 1.0;
|
||||
|
||||
ratingMultiplier *= 0.75 + Math.Pow(Math.Max(0, overallDifficulty), 2.2) / 800;
|
||||
|
||||
return readingRating * Math.Cbrt(ratingMultiplier);
|
||||
}
|
||||
|
||||
public double ComputeFlashlightRating(double flashlightDifficultyValue)
|
||||
{
|
||||
double flashlightRating = CalculateDifficultyRating(flashlightDifficultyValue);
|
||||
|
||||
double ratingMultiplier = 1.0;
|
||||
|
||||
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
||||
ratingMultiplier *= 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);
|
||||
|
||||
// It is important to consider accuracy difficulty when scaling with accuracy.
|
||||
ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
|
||||
|
||||
return flashlightRating * Math.Sqrt(ratingMultiplier);
|
||||
}
|
||||
|
||||
public static double CalculateDifficultyRating(double difficultyValue) => Math.Sqrt(difficultyValue) * difficulty_multiplier;
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
/// </summary>
|
||||
public double SmallCircleBonus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Object's immediate OverallDifficulty value calculated from the raw hitwindow.
|
||||
/// </summary>
|
||||
public double OverallDifficulty
|
||||
{
|
||||
get
|
||||
{
|
||||
double hitWindowGreat = RawHitWindow(HitResult.Great) / ClockRate;
|
||||
|
||||
return (79.5 - hitWindowGreat) / 6;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly OsuDifficultyHitObject? lastLastDifficultyObject;
|
||||
private readonly OsuDifficultyHitObject? lastDifficultyObject;
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
double decay = strainDecay(((OsuDifficultyHitObject)current).AdjustedDeltaTime);
|
||||
|
||||
currentStrain *= decay;
|
||||
currentStrain += calculateModAdjustedDifficulty(current) * (1 - decay);
|
||||
currentStrain += calculateAdjustedDifficulty(current) * (1 - decay);
|
||||
|
||||
if (current.BaseObject is Slider)
|
||||
sliderStrains.Add(currentStrain);
|
||||
@@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
return currentStrain;
|
||||
}
|
||||
|
||||
private double calculateModAdjustedDifficulty(DifficultyHitObject current)
|
||||
private double calculateAdjustedDifficulty(DifficultyHitObject current)
|
||||
{
|
||||
double snapDifficulty = SnapAimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplierSnap;
|
||||
double agilityDifficulty = AgilityEvaluator.EvaluateDifficultyOf(current) * skillMultiplierAgility;
|
||||
@@ -85,6 +85,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
totalDifficulty *= 1.0 - magnetisedStrength;
|
||||
}
|
||||
|
||||
totalDifficulty *= 0.985 + Math.Pow(Math.Max(0, ((OsuDifficultyHitObject)current).OverallDifficulty), 2) / 4000;
|
||||
|
||||
return totalDifficulty;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
@@ -17,9 +18,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// </summary>
|
||||
public class Flashlight : StrainSkill
|
||||
{
|
||||
public Flashlight(Mod[] mods)
|
||||
private readonly int totalObjects;
|
||||
|
||||
public Flashlight(Mod[] mods, int totalObjects)
|
||||
: base(mods)
|
||||
{
|
||||
this.totalObjects = totalObjects;
|
||||
}
|
||||
|
||||
private double skillMultiplier => 0.058;
|
||||
@@ -37,12 +41,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
return 0;
|
||||
|
||||
currentStrain *= strainDecay(current.DeltaTime);
|
||||
currentStrain += calculateModAdjustedDifficulty(current) * skillMultiplier;
|
||||
currentStrain += calculateAdjustedDifficulty(current) * skillMultiplier;
|
||||
|
||||
return currentStrain;
|
||||
}
|
||||
|
||||
private double calculateModAdjustedDifficulty(DifficultyHitObject current)
|
||||
private double calculateAdjustedDifficulty(DifficultyHitObject current)
|
||||
{
|
||||
double difficulty = FlashlightEvaluator.EvaluateDifficultyOf(current, Mods);
|
||||
|
||||
@@ -67,10 +71,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
if (Mods.Any(m => m is OsuModAutopilot))
|
||||
difficulty *= 0.4;
|
||||
|
||||
difficulty *= 0.985 + Math.Pow(Math.Max(0, ((OsuDifficultyHitObject)current).OverallDifficulty), 2) / 4000;
|
||||
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum();
|
||||
public override double DifficultyValue()
|
||||
{
|
||||
double sum = GetCurrentStrainPeaks().Sum();
|
||||
|
||||
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
||||
sum *= 0.7 + 0.1 * Math.Min(1.0, totalObjects / 200.0) +
|
||||
(totalObjects > 200 ? 0.2 * Math.Min(1.0, (totalObjects - 200) / 200.0) : 0.0);
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
public static double DifficultyToPerformance(double difficulty) => 25 * Math.Pow(difficulty, 2);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
@@ -40,12 +41,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
double decay = strainDecay(current.DeltaTime);
|
||||
|
||||
currentStrain *= decay;
|
||||
currentStrain += calculateModAdjustedDifficulty(current) * (1 - decay) * skillMultiplier;
|
||||
currentStrain += calculateAdjustedDifficulty(current) * (1 - decay) * skillMultiplier;
|
||||
|
||||
return currentStrain;
|
||||
}
|
||||
|
||||
private double calculateModAdjustedDifficulty(DifficultyHitObject current)
|
||||
private double calculateAdjustedDifficulty(DifficultyHitObject current)
|
||||
{
|
||||
double difficulty = ReadingEvaluator.EvaluateDifficultyOf(current, hasHiddenMod);
|
||||
|
||||
@@ -64,6 +65,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
if (Mods.Any(m => m is OsuModAutopilot))
|
||||
difficulty *= 0.1;
|
||||
|
||||
difficulty *= 0.825 + Math.Pow(Math.Max(0, ((OsuDifficultyHitObject)current).OverallDifficulty), 2.2) / 1125.0;
|
||||
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
double decay = strainDecay(((OsuDifficultyHitObject)current).AdjustedDeltaTime);
|
||||
|
||||
currentStrain *= decay;
|
||||
currentStrain += calculateModAdjustedDifficulty(current) * (1 - decay) * skillMultiplier;
|
||||
currentStrain += calculateAdjustedDifficulty(current) * (1 - decay) * skillMultiplier;
|
||||
|
||||
double currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
return totalStrain;
|
||||
}
|
||||
|
||||
private double calculateModAdjustedDifficulty(DifficultyHitObject current)
|
||||
private double calculateAdjustedDifficulty(DifficultyHitObject current)
|
||||
{
|
||||
double difficulty = SpeedEvaluator.EvaluateDifficultyOf(current);
|
||||
|
||||
|
||||
@@ -84,9 +84,17 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the full hit window for a <see cref="HitResult"/>.
|
||||
/// Retrieves the full rate-adjusted hit window for a <see cref="HitResult"/>.
|
||||
/// </summary>
|
||||
public virtual double HitWindow(HitResult hitResult)
|
||||
public double HitWindow(HitResult hitResult)
|
||||
{
|
||||
return 2 * RawHitWindow(hitResult) / ClockRate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the hit window for a <see cref="HitResult"/>.
|
||||
/// </summary>
|
||||
protected virtual double RawHitWindow(HitResult hitResult)
|
||||
{
|
||||
// Try to get HitWindows from nested hit objects
|
||||
// This is important for objects such as Slider in osu! where the object itself has HitWindows set to Empty, but the nested SliderHead has proper hit windows
|
||||
@@ -97,11 +105,11 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing
|
||||
if (nestedHitObject.HitWindows == HitWindows.Empty)
|
||||
continue;
|
||||
|
||||
return 2 * nestedHitObject.HitWindows.WindowFor(hitResult) / ClockRate;
|
||||
return nestedHitObject.HitWindows.WindowFor(hitResult);
|
||||
}
|
||||
}
|
||||
|
||||
return 2 * BaseObject.HitWindows.WindowFor(hitResult) / ClockRate;
|
||||
return BaseObject.HitWindows.WindowFor(hitResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user