diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingEvaluator.cs index 2b5c5ec974..7c6c82fdb9 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/ReadingEvaluator.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const double overlap_multiplier = 0.8; - private static double calculateDenstityOf(OsuDifficultyHitObject currObj) + public static double CalculateDenstityOf(OsuDifficultyHitObject currObj) { double pastObjectDifficultyInfluence = 0; @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators return pastObjectDifficultyInfluence; } - private static double calculateOverlapDifficultyOf(OsuDifficultyHitObject currObj) + public static double CalculateOverlapDifficultyOf(OsuDifficultyHitObject currObj) { double screenOverlapDifficulty = 0; @@ -62,15 +62,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators var currObj = (OsuDifficultyHitObject)current; - double pastObjectDifficultyInfluence = calculateDenstityOf(currObj); - double screenOverlapDifficulty = calculateOverlapDifficultyOf(currObj); + double pastObjectDifficultyInfluence = CalculateDenstityOf(currObj); + double screenOverlapDifficulty = CalculateOverlapDifficultyOf(currObj); double difficulty = Math.Pow(4 * Math.Log(Math.Max(1, pastObjectDifficultyInfluence)), 2.3); screenOverlapDifficulty = Math.Max(0, screenOverlapDifficulty - 0.5); // make overlap value =1 cost significantly less - difficulty *= getConstantAngleNerfFactor(currObj); - difficulty *= 1 + overlap_multiplier * screenOverlapDifficulty; + double overlapBonus = overlap_multiplier * screenOverlapDifficulty * difficulty; + + difficulty *= getConstantAngleNerfFactor(currObj); + difficulty += overlapBonus; + + //difficulty *= 1 + overlap_multiplier * screenOverlapDifficulty; return difficulty; } @@ -103,12 +107,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double hdDifficulty = 0; double timeSpentInvisible = getDurationSpentInvisible(currObj) / currObj.ClockRate; - double timeDifficultyFactor = calculateDenstityOf(currObj) / 1000; + + 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) + - (8 + visibleObjectFactor) * aimDifficulty; + (6 + visibleObjectFactor) * aimDifficulty; hdDifficulty *= 0.95 + 0.15 * EvaluateInpredictabilityOf(current); // Max multiplier is 1.1 @@ -175,13 +183,28 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators lastDelta = Math.Max(0, lastDelta); } - rhythmChangeBonus = 1 - Math.Min(currDelta, lastDelta) / Math.Max(currDelta, lastDelta); + rhythmChangeBonus = getRhythmDifference(currDelta, lastDelta); } double result = velocity_change_part * velocityChangeBonus + angle_change_part * angleChangeBonus + rhythm_change_part * rhythmChangeBonus; return result; } + 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; + } + // 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 retrievePastVisibleObjects(OsuDifficultyHitObject current) @@ -237,6 +260,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators int index = 0; double currentTimeGap = 0; + OsuDifficultyHitObject prevLoopObj = current; + + OsuDifficultyHitObject? prevLoopObj1 = null; + OsuDifficultyHitObject? prevLoopObj2 = null; + + double prevConstantAngle = 0; + while (currentTimeGap < time_limit) { var loopObj = (OsuDifficultyHitObject)current.Previous(index); @@ -246,18 +276,52 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double longIntervalFactor = Math.Clamp(1 - (loopObj.StrainTime - time_limit_low) / (time_limit - time_limit_low), 0, 1); - if (loopObj.Angle.IsNotNull() && current.Angle.IsNotNull()) + if (loopObj.Angle.IsNotNull() && prevLoopObj.Angle.IsNotNull()) { - double angleDifference = Math.Abs(current.Angle.Value - loopObj.Angle.Value); - constantAngleCount += Math.Cos(4 * Math.Min(Math.PI / 8, angleDifference)) * longIntervalFactor; + double angleDifference = Math.Abs(prevLoopObj.Angle.Value - loopObj.Angle.Value); + + // Nerf alternating angles case + if (prevLoopObj1.IsNotNull() && prevLoopObj2.IsNotNull() && prevLoopObj1.Angle.IsNotNull() && prevLoopObj2.Angle.IsNotNull()) + { + // Normalized difference + double angleDifference1 = Math.Abs(prevLoopObj1.Angle.Value - loopObj.Angle.Value) / Math.PI; + double angleDifference2 = Math.Abs(prevLoopObj2.Angle.Value - prevLoopObj.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, prevLoopObj.StrainTime); // 0 on different rhythm, 1 on same rhythm + rhythmFactor *= 1 - getRhythmDifference(prevLoopObj.StrainTime, prevLoopObj1.StrainTime); + rhythmFactor *= 1 - getRhythmDifference(prevLoopObj1.StrainTime, prevLoopObj2.StrainTime); + + double acuteAngleFactor = 1 - Math.Min(loopObj.Angle.Value, prevLoopObj.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; + } + + double currConstantAngle = Math.Cos(4 * Math.Min(Math.PI / 8, angleDifference)) * longIntervalFactor; + constantAngleCount += Math.Min(currConstantAngle, prevConstantAngle); + prevConstantAngle = currConstantAngle; } currentTimeGap = current.StartTime - loopObj.StartTime; index++; + + prevLoopObj2 = prevLoopObj1; + prevLoopObj1 = prevLoopObj; + prevLoopObj = loopObj; } return Math.Pow(Math.Min(1, 2 / constantAngleCount), 2); } + private static double getTimeNerfFactor(double deltaTime) { return Math.Clamp(2 - deltaTime / (reading_window_size / 2), 0, 1); @@ -270,6 +334,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators 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 diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 84f05940c8..caf9588b94 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -22,10 +22,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public class OsuDifficultyCalculator : DifficultyCalculator { - private const double difficulty_multiplier = 0.0675; + private const double difficulty_multiplier = 0.067; 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) @@ -75,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty 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 baseFlashlightARPerformance = Math.Pow(Math.Pow(baseFlashlightPerformance, 1.8) + Math.Pow(baseReadingARPerformance, 1.8), 1.0 / 1.8); + double baseFlashlightARPerformance = Math.Pow(Math.Pow(baseFlashlightPerformance, FLSumPower) + Math.Pow(baseReadingARPerformance, FLSumPower), 1.0 / FLSumPower); double baseReadingHiddenPerformance = 0; if (mods.Any(h => h is OsuModHidden)) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 7f686fd8dd..979bda168f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -16,8 +16,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. - public static double SumPower => 1.1; // Maybe it should just use OsuDifficultyCalculator SumPower - private double accuracy; private int scoreMaxCombo; private int countGreat; @@ -64,9 +62,11 @@ 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, SumPower) + Math.Pow(speedValue, SumPower), 1.0 / SumPower); + double mechanicalValue = Math.Pow(Math.Pow(aimValue, power) + Math.Pow(speedValue, power), 1.0 / power); // Cognition @@ -77,19 +77,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty flashlightValue = 0.0; double readingARValue = computeReadingARValue(score, osuAttributes); // Reduce AR reading bonus if FL is present - double flashlightARValue = Math.Pow(Math.Pow(flashlightValue, 1.8) + Math.Pow(readingARValue, 1.8), 1.0 / 1.8); + double flPower = OsuDifficultyCalculator.FLSumPower; + double flashlightARValue = Math.Pow(Math.Pow(flashlightValue, flPower) + Math.Pow(readingARValue, flPower), 1.0 / flPower); double readingNonARValue = computeReadingNonARValue(score, osuAttributes); - double cognitionValue = Math.Pow(Math.Pow(flashlightARValue, SumPower) + Math.Pow(readingNonARValue, SumPower), 1.0 / SumPower); + double cognitionValue = Math.Pow(Math.Pow(flashlightARValue, power) + Math.Pow(readingNonARValue, power), 1.0 / power); cognitionValue = AdjustCognitionPerformance(cognitionValue, mechanicalValue, potentialFlashlightValue); double accuracyValue = computeAccuracyValue(score, osuAttributes); double totalValue = Math.Pow( - Math.Pow(mechanicalValue, SumPower) + - Math.Pow(cognitionValue, SumPower) + - Math.Pow(accuracyValue, SumPower), 1.0 / SumPower + Math.Pow(mechanicalValue, power) + + Math.Pow(cognitionValue, power) + + Math.Pow(accuracyValue, power), 1.0 / power ) * multiplier; return new OsuPerformanceAttributes @@ -247,9 +248,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty 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), SumPower) + - Math.Pow(computeReadingHighARValue(score, attributes), SumPower), 1.0 / SumPower); + Math.Pow(computeReadingLowARValue(score, attributes), power) + + Math.Pow(computeReadingHighARValue(score, attributes), power), 1.0 / power); return readingValue; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs index 45b551bfbd..13fc1ef8bb 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Reading.cs @@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills public class ReadingLowAR : GraphSkill { private readonly List difficulties = new List(); - //private double skillMultiplier => 5.5; - private double skillMultiplier => 2.3; + //private double skillMultiplier => 2.3; + private double skillMultiplier => 2; public ReadingLowAR(Mod[] mods) : base(mods) @@ -132,7 +132,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; // private double currentRhythm; - private double skillMultiplier => 13; + //private double skillMultiplier => 13; + private double skillMultiplier => 14; private double strainDecayBase => 0.15; private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); @@ -145,18 +146,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills double aimDifficulty = AimEvaluator.EvaluateDifficultyOf(current, true, ((OsuDifficultyHitObject)current).Preempt); aimDifficulty *= ReadingEvaluator.EvaluateHighARDifficultyOf(current, true); - currentStrain += aimDifficulty * skillMultiplier; - - // currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current); + aimDifficulty *= skillMultiplier; double totalStrain = currentStrain; + currentStrain += aimDifficulty; + + // 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 => 650; + private double skillMultiplier => 675; private double strainDecayBase => 0.3; private double currentStrain; @@ -197,7 +204,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills } private readonly List difficulties = new List(); - private double skillMultiplier => 2.1; + private double skillMultiplier => 2.3; public override void Process(DifficultyHitObject current) {