mirror of
https://github.com/ppy/osu.git
synced 2025-03-16 00:37:19 +08:00
Merge pull request #25762 from bdach/if-i-speak-i-am-in-big-trouble-pt-2
Fix `GetRateAdjustedDisplayDifficulty()` (partially incorrectly) locally reimplementing difficulty range calculations
This commit is contained in:
commit
23fbc75bee
@ -0,0 +1,52 @@
|
||||
// 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.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class CatchRateAdjustedDisplayDifficultyTest
|
||||
{
|
||||
private static IEnumerable<float> difficultyValuesToTest()
|
||||
{
|
||||
for (float i = 0; i <= 10; i += 0.5f)
|
||||
yield return i;
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(difficultyValuesToTest))]
|
||||
public void TestApproachRateIsUnchangedWithRateEqualToOne(float originalApproachRate)
|
||||
{
|
||||
var ruleset = new CatchRuleset();
|
||||
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRateBelowOne()
|
||||
{
|
||||
var ruleset = new CatchRuleset();
|
||||
var difficulty = new BeatmapDifficulty();
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRateAboveOne()
|
||||
{
|
||||
var ruleset = new CatchRuleset();
|
||||
var difficulty = new BeatmapDifficulty();
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Difficulty;
|
||||
using osu.Game.Rulesets.Catch.Edit;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
using osu.Game.Rulesets.Catch.Scoring;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Argon;
|
||||
@ -236,17 +237,14 @@ namespace osu.Game.Rulesets.Catch
|
||||
};
|
||||
}
|
||||
|
||||
/// <seealso cref="CatchHitObject.ApplyDefaultsToSelf"/>
|
||||
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
|
||||
{
|
||||
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
|
||||
|
||||
double preempt = adjustedDifficulty.ApproachRate < 6
|
||||
? 1200.0 + 600.0 * (5 - adjustedDifficulty.ApproachRate) / 5
|
||||
: 1200.0 - 750.0 * (adjustedDifficulty.ApproachRate - 5) / 5;
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
|
||||
preempt /= rate;
|
||||
|
||||
adjustedDifficulty.ApproachRate = (float)(preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5);
|
||||
adjustedDifficulty.ApproachRate = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
|
||||
|
||||
return adjustedDifficulty;
|
||||
}
|
||||
|
@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
|
||||
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN);
|
||||
|
||||
Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize);
|
||||
}
|
||||
@ -189,6 +189,21 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
// The half of the height of the osu! playfield.
|
||||
public const float DEFAULT_LEGACY_CONVERT_Y = 192;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum preempt time at AR=10.
|
||||
/// </summary>
|
||||
public const double PREEMPT_MIN = 450;
|
||||
|
||||
/// <summary>
|
||||
/// Median preempt time at AR=5.
|
||||
/// </summary>
|
||||
public const double PREEMPT_MID = 1200;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum preempt time at AR=0.
|
||||
/// </summary>
|
||||
public const double PREEMPT_MAX = 1800;
|
||||
|
||||
/// <summary>
|
||||
/// The Y position of the hit object is not used in the normal osu!catch gameplay.
|
||||
/// It is preserved to maximize the backward compatibility with the legacy editor, in which the mappers use the Y position to organize the patterns.
|
||||
|
@ -0,0 +1,65 @@
|
||||
// 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.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class OsuRateAdjustedDisplayDifficultyTest
|
||||
{
|
||||
private static IEnumerable<float> difficultyValuesToTest()
|
||||
{
|
||||
for (float i = 0; i <= 10; i += 0.5f)
|
||||
yield return i;
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(difficultyValuesToTest))]
|
||||
public void TestApproachRateIsUnchangedWithRateEqualToOne(float originalApproachRate)
|
||||
{
|
||||
var ruleset = new OsuRuleset();
|
||||
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(difficultyValuesToTest))]
|
||||
public void TestOverallDifficultyIsUnchangedWithRateEqualToOne(float originalOverallDifficulty)
|
||||
{
|
||||
var ruleset = new OsuRuleset();
|
||||
var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
|
||||
|
||||
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRateBelowOne()
|
||||
{
|
||||
var ruleset = new OsuRuleset();
|
||||
var difficulty = new BeatmapDifficulty();
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
|
||||
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(2.22).Within(0.01));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRateAboveOne()
|
||||
{
|
||||
var ruleset = new OsuRuleset();
|
||||
var difficulty = new BeatmapDifficulty();
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
|
||||
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(7.77).Within(0.01));
|
||||
}
|
||||
}
|
||||
}
|
@ -37,6 +37,16 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// </summary>
|
||||
public const double PREEMPT_MIN = 450;
|
||||
|
||||
/// <summary>
|
||||
/// Median preempt time at AR=5.
|
||||
/// </summary>
|
||||
public const double PREEMPT_MID = 1200;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum preempt time at AR=0.
|
||||
/// </summary>
|
||||
public const double PREEMPT_MAX = 1800;
|
||||
|
||||
public double TimePreempt = 600;
|
||||
public double TimeFadeIn = 400;
|
||||
|
||||
@ -148,7 +158,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN);
|
||||
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN);
|
||||
|
||||
// Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR.
|
||||
// This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above.
|
||||
|
@ -332,23 +332,20 @@ namespace osu.Game.Rulesets.Osu
|
||||
|
||||
public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection();
|
||||
|
||||
/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>
|
||||
/// <seealso cref="OsuHitWindows"/>
|
||||
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
|
||||
{
|
||||
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
|
||||
|
||||
double preempt = adjustedDifficulty.ApproachRate < 5
|
||||
? 1200.0 + 600.0 * (5 - adjustedDifficulty.ApproachRate) / 5
|
||||
: 1200.0 - 750.0 * (adjustedDifficulty.ApproachRate - 5) / 5;
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
|
||||
preempt /= rate;
|
||||
adjustedDifficulty.ApproachRate = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
|
||||
|
||||
adjustedDifficulty.ApproachRate = (float)(preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5);
|
||||
|
||||
double hitwindow = 80.0 - 6 * adjustedDifficulty.OverallDifficulty;
|
||||
|
||||
hitwindow /= rate;
|
||||
|
||||
adjustedDifficulty.OverallDifficulty = (float)(80.0 - hitwindow) / 6;
|
||||
var greatHitWindowRange = OsuHitWindows.OSU_RANGES.Single(range => range.Result == HitResult.Great);
|
||||
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
|
||||
greatHitWindow /= rate;
|
||||
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
|
||||
|
||||
return adjustedDifficulty;
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
||||
/// </summary>
|
||||
public const double MISS_WINDOW = 400;
|
||||
|
||||
private static readonly DifficultyRange[] osu_ranges =
|
||||
internal static readonly DifficultyRange[] OSU_RANGES =
|
||||
{
|
||||
new DifficultyRange(HitResult.Great, 80, 50, 20),
|
||||
new DifficultyRange(HitResult.Ok, 140, 100, 60),
|
||||
@ -34,6 +34,6 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override DifficultyRange[] GetRanges() => osu_ranges;
|
||||
protected override DifficultyRange[] GetRanges() => OSU_RANGES;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,52 @@
|
||||
// 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.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TaikoRateAdjustedDisplayDifficultyTest
|
||||
{
|
||||
private static IEnumerable<float> difficultyValuesToTest()
|
||||
{
|
||||
for (float i = 0; i <= 10; i += 0.5f)
|
||||
yield return i;
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(difficultyValuesToTest))]
|
||||
public void TestOverallDifficultyIsUnchangedWithRateEqualToOne(float originalOverallDifficulty)
|
||||
{
|
||||
var ruleset = new TaikoRuleset();
|
||||
var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
|
||||
|
||||
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRateBelowOne()
|
||||
{
|
||||
var ruleset = new TaikoRuleset();
|
||||
var difficulty = new BeatmapDifficulty();
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
|
||||
|
||||
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(1.11).Within(0.01));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRateAboveOne()
|
||||
{
|
||||
var ruleset = new TaikoRuleset();
|
||||
var difficulty = new BeatmapDifficulty();
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
|
||||
|
||||
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(8.89).Within(0.01));
|
||||
}
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
{
|
||||
public class TaikoHitWindows : HitWindows
|
||||
{
|
||||
private static readonly DifficultyRange[] taiko_ranges =
|
||||
internal static readonly DifficultyRange[] TAIKO_RANGES =
|
||||
{
|
||||
new DifficultyRange(HitResult.Great, 50, 35, 20),
|
||||
new DifficultyRange(HitResult.Ok, 120, 80, 50),
|
||||
@ -27,6 +27,6 @@ namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override DifficultyRange[] GetRanges() => taiko_ranges;
|
||||
protected override DifficultyRange[] GetRanges() => TAIKO_RANGES;
|
||||
}
|
||||
}
|
||||
|
@ -265,15 +265,15 @@ namespace osu.Game.Rulesets.Taiko
|
||||
};
|
||||
}
|
||||
|
||||
/// <seealso cref="TaikoHitWindows"/>
|
||||
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
|
||||
{
|
||||
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
|
||||
|
||||
double hitWindow = 35.0 - 15.0 * (adjustedDifficulty.OverallDifficulty - 5) / 5;
|
||||
|
||||
hitWindow /= rate;
|
||||
|
||||
adjustedDifficulty.OverallDifficulty = (float)(5 * (35 - hitWindow) / 15 + 5);
|
||||
var greatHitWindowRange = TaikoHitWindows.TAIKO_RANGES.Single(range => range.Result == HitResult.Great);
|
||||
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
|
||||
greatHitWindow /= rate;
|
||||
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
|
||||
|
||||
return adjustedDifficulty;
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
// 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.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
@ -92,5 +94,21 @@ namespace osu.Game.Beatmaps
|
||||
/// <returns>Value to which the difficulty value maps in the specified range.</returns>
|
||||
static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range)
|
||||
=> DifficultyRange(difficulty, range.od0, range.od5, range.od10);
|
||||
|
||||
/// <summary>
|
||||
/// Inverse function to <see cref="DifficultyRange(double,double,double,double)"/>.
|
||||
/// Maps a value returned by the function above back to the difficulty that produced it.
|
||||
/// </summary>
|
||||
/// <param name="difficultyValue">The difficulty-dependent value to be unmapped.</param>
|
||||
/// <param name="diff0">Minimum of the resulting range which will be achieved by a difficulty value of 0.</param>
|
||||
/// <param name="diff5">Midpoint of the resulting range which will be achieved by a difficulty value of 5.</param>
|
||||
/// <param name="diff10">Maximum of the resulting range which will be achieved by a difficulty value of 10.</param>
|
||||
/// <returns>Value to which the difficulty value maps in the specified range.</returns>
|
||||
static double InverseDifficultyRange(double difficultyValue, double diff0, double diff5, double diff10)
|
||||
{
|
||||
return Math.Sign(difficultyValue - diff5) == Math.Sign(diff10 - diff5)
|
||||
? (difficultyValue - diff5) / (diff10 - diff5) * 5 + 5
|
||||
: (difficultyValue - diff5) / (diff5 - diff0) * 5 + 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user