mirror of
https://github.com/ppy/osu.git
synced 2025-02-13 06:22:55 +08:00
Big amount of changes
1) Fully remade HD calc: now it's strain-based 2) Remade high AR calc: now it's using more correct aim-speed summing 3) Added explicit nerf for fiery patterns 4) Fixed bug where HR pop-offing slideraim difficulty due to sliderend position not being mirrored (no longer Rat Race +50) 5) Splitted some files cuz it's more convenient for me to edit
This commit is contained in:
parent
ba265ac2d9
commit
1a68e29a95
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
/// <item><description>and slider difficulty.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliderTravelDistance, double clampPreemptTime = 0)
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliderTravelDistance)
|
||||
{
|
||||
if (current.BaseObject is Spinner || current.Index <= 1 || current.Previous(0).BaseObject is Spinner)
|
||||
return 0;
|
||||
@ -121,17 +121,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
|
||||
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier);
|
||||
|
||||
if (clampPreemptTime > 0)
|
||||
{
|
||||
// Scale if AR is too high for high AR calc
|
||||
double multiplier = osuCurrObj.StrainTime / Math.Min(osuCurrObj.StrainTime, clampPreemptTime - 150); // 150ms is considered as reaction time
|
||||
double multiplierIfAR11 = osuCurrObj.StrainTime / Math.Min(osuCurrObj.StrainTime, 150);
|
||||
|
||||
multiplier = Math.Min(multiplier, multiplierIfAR11);
|
||||
|
||||
aimStrain *= multiplier;
|
||||
}
|
||||
|
||||
// Add in additional slider velocity bonus.
|
||||
if (withSliderTravelDistance)
|
||||
aimStrain += sliderBonus * slider_multiplier;
|
||||
|
@ -11,6 +11,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
{
|
||||
// Main class with some util functions
|
||||
public static class ReadingEvaluator
|
||||
{
|
||||
private const double reading_window_size = 3000;
|
||||
@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
double loopDifficulty = currObj.OpacityAt(loopObj.BaseObject.StartTime, false);
|
||||
|
||||
// Small distances means objects may be cheesed, so it doesn't matter whether they are arranged confusingly.
|
||||
loopDifficulty *= logistic((loopObj.MinimumJumpDistance - 90) / 15);
|
||||
loopDifficulty *= logistic((loopObj.MinimumJumpDistance - 60) / 10);
|
||||
|
||||
//double timeBetweenCurrAndLoopObj = (currObj.BaseObject.StartTime - loopObj.BaseObject.StartTime) / clockRateEstimate;
|
||||
double timeBetweenCurrAndLoopObj = currObj.StartTime - loopObj.StartTime;
|
||||
@ -79,57 +80,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
public static double EvaluateHighARDifficultyOf(DifficultyHitObject current, bool applyAdjust = false)
|
||||
{
|
||||
var currObj = (OsuDifficultyHitObject)current;
|
||||
|
||||
double result = highArCurve(currObj.Preempt);
|
||||
|
||||
if (applyAdjust)
|
||||
{
|
||||
double inpredictability = EvaluateInpredictabilityOf(current);
|
||||
|
||||
// follow lines make high AR easier, so apply nerf if object isn't new combo
|
||||
inpredictability *= 1 + 0.1 * (800 - currObj.FollowLineTime) / 800;
|
||||
|
||||
result *= 0.85 + 0.75 * inpredictability;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static double EvaluateHiddenDifficultyOf(DifficultyHitObject current)
|
||||
{
|
||||
var currObj = (OsuDifficultyHitObject)current;
|
||||
|
||||
double aimDifficulty = AimEvaluator.EvaluateDifficultyOf(current, false);
|
||||
|
||||
double hdDifficulty = 0;
|
||||
|
||||
double timeSpentInvisible = getDurationSpentInvisible(currObj) / currObj.ClockRate;
|
||||
|
||||
double density = 1 + Math.Max(0, CalculateDenstityOf(currObj) - 1);
|
||||
density *= getConstantAngleNerfFactor(currObj);
|
||||
|
||||
double timeDifficultyFactor = density / 1000;
|
||||
|
||||
double visibleObjectFactor = Math.Clamp(retrieveCurrentVisibleObjects(currObj).Count - 2, 0, 15);
|
||||
|
||||
hdDifficulty += Math.Pow(visibleObjectFactor * timeSpentInvisible * timeDifficultyFactor, 1) +
|
||||
(6 + visibleObjectFactor) * aimDifficulty;
|
||||
|
||||
hdDifficulty *= 0.95 + 0.15 * EvaluateInpredictabilityOf(current); // Max multiplier is 1.1
|
||||
|
||||
return hdDifficulty;
|
||||
}
|
||||
|
||||
// Returns value from 0 to 1, where 0 is very predictable and 1 is very unpredictable
|
||||
public static double EvaluateInpredictabilityOf(DifficultyHitObject current)
|
||||
{
|
||||
// make the sum equal to 1
|
||||
const double velocity_change_part = 0.3;
|
||||
const double angle_change_part = 0.6;
|
||||
const double rhythm_change_part = 0.1;
|
||||
const double velocity_change_part = 0.25;
|
||||
const double angle_change_part = 0.45;
|
||||
const double rhythm_change_part = 0.3;
|
||||
|
||||
if (current.BaseObject is Spinner || current.Index == 0 || current.Previous(0).BaseObject is Spinner)
|
||||
return 0;
|
||||
@ -137,23 +94,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||
var osuLastObj = (OsuDifficultyHitObject)current.Previous(0);
|
||||
|
||||
double velocityChangeBonus = 0;
|
||||
// Rhythm difference punishment for velocity and angle bonuses
|
||||
double rhythmSimilarity = 1 - getRhythmDifference(osuCurrObj.StrainTime, osuLastObj.StrainTime);
|
||||
|
||||
// Make differentiation going from 1/4 to 1/2 and bigger difference
|
||||
// To 1/3 to 1/2 and smaller difference
|
||||
rhythmSimilarity = Math.Clamp(rhythmSimilarity, 0.5, 0.75);
|
||||
rhythmSimilarity = 4 * (rhythmSimilarity - 0.5);
|
||||
|
||||
double velocityChangeBonus = getVelocityChangeFactor(osuCurrObj, osuLastObj) * rhythmSimilarity;
|
||||
|
||||
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
|
||||
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
|
||||
|
||||
// https://www.desmos.com/calculator/kqxmqc8pkg
|
||||
if (currVelocity > 0 || prevVelocity > 0)
|
||||
{
|
||||
double velocityChange = Math.Max(0,
|
||||
Math.Min(
|
||||
Math.Abs(prevVelocity - currVelocity) - 0.5 * Math.Min(currVelocity, prevVelocity),
|
||||
Math.Max(((OsuHitObject)osuCurrObj.BaseObject).Radius / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Min(currVelocity, prevVelocity))
|
||||
)); // Stealed from xexxar
|
||||
velocityChangeBonus = velocityChange / Math.Max(currVelocity, prevVelocity); // maxiumum is 0.4
|
||||
velocityChangeBonus /= 0.4;
|
||||
}
|
||||
|
||||
double angleChangeBonus = 0;
|
||||
|
||||
if (osuCurrObj.Angle != null && osuLastObj.Angle != null && currVelocity > 0 && prevVelocity > 0)
|
||||
@ -162,6 +115,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
angleChangeBonus *= Math.Min(currVelocity, prevVelocity) / Math.Max(currVelocity, prevVelocity); // Prevent cheesing
|
||||
}
|
||||
|
||||
angleChangeBonus *= rhythmSimilarity;
|
||||
|
||||
// This bonus only awards rhythm changes if they're not filled with sliderends
|
||||
double rhythmChangeBonus = 0;
|
||||
|
||||
if (current.Index > 1)
|
||||
@ -190,19 +146,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
return result;
|
||||
}
|
||||
|
||||
public static double EvaluateLowDensityBonusOf(DifficultyHitObject current)
|
||||
private static double getVelocityChangeFactor(OsuDifficultyHitObject osuCurrObj, OsuDifficultyHitObject osuLastObj)
|
||||
{
|
||||
//var currObj = (OsuDifficultyHitObject)current;
|
||||
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
|
||||
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
|
||||
|
||||
//// Density = 2 in general means 3 notes on screen (it's not including current note)
|
||||
//double density = CalculateDenstityOf(currObj);
|
||||
double velocityChangeFactor = 0;
|
||||
|
||||
//// We are considering density = 1.5 as starting point, 1.0 is noticably uncomfy and 0.5 is severely uncomfy
|
||||
//double bonus = 1.5 - density;
|
||||
//if (bonus <= 0) return 0;
|
||||
// https://www.desmos.com/calculator/kqxmqc8pkg
|
||||
if (currVelocity > 0 || prevVelocity > 0)
|
||||
{
|
||||
double velocityChange = Math.Max(0,
|
||||
Math.Min(
|
||||
Math.Abs(prevVelocity - currVelocity) - 0.5 * Math.Min(currVelocity, prevVelocity),
|
||||
Math.Max(((OsuHitObject)osuCurrObj.BaseObject).Radius / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Min(currVelocity, prevVelocity))
|
||||
)); // Stealed from xexxar
|
||||
velocityChangeFactor = velocityChange / Math.Max(currVelocity, prevVelocity); // maxiumum is 0.4
|
||||
velocityChangeFactor /= 0.4;
|
||||
}
|
||||
|
||||
//return Math.Pow(bonus, 2);
|
||||
return 0;
|
||||
return velocityChangeFactor;
|
||||
}
|
||||
|
||||
// Returns a list of objects that are visible on screen at
|
||||
@ -222,35 +185,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
}
|
||||
}
|
||||
|
||||
private static List<OsuDifficultyHitObject> retrieveCurrentVisibleObjects(OsuDifficultyHitObject current)
|
||||
{
|
||||
List<OsuDifficultyHitObject> objects = new List<OsuDifficultyHitObject>();
|
||||
|
||||
for (int i = 0; i < current.Count; i++)
|
||||
{
|
||||
OsuDifficultyHitObject hitObject = (OsuDifficultyHitObject)current.Next(i);
|
||||
|
||||
if (hitObject.IsNull() ||
|
||||
(hitObject.StartTime - current.StartTime) > reading_window_size ||
|
||||
current.StartTime < hitObject.StartTime - hitObject.Preempt)
|
||||
break;
|
||||
|
||||
objects.Add(hitObject);
|
||||
}
|
||||
|
||||
return objects;
|
||||
}
|
||||
|
||||
private static double getDurationSpentInvisible(OsuDifficultyHitObject current)
|
||||
{
|
||||
var baseObject = (OsuHitObject)current.BaseObject;
|
||||
|
||||
double fadeOutStartTime = baseObject.StartTime - baseObject.TimePreempt + baseObject.TimeFadeIn;
|
||||
double fadeOutDuration = baseObject.TimePreempt * OsuModHidden.FADE_OUT_DURATION_MULTIPLIER;
|
||||
|
||||
return (fadeOutStartTime + fadeOutDuration) - (baseObject.StartTime - baseObject.TimePreempt);
|
||||
}
|
||||
|
||||
private static double getConstantAngleNerfFactor(OsuDifficultyHitObject current)
|
||||
{
|
||||
const double time_limit = 2000;
|
||||
@ -327,19 +261,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
return Math.Clamp(2 - deltaTime / (reading_window_size / 2), 0, 1);
|
||||
}
|
||||
|
||||
// https://www.desmos.com/calculator/hbj7swzlth
|
||||
private static double highArCurve(double preempt)
|
||||
{
|
||||
double value = Math.Pow(3, 3 - 0.01 * preempt); // 1 for 300ms, 0.25 for 400ms, 0.0625 for 500ms
|
||||
value = softmin(value, 2, 1.7); // use softmin to achieve full-memory cap, 2 times more than AR11 (300ms)
|
||||
return value;
|
||||
}
|
||||
|
||||
private static double getRhythmDifference(double t1, double t2) => 1 - Math.Min(t1, t2) / Math.Max(t1, t2);
|
||||
private static double logistic(double x) => 1 / (1 + Math.Exp(-x));
|
||||
|
||||
// We are using mutiply and divide instead of add and subtract, so values won't be negative
|
||||
// https://www.desmos.com/calculator/fv5xerwpd2
|
||||
private static double softmin(double a, double b, double power = Math.E) => a * b / Math.Log(Math.Pow(power, a) + Math.Pow(power, b), power);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,219 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
{
|
||||
// Class for HD calc. Split because there are a lot of things in HD calc.
|
||||
public static class ReadingHiddenEvaluator
|
||||
{
|
||||
private const double reading_window_size = 3000;
|
||||
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||
{
|
||||
var currObj = (OsuDifficultyHitObject)current;
|
||||
|
||||
double density = 0;
|
||||
double densityAnglesNerf = -2; // we have threshold of 2, so 2 or same angles won't be punished
|
||||
|
||||
OsuDifficultyHitObject? prevObj0 = null;
|
||||
OsuDifficultyHitObject? prevObj1 = null;
|
||||
OsuDifficultyHitObject? prevObj2 = null;
|
||||
|
||||
double prevConstantAngle = 0;
|
||||
|
||||
foreach (var loopObj in retrievePastVisibleObjects(currObj).Reverse())
|
||||
{
|
||||
double loopDifficulty = currObj.OpacityAt(loopObj.BaseObject.StartTime, false);
|
||||
|
||||
// Small distances means objects may be cheesed, so it doesn't matter whether they are arranged confusingly.
|
||||
// For HD: it's not subtracting anything cuz it's multiplied by the aim difficulty anyways.
|
||||
// loopDifficulty *= logistic((loopObj.MinimumJumpDistance) / 15);
|
||||
|
||||
// Reduce density bonus for this object if they're too apart in time
|
||||
// Nerf starts on 1500ms and reaches maximum (*=0) on 3000ms
|
||||
double timeBetweenCurrAndLoopObj = currObj.StartTime - loopObj.StartTime;
|
||||
loopDifficulty *= getTimeNerfFactor(timeBetweenCurrAndLoopObj);
|
||||
|
||||
if (prevObj0.IsNull())
|
||||
{
|
||||
prevObj0 = loopObj;
|
||||
continue;
|
||||
}
|
||||
|
||||
// HD-exclusive burst nerf
|
||||
|
||||
// Only if next object is slower, representing break from many notes in a row
|
||||
if (loopObj.StrainTime > prevObj0.StrainTime)
|
||||
{
|
||||
// Get rhythm similarity: 1 on same rhythms, 0.5 on 1/4 to 1/2
|
||||
double rhythmSimilarity = 1 - getRhythmDifference(loopObj.StrainTime, prevObj0.StrainTime);
|
||||
|
||||
// Make differentiation going from 1/4 to 1/2 and bigger difference
|
||||
// To 1/3 to 1/2 and smaller difference
|
||||
rhythmSimilarity = Math.Clamp(rhythmSimilarity, 0.5, 0.75);
|
||||
rhythmSimilarity = 4 * (rhythmSimilarity - 0.5);
|
||||
|
||||
// Reduce density for this objects if rhythms are different
|
||||
loopDifficulty *= rhythmSimilarity;
|
||||
}
|
||||
|
||||
density += loopDifficulty;
|
||||
|
||||
// Angles nerf
|
||||
|
||||
if (loopObj.Angle.IsNotNull() && prevObj0.Angle.IsNotNull())
|
||||
{
|
||||
double angleDifference = Math.Abs(prevObj0.Angle.Value - loopObj.Angle.Value);
|
||||
|
||||
// Nerf alternating angles case
|
||||
if (prevObj1.IsNotNull() && prevObj2.IsNotNull() && prevObj1.Angle.IsNotNull() && prevObj2.Angle.IsNotNull())
|
||||
{
|
||||
// Normalized difference
|
||||
double angleDifference1 = Math.Abs(prevObj1.Angle.Value - loopObj.Angle.Value) / Math.PI;
|
||||
double angleDifference2 = Math.Abs(prevObj2.Angle.Value - prevObj0.Angle.Value) / Math.PI;
|
||||
|
||||
// Will be close to 1 if angleDifference1 and angleDifference2 was both close to 0
|
||||
double alternatingFactor = Math.Pow((1 - angleDifference1) * (1 - angleDifference2), 2);
|
||||
|
||||
// Be sure to nerf only same rhythms
|
||||
double rhythmFactor = 1 - getRhythmDifference(loopObj.StrainTime, prevObj0.StrainTime); // 0 on different rhythm, 1 on same rhythm
|
||||
rhythmFactor *= 1 - getRhythmDifference(prevObj0.StrainTime, prevObj1.StrainTime);
|
||||
rhythmFactor *= 1 - getRhythmDifference(prevObj1.StrainTime, prevObj2.StrainTime);
|
||||
|
||||
double acuteAngleFactor = 1 - Math.Min(loopObj.Angle.Value, prevObj0.Angle.Value) / Math.PI;
|
||||
|
||||
double prevAngleAdjust = Math.Max(angleDifference - angleDifference1, 0);
|
||||
|
||||
prevAngleAdjust *= alternatingFactor; // Nerf if alternating
|
||||
prevAngleAdjust *= rhythmFactor; // Nerf if same rhythms
|
||||
prevAngleAdjust *= acuteAngleFactor;
|
||||
|
||||
angleDifference -= prevAngleAdjust;
|
||||
}
|
||||
|
||||
// Reduce angles nerf if objects are too apart in time
|
||||
// Angle nerf is starting being reduced from 200ms (150BPM jump) and it reduced to 0 on 2000ms
|
||||
double longIntervalFactor = Math.Clamp(1 - (loopObj.StrainTime - 200) / (2000 - 200), 0, 1);
|
||||
|
||||
// Current angle nerf. Angle difference less than 15 degrees is considered the same
|
||||
double currConstantAngle = Math.Cos(4 * Math.Min(Math.PI / 12, angleDifference)) * longIntervalFactor;
|
||||
|
||||
// Apply the nerf only when it's repeated
|
||||
double currentAngleNerf = Math.Min(currConstantAngle, prevConstantAngle);
|
||||
|
||||
densityAnglesNerf += Math.Min(currentAngleNerf, loopDifficulty);
|
||||
prevConstantAngle = currConstantAngle;
|
||||
}
|
||||
|
||||
prevObj2 = prevObj1;
|
||||
prevObj1 = prevObj0;
|
||||
prevObj0 = loopObj;
|
||||
}
|
||||
|
||||
// Apply angles nerf
|
||||
density -= Math.Max(0, densityAnglesNerf);
|
||||
|
||||
// Consider that density matters only starting from 3rd note on the screen
|
||||
double densityFactor = Math.Max(0, density - 1) / 4;
|
||||
|
||||
// This is kinda wrong cuz it returns value bigger than preempt
|
||||
// double timeSpentInvisible = getDurationSpentInvisible(currObj) / 1000 / currObj.ClockRate;
|
||||
|
||||
// The closer timeSpentInvisible is to 0 -> the less difference there are between NM and HD
|
||||
// So we will reduce base according to this
|
||||
// It will be 0.354 on AR11 value
|
||||
double invisibilityFactor = logistic(currObj.Preempt / 120 - 4);
|
||||
|
||||
double hdDifficulty = invisibilityFactor + densityFactor;
|
||||
|
||||
// Scale by inpredictability slightly
|
||||
hdDifficulty *= 0.95 + 0.15 * ReadingEvaluator.EvaluateInpredictabilityOf(current); // Max multiplier is 1.1
|
||||
|
||||
return hdDifficulty;
|
||||
}
|
||||
|
||||
//public static double EvaluateHiddenDifficultyOfOld(DifficultyHitObject current)
|
||||
//{
|
||||
// var currObj = (OsuDifficultyHitObject)current;
|
||||
|
||||
// double aimDifficulty = AimEvaluator.EvaluateDifficultyOf(current, false);
|
||||
|
||||
// double timeSpentInvisible = getDurationSpentInvisible(currObj) / currObj.ClockRate;
|
||||
|
||||
// double density = 1 + Math.Max(0, CalculateDenstityOf(currObj) - 1);
|
||||
|
||||
// double timeDifficultyFactor = density / 1000;
|
||||
// timeDifficultyFactor *= getConstantAngleNerfFactor(currObj);
|
||||
|
||||
// double visibleObjectFactor = Math.Clamp(retrieveCurrentVisibleObjects(currObj).Count - 2, 0, 15);
|
||||
|
||||
// double hdDifficulty = visibleObjectFactor * timeSpentInvisible * timeDifficultyFactor +
|
||||
// (6 + visibleObjectFactor) * aimDifficulty;
|
||||
|
||||
// hdDifficulty *= 0.95 + 0.15 * EvaluateInpredictabilityOf(current); // Max multiplier is 1.1
|
||||
|
||||
// return hdDifficulty;
|
||||
//}
|
||||
|
||||
// Returns a list of objects that are visible on screen at
|
||||
// the point in time at which the current object becomes visible.
|
||||
private static IEnumerable<OsuDifficultyHitObject> retrievePastVisibleObjects(OsuDifficultyHitObject current)
|
||||
{
|
||||
for (int i = 0; i < current.Index; i++)
|
||||
{
|
||||
OsuDifficultyHitObject hitObject = (OsuDifficultyHitObject)current.Previous(i);
|
||||
|
||||
if (hitObject.IsNull() ||
|
||||
current.StartTime - hitObject.StartTime > reading_window_size ||
|
||||
hitObject.StartTime < current.StartTime - current.Preempt)
|
||||
break;
|
||||
|
||||
yield return hitObject;
|
||||
}
|
||||
}
|
||||
|
||||
//private static double getDurationSpentInvisible(OsuDifficultyHitObject current)
|
||||
//{
|
||||
// var baseObject = (OsuHitObject)current.BaseObject;
|
||||
|
||||
// double fadeOutStartTime = baseObject.StartTime - baseObject.TimePreempt + baseObject.TimeFadeIn;
|
||||
// double fadeOutDuration = baseObject.TimePreempt * OsuModHidden.FADE_OUT_DURATION_MULTIPLIER;
|
||||
|
||||
// return (fadeOutStartTime + fadeOutDuration) - (baseObject.StartTime - baseObject.TimePreempt);
|
||||
//}
|
||||
|
||||
//private static List<OsuDifficultyHitObject> retrieveCurrentVisibleObjects(OsuDifficultyHitObject current)
|
||||
//{
|
||||
// List<OsuDifficultyHitObject> objects = new List<OsuDifficultyHitObject>();
|
||||
|
||||
// for (int i = 0; i < current.Count; i++)
|
||||
// {
|
||||
// OsuDifficultyHitObject hitObject = (OsuDifficultyHitObject)current.Next(i);
|
||||
|
||||
// if (hitObject.IsNull() ||
|
||||
// (hitObject.StartTime - current.StartTime) > reading_window_size ||
|
||||
// current.StartTime < hitObject.StartTime - hitObject.Preempt)
|
||||
// break;
|
||||
|
||||
// objects.Add(hitObject);
|
||||
// }
|
||||
|
||||
// return objects;
|
||||
//}
|
||||
|
||||
private static double getTimeNerfFactor(double deltaTime)
|
||||
{
|
||||
return Math.Clamp(2 - deltaTime / (reading_window_size / 2), 0, 1);
|
||||
}
|
||||
|
||||
private static double getRhythmDifference(double t1, double t2) => 1 - Math.Min(t1, t2) / Math.Max(t1, t2);
|
||||
private static double logistic(double x) => 1 / (1 + Math.Exp(-x));
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
{
|
||||
// Main class with some util functions
|
||||
public static class ReadingHighAREvaluator
|
||||
{
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool applyAdjust = false)
|
||||
{
|
||||
var currObj = (OsuDifficultyHitObject)current;
|
||||
|
||||
double result = highArCurve(currObj.Preempt);
|
||||
|
||||
if (applyAdjust)
|
||||
{
|
||||
double inpredictability = ReadingEvaluator.EvaluateInpredictabilityOf(current);
|
||||
|
||||
// follow lines make high AR easier, so apply nerf if object isn't new combo
|
||||
inpredictability *= 1 + 0.1 * (800 - currObj.FollowLineTime) / 800;
|
||||
|
||||
result *= 0.85 + 1 * inpredictability;
|
||||
result *= 1.05 - 0.4 * EvaluateFieryAnglePunishmentOf(current);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Explicitely nerfs edgecased fiery-type jumps for high AR. The difference from Inpredictability is that this is not used in HD calc
|
||||
public static double EvaluateFieryAnglePunishmentOf(DifficultyHitObject current)
|
||||
{
|
||||
if (current.Index <= 2)
|
||||
return 0;
|
||||
|
||||
var currObj = (OsuDifficultyHitObject)current;
|
||||
var lastObj0 = (OsuDifficultyHitObject)current.Previous(0);
|
||||
var lastObj1 = (OsuDifficultyHitObject)current.Previous(1);
|
||||
var lastObj2 = (OsuDifficultyHitObject)current.Previous(2);
|
||||
|
||||
if (currObj.Angle.IsNull() || lastObj0.Angle.IsNull() || lastObj1.Angle.IsNull() || lastObj2.Angle.IsNull())
|
||||
return 0;
|
||||
|
||||
// Punishment will be reduced if velocity is changing
|
||||
double velocityChangeFactor = getVelocityChangeFactor(currObj, lastObj0);
|
||||
velocityChangeFactor = 1 - Math.Pow(velocityChangeFactor, 2);
|
||||
|
||||
double a1 = currObj.Angle.Value / Math.PI;
|
||||
double a2 = lastObj0.Angle.Value / Math.PI;
|
||||
double a3 = lastObj1.Angle.Value / Math.PI;
|
||||
double a4 = lastObj2.Angle.Value / Math.PI;
|
||||
|
||||
// - 4 same sharp angles in a row: (0.3 0.3 0.3 0.3) -> max punishment
|
||||
|
||||
// Normalized difference
|
||||
double angleDifference1 = Math.Abs(a1 - a2);
|
||||
double angleDifference2 = Math.Abs(a1 - a3);
|
||||
double angleDifference3 = Math.Abs(a1 - a4);
|
||||
|
||||
// Will be close to 1 if angleDifference1 and angleDifference2 was both close to 0
|
||||
double sameAnglePunishment = Math.Pow((1 - angleDifference1) * (1 - angleDifference2) * (1 - angleDifference3), 3);
|
||||
|
||||
// Starting from 60 degrees - reduce same angle punishment
|
||||
double angleSharpnessFactor = Math.Max(0, a1 - 1.0 / 3);
|
||||
angleSharpnessFactor = 1 - angleSharpnessFactor;
|
||||
|
||||
sameAnglePunishment *= angleSharpnessFactor;
|
||||
sameAnglePunishment *= velocityChangeFactor;
|
||||
sameAnglePunishment *= 0.75;
|
||||
|
||||
// - Alternating angles with 0: (0.3 0 0.3 0) or (0 0.3 0 0.3) -> max punishment, (0.3 0 0.1 0) -> some punishment
|
||||
|
||||
double alternateWithZeroAnglePunishment = Math.Max(
|
||||
getAlternateWithZeroAnglePunishment(a1, a2, a3, a4),
|
||||
getAlternateWithZeroAnglePunishment(a2, a1, a4, a3));
|
||||
alternateWithZeroAnglePunishment *= velocityChangeFactor;
|
||||
|
||||
return Math.Min(1, sameAnglePunishment + alternateWithZeroAnglePunishment);
|
||||
}
|
||||
|
||||
private static double getVelocityChangeFactor(OsuDifficultyHitObject osuCurrObj, OsuDifficultyHitObject osuLastObj)
|
||||
{
|
||||
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
|
||||
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
|
||||
|
||||
double velocityChangeFactor = 0;
|
||||
|
||||
// https://www.desmos.com/calculator/kqxmqc8pkg
|
||||
if (currVelocity > 0 || prevVelocity > 0)
|
||||
{
|
||||
double velocityChange = Math.Max(0,
|
||||
Math.Min(
|
||||
Math.Abs(prevVelocity - currVelocity) - 0.5 * Math.Min(currVelocity, prevVelocity),
|
||||
Math.Max(((OsuHitObject)osuCurrObj.BaseObject).Radius / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Min(currVelocity, prevVelocity))
|
||||
)); // Stealed from xexxar
|
||||
velocityChangeFactor = velocityChange / Math.Max(currVelocity, prevVelocity); // maxiumum is 0.4
|
||||
velocityChangeFactor /= 0.4;
|
||||
}
|
||||
|
||||
return velocityChangeFactor;
|
||||
}
|
||||
|
||||
private static double getAlternateWithZeroAnglePunishment(double a1, double a2, double a3, double a4)
|
||||
{
|
||||
// We assume that a1 and a3 are 0
|
||||
double zeroFactor = Math.Pow((1 - a1) * (1 - a3), 8);
|
||||
zeroFactor *= Math.Pow(1 - Math.Abs(a1 - a3), 2);
|
||||
|
||||
double angleSimilarityFactor = 1 - Math.Abs(a2 - a4);
|
||||
double angleSharpnessFactor = Math.Min(1 - Math.Max(0, a2 - 1.0 / 3), 1 - Math.Max(0, a4 - 1.0 / 3));
|
||||
|
||||
return zeroFactor * angleSimilarityFactor * angleSharpnessFactor;
|
||||
}
|
||||
|
||||
public static double EvaluateLowDensityBonusOf(DifficultyHitObject current)
|
||||
{
|
||||
//var currObj = (OsuDifficultyHitObject)current;
|
||||
|
||||
//// Density = 2 in general means 3 notes on screen (it's not including current note)
|
||||
//double density = CalculateDenstityOf(currObj);
|
||||
|
||||
//// We are considering density = 1.5 as starting point, 1.0 is noticably uncomfy and 0.5 is severely uncomfy
|
||||
//double bonus = 1.5 - density;
|
||||
//if (bonus <= 0) return 0;
|
||||
|
||||
//return Math.Pow(bonus, 2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// https://www.desmos.com/calculator/hbj7swzlth
|
||||
private static double highArCurve(double preempt)
|
||||
{
|
||||
double value = Math.Pow(3, 3 - 0.01 * preempt); // 1 for 300ms, 0.25 for 400ms, 0.0625 for 500ms
|
||||
value = softmin(value, 2, 1.7); // use softmin to achieve full-memory cap, 2 times more than AR11 (300ms)
|
||||
return value;
|
||||
}
|
||||
|
||||
// We are using mutiply and divide instead of add and subtract, so values won't be negative
|
||||
// https://www.desmos.com/calculator/fv5xerwpd2
|
||||
private static double softmin(double a, double b, double power = Math.E) => a * b / Math.Log(Math.Pow(power, a) + Math.Pow(power, b), power);
|
||||
}
|
||||
}
|
@ -22,11 +22,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
public class OsuDifficultyCalculator : DifficultyCalculator
|
||||
{
|
||||
private const double difficulty_multiplier = 0.067;
|
||||
|
||||
public const double DIFFICULTY_MULTIPLIER = 0.067;
|
||||
public const double SUM_POWER = 1.1;
|
||||
public const double FL_SUM_POWER = 1.6;
|
||||
public override int Version => 20220902;
|
||||
public static double SumPower => 1.1;
|
||||
public static double FLSumPower => 1.6;
|
||||
|
||||
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
@ -38,16 +37,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
return new OsuDifficultyAttributes { Mods = mods };
|
||||
|
||||
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
|
||||
double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
|
||||
double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
|
||||
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
|
||||
double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
|
||||
double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
|
||||
double speedNotes = ((Speed)skills[2]).RelevantNoteCount();
|
||||
double flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier;
|
||||
double flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
double readingLowARRating = Math.Sqrt(skills[4].DifficultyValue()) * difficulty_multiplier;
|
||||
double readingHighARRating = Math.Sqrt(skills[5].DifficultyValue()) * difficulty_multiplier;
|
||||
double readingLowARRating = Math.Sqrt(skills[4].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
|
||||
double readingHighARRating = Math.Sqrt(skills[5].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
|
||||
double readingSlidersRating = 0;
|
||||
double hiddenRating = Math.Sqrt(skills[6].DifficultyValue()) * difficulty_multiplier;
|
||||
double hiddenRating = Math.Sqrt(skills[6].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
|
||||
|
||||
@ -64,8 +63,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
flashlightRating *= 0.7;
|
||||
}
|
||||
|
||||
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
|
||||
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
|
||||
double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
|
||||
double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);
|
||||
|
||||
// Cognition
|
||||
double baseFlashlightPerformance = 0.0;
|
||||
@ -73,10 +72,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
baseFlashlightPerformance = Math.Pow(flashlightRating, 2.0) * 25.0;
|
||||
|
||||
double baseReadingLowARPerformance = Math.Pow(readingLowARRating, 2.5) * 17.0;
|
||||
double baseReadingHighARPerformance = Math.Pow(5 * Math.Max(1, readingHighARRating / 0.0675) - 4, 3) / 100000;
|
||||
double baseReadingARPerformance = Math.Pow(Math.Pow(baseReadingLowARPerformance, SumPower) + Math.Pow(baseReadingHighARPerformance, SumPower), 1.0 / SumPower);
|
||||
double baseReadingHighARPerformance = OsuStrainSkill.DifficultyToPerformance(readingHighARRating);
|
||||
double baseReadingARPerformance = Math.Pow(Math.Pow(baseReadingLowARPerformance, SUM_POWER) + Math.Pow(baseReadingHighARPerformance, SUM_POWER), 1.0 / SUM_POWER);
|
||||
|
||||
double baseFlashlightARPerformance = Math.Pow(Math.Pow(baseFlashlightPerformance, FLSumPower) + Math.Pow(baseReadingARPerformance, FLSumPower), 1.0 / FLSumPower);
|
||||
double baseFlashlightARPerformance = Math.Pow(Math.Pow(baseFlashlightPerformance, FL_SUM_POWER) + Math.Pow(baseReadingARPerformance, FL_SUM_POWER), 1.0 / FL_SUM_POWER);
|
||||
|
||||
double baseReadingHiddenPerformance = 0;
|
||||
if (mods.Any(h => h is OsuModHidden))
|
||||
@ -86,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
double baseReadingNonARPerformance = baseReadingHiddenPerformance + baseReadingSliderPerformance;
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
double drainRate = beatmap.Difficulty.DrainRate;
|
||||
int maxCombo = beatmap.GetMaxCombo();
|
||||
|
||||
@ -94,16 +94,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
|
||||
|
||||
// Limit cognition by full memorisation difficulty
|
||||
double cognitionPerformance = Math.Pow(Math.Pow(baseFlashlightARPerformance, SumPower) + Math.Pow(baseReadingNonARPerformance, SumPower), 1.0 / SumPower);
|
||||
double mechanicalPerformance = Math.Pow(Math.Pow(baseAimPerformance, SumPower) + Math.Pow(baseSpeedPerformance, SumPower), 1.0 / SumPower);
|
||||
double cognitionPerformance = Math.Pow(Math.Pow(baseFlashlightARPerformance, SUM_POWER) + Math.Pow(baseReadingNonARPerformance, SUM_POWER), 1.0 / SUM_POWER);
|
||||
double mechanicalPerformance = Math.Pow(Math.Pow(baseAimPerformance, SUM_POWER) + Math.Pow(baseSpeedPerformance, SUM_POWER), 1.0 / SUM_POWER);
|
||||
double potentialFlashlightPerformance = OsuPerformanceCalculator.ComputePerfectFlashlightValue(flashlightRating, hitCirclesCount + sliderCount);
|
||||
cognitionPerformance = OsuPerformanceCalculator.AdjustCognitionPerformance(cognitionPerformance, mechanicalPerformance, potentialFlashlightPerformance);
|
||||
|
||||
double basePerformance =
|
||||
Math.Pow(
|
||||
Math.Pow(mechanicalPerformance, SumPower) +
|
||||
Math.Pow(cognitionPerformance, SumPower)
|
||||
, 1.0 / SumPower
|
||||
Math.Pow(mechanicalPerformance, SUM_POWER) +
|
||||
Math.Pow(cognitionPerformance, SUM_POWER)
|
||||
, 1.0 / SUM_POWER
|
||||
);
|
||||
|
||||
double starRating = basePerformance > 0.00001
|
||||
@ -115,6 +115,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
|
||||
//var test = ((ReadingHighAR)skills[5]).GetAimSpeed();
|
||||
|
||||
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
|
||||
{
|
||||
StarRating = starRating,
|
||||
@ -128,7 +130,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
HiddenDifficulty = hiddenRating,
|
||||
FlashlightDifficulty = flashlightRating,
|
||||
SliderFactor = sliderFactor,
|
||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||
ApproachRate = IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, 1800, 1200, 450),
|
||||
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
||||
DrainRate = drainRate,
|
||||
MaxCombo = maxCombo,
|
||||
|
@ -4,8 +4,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
|
||||
|
||||
double multiplier = PERFORMANCE_BASE_MULTIPLIER;
|
||||
double power = OsuDifficultyCalculator.SUM_POWER;
|
||||
|
||||
if (score.Mods.Any(m => m is OsuModNoFail))
|
||||
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
|
||||
@ -62,8 +63,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
|
||||
}
|
||||
|
||||
double power = OsuDifficultyCalculator.SumPower;
|
||||
|
||||
double aimValue = computeAimValue(score, osuAttributes);
|
||||
double speedValue = computeSpeedValue(score, osuAttributes);
|
||||
double mechanicalValue = Math.Pow(Math.Pow(aimValue, power) + Math.Pow(speedValue, power), 1.0 / power);
|
||||
@ -75,12 +74,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
double flashlightValue = potentialFlashlightValue;
|
||||
if (!score.Mods.Any(h => h is OsuModFlashlight))
|
||||
flashlightValue = 0.0;
|
||||
double readingARValue = computeReadingARValue(score, osuAttributes);
|
||||
|
||||
double lowARValue = computeReadingLowARValue(score, osuAttributes);
|
||||
double readingHDValue = computeReadingHiddenValue(score, osuAttributes);
|
||||
double readingSlidersValue = 0;
|
||||
|
||||
double highARValue = computeReadingHighARValue(score, osuAttributes);
|
||||
|
||||
double readingARValue = Math.Pow(
|
||||
Math.Pow(lowARValue, power) +
|
||||
Math.Pow(highARValue, power), 1.0 / power);
|
||||
|
||||
// Reduce AR reading bonus if FL is present
|
||||
double flPower = OsuDifficultyCalculator.FLSumPower;
|
||||
double flPower = OsuDifficultyCalculator.FL_SUM_POWER;
|
||||
double flashlightARValue = Math.Pow(Math.Pow(flashlightValue, flPower) + Math.Pow(readingARValue, flPower), 1.0 / flPower);
|
||||
|
||||
double readingNonARValue = computeReadingNonARValue(score, osuAttributes);
|
||||
double readingNonARValue = readingHDValue + readingSlidersValue;
|
||||
double cognitionValue = Math.Pow(Math.Pow(flashlightARValue, power) + Math.Pow(readingNonARValue, power), 1.0 / power);
|
||||
cognitionValue = AdjustCognitionPerformance(cognitionValue, mechanicalValue, potentialFlashlightValue);
|
||||
|
||||
@ -106,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||
double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty);
|
||||
|
||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||
@ -143,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
if (score.Mods.Any(h => h is OsuModRelax))
|
||||
return 0.0;
|
||||
|
||||
double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||
|
||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||
@ -245,25 +254,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return flashlightValue;
|
||||
}
|
||||
|
||||
private double computeReadingARValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
//double readingARValue = Math.Max(computeReadingLowARValue(score, attributes), computeReadingHighARValue(score, attributes));
|
||||
double power = OsuDifficultyCalculator.SumPower;
|
||||
double readingValue = Math.Pow(
|
||||
Math.Pow(computeReadingLowARValue(score, attributes), power) +
|
||||
Math.Pow(computeReadingHighARValue(score, attributes), power), 1.0 / power);
|
||||
|
||||
return readingValue;
|
||||
}
|
||||
|
||||
private double computeReadingNonARValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
double readingHDValue = computeReadingHiddenValue(score, attributes);
|
||||
double readingSlidersValue = 0;
|
||||
|
||||
return readingHDValue + readingSlidersValue;
|
||||
}
|
||||
|
||||
private double computeReadingLowARValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
double rawReading = attributes.ReadingDifficultyLowAR;
|
||||
@ -289,8 +279,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
private double computeReadingHighARValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
// Copied from aim
|
||||
double highARValue = Math.Pow(5.0 * Math.Max(1.0, attributes.ReadingDifficultyHighAR / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||
double highARValue = OsuStrainSkill.DifficultyToPerformance(attributes.ReadingDifficultyHighAR);
|
||||
|
||||
// High AR should have length bonus, even more agressive than normal aim
|
||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||
@ -303,11 +292,48 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
highARValue *= getComboScalingFactor(attributes);
|
||||
|
||||
highARValue *= accuracy * accuracy;
|
||||
// It is important to consider accuracy difficulty when scaling with accuracy.
|
||||
highARValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
|
||||
// Approximate how much of high AR difficulty is aim
|
||||
double aimPerformance = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty);
|
||||
double speedPerformance = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||
|
||||
return highARValue;
|
||||
double aimRatio = aimPerformance / (aimPerformance + speedPerformance);
|
||||
|
||||
// Aim part calculation
|
||||
double aimPartValue = highARValue * aimRatio;
|
||||
{
|
||||
// We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator.
|
||||
double estimateDifficultSliders = attributes.SliderCount * 0.15;
|
||||
|
||||
if (attributes.SliderCount > 0)
|
||||
{
|
||||
double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
|
||||
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + attributes.SliderFactor;
|
||||
aimPartValue *= sliderNerfFactor;
|
||||
}
|
||||
|
||||
aimPartValue *= accuracy;
|
||||
// It is important to consider accuracy difficulty when scaling with accuracy.
|
||||
aimPartValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
|
||||
}
|
||||
|
||||
// Speed part calculation
|
||||
double speedPartValue = highARValue * (1 - aimRatio);
|
||||
{
|
||||
// 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.
|
||||
speedPartValue *= (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.
|
||||
speedPartValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
||||
}
|
||||
|
||||
return aimPartValue + speedPartValue;
|
||||
}
|
||||
|
||||
private double computeReadingHiddenValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
@ -316,6 +342,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return 0.0;
|
||||
|
||||
double rawReading = attributes.HiddenDifficulty;
|
||||
//double readingValue = Math.Pow(rawReading, 2.0) * 25.0;
|
||||
double readingValue = Math.Pow(rawReading, 2.0) * 25.0;
|
||||
|
||||
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
||||
|
@ -95,11 +95,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
/// </summary>
|
||||
public IList<OverlapObject> OverlapObjects { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time in ms between appearence of this <see cref="OsuDifficultyHitObject"/> and moment to click on it.
|
||||
/// </summary>
|
||||
public readonly double Preempt;
|
||||
|
||||
/// <summary>
|
||||
/// Preempt of follow line for this <see cref="OsuDifficultyHitObject"/> adjusted by clockrate.
|
||||
/// Will be equal to 0 if object is New Combo.
|
||||
/// </summary>
|
||||
public readonly double FollowLineTime;
|
||||
|
||||
/// <summary>
|
||||
/// Playback rate of beatmap.
|
||||
/// Will be equal 1.5 on DT and 0.75 on HT.
|
||||
/// </summary>
|
||||
public readonly double ClockRate;
|
||||
|
||||
private readonly OsuHitObject? lastLastObject;
|
||||
private readonly OsuHitObject lastObject;
|
||||
public readonly double Preempt;
|
||||
public readonly double FollowLineTime;
|
||||
public readonly double ClockRate;
|
||||
|
||||
public OsuDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject? lastLastObject, double clockRate, List<DifficultyHitObject> objects, int index)
|
||||
: base(hitObject, lastObject, clockRate, objects, index)
|
||||
|
@ -7,6 +7,7 @@ using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using System.Linq;
|
||||
using osu.Framework.Utils;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
@ -67,5 +68,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
|
||||
return difficulty * DifficultyMultiplier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts difficulty value from <see cref="OsuDifficultyAttributes"/> to base performance.
|
||||
/// </summary>
|
||||
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||
|
||||
/// <summary>
|
||||
/// Converts base performance to <see cref="OsuDifficultyAttributes"/> difficulty value.s
|
||||
/// </summary>
|
||||
public static double PerformanceToDifficulty(double performance) => (Math.Pow(100000.0 * performance, 1.0 / 3.0) + 4.0) / 5.0 * 0.0675;
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
@ -75,65 +74,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
}
|
||||
}
|
||||
|
||||
public class ReadingHighAR : GraphSkill
|
||||
public class ReadingHidden : OsuStrainSkill
|
||||
{
|
||||
public ReadingHighAR(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
aimComponent = new HighARAimComponent(mods);
|
||||
speedComponent = new HighARSpeedComponent(mods);
|
||||
}
|
||||
|
||||
private HighARAimComponent aimComponent;
|
||||
private HighARSpeedComponent speedComponent;
|
||||
|
||||
private readonly List<double> difficulties = new List<double>();
|
||||
|
||||
public override void Process(DifficultyHitObject current)
|
||||
{
|
||||
aimComponent.Process(current);
|
||||
speedComponent.Process(current);
|
||||
|
||||
double power = OsuDifficultyCalculator.SumPower;
|
||||
double mergedDifficulty = Math.Pow(
|
||||
Math.Pow(aimComponent.CurrentSectionPeak, power) +
|
||||
Math.Pow(speedComponent.CurrentSectionPeak, power), 1.0 / power);
|
||||
|
||||
difficulties.Add(mergedDifficulty);
|
||||
|
||||
if (current.Index == 0)
|
||||
CurrentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
|
||||
|
||||
while (current.StartTime > CurrentSectionEnd)
|
||||
{
|
||||
StrainPeaks.Add(CurrentSectionPeak);
|
||||
CurrentSectionPeak = 0;
|
||||
CurrentSectionEnd += SectionLength;
|
||||
}
|
||||
|
||||
CurrentSectionPeak = Math.Max(mergedDifficulty, CurrentSectionPeak);
|
||||
}
|
||||
public override double DifficultyValue()
|
||||
{
|
||||
double power = OsuDifficultyCalculator.SumPower;
|
||||
return Math.Pow(
|
||||
Math.Pow(aimComponent.DifficultyValue(), power) +
|
||||
Math.Pow(speedComponent.DifficultyValue(), power), 1.0 / power);
|
||||
}
|
||||
}
|
||||
|
||||
public class HighARAimComponent : OsuStrainSkill
|
||||
{
|
||||
public HighARAimComponent(Mod[] mods)
|
||||
public ReadingHidden(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
}
|
||||
|
||||
private double currentStrain;
|
||||
// private double currentRhythm;
|
||||
|
||||
//private double skillMultiplier => 13;
|
||||
private double skillMultiplier => 14;
|
||||
private double skillMultiplier => 5;
|
||||
private double strainDecayBase => 0.15;
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
@ -144,105 +93,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
currentStrain *= strainDecay(current.DeltaTime);
|
||||
|
||||
double aimDifficulty = AimEvaluator.EvaluateDifficultyOf(current, true, ((OsuDifficultyHitObject)current).Preempt);
|
||||
aimDifficulty *= ReadingEvaluator.EvaluateHighARDifficultyOf(current, true);
|
||||
aimDifficulty *= skillMultiplier;
|
||||
// We're not using slider aim because we assuming that HD doesn't makes sliders harder (what is not true, but we will ignore this for now)
|
||||
double hiddenDifficulty = AimEvaluator.EvaluateDifficultyOf(current, false);
|
||||
hiddenDifficulty *= ReadingHiddenEvaluator.EvaluateDifficultyOf(current);
|
||||
hiddenDifficulty *= skillMultiplier;
|
||||
|
||||
double totalStrain = currentStrain;
|
||||
currentStrain += aimDifficulty;
|
||||
currentStrain += hiddenDifficulty;
|
||||
|
||||
// Warning: this line is unstable, so increasing amount of objects can decrease pp
|
||||
totalStrain += aimDifficulty * (1 + ReadingEvaluator.EvaluateLowDensityBonusOf(current));
|
||||
|
||||
|
||||
//Console.WriteLine($"{current.StartTime} - {ReadingEvaluator.EvaluateLowDensityBonusOf(current)}");
|
||||
|
||||
return totalStrain;
|
||||
}
|
||||
}
|
||||
|
||||
public class HighARSpeedComponent : OsuStrainSkill
|
||||
{
|
||||
private double skillMultiplier => 675;
|
||||
private double strainDecayBase => 0.3;
|
||||
|
||||
private double currentStrain;
|
||||
private double currentRhythm;
|
||||
|
||||
public HighARSpeedComponent(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
}
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
|
||||
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => (currentStrain * currentRhythm) * strainDecay(time - current.Previous(0).StartTime);
|
||||
|
||||
protected override double StrainValueAt(DifficultyHitObject current)
|
||||
{
|
||||
OsuDifficultyHitObject currODHO = (OsuDifficultyHitObject)current;
|
||||
|
||||
currentStrain *= strainDecay(currODHO.StrainTime);
|
||||
|
||||
double speedDifficulty = SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier;
|
||||
speedDifficulty *= ReadingEvaluator.EvaluateHighARDifficultyOf(current, false);
|
||||
currentStrain += speedDifficulty;
|
||||
|
||||
currentRhythm = currODHO.RhythmDifficulty;
|
||||
// currentRhythm *= currentRhythm; // Squaring is broken cuz rhythm is broken ((((
|
||||
|
||||
double totalStrain = currentStrain * currentRhythm;
|
||||
return totalStrain;
|
||||
}
|
||||
}
|
||||
|
||||
public class ReadingHidden : GraphSkill
|
||||
{
|
||||
public ReadingHidden(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
}
|
||||
|
||||
private readonly List<double> difficulties = new List<double>();
|
||||
private double skillMultiplier => 2.3;
|
||||
|
||||
public override void Process(DifficultyHitObject current)
|
||||
{
|
||||
double currentDifficulty = ReadingEvaluator.EvaluateHiddenDifficultyOf(current) * skillMultiplier;
|
||||
|
||||
difficulties.Add(currentDifficulty);
|
||||
|
||||
if (current.Index == 0)
|
||||
CurrentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
|
||||
|
||||
while (current.StartTime > CurrentSectionEnd)
|
||||
{
|
||||
StrainPeaks.Add(CurrentSectionPeak);
|
||||
CurrentSectionPeak = 0;
|
||||
CurrentSectionEnd += SectionLength;
|
||||
}
|
||||
|
||||
CurrentSectionPeak = Math.Max(currentDifficulty, CurrentSectionPeak);
|
||||
}
|
||||
|
||||
public override double DifficultyValue()
|
||||
{
|
||||
double difficulty = 0;
|
||||
|
||||
// Sections with 0 difficulty are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
||||
// These sections will not contribute to the difficulty.
|
||||
var peaks = difficulties.Where(p => p > 0);
|
||||
|
||||
List<double> values = peaks.OrderByDescending(d => d).ToList();
|
||||
|
||||
// Difficulty is the weighted sum of the highest strains from every section.
|
||||
// We're sorting from highest to lowest strain.
|
||||
for (int i = 0; i < values.Count; i++)
|
||||
{
|
||||
difficulty += values[i] / (i + 1);
|
||||
}
|
||||
|
||||
return difficulty;
|
||||
return currentStrain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
146
osu.Game.Rulesets.Osu/Difficulty/Skills/ReadingHighAR.cs
Normal file
146
osu.Game.Rulesets.Osu/Difficulty/Skills/ReadingHighAR.cs
Normal file
@ -0,0 +1,146 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
public class ReadingHighAR : GraphSkill
|
||||
{
|
||||
public ReadingHighAR(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
aimComponent = new HighARAimComponent(mods);
|
||||
speedComponent = new HighARSpeedComponent(mods);
|
||||
|
||||
aimComponentNoAdjust = new HighARAimComponent(mods, false);
|
||||
}
|
||||
|
||||
private HighARAimComponent aimComponent;
|
||||
private HighARAimComponent aimComponentNoAdjust;
|
||||
private HighARSpeedComponent speedComponent;
|
||||
|
||||
private readonly List<double> difficulties = new List<double>();
|
||||
|
||||
public override void Process(DifficultyHitObject current)
|
||||
{
|
||||
aimComponent.Process(current);
|
||||
speedComponent.Process(current);
|
||||
|
||||
aimComponentNoAdjust.Process(current);
|
||||
|
||||
double power = OsuDifficultyCalculator.SUM_POWER;
|
||||
double mergedDifficulty = Math.Pow(
|
||||
Math.Pow(aimComponent.CurrentSectionPeak, power) +
|
||||
Math.Pow(speedComponent.CurrentSectionPeak, power), 1.0 / power);
|
||||
|
||||
difficulties.Add(mergedDifficulty);
|
||||
|
||||
if (current.Index == 0)
|
||||
CurrentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
|
||||
|
||||
while (current.StartTime > CurrentSectionEnd)
|
||||
{
|
||||
StrainPeaks.Add(CurrentSectionPeak);
|
||||
CurrentSectionPeak = 0;
|
||||
CurrentSectionEnd += SectionLength;
|
||||
}
|
||||
|
||||
CurrentSectionPeak = Math.Max(mergedDifficulty, CurrentSectionPeak);
|
||||
}
|
||||
public override double DifficultyValue()
|
||||
{
|
||||
Console.WriteLine($"Degree of High AR Complexity = {aimComponent.DifficultyValue() / aimComponentNoAdjust.DifficultyValue():0.##}");
|
||||
|
||||
// Simulating summing
|
||||
double aimValue = Math.Sqrt(aimComponent.DifficultyValue()) * OsuDifficultyCalculator.DIFFICULTY_MULTIPLIER;
|
||||
double speedValue = Math.Sqrt(speedComponent.DifficultyValue()) * OsuDifficultyCalculator.DIFFICULTY_MULTIPLIER;
|
||||
|
||||
double aimPerformance = OsuStrainSkill.DifficultyToPerformance(aimValue);
|
||||
double speedPerformance = OsuStrainSkill.DifficultyToPerformance(speedValue);
|
||||
|
||||
double power = OsuDifficultyCalculator.SUM_POWER;
|
||||
double totalPerformance = Math.Pow(Math.Pow(aimPerformance, power) + Math.Pow(speedPerformance, power), 1.0 / power);
|
||||
|
||||
double adjustedDifficulty = OsuStrainSkill.PerformanceToDifficulty(totalPerformance);
|
||||
|
||||
return Math.Pow(adjustedDifficulty / OsuDifficultyCalculator.DIFFICULTY_MULTIPLIER, 2.0);
|
||||
}
|
||||
}
|
||||
|
||||
public class HighARAimComponent : OsuStrainSkill
|
||||
{
|
||||
public HighARAimComponent(Mod[] mods, bool adjustHighAR = true)
|
||||
: base(mods)
|
||||
{
|
||||
this.adjustHighAR = adjustHighAR;
|
||||
}
|
||||
|
||||
private bool adjustHighAR;
|
||||
private double currentStrain;
|
||||
|
||||
private double skillMultiplier => 19;
|
||||
private double strainDecayBase => 0.15;
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
|
||||
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime);
|
||||
|
||||
protected override double StrainValueAt(DifficultyHitObject current)
|
||||
{
|
||||
currentStrain *= strainDecay(current.DeltaTime);
|
||||
|
||||
double aimDifficulty = AimEvaluator.EvaluateDifficultyOf(current, true);
|
||||
aimDifficulty *= ReadingHighAREvaluator.EvaluateDifficultyOf(current, adjustHighAR);
|
||||
aimDifficulty *= skillMultiplier;
|
||||
|
||||
double totalStrain = currentStrain;
|
||||
|
||||
currentStrain += aimDifficulty;
|
||||
totalStrain += aimDifficulty;
|
||||
|
||||
// Console.WriteLine($"{current.BaseObject.StartTime},{aimDifficulty:0.#}");
|
||||
|
||||
return totalStrain;
|
||||
}
|
||||
}
|
||||
|
||||
public class HighARSpeedComponent : OsuStrainSkill
|
||||
{
|
||||
private double skillMultiplier => 850;
|
||||
private double strainDecayBase => 0.3;
|
||||
|
||||
private double currentStrain;
|
||||
private double currentRhythm;
|
||||
|
||||
public HighARSpeedComponent(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
}
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
|
||||
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => (currentStrain * currentRhythm) * strainDecay(time - current.Previous(0).StartTime);
|
||||
|
||||
protected override double StrainValueAt(DifficultyHitObject current)
|
||||
{
|
||||
OsuDifficultyHitObject currODHO = (OsuDifficultyHitObject)current;
|
||||
|
||||
currentStrain *= strainDecay(currODHO.StrainTime);
|
||||
|
||||
double speedDifficulty = SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier;
|
||||
speedDifficulty *= ReadingHighAREvaluator.EvaluateDifficultyOf(current, false);
|
||||
currentStrain += speedDifficulty;
|
||||
|
||||
currentRhythm = currODHO.RhythmDifficulty;
|
||||
double totalStrain = currentStrain * currentRhythm;
|
||||
return totalStrain;
|
||||
}
|
||||
}
|
||||
}
|
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
set
|
||||
{
|
||||
repeatCount = value;
|
||||
endPositionCache.Invalidate();
|
||||
updateNestedPositions();
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
public Slider()
|
||||
{
|
||||
SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples();
|
||||
Path.Version.ValueChanged += _ => endPositionCache.Invalidate();
|
||||
Path.Version.ValueChanged += _ => updateNestedPositions();
|
||||
}
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
|
Loading…
Reference in New Issue
Block a user