From cb73717a3419afb539764d37df53b3fc50aa5f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 31 Jul 2025 12:40:48 +0200 Subject: [PATCH] Move "attribute adjusted" tooltips to individual attribute displays Pre-requisite for displaying additional information regarding the impact of particular attributes on various beatmap metrics. --- ...Tooltip.cs => AdjustedAttributeTooltip.cs} | 65 ++------- .../Overlays/Mods/BeatmapAttributesDisplay.cs | 17 +-- .../Overlays/Mods/VerticalAttributeDisplay.cs | 126 ++++++++++-------- .../Screens/Select/Details/AdvancedStats.cs | 40 ++++-- .../BeatmapTitleWedge_DifficultyDisplay.cs | 23 +--- .../BeatmapTitleWedge_StatisticDifficulty.cs | 11 +- 6 files changed, 121 insertions(+), 161 deletions(-) rename osu.Game/Overlays/Mods/{AdjustedAttributesTooltip.cs => AdjustedAttributeTooltip.cs} (53%) diff --git a/osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs b/osu.Game/Overlays/Mods/AdjustedAttributeTooltip.cs similarity index 53% rename from osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs rename to osu.Game/Overlays/Mods/AdjustedAttributeTooltip.cs index 4df7e18997..7e7c6fa951 100644 --- a/osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs +++ b/osu.Game/Overlays/Mods/AdjustedAttributeTooltip.cs @@ -1,8 +1,6 @@ // 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.Graphics; using osu.Framework.Graphics.Containers; @@ -16,19 +14,19 @@ using osuTK; namespace osu.Game.Overlays.Mods { - public partial class AdjustedAttributesTooltip : VisibilityContainer, ITooltip + public partial class AdjustedAttributeTooltip : VisibilityContainer, ITooltip { private readonly OverlayColourProvider? colourProvider; - private FillFlowContainer attributesFillFlow = null!; private Container content = null!; - private Data? data; + private RulesetBeatmapAttribute? attribute; + private OsuSpriteText adjustedByModsText = null!; [Resolved] private OsuColour colours { get; set; } = null!; - public AdjustedAttributesTooltip(OverlayColourProvider? colourProvider = null) + public AdjustedAttributeTooltip(OverlayColourProvider? colourProvider = null) { this.colourProvider = colourProvider; } @@ -60,17 +58,12 @@ namespace osu.Game.Overlays.Mods Direction = FillDirection.Vertical, Children = new Drawable[] { - new OsuSpriteText + adjustedByModsText = new OsuSpriteText { - Text = "One or more values are being adjusted by mods.", + Font = OsuFont.Default.With(weight: FontWeight.Bold), }, - attributesFillFlow = new FillFlowContainer - { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both - } } - } + }, } }, }; @@ -80,29 +73,21 @@ namespace osu.Game.Overlays.Mods private void updateDisplay() { - attributesFillFlow.Clear(); - - if (data != null) + if (attribute != null && !Precision.AlmostEquals(attribute.OriginalValue, attribute.AdjustedValue)) { - foreach (var attribute in data.Attributes) - { - if (!Precision.AlmostEquals(attribute.OriginalValue, attribute.AdjustedValue)) - attributesFillFlow.Add(new AttributeDisplay(attribute.Acronym, attribute.OriginalValue, attribute.AdjustedValue)); - } - } - - if (attributesFillFlow.Any()) + adjustedByModsText.Text = $"This value is being adjusted by mods ({attribute.OriginalValue:0.0#} → {attribute.AdjustedValue:0.0#})."; content.Show(); + } else content.Hide(); } - public void SetContent(Data? data) + public void SetContent(RulesetBeatmapAttribute? attribute) { - if (this.data == data) + if (this.attribute == attribute) return; - this.data = data; + this.attribute = attribute; updateDisplay(); } @@ -110,29 +95,5 @@ namespace osu.Game.Overlays.Mods protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); public void Move(Vector2 pos) => Position = pos; - - public class Data - { - public IReadOnlyCollection Attributes { get; } - - public Data(IReadOnlyCollection attributes) - { - Attributes = attributes; - } - } - - private partial class AttributeDisplay : CompositeDrawable - { - public AttributeDisplay(string name, double original, double adjusted) - { - AutoSizeAxes = Axes.Both; - - InternalChild = new OsuSpriteText - { - Font = OsuFont.Default.With(weight: FontWeight.Bold), - Text = $"{name}: {original:0.0#} → {adjusted:0.0#}" - }; - } - } } } diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index f714cb3798..e4ca354ffd 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Cursor; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -27,7 +26,7 @@ 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 : ModFooterInformationDisplay, IHasCustomTooltip + public partial class BeatmapAttributesDisplay : ModFooterInformationDisplay { private StarRatingDisplay starRatingDisplay = null!; private BPMDisplay bpmDisplay = null!; @@ -51,10 +50,6 @@ namespace osu.Game.Overlays.Mods private CancellationTokenSource? cancellationSource; private IBindable starDifficulty = null!; - public ITooltip GetCustomTooltip() => new AdjustedAttributesTooltip(); - - public AdjustedAttributesTooltip.Data? TooltipContent { get; private set; } - private const float transition_duration = 250; [BackgroundDependencyLoader] @@ -164,8 +159,6 @@ namespace osu.Game.Overlays.Mods Ruleset ruleset = GameRuleset.Value.CreateInstance(); var displayAttributes = ruleset.GetBeatmapAttributesForDisplay(BeatmapInfo.Value, Mods.Value).ToList(); - TooltipContent = new AdjustedAttributesTooltip.Data(displayAttributes); - // if there are not enough attribute displays, make more for (int i = RightContent.Count; i < displayAttributes.Count; i++) RightContent.Add(new VerticalAttributeDisplay { Shear = -OsuGame.SHEAR }); @@ -175,16 +168,12 @@ namespace osu.Game.Overlays.Mods { var attribute = displayAttributes[i]; var display = (VerticalAttributeDisplay)RightContent[i]; - - display.Label = attribute.Acronym; - display.Current.Value = attribute.AdjustedValue; - display.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(attribute.OriginalValue, attribute.AdjustedValue); - display.Alpha = 1; + display.SetAttribute(attribute); } // and hide any extra ones for (int i = displayAttributes.Count; i < RightContent.Count; i++) - RightContent[i].Alpha = 0; + ((VerticalAttributeDisplay)RightContent[i]).SetAttribute(null); }); private void updateCollapsedState() diff --git a/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs b/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs index 572d5f89e5..ee6d88b265 100644 --- a/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs +++ b/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs @@ -7,29 +7,22 @@ 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.Graphics.Cursor; using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osuTK.Graphics; namespace osu.Game.Overlays.Mods { - public partial class VerticalAttributeDisplay : Container, IHasCurrentValue + public partial class VerticalAttributeDisplay : Container, IHasCustomTooltip { - public Bindable Current - { - get => current.Current; - set => current.Current = value; - } - private readonly BindableWithCurrent current = new BindableWithCurrent(); - public Bindable AdjustType = new Bindable(); - /// /// Text to display in the top area of the display. /// @@ -45,11 +38,70 @@ namespace osu.Game.Overlays.Mods [Resolved] private OsuColour colours { get; set; } = null!; - private void updateTextColor() + public VerticalAttributeDisplay() + { + AutoSizeAxes = Axes.X; + RelativeSizeAxes = Axes.Y; + + Origin = Anchor.CentreLeft; + Anchor = Anchor.CentreLeft; + + InternalChild = new FillFlowContainer + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Y, + Width = 42, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + text = new OsuSpriteText + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold) + }, + counter = new EffectCounter + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Current = { BindTarget = current }, + } + } + }; + } + + public void SetAttribute(RulesetBeatmapAttribute? attribute) + { + if (attribute != null) + { + text.Text = attribute.Acronym; + current.Value = attribute.AdjustedValue; + var effect = calculateEffect(attribute.OriginalValue, attribute.AdjustedValue); + updateTextColor(effect); + Alpha = 1; + } + else + Alpha = 0; + + TooltipContent = attribute; + } + + private static ModEffect calculateEffect(double oldValue, double newValue) + { + if (Precision.AlmostEquals(newValue, oldValue, 0.01)) + return ModEffect.NotChanged; + if (newValue < oldValue) + return ModEffect.DifficultyReduction; + + return ModEffect.DifficultyIncrease; + } + + private void updateTextColor(ModEffect effect) { Color4 newColor; - switch (AdjustType.Value) + switch (effect) { case ModEffect.NotChanged: newColor = Color4.White; @@ -64,58 +116,13 @@ namespace osu.Game.Overlays.Mods break; default: - throw new ArgumentOutOfRangeException(nameof(AdjustType.Value)); + throw new ArgumentOutOfRangeException(nameof(effect), effect, null); } text.Colour = newColor; counter.Colour = newColor; } - public VerticalAttributeDisplay() - { - AutoSizeAxes = Axes.X; - - Origin = Anchor.CentreLeft; - Anchor = Anchor.CentreLeft; - - AdjustType.BindValueChanged(_ => updateTextColor()); - - InternalChild = new FillFlowContainer - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - AutoSizeAxes = Axes.Y, - Width = 50, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - text = new OsuSpriteText - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Margin = new MarginPadding { Horizontal = 15 }, // to reserve space for 0.XX value - Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold) - }, - counter = new EffectCounter - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Current = { BindTarget = Current }, - } - } - }; - } - - public static ModEffect CalculateEffect(double oldValue, double newValue) - { - if (Precision.AlmostEquals(newValue, oldValue, 0.01)) - return ModEffect.NotChanged; - if (newValue < oldValue) - return ModEffect.DifficultyReduction; - - return ModEffect.DifficultyIncrease; - } - public enum ModEffect { NotChanged, @@ -134,5 +141,8 @@ namespace osu.Game.Overlays.Mods Font = OsuFont.Default.With(size: 18, weight: FontWeight.SemiBold) }; } + + public ITooltip GetCustomTooltip() => new AdjustedAttributeTooltip(); + public RulesetBeatmapAttribute? TooltipContent { get; set; } } } diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 6403eb01b0..143c41da5e 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -21,6 +21,7 @@ using System.Linq; using osu.Game.Rulesets.Mods; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Extensions; using osu.Framework.Localisation; using osu.Framework.Threading; @@ -29,11 +30,12 @@ using osu.Game.Configuration; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Difficulty; using osu.Game.Utils; namespace osu.Game.Screens.Select.Details { - public partial class AdvancedStats : Container, IHasCustomTooltip + public partial class AdvancedStats : Container { private readonly int columns; @@ -43,9 +45,6 @@ namespace osu.Game.Screens.Select.Details protected FillFlowContainer Flow { get; private set; } private readonly StatisticRow starDifficulty; - public ITooltip GetCustomTooltip() => new AdjustedAttributesTooltip(); - public AdjustedAttributesTooltip.Data TooltipContent { get; private set; } - private IBeatmapInfo beatmapInfo; public IBeatmapInfo BeatmapInfo @@ -160,7 +159,6 @@ namespace osu.Game.Screens.Select.Details if (BeatmapInfo != null && Ruleset.Value != null) { var displayAttributes = Ruleset.Value.CreateInstance().GetBeatmapAttributesForDisplay(BeatmapInfo, Mods.Value).ToList(); - TooltipContent = new AdjustedAttributesTooltip.Data(displayAttributes); // if there are not enough attribute displays, make more // the subtraction of 1 is to exclude the star rating row which is always present (and always last) @@ -177,17 +175,13 @@ namespace osu.Game.Screens.Select.Details for (int i = 0; i < displayAttributes.Count; i++) { var attribute = displayAttributes[i]; - var display = (StatisticRow)Flow.Where(r => r != starDifficulty).ElementAt(i); - - display.Title = attribute.Label; - display.MaxValue = attribute.MaxValue; - display.Value = (attribute.OriginalValue, attribute.AdjustedValue); - display.Alpha = 1; + var row = (StatisticRow)Flow.Where(r => r != starDifficulty).ElementAt(i); + row.SetAttribute(attribute); } // and hide any extra ones foreach (var row in Flow.Where(r => r != starDifficulty).Skip(displayAttributes.Count)) - row.Alpha = 0; + ((StatisticRow)row).SetAttribute(null); } updateStarDifficulty(); @@ -233,7 +227,7 @@ namespace osu.Game.Screens.Select.Details starDifficultyCancellationSource?.Cancel(); } - public partial class StatisticRow : Container, IHasAccentColour + public partial class StatisticRow : Container, IHasAccentColour, IHasCustomTooltip { private const float value_width = 25; private const float name_width = 70; @@ -352,6 +346,26 @@ namespace osu.Game.Screens.Select.Details }, }; } + + public void SetAttribute([CanBeNull] RulesetBeatmapAttribute attribute) + { + if (attribute != null) + { + Title = attribute.Label; + MaxValue = attribute.MaxValue; + Value = (attribute.OriginalValue, attribute.AdjustedValue); + Alpha = 1; + } + else + Alpha = 0; + + TooltipContent = attribute; + } + + public ITooltip GetCustomTooltip() => new AdjustedAttributeTooltip(); + + [CanBeNull] + public RulesetBeatmapAttribute TooltipContent { get; set; } } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index 061eee1cc8..0e880a740f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -11,7 +11,6 @@ using osu.Framework.Bindables; 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.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; @@ -23,7 +22,6 @@ using osu.Game.Localisation; using osu.Game.Online; using osu.Game.Online.Chat; using osu.Game.Overlays; -using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osuTK.Graphics; @@ -62,7 +60,7 @@ namespace osu.Game.Screens.SelectV2 private GridContainer ratingAndNameContainer = null!; private DifficultyStatisticsDisplay countStatisticsDisplay = null!; - private AdjustableDifficultyStatisticsDisplay difficultyStatisticsDisplay = null!; + private DifficultyStatisticsDisplay difficultyStatisticsDisplay = null!; private CancellationTokenSource? cancellationSource; @@ -195,7 +193,7 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.X, }, Empty(), - difficultyStatisticsDisplay = new AdjustableDifficultyStatisticsDisplay(autoSize: true), + difficultyStatisticsDisplay = new DifficultyStatisticsDisplay(autoSize: true), } }, } @@ -289,7 +287,6 @@ namespace osu.Game.Screens.SelectV2 { if (beatmap.IsDefault || ruleset.Value == null) { - difficultyStatisticsDisplay.TooltipContent = null; difficultyStatisticsDisplay.Statistics = Array.Empty(); return; } @@ -297,7 +294,6 @@ namespace osu.Game.Screens.SelectV2 Ruleset rulesetInstance = ruleset.Value.CreateInstance(); var displayAttributes = rulesetInstance.GetBeatmapAttributesForDisplay(beatmap.Value.BeatmapInfo, mods.Value).ToList(); - difficultyStatisticsDisplay.TooltipContent = new AdjustedAttributesTooltip.Data(displayAttributes); difficultyStatisticsDisplay.Statistics = displayAttributes.Select(a => new StatisticDifficulty.Data(a)).ToList(); }); @@ -325,21 +321,6 @@ namespace osu.Game.Screens.SelectV2 IdleColour = overlayColourProvider?.Light2 ?? colours.Blue; } } - - private partial class AdjustableDifficultyStatisticsDisplay : DifficultyStatisticsDisplay, IHasCustomTooltip - { - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; - - public ITooltip GetCustomTooltip() => new AdjustedAttributesTooltip(colourProvider); - - public AdjustedAttributesTooltip.Data? TooltipContent { get; set; } - - public AdjustableDifficultyStatisticsDisplay(bool autoSize) - : base(autoSize) - { - } - } } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticDifficulty.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticDifficulty.cs index 65d8ba3951..140b4a6512 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticDifficulty.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticDifficulty.cs @@ -7,12 +7,14 @@ using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; +using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Difficulty; using osuTK; using osuTK.Graphics; @@ -21,7 +23,7 @@ namespace osu.Game.Screens.SelectV2 { public partial class BeatmapTitleWedge { - public partial class StatisticDifficulty : CompositeDrawable, IHasAccentColour + public partial class StatisticDifficulty : CompositeDrawable, IHasAccentColour, IHasCustomTooltip { private Data value = new Data(string.Empty, 0, 0, 0); @@ -192,13 +194,16 @@ namespace osu.Game.Screens.SelectV2 } } - public record Data(LocalisableString Label, float Value, float AdjustedValue, float Maximum, string? Content = null) + public record Data(LocalisableString Label, float Value, float AdjustedValue, float Maximum, string? Content = null, RulesetBeatmapAttribute? BeatmapAttribute = null) { public Data(RulesetBeatmapAttribute attribute) - : this(attribute.Label, attribute.OriginalValue, attribute.AdjustedValue, attribute.MaxValue) + : this(attribute.Label, attribute.OriginalValue, attribute.AdjustedValue, attribute.MaxValue, BeatmapAttribute: attribute) { } } + + public ITooltip GetCustomTooltip() => new AdjustedAttributeTooltip(); + public RulesetBeatmapAttribute? TooltipContent => value.BeatmapAttribute; } } }