Skip to content

Commit 131f828

Browse files
committed
Attempt to properly quantify the impact of mania Hard Rock / Easy mod application on overall difficulty
In stable mania, Hard Rock and Easy mods do not work the same way as they do on all of the rulesets. The difference is that mania HR and EZ, rather than apply a multiplier to the map's original Overall Difficulty, apply multipliers to *the durations of hit windows themselves*. Prior to the last release, lazer was oblivious to this reality and just treated mania HR / EZ as it did every other ruleset. Last release, for the sake for gameplay parity across rulesets, the mods in question were adjusted to match stable, but in the process, it started looking like HR / EZ did not change OD anymore. The problem is that they do, but applying a multiplier to the map's OD and applying a multiplier to the hit window duration is not the same thing. The second thing is actually *much harsher* in magnitude, to the point where applying HR to any map is almost guaranteed to exceed "the effective OD" of 10, and applying EZ to any map is almost guaranteed to result in "negative effective OD". This change attempts to convey that reality by displaying "effective OD", similar to what's already done in other rulesets when rate-changing mods are active. Note that the values this will display *do not match* stable *and that is correct*, because stable song select *lies* about the actual impact on OD by just assuming it can treat all rulesets in the same way. --- Would close #34150 I guess. And yes I would like *all of the above* to land on the changelog if possible if this is merged. For further convincing that this makes any semblance of sense please see the following: https://www.desmos.com/calculator/yigt7jycdv
1 parent 0d1788e commit 131f828

File tree

16 files changed

+68
-41
lines changed

16 files changed

+68
-41
lines changed

osu.Game.Rulesets.Catch.Tests/CatchRateAdjustedDisplayDifficultyTest.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections.Generic;
55
using NUnit.Framework;
66
using osu.Game.Beatmaps;
7+
using osu.Game.Rulesets.Catch.Mods;
78

89
namespace osu.Game.Rulesets.Catch.Tests
910
{
@@ -22,7 +23,7 @@ public void TestApproachRateIsUnchangedWithRateEqualToOne(float originalApproach
2223
var ruleset = new CatchRuleset();
2324
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
2425

25-
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
26+
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
2627

2728
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
2829
}
@@ -33,7 +34,7 @@ public void TestRateBelowOne()
3334
var ruleset = new CatchRuleset();
3435
var difficulty = new BeatmapDifficulty();
3536

36-
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
37+
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new CatchModHalfTime()]);
3738

3839
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
3940
}
@@ -44,7 +45,7 @@ public void TestRateAboveOne()
4445
var ruleset = new CatchRuleset();
4546
var difficulty = new BeatmapDifficulty();
4647

47-
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
48+
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new CatchModDoubleTime()]);
4849

4950
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
5051
}

osu.Game.Rulesets.Catch/CatchRuleset.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
using osu.Game.Screens.Edit.Setup;
3434
using osu.Game.Screens.Ranking.Statistics;
3535
using osu.Game.Skinning;
36+
using osu.Game.Utils;
3637
using osuTK;
3738

3839
namespace osu.Game.Rulesets.Catch
@@ -265,9 +266,10 @@ public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatm
265266
}
266267

267268
/// <seealso cref="CatchHitObject.ApplyDefaultsToSelf"/>
268-
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
269+
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection<Mod> mods)
269270
{
270271
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
272+
double rate = ModUtils.CalculateRateWithMods(mods);
271273

272274
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
273275
preempt /= rate;

osu.Game.Rulesets.Mania/ManiaRuleset.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,32 @@ public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatm
414414
}), true)
415415
};
416416

