diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 1312f45cdc..c300afa79f 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Mods; @@ -36,6 +37,24 @@ namespace osu.Game.Rulesets.Catch.Mods [SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")] public BindableBool HardRockOffsets { get; } = new BindableBool(); + public override string ExtendedIconInformation + { + get + { + if (UserAdjustedSettingsCount != 1) + return string.Empty; + + if (!CircleSize.IsDefault) return format("CS", CircleSize); + if (!ApproachRate.IsDefault) return format("AR", ApproachRate); + if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty); + if (!DrainRate.IsDefault) return format("HP", DrainRate); + + return string.Empty; + + string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}"; + } + } + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription { get diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 77e9aeb123..1d94ac6335 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -7,6 +7,7 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; @@ -36,6 +37,24 @@ namespace osu.Game.Rulesets.Osu.Mods ReadCurrentFromDifficulty = diff => diff.ApproachRate, }; + public override string ExtendedIconInformation + { + get + { + if (UserAdjustedSettingsCount != 1) + return string.Empty; + + if (!CircleSize.IsDefault) return format("CS", CircleSize); + if (!ApproachRate.IsDefault) return format("AR", ApproachRate); + if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty); + if (!DrainRate.IsDefault) return format("HP", DrainRate); + + return string.Empty; + + string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}"; + } + } + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription { get diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs index 000736e9f7..57b57555c2 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Taiko.Mods @@ -20,6 +21,23 @@ namespace osu.Game.Rulesets.Taiko.Mods ReadCurrentFromDifficulty = _ => 1, }; + public override string ExtendedIconInformation + { + get + { + if (UserAdjustedSettingsCount != 1) + return string.Empty; + + if (!ScrollSpeed.IsDefault) return format("SC", ScrollSpeed); + if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty); + if (!DrainRate.IsDefault) return format("HP", DrainRate); + + return string.Empty; + + string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}"; + } + } + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription { get diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs index 11cd122c99..c8283d0956 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs @@ -12,22 +12,81 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play.HUD; namespace osu.Game.Tests.Visual.UserInterface { public partial class TestSceneModIcon : OsuTestScene { + private FillFlowContainer spreadOutFlow = null!; + private ModDisplay modDisplay = null!; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create flows", () => + { + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 0.5f), + new Dimension(GridSizeMode.Relative, 0.5f), + }, + Content = new[] + { + new Drawable[] + { + modDisplay = new ModDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }, + new Drawable[] + { + spreadOutFlow = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + } + } + } + }; + }); + } + + private void addRange(IEnumerable mods) + { + spreadOutFlow.AddRange(mods.Select(m => new ModIcon(m))); + modDisplay.Current.Value = modDisplay.Current.Value.Concat(mods.OfType()).ToList(); + } + [Test] public void TestShowAllMods() { AddStep("create mod icons", () => { - Child = new FillFlowContainer + addRange(Ruleset.Value.CreateInstance().CreateAllMods().Select(m => { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Full, - ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods().Select(m => new ModIcon(m)), - }; + if (m is OsuModFlashlight fl) + fl.FollowDelay.Value = 1245; + + if (m is OsuModDaycore dc) + dc.SpeedChange.Value = 0.74f; + + if (m is OsuModDifficultyAdjust da) + da.CircleSize.Value = 8.2f; + + if (m is ModAdaptiveSpeed ad) + ad.AdjustPitch.Value = false; + + return m; + })); }); AddStep("toggle selected", () => @@ -42,26 +101,22 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep("create mod icons", () => { - Child = new FillFlowContainer + var rateAdjustMods = Ruleset.Value.CreateInstance().CreateAllMods() + .OfType(); + + addRange(rateAdjustMods.SelectMany(m => { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Full, - ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods() - .OfType() - .SelectMany(m => - { - List icons = new List { new ModIcon(m) }; + List mods = new List { m }; - for (double i = m.SpeedChange.MinValue; i < m.SpeedChange.MaxValue; i += m.SpeedChange.Precision * 10) - { - m = (ModRateAdjust)m.DeepClone(); - m.SpeedChange.Value = i; - icons.Add(new ModIcon(m)); - } + for (double i = m.SpeedChange.MinValue; i < m.SpeedChange.MaxValue; i += m.SpeedChange.Precision * 10) + { + m = (ModRateAdjust)m.DeepClone(); + m.SpeedChange.Value = i; + mods.Add(m); + } - return icons; - }), - }; + return mods; + })); }); AddStep("adjust rates", () => @@ -81,21 +136,50 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestChangeModType() { - ModIcon icon = null!; - - AddStep("create mod icon", () => Child = icon = new ModIcon(new OsuModDoubleTime())); - AddStep("change mod", () => icon.Mod = new OsuModEasy()); + AddStep("create mod icon", () => addRange([new OsuModDoubleTime()])); + AddStep("change mod", () => + { + foreach (var modIcon in this.ChildrenOfType()) + modIcon.Mod = new OsuModEasy(); + }); } [Test] public void TestInterfaceModType() { - ModIcon icon = null!; - var ruleset = new OsuRuleset(); - AddStep("create mod icon", () => Child = icon = new ModIcon(ruleset.AllMods.First(m => m.Acronym == "DT"))); - AddStep("change mod", () => icon.Mod = ruleset.AllMods.First(m => m.Acronym == "EZ")); + AddStep("create mod icon", () => addRange([ruleset.AllMods.First(m => m.Acronym == "DT")])); + AddStep("change mod", () => + { + foreach (var modIcon in this.ChildrenOfType()) + modIcon.Mod = ruleset.AllMods.First(m => m.Acronym == "EZ"); + }); + } + + [Test] + public void TestDifficultyAdjust() + { + AddStep("create icons", () => + { + addRange([ + new OsuModDifficultyAdjust + { + CircleSize = { Value = 8 } + }, + new OsuModDifficultyAdjust + { + CircleSize = { Value = 5.5f } + }, + new OsuModDifficultyAdjust + { + CircleSize = { Value = 8 }, + ApproachRate = { Value = 8 }, + OverallDifficulty = { Value = 8 }, + DrainRate = { Value = 8 }, + } + ]); + }); } } } diff --git a/osu.Game/Extensions/NumberFormattingExtensions.cs b/osu.Game/Extensions/NumberFormattingExtensions.cs index 618b086a5b..33252448fc 100644 --- a/osu.Game/Extensions/NumberFormattingExtensions.cs +++ b/osu.Game/Extensions/NumberFormattingExtensions.cs @@ -17,7 +17,7 @@ namespace osu.Game.Extensions /// The maximum number of decimals to be considered in the original value. /// Whether the output should be a percentage. For integer types, 0-100 is mapped to 0-100%; for other types 0-1 is mapped to 0-100%. /// The formatted output. - public static string ToStandardFormattedString(this T value, int maxDecimalDigits, bool asPercentage) where T : struct, INumber, IMinMaxValue + public static string ToStandardFormattedString(this T value, int maxDecimalDigits, bool asPercentage = false) where T : struct, INumber, IMinMaxValue { double floatValue = double.CreateTruncating(value); diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index 5d4cc5fd12..08e64c4aa9 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Configuration; namespace osu.Game.Rulesets.Mods { @@ -81,5 +83,33 @@ namespace osu.Game.Rulesets.Mods /// Create a fresh instance based on this mod. /// Mod CreateInstance() => (Mod)Activator.CreateInstance(GetType())!; + + /// + /// Whether any user adjustable setting attached to this mod has a non-default value. + /// + /// + /// This returns the instantaneous state of this mod. It may change over time. + /// For tracking changes on a dynamic display, make sure to setup a . + /// + bool HasNonDefaultSettings + { + get + { + bool hasAdjustments = false; + + foreach (var (_, property) in this.GetSettingsSourceProperties()) + { + var bindable = (IBindable)property.GetValue(this)!; + + if (!bindable.IsDefault) + { + hasAdjustments = true; + break; + } + } + + return hasAdjustments; + } + } } } diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 79fc918487..15ce583413 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Extensions; namespace osu.Game.Rulesets.Mods { @@ -67,6 +68,22 @@ namespace osu.Game.Rulesets.Mods } } + public override string ExtendedIconInformation + { + get + { + if (UserAdjustedSettingsCount != 1) + return string.Empty; + + if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty); + if (!DrainRate.IsDefault) return format("HP", DrainRate); + + return string.Empty; + + string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}"; + } + } + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription { get @@ -94,5 +111,26 @@ namespace osu.Game.Rulesets.Mods if (DrainRate.Value != null) difficulty.DrainRate = DrainRate.Value.Value; if (OverallDifficulty.Value != null) difficulty.OverallDifficulty = OverallDifficulty.Value.Value; } + + /// + /// The number of settings on this mod instance which have been adjusted by the user from their default values. + /// + protected int UserAdjustedSettingsCount + { + get + { + int count = 0; + + foreach (var (_, property) in this.GetSettingsSourceProperties()) + { + var bindable = (IBindable)property.GetValue(this)!; + + if (!bindable.IsDefault) + count++; + } + + return count; + } + } } } diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index ee0103a8e5..d3f04e7e74 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; @@ -81,6 +82,11 @@ namespace osu.Game.Rulesets.UI private Container extendedContent = null!; + private Drawable adjustmentMarker = null!; + + private Circle cogBackground = null!; + private SpriteIcon cog = null!; + private ModSettingChangeTracker? modSettingsChangeTracker; /// @@ -139,7 +145,7 @@ namespace osu.Game.Rulesets.UI Origin = Anchor.CentreLeft, Name = "main content", Size = MOD_ICON_SIZE, - Children = new Drawable[] + Children = new[] { background = new Sprite { @@ -165,6 +171,29 @@ namespace osu.Game.Rulesets.UI Size = new Vector2(45), Icon = FontAwesome.Solid.Question }, + adjustmentMarker = new Container + { + Size = new Vector2(20), + Origin = Anchor.Centre, + Position = new Vector2(64, 14), + Children = new Drawable[] + { + cogBackground = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }, + cog = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.Cog, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.6f), + } + } + }, } }, }; @@ -216,11 +245,18 @@ namespace osu.Game.Rulesets.UI extendedContent.Alpha = showExtended ? 1 : 0; extendedText.Text = mod.ExtendedIconInformation; + + if (mod.HasNonDefaultSettings) + adjustmentMarker.Show(); + else + adjustmentMarker.Hide(); } private void updateColour() { modAcronym.Colour = modIcon.Colour = Interpolation.ValueAt(0.1f, Colour4.Black, backgroundColour, 0, 1); + cogBackground.Colour = Interpolation.ValueAt(0.1f, Colour4.Black, backgroundColour, 0, 1); + cog.Colour = backgroundColour; extendedText.Colour = background.Colour = Selected.Value ? backgroundColour.Lighten(0.2f) : backgroundColour; extendedBackground.Colour = Selected.Value ? backgroundColour.Darken(2.4f) : backgroundColour.Darken(2.8f);