From 6b91b4abf41660fb030f2d593df28159442cf145 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 8 May 2024 03:58:10 +0300 Subject: [PATCH] Add simple implementation for extended mods display in new footer design --- .../TestSceneFooterButtonModsV2.cs | 120 +++++++++ osu.Game/Screens/Play/HUD/ModDisplay.cs | 7 +- .../Select/FooterV2/FooterButtonModsV2.cs | 249 +++++++++++++++++- .../Screens/Select/FooterV2/FooterButtonV2.cs | 126 ++++----- 4 files changed, 436 insertions(+), 66 deletions(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs new file mode 100644 index 0000000000..7e8bba6573 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs @@ -0,0 +1,120 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.Select.FooterV2; +using osu.Game.Utils; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneFooterButtonModsV2 : OsuTestScene + { + private readonly TestFooterButtonModsV2 footerButtonMods; + + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + public TestSceneFooterButtonModsV2() + { + Add(footerButtonMods = new TestFooterButtonModsV2 + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + X = -100, + Action = () => { }, + }); + } + + [Test] + public void TestDisplay() + { + AddStep("one mod", () => changeMods(new List { new OsuModHidden() })); + AddStep("two mods", () => changeMods(new List { new OsuModHidden(), new OsuModHardRock() })); + AddStep("three mods", () => changeMods(new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() })); + AddStep("four mods", () => changeMods(new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic() })); + AddStep("five mods", () => changeMods(new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() })); + + AddStep("clear mods", () => changeMods(Array.Empty())); + AddWaitStep("wait", 3); + AddStep("one mod", () => changeMods(new List { new OsuModHidden() })); + + AddStep("clear mods", () => changeMods(Array.Empty())); + AddWaitStep("wait", 3); + AddStep("five mods", () => changeMods(new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() })); + } + + [Test] + public void TestIncrementMultiplier() + { + 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] + public void TestDecrementMultiplier() + { + 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] + public void TestUnrankedBadge() + { + AddStep(@"Add unranked mod", () => changeMods(new[] { new OsuModDeflate() })); + AddUntilStep("Unranked badge shown", () => footerButtonMods.UnrankedBadge.Alpha == 1); + AddStep(@"Clear selected mod", () => changeMods(Array.Empty())); + AddUntilStep("Unranked badge not shown", () => footerButtonMods.UnrankedBadge.Alpha == 0); + } + + 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 = ModUtils.FormatScoreMultiplier(multiplier).ToString(); + + return expectedValue == footerButtonMods.MultiplierText.Current.Value; + } + + private partial class TestFooterButtonModsV2 : FooterButtonModsV2 + { + public new Container UnrankedBadge => base.UnrankedBadge; + public new OsuSpriteText MultiplierText => base.MultiplierText; + } + } +} diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index ba948b516e..75db720603 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -20,6 +20,7 @@ namespace osu.Game.Screens.Play.HUD /// public partial class ModDisplay : CompositeDrawable, IHasCurrentValue> { + private readonly bool showExtendedInformation; private const int fade_duration = 1000; public ExpansionMode ExpansionMode = ExpansionMode.ExpandOnHover; @@ -39,8 +40,10 @@ namespace osu.Game.Screens.Play.HUD private readonly FillFlowContainer iconsContainer; - public ModDisplay() + public ModDisplay(bool showExtendedInformation = true) { + this.showExtendedInformation = showExtendedInformation; + AutoSizeAxes = Axes.Both; InternalChild = iconsContainer = new ReverseChildIDFillFlowContainer @@ -64,7 +67,7 @@ namespace osu.Game.Screens.Play.HUD iconsContainer.Clear(); foreach (Mod mod in mods.NewValue.AsOrdered()) - iconsContainer.Add(new ModIcon(mod) { Scale = new Vector2(0.6f) }); + iconsContainer.Add(new ModIcon(mod, showExtendedInformation: showExtendedInformation) { Scale = new Vector2(0.6f) }); appearTransform(); } diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs index b8c9f0b34b..f9d923ddcf 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs @@ -1,20 +1,261 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; +using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play.HUD; +using osu.Game.Utils; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Select.FooterV2 { - public partial class FooterButtonModsV2 : FooterButtonV2 + public partial class FooterButtonModsV2 : FooterButtonV2, IHasCurrentValue> { - [BackgroundDependencyLoader] - private void load(OsuColour colour) + // todo: see https://github.com/ppy/osu-framework/issues/3271 + private const float torus_scale_factor = 1.2f; + + private readonly BindableWithCurrent> current = new BindableWithCurrent>(Array.Empty()); + + public Bindable> Current { + get => current.Current; + set => current.Current = value; + } + + private Container modDisplayBar = null!; + + protected Container UnrankedBadge { get; private set; } = null!; + + private ModDisplay modDisplay = null!; + private OsuSpriteText modCountText = null!; + + protected OsuSpriteText MultiplierText { get; private set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + const float bar_shear_width = 7f; + const float bar_height = 37f; + const float display_rel_width = 0.65f; + + var barShear = new Vector2(bar_shear_width / bar_height, 0); + Text = "Mods"; Icon = FontAwesome.Solid.ExchangeAlt; - AccentColour = colour.Lime1; + AccentColour = colours.Lime1; + + AddRange(new[] + { + UnrankedBadge = new Container + { + Position = new Vector2(BUTTON_WIDTH + 5f, -5f), + Depth = float.MaxValue, + Origin = Anchor.BottomLeft, + Shear = barShear, + CornerRadius = CORNER_RADIUS, + AutoSizeAxes = Axes.Both, + Masking = true, + BorderColour = Color4.White, + BorderThickness = 2f, + Children = new Drawable[] + { + new Box + { + Colour = colours.Red2, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Shear = -barShear, + Text = ModSelectOverlayStrings.Unranked.ToUpper(), + Margin = new MarginPadding { Horizontal = 15, Vertical = 5 }, + Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold), + Colour = Color4.Black, + } + } + }, + modDisplayBar = new Container + { + Y = -5f, + Depth = float.MaxValue, + Origin = Anchor.BottomLeft, + Shear = barShear, + CornerRadius = CORNER_RADIUS, + Size = new Vector2(BUTTON_WIDTH, bar_height), + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 4, + // Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad. + Colour = Colour4.Black.Opacity(0.25f), + Offset = new Vector2(0, 2), + }, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background4, + RelativeSizeAxes = Axes.Both, + }, + new Container + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Width = 1f - display_rel_width, + Masking = true, + Child = MultiplierText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shear = -barShear, + Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold) + } + }, + new Container + { + CornerRadius = CORNER_RADIUS, + RelativeSizeAxes = Axes.Both, + Width = display_rel_width, + Masking = true, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }, + modDisplay = new ModDisplay(showExtendedInformation: false) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shear = -barShear, + Scale = new Vector2(0.6f), + Current = { Value = new List { new ModCinema() } }, + ExpansionMode = ExpansionMode.AlwaysContracted, + }, + modCountText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shear = -barShear, + Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold), + } + } + }, + } + }, + }); + } + + private ModSettingChangeTracker? modSettingChangeTracker; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(m => + { + modSettingChangeTracker?.Dispose(); + + updateDisplay(); + + if (m.NewValue != null) + { + modSettingChangeTracker = new ModSettingChangeTracker(m.NewValue); + modSettingChangeTracker.SettingChanged += _ => updateDisplay(); + } + }, true); + + FinishTransforms(true); + } + + private const double duration = 240; + private const Easing easing = Easing.OutQuint; + + private void updateDisplay() + { + if (Current.Value.Count == 0) + { + modDisplayBar.MoveToY(20, duration, easing); + modDisplayBar.FadeOut(duration, easing); + modDisplay.FadeOut(duration, easing); + modCountText.FadeOut(duration, easing); + + UnrankedBadge.MoveToY(20, duration, easing); + UnrankedBadge.FadeOut(duration, easing); + + // add delay to let unranked indicator hide first before resizing the button back to its original width. + this.Delay(duration).ResizeWidthTo(BUTTON_WIDTH, duration, easing); + } + else + { + if (Current.Value.Count >= 5) + { + modCountText.Text = $"{Current.Value.Count} MODS"; + modCountText.FadeIn(duration, easing); + modDisplay.FadeOut(duration, easing); + } + else + { + modDisplay.Current.Value = Current.Value; + modDisplay.FadeIn(duration, easing); + modCountText.FadeOut(duration, easing); + } + + if (Current.Value.Any(m => !m.Ranked)) + { + UnrankedBadge.MoveToX(BUTTON_WIDTH + 5, duration, easing); + UnrankedBadge.FadeIn(duration, easing); + + this.ResizeWidthTo(BUTTON_WIDTH + UnrankedBadge.DrawWidth + 10, duration, easing); + } + else + { + UnrankedBadge.MoveToX(BUTTON_WIDTH + 5 - UnrankedBadge.DrawWidth, duration, easing); + UnrankedBadge.FadeOut(duration, easing); + + this.ResizeWidthTo(BUTTON_WIDTH, duration, easing); + } + + modDisplayBar.MoveToY(-5, duration, Easing.OutQuint); + UnrankedBadge.MoveToY(-5, duration, easing); + modDisplayBar.FadeIn(duration, easing); + } + + double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1; + MultiplierText.Text = ModUtils.FormatScoreMultiplier(multiplier); + + if (multiplier > 1) + MultiplierText.FadeColour(colours.Red1, duration, easing); + else if (multiplier < 1) + MultiplierText.FadeColour(colours.Lime1, duration, easing); + else + MultiplierText.FadeColour(Color4.White, duration, easing); } } } diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs index 2f5046d2bb..a7bd1b8abd 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs @@ -24,17 +24,18 @@ namespace osu.Game.Screens.Select.FooterV2 { public partial class FooterButtonV2 : OsuClickableContainer, IKeyBindingHandler { - private const int button_height = 90; - private const int button_width = 140; - private const int corner_radius = 10; private const int transition_length = 500; // This should be 12 by design, but an extra allowance is added due to the corner radius specification. public const float SHEAR_WIDTH = 13.5f; + public const int CORNER_RADIUS = 10; + public const int BUTTON_HEIGHT = 90; + public const int BUTTON_WIDTH = 140; + public Bindable OverlayState = new Bindable(); - protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / button_height, 0); + protected static readonly Vector2 BUTTON_SHEAR = new Vector2(SHEAR_WIDTH / BUTTON_HEIGHT, 0); [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -70,68 +71,73 @@ namespace osu.Game.Screens.Select.FooterV2 public FooterButtonV2() { - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Radius = 4, - // Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad. - Colour = Colour4.Black.Opacity(0.25f), - Offset = new Vector2(0, 2), - }; - Shear = SHEAR; - Size = new Vector2(button_width, button_height); - Masking = true; - CornerRadius = corner_radius; - Children = new Drawable[] - { - backgroundBox = new Box - { - RelativeSizeAxes = Axes.Both - }, + Size = new Vector2(BUTTON_WIDTH, BUTTON_HEIGHT); + Margin = new MarginPadding { Horizontal = SHEAR_WIDTH / 2f }; - // For elements that should not be sheared. - new Container + Child = new Container + { + EdgeEffect = new EdgeEffectParameters { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Shear = -SHEAR, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - TextContainer = new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Y = 42, - AutoSizeAxes = Axes.Both, - Child = text = new OsuSpriteText - { - // figma design says the size is 16, but due to the issues with font sizes 19 matches better - Font = OsuFont.TorusAlternate.With(size: 19), - AlwaysPresent = true - } - }, - icon = new SpriteIcon - { - Y = 12, - Size = new Vector2(20), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - }, - } + Type = EdgeEffectType.Shadow, + Radius = 4, + // Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad. + Colour = Colour4.Black.Opacity(0.25f), + Offset = new Vector2(0, 2), }, - new Container + Shear = BUTTON_SHEAR, + Masking = true, + CornerRadius = CORNER_RADIUS, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Shear = -SHEAR, - Anchor = Anchor.BottomCentre, - Origin = Anchor.Centre, - Y = -corner_radius, - Size = new Vector2(120, 6), - Masking = true, - CornerRadius = 3, - Child = bar = new Box + backgroundBox = new Box { + RelativeSizeAxes = Axes.Both + }, + // For elements that should not be sheared. + new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Shear = -BUTTON_SHEAR, RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + TextContainer = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Y = 42, + AutoSizeAxes = Axes.Both, + Child = text = new OsuSpriteText + { + // figma design says the size is 16, but due to the issues with font sizes 19 matches better + Font = OsuFont.TorusAlternate.With(size: 19), + AlwaysPresent = true + } + }, + icon = new SpriteIcon + { + Y = 12, + Size = new Vector2(20), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }, + } + }, + new Container + { + Shear = -BUTTON_SHEAR, + Anchor = Anchor.BottomCentre, + Origin = Anchor.Centre, + Y = -CORNER_RADIUS, + Size = new Vector2(120, 6), + Masking = true, + CornerRadius = 3, + Child = bar = new Box + { + RelativeSizeAxes = Axes.Both, + } } } };