1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-08 22:44:40 +08:00

Merge pull request #32970 from peppy/mod-icon-improvements

Improve visibility of setting adjustments on mod icons
This commit is contained in:
Dean Herbert
2025-04-30 17:21:12 +09:00
committed by GitHub
Unverified
8 changed files with 276 additions and 32 deletions
@@ -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
@@ -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
@@ -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
@@ -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<IMod> mods)
{
spreadOutFlow.AddRange(mods.Select(m => new ModIcon(m)));
modDisplay.Current.Value = modDisplay.Current.Value.Concat(mods.OfType<Mod>()).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<ModRateAdjust>();
addRange(rateAdjustMods.SelectMany(m =>
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Full,
ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods()
.OfType<ModRateAdjust>()
.SelectMany(m =>
{
List<ModIcon> icons = new List<ModIcon> { new ModIcon(m) };
List<Mod> mods = new List<Mod> { 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>())
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>())
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 },
}
]);
});
}
}
}
@@ -17,7 +17,7 @@ namespace osu.Game.Extensions
/// <param name="maxDecimalDigits">The maximum number of decimals to be considered in the original value.</param>
/// <param name="asPercentage">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%.</param>
/// <returns>The formatted output.</returns>
public static string ToStandardFormattedString<T>(this T value, int maxDecimalDigits, bool asPercentage) where T : struct, INumber<T>, IMinMaxValue<T>
public static string ToStandardFormattedString<T>(this T value, int maxDecimalDigits, bool asPercentage = false) where T : struct, INumber<T>, IMinMaxValue<T>
{
double floatValue = double.CreateTruncating(value);
+30
View File
@@ -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 <see cref="Mod"/> instance based on this mod.
/// </summary>
Mod CreateInstance() => (Mod)Activator.CreateInstance(GetType())!;
/// <summary>
/// Whether any user adjustable setting attached to this mod has a non-default value.
/// </summary>
/// <remarks>
/// 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 <see cref="ModSettingChangeTracker"/>.
/// </remarks>
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;
}
}
}
}
@@ -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;
}
/// <summary>
/// The number of settings on this mod instance which have been adjusted by the user from their default values.
/// </summary>
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;
}
}
}
}
+37 -1
View File
@@ -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;
/// <summary>
@@ -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<Colour4>(0.1f, Colour4.Black, backgroundColour, 0, 1);
cogBackground.Colour = Interpolation.ValueAt<Colour4>(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);