417+
/// <seealso cref="ManiaHitWindows"/>
418+
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection<Mod> mods)
419+
{
420+
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
421+
422+
// notably, in mania, hit windows are designed to be independent of track playback rate (see `ManiaHitWindows.SpeedMultiplier`).
423+
// *however*, to not make matters *too* simple, mania Hard Rock and Easy differ from all other rulesets
424+
// in that they apply multipliers *to hit window durations directly* rather than to the Overall Difficulty attribute itself.
425+
// because the duration of hit window durations as a function of OD is not a linear function,
426+
// this means that multiplying the OD is *not* the same thing as multiplying the hit window duration.
427+
// in fact, the second operation is *much* harsher and will produce values much farther outside of normal operating range
428+
// (even negative in the case of Easy).
429+
// stable handles this wrong on song select and just assumes that it can handle mania EZ / HR the same way as all other rulesets.
430+
431+
double perfectHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, ManiaHitWindows.PERFECT_WINDOW_RANGE);
432+
433+
if (mods.Any(m => m is ManiaModHardRock))
434+
perfectHitWindow /= ManiaModHardRock.HIT_WINDOW_DIFFICULTY_MULTIPLIER;
435+
else if (mods.Any(m => m is ManiaModEasy))
436+
perfectHitWindow /= ManiaModEasy.HIT_WINDOW_DIFFICULTY_MULTIPLIER;
437+
438+
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(perfectHitWindow, ManiaHitWindows.PERFECT_WINDOW_RANGE);
439+
440+
return adjustedDifficulty;
441+
}
442+
417443
public override IRulesetFilterCriteria CreateRulesetFilterCriteria()
418444
{
419445
return new ManiaFilterCriteria();

osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,19 @@ public class ManiaModEasy : ModEasyWithExtraLives, IApplicableToHitObject
1313
{
1414
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and extra lives!";
1515

16+
public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1 / 1.4;
17+
1618
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
1719
{
18-
const double multiplier = 1 / 1.4;
19-
2020
switch (hitObject)
2121
{
2222
case Note:
23-
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = multiplier;
23+
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
2424
break;
2525

2626
case HoldNote hold:
27-
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = multiplier;
28-
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = multiplier;
27+
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
28+
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
2929
break;
3030
}
3131
}

osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,19 @@ public class ManiaModHardRock : ModHardRock, IApplicableToHitObject
1313
public override double ScoreMultiplier => 1;
1414
public override bool Ranked => false;
1515

16+
public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1.4;
17+
1618
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
1719
{
18-
const double multiplier = 1.4;
19-
2020
switch (hitObject)
2121
{
2222
case Note:
23-
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = multiplier;
23+
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
2424
break;
2525

2626
case HoldNote hold:
27-
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = multiplier;
28-
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = multiplier;
27+
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
28+
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
2929
break;
3030
}
3131
}

osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
99
{
1010
public class ManiaHitWindows : HitWindows
1111
{
12-
private static readonly DifficultyRange perfect_window_range = new DifficultyRange(22.4D, 19.4D, 13.9D);
12+
public static readonly DifficultyRange PERFECT_WINDOW_RANGE = new DifficultyRange(22.4D, 19.4D, 13.9D);
1313
private static readonly DifficultyRange great_window_range = new DifficultyRange(64, 49, 34);
1414
private static readonly DifficultyRange good_window_range = new DifficultyRange(97, 82, 67);
1515
private static readonly DifficultyRange ok_window_range = new DifficultyRange(127, 112, 97);
@@ -151,7 +151,7 @@ private void updateWindows()
151151
}
152152
else
153153
{
154-
perfect = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, perfect_window_range) * totalMultiplier) + 0.5;
154+
perfect = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, PERFECT_WINDOW_RANGE) * totalMultiplier) + 0.5;
155155
great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, great_window_range) * totalMultiplier) + 0.5;
156156
good = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, good_window_range) * totalMultiplier) + 0.5;
157157
ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, ok_window_range) * totalMultiplier) + 0.5;

osu.Game.Rulesets.Osu.Tests/OsuRateAdjustedDisplayDifficultyTest.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections.Generic;
55
using NUnit.Framework;
66
using osu.Game.Beatmaps;
7+
using osu.Game.Rulesets.Osu.Mods;
78

