1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-13 15:43:22 +08:00

Merge branch 'freemod-select-overlay' into freemods

This commit is contained in:
smoogipoo 2021-02-02 21:43:35 +09:00
commit 6453367a9c
8 changed files with 145 additions and 57 deletions

View File

@ -3,7 +3,7 @@
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay;
namespace osu.Game.Tests.Visual.Multiplayer
{

View File

@ -38,28 +38,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}
[SetUp]
public void SetUp() => Schedule(() =>
{
SelectedMods.Value = Array.Empty<Mod>();
Children = new Drawable[]
{
modSelect = new TestModSelectOverlay
{
Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
SelectedMods = { BindTarget = SelectedMods }
},
modDisplay = new ModDisplay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Position = new Vector2(-5, 25),
Current = { BindTarget = modSelect.SelectedMods }
}
};
});
public void SetUp() => Schedule(() => createDisplay(() => new TestModSelectOverlay()));
[SetUpSteps]
public void SetUpSteps()
@ -146,6 +125,46 @@ namespace osu.Game.Tests.Visual.UserInterface
});
}
[Test]
public void TestNonStacked()
{
changeRuleset(0);
AddStep("create overlay", () => createDisplay(() => new TestNonStackedModSelectOverlay()));
AddStep("show", () => modSelect.Show());
AddAssert("ensure all buttons are spread out", () => modSelect.ChildrenOfType<ModButton>().All(m => m.Mods.Length <= 1));
}
[Test]
public void TestChangeIsValidChangesButtonVisibility()
{
changeRuleset(0);
AddAssert("double time visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModDoubleTime)));
AddStep("make double time invalid", () => modSelect.IsValidMod = m => !(m is OsuModDoubleTime));
AddAssert("double time not visible", () => modSelect.ChildrenOfType<ModButton>().All(b => !b.Mods.Any(m => m is OsuModDoubleTime)));
AddAssert("nightcore still visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModNightcore)));
AddStep("make double time valid again", () => modSelect.IsValidMod = m => true);
AddAssert("double time visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModDoubleTime)));
AddAssert("nightcore still visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModNightcore)));
}
[Test]
public void TestChangeIsValidPreservesSelection()
{
changeRuleset(0);
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
AddAssert("DT + HD selected", () => modSelect.ChildrenOfType<ModButton>().Count(b => b.Selected) == 2);
AddStep("make NF invalid", () => modSelect.IsValidMod = m => !(m is ModNoFail));
AddAssert("DT + HD still selected", () => modSelect.ChildrenOfType<ModButton>().Count(b => b.Selected) == 2);
}
private void testSingleMod(Mod mod)
{
selectNext(mod);
@ -265,6 +284,28 @@ namespace osu.Game.Tests.Visual.UserInterface
private void checkLabelColor(Func<Color4> getColour) => AddAssert("check label has expected colour", () => modSelect.MultiplierLabel.Colour.AverageColour == getColour());
private void createDisplay(Func<TestModSelectOverlay> createOverlayFunc)
{
SelectedMods.Value = Array.Empty<Mod>();
Children = new Drawable[]
{
modSelect = createOverlayFunc().With(d =>
{
d.Origin = Anchor.BottomCentre;
d.Anchor = Anchor.BottomCentre;
d.SelectedMods.BindTarget = SelectedMods;
}),
modDisplay = new ModDisplay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Position = new Vector2(-5, 25),
Current = { BindTarget = modSelect.SelectedMods }
}
};
}
private class TestModSelectOverlay : SoloModSelectOverlay
{
public new Bindable<IReadOnlyList<Mod>> SelectedMods => base.SelectedMods;
@ -283,5 +324,10 @@ namespace osu.Game.Tests.Visual.UserInterface
public new Color4 LowMultiplierColour => base.LowMultiplierColour;
public new Color4 HighMultiplierColour => base.HighMultiplierColour;
}
private class TestNonStackedModSelectOverlay : TestModSelectOverlay
{
protected override bool Stacked => false;
}
}
}

View File

@ -151,7 +151,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("wait for ready", () => modSelect.State.Value == Visibility.Visible && modSelect.ButtonsLoaded);
}
private class TestModSelectOverlay : ModSelectOverlay
private class TestModSelectOverlay : SoloModSelectOverlay
{
public new VisibilityContainer ModSettingsContainer => base.ModSettingsContainer;
public new TriangleButton CustomiseButton => base.CustomiseButton;

View File

@ -18,6 +18,11 @@ namespace osu.Game.Graphics.UserInterface
public Color4 UncheckedColor { get; set; } = Color4.White;
public int FadeDuration { get; set; }
/// <summary>
/// Whether to play sounds when the state changes as a result of user interaction.
/// </summary>
protected virtual bool PlaySoundsOnUserChange => true;
public string LabelText
{
set
@ -96,10 +101,14 @@ namespace osu.Game.Graphics.UserInterface
protected override void OnUserChange(bool value)
{
base.OnUserChange(value);
if (value)
sampleChecked?.Play();
else
sampleUnchecked?.Play();
if (PlaySoundsOnUserChange)
{
if (value)
sampleChecked?.Play();
else
sampleUnchecked?.Play();
}
}
}
}

View File

@ -17,7 +17,7 @@ using osu.Game.Graphics;
namespace osu.Game.Overlays.Mods
{
public class ModSection : Container
public class ModSection : CompositeDrawable
{
private readonly Drawable header;
@ -47,7 +47,10 @@ namespace osu.Game.Overlays.Mods
if (m == null)
return new ModButtonEmpty();
return CreateModButton(m).With(b => b.SelectionChanged = Action);
return new ModButton(m)
{
SelectionChanged = Action,
};
}).ToArray();
modsLoadCts?.Cancel();
@ -91,12 +94,19 @@ namespace osu.Game.Overlays.Mods
return base.OnKeyDown(e);
}
/// <summary>
/// Selects all mods.
/// </summary>
public void SelectAll()
{
foreach (var button in buttons.Where(b => !b.Selected))
button.SelectAt(0);
}
/// <summary>
/// Deselects all mods.
/// </summary>
/// <param name="immediate">Set to true to bypass animations and update selections immediately.</param>
public void DeselectAll(bool immediate = false) => DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null), immediate);
/// <summary>
@ -163,7 +173,7 @@ namespace osu.Game.Overlays.Mods
Origin = Anchor.TopCentre;
Anchor = Anchor.TopCentre;
Children = new[]
InternalChildren = new[]
{
header = CreateHeader(type.Humanize(LetterCasing.Title)),
ButtonsContainer = new FillFlowContainer<ModButtonEmpty>
@ -182,8 +192,6 @@ namespace osu.Game.Overlays.Mods
};
}
protected virtual ModButton CreateModButton(Mod mod) => new ModButton(mod);
protected virtual Drawable CreateHeader(string text) => new OsuSpriteText
{
Font = OsuFont.GetFont(weight: FontWeight.Bold),

View File

@ -14,6 +14,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
@ -33,7 +34,6 @@ namespace osu.Game.Overlays.Mods
{
public const float HEIGHT = 510;
protected readonly FillFlowContainer FooterContainer;
protected readonly TriangleButton DeselectAllButton;
protected readonly TriangleButton CustomiseButton;
protected readonly TriangleButton CloseButton;
@ -41,23 +41,16 @@ namespace osu.Game.Overlays.Mods
protected readonly Drawable MultiplierSection;
protected readonly OsuSpriteText MultiplierLabel;
/// <summary>
/// Whether to allow customisation of mod settings.
/// </summary>
protected virtual bool AllowCustomisation => true;
/// <summary>
/// Whether mod icons should be stacked, or appear as individual buttons.
/// </summary>
protected virtual bool Stacked => true;
protected readonly FillFlowContainer FooterContainer;
protected override bool BlockNonPositionalInput => false;
protected override bool DimMainContent => false;
protected readonly FillFlowContainer<ModSection> ModSectionsContainer;
protected readonly ModSettingsContainer ModSettingsContainer;
/// <summary>
/// Whether <see cref="Mod"/>s underneath the same <see cref="MultiMod"/> instance should appear as stacked buttons.
/// </summary>
protected virtual bool Stacked => true;
[NotNull]
private Func<Mod, bool> isValidMod = m => true;
@ -76,6 +69,10 @@ namespace osu.Game.Overlays.Mods
}
}
protected readonly FillFlowContainer<ModSection> ModSectionsContainer;
protected readonly ModSettingsContainer ModSettingsContainer;
public readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
private Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods;
@ -301,7 +298,6 @@ namespace osu.Game.Overlays.Mods
CustomiseButton = new TriangleButton
{
Width = 180,
Alpha = AllowCustomisation ? 1 : 0,
Text = "Customisation",
Action = () => ModSettingsContainer.ToggleVisibility(),
Enabled = { Value = false },
@ -443,7 +439,7 @@ namespace osu.Game.Overlays.Mods
if (!Stacked)
modEnumeration = ModUtils.FlattenMods(modEnumeration);
section.Mods = modEnumeration.Select(validModOrNull).Where(m => m != null);
section.Mods = modEnumeration.Select(getValidModOrNull).Where(m => m != null);
}
updateSelectedButtons();
@ -458,13 +454,17 @@ namespace osu.Game.Overlays.Mods
/// <param name="mod">The <see cref="Mod"/> to check.</param>
/// <returns>A valid form of <paramref name="mod"/> if exists, or null otherwise.</returns>
[CanBeNull]
private Mod validModOrNull([NotNull] Mod mod)
private Mod getValidModOrNull([NotNull] Mod mod)
{
if (!(mod is MultiMod multi))
return IsValidMod(mod) ? mod : null;
var validSubset = multi.Mods.Select(validModOrNull).Where(m => m != null).ToArray();
return validSubset.Length == 0 ? null : new MultiMod(validSubset);
var validSubset = multi.Mods.Select(getValidModOrNull).Where(m => m != null).ToArray();
if (validSubset.Length == 0)
return null;
return validSubset.Length == 1 ? validSubset[0] : new MultiMod(validSubset);
}
private void updateSelectedButtons()
@ -496,11 +496,17 @@ namespace osu.Game.Overlays.Mods
MultiplierLabel.FadeColour(Color4.White, 200);
}
private ScheduledDelegate sampleOnDelegate;
private ScheduledDelegate sampleOffDelegate;
private void modButtonPressed(Mod selectedMod)
{
if (selectedMod != null)
{
if (State.Value == Visibility.Visible) sampleOn?.Play();
// Fixes buzzing when multiple mods are selected in the same frame.
sampleOnDelegate?.Cancel();
if (State.Value == Visibility.Visible)
sampleOnDelegate = Scheduler.Add(() => sampleOn?.Play());
OnModSelected(selectedMod);
@ -508,7 +514,10 @@ namespace osu.Game.Overlays.Mods
}
else
{
if (State.Value == Visibility.Visible) sampleOff?.Play();
// Fixes buzzing when multiple mods are deselected in the same frame.
sampleOffDelegate?.Cancel();
if (State.Value == Visibility.Visible)
sampleOffDelegate = Scheduler.Add(() => sampleOff?.Play());
}
refreshSelectedMods();
@ -524,6 +533,11 @@ namespace osu.Game.Overlays.Mods
private void refreshSelectedMods() => SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray();
/// <summary>
/// Creates a <see cref="ModSection"/> that groups <see cref="Mod"/>s with the same <see cref="ModType"/>.
/// </summary>
/// <param name="type">The <see cref="ModType"/> of <see cref="Mod"/>s in the section.</param>
/// <returns>The <see cref="ModSection"/>.</returns>
protected virtual ModSection CreateModSection(ModType type) => new ModSection(type);
#region Disposal

View File

@ -9,19 +9,25 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Screens.OnlinePlay.Match
namespace osu.Game.Screens.OnlinePlay
{
/// <summary>
/// A <see cref="ModSelectOverlay"/> used for free-mod selection in online play.
/// </summary>
public class FreeModSelectOverlay : ModSelectOverlay
{
protected override bool AllowCustomisation => false;
protected override bool Stacked => false;
public new Func<Mod, bool> IsValidMod
{
get => base.IsValidMod;
set => base.IsValidMod = m => m.HasImplementation && !m.RequiresConfiguration && !(m is ModAutoplay) && value(m);
}
public FreeModSelectOverlay()
{
IsValidMod = m => true;
CustomiseButton.Alpha = 0;
MultiplierSection.Alpha = 0;
DeselectAllButton.Alpha = 0;
@ -112,6 +118,8 @@ namespace osu.Game.Screens.OnlinePlay.Match
{
public Action<bool> Changed;
protected override bool PlaySoundsOnUserChange => false;
protected override void OnUserChange(bool value)
{
base.OnUserChange(value);

View File

@ -355,7 +355,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private class UserModSelectOverlay : ModSelectOverlay
{
protected override bool AllowCustomisation => false;
public UserModSelectOverlay()
{
CustomiseButton.Alpha = 0;
}
}
}
}