diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index beba29b8bb..8a8c41bb8a 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -43,11 +43,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); - if (Attributes.ScoreMultiplier > 0) - { - // Scale score up, so it's comparable to other keymods - scaledScore *= 1.0 / Attributes.ScoreMultiplier; - } + IEnumerable scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease); + + double scoreMultiplier = 1.0; + foreach (var m in mods.Where(m => !scoreIncreaseMods.Contains(m))) + scoreMultiplier *= m.ScoreMultiplier; + + // Scale score up, so it's comparable to other keymods + scaledScore *= 1.0 / scoreMultiplier; // Arbitrary initial value for scaling pp in order to standardize distributions across game modes. // The specific number has no intrinsic meaning and can be adjusted as needed. @@ -77,9 +80,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty private double computeDifficultyValue() { - if (Attributes.ScoreMultiplier <= 0) - return 0; - double difficultyValue = Math.Pow(5 * Math.Max(1, Attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0; difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 0b9db8e20a..de795241bf 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -147,7 +147,8 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("player score matching expected bonus score", () => { - double totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value; + // multipled by 2 to nullify the score multiplier. (autoplay mod selected) + double totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; }); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs index e9014c0941..0631059d1a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; +using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Select; @@ -12,11 +14,11 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneFooterButtonMods : OsuTestScene { - private readonly FooterButtonMods footerButtonMods; + private readonly TestFooterButtonMods footerButtonMods; public TestSceneFooterButtonMods() { - Add(footerButtonMods = new FooterButtonMods()); + Add(footerButtonMods = new TestFooterButtonMods()); } [Test] @@ -24,15 +26,19 @@ namespace osu.Game.Tests.Visual.UserInterface { var hiddenMod = new Mod[] { new OsuModHidden() }; AddStep(@"Add Hidden", () => changeMods(hiddenMod)); + AddAssert(@"Check Hidden multiplier", () => assertModsMultiplier(hiddenMod)); var hardRockMod = new Mod[] { new OsuModHardRock() }; AddStep(@"Add HardRock", () => changeMods(hardRockMod)); + AddAssert(@"Check HardRock multiplier", () => assertModsMultiplier(hardRockMod)); var doubleTimeMod = new Mod[] { new OsuModDoubleTime() }; AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod)); + AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod)); var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() }; AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods)); + AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleIncrementMods)); } [Test] @@ -40,12 +46,15 @@ namespace osu.Game.Tests.Visual.UserInterface { var easyMod = new Mod[] { new OsuModEasy() }; AddStep(@"Add Easy", () => changeMods(easyMod)); + AddAssert(@"Check Easy multiplier", () => assertModsMultiplier(easyMod)); var noFailMod = new Mod[] { new OsuModNoFail() }; AddStep(@"Add NoFail", () => changeMods(noFailMod)); + AddAssert(@"Check NoFail multiplier", () => assertModsMultiplier(noFailMod)); var multipleDecrementMods = new Mod[] { new OsuModEasy(), new OsuModNoFail() }; AddStep(@"Add Multiple Mods", () => changeMods(multipleDecrementMods)); + AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleDecrementMods)); } [Test] @@ -54,11 +63,25 @@ namespace osu.Game.Tests.Visual.UserInterface var multipleMods = new Mod[] { new OsuModDoubleTime(), new OsuModFlashlight() }; AddStep(@"Add mods", () => changeMods(multipleMods)); AddStep(@"Clear selected mod", () => changeMods(Array.Empty())); + AddAssert(@"Check empty multiplier", () => assertModsMultiplier(Array.Empty())); } private void changeMods(IReadOnlyList mods) { footerButtonMods.Current.Value = mods; } + + private bool assertModsMultiplier(IEnumerable mods) + { + double multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + string expectedValue = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; + + return expectedValue == footerButtonMods.MultiplierText.Current.Value; + } + + private class TestFooterButtonMods : FooterButtonMods + { + public new OsuSpriteText MultiplierText => base.MultiplierText; + } } } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 9a45e2458d..7136795461 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -80,13 +80,10 @@ namespace osu.Game.Rulesets.Mods } /// - /// The (legacy) score multiplier of this mod. + /// The score multiplier of this mod. /// - /// - /// This is not applied for newly set scores, but may be required for display purposes when showing legacy scores. - /// [JsonIgnore] - public virtual double ScoreMultiplier => 1; + public abstract double ScoreMultiplier { get; } /// /// Returns true if this mod is implemented (and playable). diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index ce1486b02f..79861c0ecc 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -92,6 +92,8 @@ namespace osu.Game.Rulesets.Scoring private readonly List hitEvents = new List(); private HitObject lastHitObject; + private double scoreMultiplier = 1; + public ScoreProcessor() { accuracyPortion = DefaultAccuracyPortion; @@ -109,6 +111,15 @@ namespace osu.Game.Rulesets.Scoring }; Mode.ValueChanged += _ => updateScore(); + Mods.ValueChanged += mods => + { + scoreMultiplier = 1; + + foreach (var m in mods.NewValue) + scoreMultiplier *= m.ScoreMultiplier; + + updateScore(); + }; } private readonly Dictionary scoreResultCounts = new Dictionary(); @@ -224,7 +235,7 @@ namespace osu.Game.Rulesets.Scoring case ScoringMode.Standardised: double accuracyScore = accuracyPortion * accuracyRatio; double comboScore = comboPortion * comboRatio; - return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)); + return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)) * scoreMultiplier; case ScoringMode.Classic: // This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring. diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index 1f1aa4c4b3..5bbca5ca1a 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -6,11 +6,14 @@ using osu.Framework.Graphics; using osu.Game.Screens.Play.HUD; using osu.Game.Rulesets.Mods; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osuTK; +using osuTK.Graphics; using osu.Game.Input.Bindings; namespace osu.Game.Screens.Select @@ -23,7 +26,10 @@ namespace osu.Game.Screens.Select set => modDisplay.Current = value; } + protected readonly OsuSpriteText MultiplierText; private readonly ModDisplay modDisplay; + private Color4 lowMultiplierColour; + private Color4 highMultiplierColour; public FooterButtonMods() { @@ -34,6 +40,12 @@ namespace osu.Game.Screens.Select Scale = new Vector2(0.8f), ExpansionMode = ExpansionMode.AlwaysContracted, }); + ButtonContentContainer.Add(MultiplierText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(weight: FontWeight.Bold), + }); } [BackgroundDependencyLoader] @@ -41,6 +53,8 @@ namespace osu.Game.Screens.Select { SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); + lowMultiplierColour = colours.Red; + highMultiplierColour = colours.Green; Text = @"mods"; Hotkey = GlobalAction.ToggleModSelection; } @@ -54,6 +68,17 @@ namespace osu.Game.Screens.Select private void updateMultiplierText() { + double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1; + + MultiplierText.Text = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; + + if (multiplier > 1.0) + MultiplierText.FadeColour(highMultiplierColour, 200); + else if (multiplier < 1.0) + MultiplierText.FadeColour(lowMultiplierColour, 200); + else + MultiplierText.FadeColour(Color4.White, 200); + if (Current.Value?.Count > 0) modDisplay.FadeIn(); else