mirror of
https://github.com/ppy/osu.git
synced 2025-01-13 19:04:06 +08:00
Merge branch 'freemod-select-overlay' into freemods
This commit is contained in:
commit
6453367a9c
@ -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
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
@ -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);
|
@ -355,7 +355,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
|
||||
private class UserModSelectOverlay : ModSelectOverlay
|
||||
{
|
||||
protected override bool AllowCustomisation => false;
|
||||
public UserModSelectOverlay()
|
||||
{
|
||||
CustomiseButton.Alpha = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user