89
namespace osu.Game.Rulesets.Osu.Tests
910
{
@@ -22,7 +23,7 @@ public void TestApproachRateIsUnchangedWithRateEqualToOne(float originalApproach
2223
var ruleset = new OsuRuleset();
2324
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
2425

25-
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
26+
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
2627

2728
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
2829
}
@@ -33,7 +34,7 @@ public void TestOverallDifficultyIsUnchangedWithRateEqualToOne(float originalOve
3334
var ruleset = new OsuRuleset();
3435
var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };
3536

36-
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
37+
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
3738

3839
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
3940
}
@@ -44,7 +45,7 @@ public void TestRateBelowOne()
4445
var ruleset = new OsuRuleset();
4546
var difficulty = new BeatmapDifficulty();
4647

47-
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
48+
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new OsuModHalfTime()]);
4849

4950
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
5051
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(2.22).Within(0.01));
@@ -56,7 +57,7 @@ public void TestRateAboveOne()
5657
var ruleset = new OsuRuleset();
5758
var difficulty = new BeatmapDifficulty();
5859

59-
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
60+
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new OsuModDoubleTime()]);
6061

6162
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
6263
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(7.77).Within(0.01));

osu.Game.Rulesets.Osu/OsuRuleset.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
using osu.Game.Screens.Edit.Setup;
4141
using osu.Game.Screens.Ranking.Statistics;
4242
using osu.Game.Skinning;
43+
using osu.Game.Utils;
4344
using osuTK;
4445

4546
namespace osu.Game.Rulesets.Osu
@@ -365,9 +366,10 @@ public override IEnumerable<Drawable> CreateEditorSetupSections() =>
365366

366367
/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>
367368
/// <seealso cref="OsuHitWindows"/>
368-
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
369+
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection<Mod> mods)
369370
{
370371
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
372+
double rate = ModUtils.CalculateRateWithMods(mods);
371373

372374
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
373375
preempt /= rate;

osu.Game.Rulesets.Taiko.Tests/TaikoRateAdjustedDisplayDifficultyTest.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections.Generic;
55
using NUnit.Framework;
66
using osu.Game.Beatmaps;
7+
using osu.Game.Rulesets.Taiko.Mods;
78

89
namespace osu.Game.Rulesets.Taiko.Tests
910
{
@@ -22,7 +23,7 @@ public void TestOverallDifficultyIsUnchangedWithRateEqualToOne(float originalOve
2223
var ruleset = new TaikoRuleset();
2324
var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };
2425

25-
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
26+
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
2627

2728
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
2829
}
@@ -33,7 +34,7 @@ public void TestRateBelowOne()
3334
var ruleset = new TaikoRuleset();
3435
var difficulty = new BeatmapDifficulty();
3536

36-
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
37+
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new TaikoModHalfTime()]);
3738

3839
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(1.11).Within(0.01));
3940
}
@@ -44,7 +45,7 @@ public void TestRateAboveOne()
4445
var ruleset = new TaikoRuleset();
4546
var difficulty = new BeatmapDifficulty();
4647

47-
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
48+
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new TaikoModDoubleTime()]);
4849

4950
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(8.89).Within(0.01));
5051
}

osu.Game.Rulesets.Taiko/TaikoRuleset.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
using osu.Game.Rulesets.Taiko.Edit.Setup;
3939
using osu.Game.Rulesets.Taiko.Skinning.Default;
4040
using osu.Game.Screens.Edit.Setup;
41+
using osu.Game.Utils;
4142

4243
namespace osu.Game.Rulesets.Taiko
4344
{
@@ -270,9 +271,10 @@ public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatm
270271
}
271272

272273
/// <seealso cref="TaikoHitWindows"/>
273-
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
274+
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection<Mod> mods)
274275
{
275276
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
277+
double rate = ModUtils.CalculateRateWithMods(mods);
276278

277279
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, TaikoHitWindows.GREAT_WINDOW_RANGE);
278280
greatHitWindow /= rate;

0 commit comments

Comments
 (0)