diff --git a/osu.Game.Tests/Resources/special-skin/selection-mod-autoplay.png b/osu.Game.Tests/Resources/special-skin/selection-mod-autoplay.png new file mode 100644 index 000000000000..ea28d7038584 Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/selection-mod-autoplay.png differ diff --git a/osu.Game.Tests/Resources/special-skin/selection-mod-doubletime.png b/osu.Game.Tests/Resources/special-skin/selection-mod-doubletime.png new file mode 100644 index 000000000000..3bd9e06f6647 Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/selection-mod-doubletime.png differ diff --git a/osu.Game.Tests/Resources/special-skin/selection-mod-easy.png b/osu.Game.Tests/Resources/special-skin/selection-mod-easy.png new file mode 100644 index 000000000000..cb45b1f4887e Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/selection-mod-easy.png differ diff --git a/osu.Game.Tests/Resources/special-skin/selection-mod-flashlight.png b/osu.Game.Tests/Resources/special-skin/selection-mod-flashlight.png new file mode 100644 index 000000000000..62f1aaf598a8 Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/selection-mod-flashlight.png differ diff --git a/osu.Game.Tests/Resources/special-skin/selection-mod-halftime.png b/osu.Game.Tests/Resources/special-skin/selection-mod-halftime.png new file mode 100644 index 000000000000..454ac279108e Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/selection-mod-halftime.png differ diff --git a/osu.Game.Tests/Resources/special-skin/selection-mod-hardrock.png b/osu.Game.Tests/Resources/special-skin/selection-mod-hardrock.png new file mode 100644 index 000000000000..342bc707631b Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/selection-mod-hardrock.png differ diff --git a/osu.Game.Tests/Resources/special-skin/selection-mod-hidden.png b/osu.Game.Tests/Resources/special-skin/selection-mod-hidden.png new file mode 100644 index 000000000000..6a686478841a Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/selection-mod-hidden.png differ diff --git a/osu.Game.Tests/Resources/special-skin/selection-mod-nightcore.png b/osu.Game.Tests/Resources/special-skin/selection-mod-nightcore.png new file mode 100644 index 000000000000..068d9cbc0020 Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/selection-mod-nightcore.png differ diff --git a/osu.Game.Tests/Resources/special-skin/selection-mod-nofail.png b/osu.Game.Tests/Resources/special-skin/selection-mod-nofail.png new file mode 100644 index 000000000000..80aebbb0a63e Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/selection-mod-nofail.png differ diff --git a/osu.Game.Tests/Resources/special-skin/selection-mod-perfect.png b/osu.Game.Tests/Resources/special-skin/selection-mod-perfect.png new file mode 100644 index 000000000000..867c086946f8 Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/selection-mod-perfect.png differ diff --git a/osu.Game.Tests/Resources/special-skin/selection-mod-relax.png b/osu.Game.Tests/Resources/special-skin/selection-mod-relax.png new file mode 100644 index 000000000000..2b7ad3f4d39c Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/selection-mod-relax.png differ diff --git a/osu.Game.Tests/Resources/special-skin/selection-mod-relax2.png b/osu.Game.Tests/Resources/special-skin/selection-mod-relax2.png new file mode 100644 index 000000000000..5949c8ed0b48 Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/selection-mod-relax2.png differ diff --git a/osu.Game.Tests/Resources/special-skin/selection-mod-spunout.png b/osu.Game.Tests/Resources/special-skin/selection-mod-spunout.png new file mode 100644 index 000000000000..fca7c95441cf Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/selection-mod-spunout.png differ diff --git a/osu.Game.Tests/Resources/special-skin/selection-mod-suddendeath.png b/osu.Game.Tests/Resources/special-skin/selection-mod-suddendeath.png new file mode 100644 index 000000000000..fff5ae070943 Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/selection-mod-suddendeath.png differ diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModDisplay.cs index 1bb83eeddfcd..1918b464ef47 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModDisplay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModDisplay.cs @@ -1,38 +1,103 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Play.HUD; +using osu.Game.Tests.Visual.Gameplay; namespace osu.Game.Tests.Visual.UserInterface { - public partial class TestSceneModDisplay : OsuTestScene + public partial class TestSceneModDisplay : SkinnableHUDComponentTestScene { - [Test] - public void TestMode([Values] ExpansionMode mode) + [SetUpSteps] + public override void SetUpSteps() { - AddStep("create mod display", () => + AddStep("setup mods", () => { - Child = new ModDisplay + SelectedMods.Value = new Mod[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - ExpansionMode = mode, - Current = - { - Value = new Mod[] - { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModDifficultyAdjust(), - new OsuModEasy(), - } - } + new OsuModHardRock(), + new OsuModDoubleTime { SpeedChange = { Value = 2.0 } }, + new OsuModDifficultyAdjust(), + new OsuModEasy(), }; }); + + base.SetUpSteps(); + } + + protected override Drawable CreateDefaultImplementation() + { + return new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + Children = new[] + { + new ModDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = SelectedMods, + ExpansionMode = ExpansionMode.AlwaysContracted + }, + new ModDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = SelectedMods, + ExpansionMode = ExpansionMode.AlwaysExpanded + }, + new ModDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = SelectedMods, + ExpansionMode = ExpansionMode.ExpandOnHover + }, + } + }; } + + protected override Drawable CreateLegacyImplementation() + { + return new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + Children = new[] + { + new ModDisplay(useSkinIcons: true) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = SelectedMods, + ExpansionMode = ExpansionMode.AlwaysContracted + }, + new ModDisplay(useSkinIcons: true) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = SelectedMods, + ExpansionMode = ExpansionMode.AlwaysExpanded + }, + new ModDisplay(useSkinIcons: true) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = SelectedMods, + ExpansionMode = ExpansionMode.ExpandOnHover + }, + } + }; + } + + protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); } } diff --git a/osu.Game/Localisation/SkinComponents/SkinnableModDisplayStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableModDisplayStrings.cs index 22f9fe6d023f..aa6965436c32 100644 --- a/osu.Game/Localisation/SkinComponents/SkinnableModDisplayStrings.cs +++ b/osu.Game/Localisation/SkinComponents/SkinnableModDisplayStrings.cs @@ -50,6 +50,16 @@ public static class SkinnableModDisplayStrings /// public static LocalisableString AlwaysExpanded => new TranslatableString(getKey(@"always_expanded"), @"Always expanded"); + /// + /// "Use legacy skin icons" + /// + public static LocalisableString UseSkinIcons => new TranslatableString(getKey(@"use_skin_icons"), @"Use legacy skin icons"); + + /// + /// "Whether to use legacy skin mod icons or the new ones." + /// + public static LocalisableString UseSkinIconsDescription => new TranslatableString(getKey(@"use_skin_icons_description"), @"Whether to use legacy skin mod icons or the new ones."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 79cf073a42be..4832b317a408 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -16,6 +16,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Rulesets.Mods; +using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -41,6 +42,8 @@ public partial class ModIcon : Container, IHasCustomTooltip private readonly bool showTooltip; private bool showExtendedInformation; + private bool useSkinIcon; + private bool canShowExtendedInformation = true; public bool ShowExtendedInformation { @@ -52,6 +55,16 @@ public bool ShowExtendedInformation } } + public bool UseSkinIcon + { + get => useSkinIcon; + set + { + useSkinIcon = value; + updateMod(mod); + } + } + public IMod Mod { get => mod; @@ -86,6 +99,9 @@ public IMod Mod private SpriteIcon cogBackground = null!; private SpriteIcon cog = null!; + private readonly Bindable skin = new Bindable(); + private Sprite skinIcon = null!; + private ModSettingChangeTracker? modSettingsChangeTracker; /// @@ -94,7 +110,8 @@ public IMod Mod /// The mod to be displayed /// Whether a tooltip describing the mod should display on hover. /// Whether to display a mod's extended information, if available. - public ModIcon(IMod mod, bool showTooltip = true, bool showExtendedInformation = true) + /// Whether the icon should be skin-sourced, if available. + public ModIcon(IMod mod, bool showTooltip = true, bool showExtendedInformation = true, bool useSkinIcon = false) { // May expand due to expanded content, so autosize here. AutoSizeAxes = Axes.X; @@ -103,10 +120,11 @@ public ModIcon(IMod mod, bool showTooltip = true, bool showExtendedInformation = this.mod = mod ?? throw new ArgumentNullException(nameof(mod)); this.showTooltip = showTooltip; this.showExtendedInformation = showExtendedInformation; + this.useSkinIcon = useSkinIcon; } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(TextureStore textures, SkinManager skinManager) { Children = new Drawable[] { @@ -176,6 +194,12 @@ private void load(TextureStore textures) Height = 92 / 135f, Icon = FontAwesome.Solid.Question }, + skinIcon = new Sprite + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.Both + }, adjustmentMarker = new Container { Size = new Vector2(20), @@ -203,6 +227,8 @@ private void load(TextureStore textures) } }, }; + + skin.BindTarget = skinManager.CurrentSkin; } protected override void LoadComplete() @@ -211,6 +237,9 @@ protected override void LoadComplete() Selected.BindValueChanged(_ => updateColour()); + if (useSkinIcon) + skin.BindValueChanged(_ => updateMod(mod)); + updateMod(mod); } @@ -224,21 +253,50 @@ private void updateMod(IMod value) modSettingsChangeTracker.SettingChanged = _ => updateExtendedInformation(); } - modAcronym.Text = value.Acronym; - modIcon.Icon = value.Icon ?? FontAwesome.Solid.Question; - TooltipContent = showTooltip ? value as Mod : null; + Texture? skinIconTexture = null; + + if (useSkinIcon) + { + string textureName = getModIconSpriteName(mod); + skinIconTexture = skin.Value.GetTexture(textureName); + } - if (value.Icon == null) + if (skinIconTexture != null) { + skinIcon.Texture = skinIconTexture; + skinIcon.FadeIn(); + + modAcronym.FadeOut(); modIcon.FadeOut(); - modAcronym.FadeIn(); + background.FadeOut(); + + // we want to hide the extended information for skin icons because the extended background clashes with custom icons + canShowExtendedInformation = false; } else { - modIcon.FadeIn(); - modAcronym.FadeOut(); + skinIcon.FadeOut(); + background.FadeIn(); + + modAcronym.Text = value.Acronym; + modIcon.Icon = value.Icon ?? FontAwesome.Solid.Question; + + if (value.Icon == null) + { + modIcon.FadeOut(); + modAcronym.FadeIn(); + } + else + { + modIcon.FadeIn(); + modAcronym.FadeOut(); + } + + canShowExtendedInformation = true; } + TooltipContent = showTooltip ? value as Mod : null; + backgroundColour = colours.ForModType(value.Type); updateColour(); @@ -247,7 +305,7 @@ private void updateMod(IMod value) private void updateExtendedInformation() { - bool showExtended = showExtendedInformation && !string.IsNullOrEmpty(mod.ExtendedIconInformation); + bool showExtended = showExtendedInformation && canShowExtendedInformation && !string.IsNullOrEmpty(mod.ExtendedIconInformation); extendedContent.Alpha = showExtended ? 1 : 0; extendedText.Text = mod.ExtendedIconInformation; @@ -268,6 +326,16 @@ private void updateColour() extendedBackground.Colour = Selected.Value ? backgroundColour.Darken(2.4f) : backgroundColour.Darken(2.8f); } + private string getModIconSpriteName(IMod mod) + { + // autopilot has a special legacy name + string modName = mod.Name == "Autopilot" + ? "relax2" + : mod.Name.Replace(" ", "").Trim().ToLowerInvariant(); + + return $"selection-mod-{modName}"; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 986bc525ccd3..d545b7a3e1d5 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -55,6 +55,7 @@ public Bindable> Current } private bool showExtendedInformation; + private bool useSkinIcons; public bool ShowExtendedInformation { @@ -73,11 +74,23 @@ public FillDirection FillDirection set => iconsContainer.Direction = value; } + public bool UseSkinIcons + { + get => useSkinIcons; + set + { + useSkinIcons = value; + foreach (var icon in iconsContainer) + icon.UseSkinIcon = value; + } + } + private readonly FillFlowContainer iconsContainer; - public ModDisplay(bool showExtendedInformation = true) + public ModDisplay(bool showExtendedInformation = true, bool useSkinIcons = false) { this.showExtendedInformation = showExtendedInformation; + this.useSkinIcons = useSkinIcons; AutoSizeAxes = Axes.Both; @@ -101,7 +114,7 @@ private void updateDisplay(ValueChangedEvent> mods) iconsContainer.Clear(); foreach (Mod mod in mods.NewValue.AsOrdered()) - iconsContainer.Add(new ModIcon(mod, showExtendedInformation: showExtendedInformation) { Scale = new Vector2(MOD_ICON_SCALE) }); + iconsContainer.Add(new ModIcon(mod, showExtendedInformation: showExtendedInformation, useSkinIcon: useSkinIcons) { Scale = new Vector2(MOD_ICON_SCALE) }); } private void updateExpansionMode(double duration = 500) diff --git a/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs b/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs index 29b842953921..60f22eed048f 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs @@ -24,8 +24,11 @@ public partial class SkinnableModDisplay : CompositeDrawable, ISerialisableDrawa [Resolved] private Bindable> mods { get; set; } = null!; + [SettingSource(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.UseSkinIcons), nameof(SkinnableModDisplayStrings.UseSkinIconsDescription))] + public Bindable UseSkinIcons { get; } = new Bindable(true); + [SettingSource(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.ShowExtendedInformation), nameof(SkinnableModDisplayStrings.ShowExtendedInformationDescription))] - public Bindable ShowExtendedInformation { get; } = new Bindable(true); + public Bindable ShowExtendedInformation { get; } = new Bindable(false); [SettingSource(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.ExpansionMode), nameof(SkinnableModDisplayStrings.ExpansionModeDescription))] public Bindable ExpansionModeSetting { get; } = new Bindable(); @@ -40,7 +43,7 @@ private void load() { // Provide a minimum autosize. new Container { Size = ModIcon.MOD_ICON_SIZE * ModDisplay.MOD_ICON_SCALE }, - modDisplay = new ModDisplay(), + modDisplay = new ModDisplay(useSkinIcons: UseSkinIcons.Value), }; modDisplay.Current = mods; @@ -54,6 +57,16 @@ protected override void LoadComplete() ShowExtendedInformation.BindValueChanged(_ => modDisplay.ShowExtendedInformation = ShowExtendedInformation.Value, true); ExpansionModeSetting.BindValueChanged(_ => modDisplay.ExpansionMode = ExpansionModeSetting.Value, true); Direction.BindValueChanged(_ => modDisplay.FillDirection = Direction.Value == Framework.Graphics.Direction.Horizontal ? FillDirection.Horizontal : FillDirection.Vertical, true); + UseSkinIcons.BindValueChanged(_ => + { + modDisplay.UseSkinIcons = UseSkinIcons.Value; + + // we can't display extended information on legacy icons + if (UseSkinIcons.Value) + ShowExtendedInformation.Value = false; + + ShowExtendedInformation.Disabled = UseSkinIcons.Value; + }, true); FinishTransforms(true); } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 7da996c20897..05ca52e48803 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -380,7 +380,7 @@ protected virtual void BindDrawableRuleset(DrawableRuleset drawableRuleset) Origin = Anchor.BottomRight, }; - protected ModDisplay CreateModsContainer() => new ModDisplay + protected ModDisplay CreateModsContainer() => new ModDisplay(useSkinIcons: true) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight,