diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModEffectPreviewPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModEffectPreviewPanel.cs new file mode 100644 index 0000000000..b3ad5a499e --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModEffectPreviewPanel.cs @@ -0,0 +1,131 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public partial class TestSceneModEffectPreviewPanel : OsuTestScene + { + [Cached(typeof(BeatmapDifficultyCache))] + private TestBeatmapDifficultyCache difficultyCache = new TestBeatmapDifficultyCache(); + + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + private Container content = null!; + protected override Container Content => content; + + private BeatmapAttributesDisplay panel = null!; + + [BackgroundDependencyLoader] + private void load() + { + base.Content.AddRange(new Drawable[] + { + difficultyCache, + content = new Container + { + RelativeSizeAxes = Axes.Both + } + }); + } + + [Test] + public void TestDisplay() + { + OsuModDifficultyAdjust difficultyAdjust = new OsuModDifficultyAdjust(); + OsuModDoubleTime doubleTime = new OsuModDoubleTime(); + + AddStep("create display", () => Child = panel = new BeatmapAttributesDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + AddStep("set beatmap", () => + { + var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo) + { + BeatmapInfo = + { + BPM = 120 + } + }; + + Ruleset.Value = beatmap.BeatmapInfo.Ruleset; + panel.BeatmapInfo.Value = beatmap.BeatmapInfo; + }); + + AddSliderStep("change star rating", 0, 10d, 5, stars => + { + if (panel.IsNotNull()) + previewStarRating(stars); + }); + AddStep("preview ridiculously high SR", () => previewStarRating(1234)); + + AddStep("add DA to mods", () => SelectedMods.Value = new[] { difficultyAdjust }); + + AddSliderStep("change AR", 0, 10f, 5, ar => + { + if (panel.IsNotNull()) + difficultyAdjust.ApproachRate.Value = ar; + }); + AddSliderStep("change CS", 0, 10f, 5, cs => + { + if (panel.IsNotNull()) + difficultyAdjust.CircleSize.Value = cs; + }); + AddSliderStep("change HP", 0, 10f, 5, hp => + { + if (panel.IsNotNull()) + difficultyAdjust.DrainRate.Value = hp; + }); + AddSliderStep("change OD", 0, 10f, 5, od => + { + if (panel.IsNotNull()) + difficultyAdjust.OverallDifficulty.Value = od; + }); + + AddStep("add DT to mods", () => SelectedMods.Value = new Mod[] { difficultyAdjust, doubleTime }); + AddSliderStep("change rate", 1.01d, 2d, 1.5d, rate => + { + if (panel.IsNotNull()) + doubleTime.SpeedChange.Value = rate; + }); + + AddToggleStep("toggle collapsed", collapsed => panel.Collapsed.Value = collapsed); + } + + private void previewStarRating(double stars) + { + difficultyCache.Difficulty = new StarDifficulty(stars, 0); + panel.BeatmapInfo.TriggerChange(); + } + + private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache + { + public StarDifficulty? Difficulty { get; set; } + + public override Task GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable? mods = null, + CancellationToken cancellationToken = default) + => Task.FromResult(Difficulty); + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index ad79865ad9..029a7f8b9e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -51,6 +51,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("clear contents", Clear); AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0)); AddStep("reset mods", () => SelectedMods.SetDefault()); + AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo)); AddStep("set up presets", () => { Realm.Write(r => @@ -92,6 +93,7 @@ namespace osu.Game.Tests.Visual.UserInterface { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, + Beatmap = Beatmap.Value, SelectedMods = { BindTarget = SelectedMods } }); waitForColumnLoad(); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModsEffectDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModsEffectDisplay.cs index a1c8bef1de..bf6a718167 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModsEffectDisplay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModsEffectDisplay.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("colours are correct", () => testDisplay.Container.Colour == colourProvider.Background5 && background.Colour == colours.ForModType(ModType.DifficultyIncrease)); } - private partial class TestDisplay : ModsEffectDisplay + private partial class TestDisplay : ModCounterDisplay { public Container Container => Content; diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index f1afacb2f4..b1e7066a01 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -17,6 +17,10 @@ namespace osu.Game.Graphics.UserInterface { public partial class ShearedButton : OsuClickableContainer { + public const float HEIGHT = 50; + public const float CORNER_RADIUS = 7; + public const float BORDER_THICKNESS = 2; + public LocalisableString Text { get => text.Text; @@ -83,12 +87,10 @@ namespace osu.Game.Graphics.UserInterface /// public ShearedButton(float? width = null) { - Height = 50; + Height = HEIGHT; Padding = new MarginPadding { Horizontal = shear * 50 }; - const float corner_radius = 7; - - Content.CornerRadius = corner_radius; + Content.CornerRadius = CORNER_RADIUS; Content.Shear = new Vector2(shear, 0); Content.Masking = true; Content.Anchor = Content.Origin = Anchor.Centre; @@ -98,9 +100,9 @@ namespace osu.Game.Graphics.UserInterface backgroundLayer = new Container { RelativeSizeAxes = Axes.Y, - CornerRadius = corner_radius, + CornerRadius = CORNER_RADIUS, Masking = true, - BorderThickness = 2, + BorderThickness = BORDER_THICKNESS, Children = new Drawable[] { background = new Box diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs new file mode 100644 index 0000000000..2aea53090a --- /dev/null +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -0,0 +1,274 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +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.Colour; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Mods; +using osuTK; +using osuTK.Graphics; +using System.Threading; +using osu.Framework.Input.Events; +using osu.Game.Configuration; + +namespace osu.Game.Overlays.Mods +{ + /// + /// On the mod select overlay, this provides a local updating view of BPM, star rating and other + /// difficulty attributes so the user can have a better insight into what mods are changing. + /// + public partial class BeatmapAttributesDisplay : CompositeDrawable + { + private Container content = null!; + private Container innerContent = null!; + + private Box background = null!; + private Box innerBackground = null!; + + private StarRatingDisplay starRatingDisplay = null!; + private BPMDisplay bpmDisplay = null!; + + private FillFlowContainer outerContent = null!; + private VerticalAttributeDisplay circleSizeDisplay = null!; + private VerticalAttributeDisplay drainRateDisplay = null!; + private VerticalAttributeDisplay approachRateDisplay = null!; + private VerticalAttributeDisplay overallDifficultyDisplay = null!; + + private const float transition_duration = 250; + + public Bindable BeatmapInfo { get; } = new Bindable(); + + public BindableBool Collapsed { get; } = new BindableBool(true); + + [Resolved] + private Bindable> mods { get; set; } = null!; + + private ModSettingChangeTracker? modSettingChangeTracker; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } = null!; + + private CancellationTokenSource? cancellationSource; + private IBindable starDifficulty = null!; + + [BackgroundDependencyLoader] + private void load() + { + const float shear = ShearedOverlayContainer.SHEAR; + + AutoSizeAxes = Axes.Both; + InternalChild = content = new Container + { + Origin = Anchor.BottomRight, + Anchor = Anchor.BottomRight, + AutoSizeAxes = Axes.X, + Height = ShearedButton.HEIGHT, + Shear = new Vector2(shear, 0), + CornerRadius = ShearedButton.CORNER_RADIUS, + BorderThickness = ShearedButton.BORDER_THICKNESS, + Masking = true, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + new FillFlowContainer // divide inner and outer content + { + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + innerContent = new Container + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + BorderThickness = ShearedButton.BORDER_THICKNESS, + CornerRadius = ShearedButton.CORNER_RADIUS, + Masking = true, + Children = new Drawable[] + { + innerBackground = new Box + { + RelativeSizeAxes = Axes.Both + }, + new Container // actual inner content + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Width = 140, + RelativeSizeAxes = Axes.Y, + Margin = new MarginPadding { Horizontal = 15 }, + Children = new Drawable[] + { + starRatingDisplay = new StarRatingDisplay(default, animated: true) + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Shear = new Vector2(-shear, 0), + }, + bpmDisplay = new BPMDisplay + { + Origin = Anchor.CentreRight, + Anchor = Anchor.CentreRight, + Shear = new Vector2(-shear, 0), + } + } + } + } + }, + outerContent = new FillFlowContainer + { + Alpha = 0, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Children = new[] + { + circleSizeDisplay = new VerticalAttributeDisplay("CS") + { + Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + }, + drainRateDisplay = new VerticalAttributeDisplay("HP") + { + Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + }, + approachRateDisplay = new VerticalAttributeDisplay("AR") + { + Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + }, + overallDifficultyDisplay = new VerticalAttributeDisplay("OD") + { + Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + }, + } + } + } + } + } + }; + } + + protected override void LoadComplete() + { + background.Colour = colourProvider.Background4; + innerBackground.Colour = colourProvider.Background3; + Color4 glowColour = colourProvider.Background1; + + content.BorderColour = ColourInfo.GradientVertical(background.Colour, glowColour); + innerContent.BorderColour = ColourInfo.GradientVertical(innerBackground.Colour, glowColour); + + mods.BindValueChanged(_ => + { + modSettingChangeTracker?.Dispose(); + + modSettingChangeTracker = new ModSettingChangeTracker(mods.Value); + modSettingChangeTracker.SettingChanged += _ => updateValues(); + updateValues(); + }, true); + + Collapsed.BindValueChanged(_ => + { + // Only start autosize animations on first collapse toggle. This avoids an ugly initial presentation. + startAnimating(); + + updateCollapsedState(); + }); + + BeatmapInfo.BindValueChanged(_ => updateValues(), true); + } + + protected override bool OnHover(HoverEvent e) + { + startAnimating(); + updateCollapsedState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateCollapsedState(); + base.OnHoverLost(e); + } + + protected override bool OnMouseDown(MouseDownEvent e) => true; + + protected override bool OnClick(ClickEvent e) => true; + + private void startAnimating() + { + content.AutoSizeEasing = Easing.OutQuint; + content.AutoSizeDuration = transition_duration; + } + + private void updateValues() => Scheduler.AddOnce(() => + { + if (BeatmapInfo.Value == null) + return; + + cancellationSource?.Cancel(); + + starDifficulty = difficultyCache.GetBindableDifficulty(BeatmapInfo.Value, (cancellationSource = new CancellationTokenSource()).Token); + starDifficulty.BindValueChanged(s => + { + starRatingDisplay.Current.Value = s.NewValue ?? default; + + if (!starRatingDisplay.IsPresent) + starRatingDisplay.FinishTransforms(true); + }); + + double rate = 1; + foreach (var mod in mods.Value.OfType()) + rate = mod.ApplyToRate(0, rate); + + bpmDisplay.Current.Value = BeatmapInfo.Value.BPM * rate; + + BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty); + foreach (var mod in mods.Value.OfType()) + mod.ApplyToDifficulty(adjustedDifficulty); + + circleSizeDisplay.Current.Value = adjustedDifficulty.CircleSize; + drainRateDisplay.Current.Value = adjustedDifficulty.DrainRate; + approachRateDisplay.Current.Value = adjustedDifficulty.ApproachRate; + overallDifficultyDisplay.Current.Value = adjustedDifficulty.OverallDifficulty; + }); + + private void updateCollapsedState() + { + outerContent.FadeTo(Collapsed.Value && !IsHovered ? 0 : 1, transition_duration, Easing.OutQuint); + } + + private partial class BPMDisplay : RollingCounter + { + protected override double RollingDuration => 500; + + protected override LocalisableString FormatCount(double count) => count.ToLocalisableString("0 BPM"); + + protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText + { + Font = OsuFont.Default.With(size: 20, weight: FontWeight.SemiBold), + UseFullGlyphHeight = false, + }; + } + } +} diff --git a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs index 9e16aa926f..9f4d187879 100644 --- a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs +++ b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs @@ -9,7 +9,7 @@ using osu.Game.Localisation; namespace osu.Game.Overlays.Mods { - public sealed partial class DifficultyMultiplierDisplay : ModsEffectDisplay + public sealed partial class DifficultyMultiplierDisplay : ModCounterDisplay { protected override LocalisableString Label => DifficultyMultiplierDisplayStrings.DifficultyMultiplier; diff --git a/osu.Game/Overlays/Mods/ModsEffectDisplay.cs b/osu.Game/Overlays/Mods/ModCounterDisplay.cs similarity index 97% rename from osu.Game/Overlays/Mods/ModsEffectDisplay.cs rename to osu.Game/Overlays/Mods/ModCounterDisplay.cs index 3f31736ee1..3bec27d290 100644 --- a/osu.Game/Overlays/Mods/ModsEffectDisplay.cs +++ b/osu.Game/Overlays/Mods/ModCounterDisplay.cs @@ -19,9 +19,9 @@ using osuTK; namespace osu.Game.Overlays.Mods { /// - /// Base class for displays of mods effects. + /// Base class for displays of singular counters. Not to be confused with which aggregates multiple attributes. /// - public abstract partial class ModsEffectDisplay : Container, IHasCurrentValue + public abstract partial class ModCounterDisplay : Container, IHasCurrentValue { public const float HEIGHT = 42; private const float transition_duration = 200; @@ -57,7 +57,7 @@ namespace osu.Game.Overlays.Mods protected readonly RollingCounter Counter; - protected ModsEffectDisplay() + protected ModCounterDisplay() { Height = HEIGHT; AutoSizeAxes = Axes.X; diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 9e92e9d959..666e2849e8 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -17,6 +17,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Audio; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -77,9 +78,9 @@ namespace osu.Game.Overlays.Mods public ShearedSearchTextBox SearchTextBox { get; private set; } = null!; /// - /// Whether the total score multiplier calculated from the current selected set of mods should be shown. + /// Whether the effects (on score multiplier, on or beatmap difficulty) of the current selected set of mods should be shown. /// - protected virtual bool ShowTotalMultiplier => true; + protected virtual bool ShowModEffects => true; /// /// Whether per-mod customisation controls are visible. @@ -123,6 +124,7 @@ namespace osu.Game.Overlays.Mods private Container aboveColumnsContent = null!; private DifficultyMultiplierDisplay? multiplierDisplay; + private BeatmapAttributesDisplay? modEffectPreviewPanel; protected ShearedButton BackButton { get; private set; } = null!; protected ShearedToggleButton? CustomisationButton { get; private set; } @@ -130,6 +132,21 @@ namespace osu.Game.Overlays.Mods private Sample? columnAppearSample; + private WorkingBeatmap? beatmap; + + public WorkingBeatmap? Beatmap + { + get => beatmap; + set + { + if (beatmap == value) return; + + beatmap = value; + if (IsLoaded && modEffectPreviewPanel != null) + modEffectPreviewPanel.BeatmapInfo.Value = beatmap?.BeatmapInfo; + } + } + protected ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green) : base(colourScheme) { @@ -164,7 +181,7 @@ namespace osu.Game.Overlays.Mods aboveColumnsContent = new Container { RelativeSizeAxes = Axes.X, - Height = ModsEffectDisplay.HEIGHT, + Height = ModCounterDisplay.HEIGHT, Padding = new MarginPadding { Horizontal = 100 }, Child = SearchTextBox = new ShearedSearchTextBox { @@ -179,7 +196,7 @@ namespace osu.Game.Overlays.Mods { Padding = new MarginPadding { - Top = ModsEffectDisplay.HEIGHT + PADDING, + Top = ModCounterDisplay.HEIGHT + PADDING, Bottom = PADDING }, RelativeSizeAxes = Axes.Both, @@ -210,16 +227,7 @@ namespace osu.Game.Overlays.Mods } }); - if (ShowTotalMultiplier) - { - aboveColumnsContent.Add(multiplierDisplay = new DifficultyMultiplierDisplay - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight - }); - } - - FooterContent.Child = footerButtonFlow = new FillFlowContainer + FooterContent.Add(footerButtonFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -239,7 +247,28 @@ namespace osu.Game.Overlays.Mods DarkerColour = colours.Pink2, LighterColour = colours.Pink1 }) - }; + }); + + if (ShowModEffects) + { + aboveColumnsContent.Add(multiplierDisplay = new DifficultyMultiplierDisplay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight + }); + + FooterContent.Add(modEffectPreviewPanel = new BeatmapAttributesDisplay + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding + { + Vertical = PADDING, + Horizontal = 70 + }, + BeatmapInfo = { Value = beatmap?.BeatmapInfo } + }); + } globalAvailableMods.BindTo(game.AvailableMods); } @@ -309,6 +338,17 @@ namespace osu.Game.Overlays.Mods base.Update(); SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? Resources.Localisation.Web.CommonStrings.InputSearch : ModSelectOverlayStrings.TabToSearch; + + // only update preview panel's collapsed state after we are fully visible, to ensure all the buttons are where we expect them to be. + if (modEffectPreviewPanel != null && Alpha == 1) + { + float rightEdgeOfLastButton = footerButtonFlow.Last().ScreenSpaceDrawQuad.TopRight.X; + + // this is cheating a bit; the 375 value is hardcoded based on how wide the expanded panel _generally_ is. + // due to the transition applied, the raw screenspace quad of the panel cannot be used, as it will trigger an ugly feedback cycle of expanding and collapsing. + float projectedLeftEdgeOfExpandedModEffectPreviewPanel = footerButtonFlow.ToScreenSpace(footerButtonFlow.DrawSize - new Vector2(375 + 70, 0)).X; + modEffectPreviewPanel.Collapsed.Value = rightEdgeOfLastButton > projectedLeftEdgeOfExpandedModEffectPreviewPanel; + } } /// @@ -886,6 +926,9 @@ namespace osu.Game.Overlays.Mods OnClicked?.Invoke(); return true; + case HoverEvent: + return false; + case MouseEvent: return true; } diff --git a/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs b/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs new file mode 100644 index 0000000000..60cc875dbb --- /dev/null +++ b/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs @@ -0,0 +1,78 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Mods +{ + public partial class VerticalAttributeDisplay : Container, IHasCurrentValue + { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + /// + /// Text to display in the top area of the display. + /// + public LocalisableString Label { get; protected set; } + + public VerticalAttributeDisplay(LocalisableString label) + { + Label = label; + + AutoSizeAxes = Axes.X; + + Origin = Anchor.CentreLeft; + Anchor = Anchor.CentreLeft; + + InternalChild = new FillFlowContainer + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Text = Label, + Margin = new MarginPadding { Horizontal = 15 }, // to reserve space for 0.XX value + Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold) + }, + new EffectCounter + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Current = { BindTarget = Current }, + } + } + }; + } + + private partial class EffectCounter : RollingCounter + { + protected override double RollingDuration => 500; + + protected override LocalisableString FormatCount(double count) => count.ToLocalisableString("0.0"); + + protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText + { + Font = OsuFont.Default.With(size: 18, weight: FontWeight.SemiBold) + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 4d5d724089..7f090aca57 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.OnlinePlay { public partial class FreeModSelectOverlay : ModSelectOverlay { - protected override bool ShowTotalMultiplier => false; + protected override bool ShowModEffects => false; protected override bool AllowCustomisation => false; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 809ffb307c..34ee0ae4e8 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -792,6 +792,8 @@ namespace osu.Game.Screens.Select BeatmapDetails.Beatmap = beatmap; + ModSelect.Beatmap = beatmap; + bool beatmapSelected = beatmap is not DummyWorkingBeatmap; if (beatmapSelected)