osu/osu.Game/Overlays/Mods/ModSelectOverlay.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

533 lines
24 KiB
C#
Raw Normal View History

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2018-04-13 09:19:50 +00:00
using System;
using System.Collections.Generic;
using System.Linq;
2021-02-02 11:47:50 +00:00
using JetBrains.Annotations;
2019-06-07 06:58:24 +00:00
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
2019-02-21 10:04:31 +00:00
using osu.Framework.Bindables;
2019-06-07 06:58:24 +00:00
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
2019-06-07 06:58:24 +00:00
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
2019-06-07 06:58:24 +00:00
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Resources.Localisation.Web;
2019-06-07 06:58:24 +00:00
using osu.Game.Rulesets.Mods;
2019-01-25 05:10:59 +00:00
using osu.Game.Screens;
2021-02-02 11:27:41 +00:00
using osu.Game.Utils;
2019-06-07 06:58:24 +00:00
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
2018-04-13 09:19:50 +00:00
2017-02-23 02:16:23 +00:00
namespace osu.Game.Overlays.Mods
{
2021-02-02 11:58:31 +00:00
public abstract class ModSelectOverlay : WaveOverlayContainer
2017-02-23 02:16:23 +00:00
{
2020-02-04 01:21:06 +00:00
public const float HEIGHT = 510;
protected readonly TriangleButton DeselectAllButton;
2019-12-06 09:57:11 +00:00
protected readonly TriangleButton CustomiseButton;
protected readonly TriangleButton CloseButton;
2019-06-07 06:58:24 +00:00
protected readonly FillFlowContainer FooterContainer;
protected override bool BlockNonPositionalInput => false;
2018-04-13 09:19:50 +00:00
2019-03-02 05:48:05 +00:00
protected override bool DimMainContent => false;
2021-02-02 11:27:41 +00:00
/// <summary>
/// Whether <see cref="Mod"/>s underneath the same <see cref="MultiMod"/> instance should appear as stacked buttons.
/// </summary>
protected virtual bool Stacked => true;
2021-02-10 10:56:59 +00:00
/// <summary>
/// Whether configurable <see cref="Mod"/>s can be configured by the local user.
/// </summary>
protected virtual bool AllowConfiguration => true;
2021-02-02 11:47:50 +00:00
[NotNull]
private Func<Mod, bool> isValidMod = m => true;
/// <summary>
/// A function that checks whether a given mod is selectable.
/// </summary>
[NotNull]
public Func<Mod, bool> IsValidMod
{
get => isValidMod;
set
{
isValidMod = value ?? throw new ArgumentNullException(nameof(value));
updateAvailableMods();
}
}
protected readonly FillFlowContainer<ModSection> ModSectionsContainer;
2018-04-13 09:19:50 +00:00
protected readonly ModSettingsContainer ModSettingsContainer;
[Cached]
public readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
2018-04-13 09:19:50 +00:00
private Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods;
2018-04-13 09:19:50 +00:00
2019-06-07 06:58:24 +00:00
protected Color4 LowMultiplierColour;
protected Color4 HighMultiplierColour;
2018-04-13 09:19:50 +00:00
2019-06-07 06:58:24 +00:00
private const float content_width = 0.8f;
private const float footer_button_spacing = 20;
2021-01-19 08:11:40 +00:00
private Sample sampleOn, sampleOff;
2018-04-13 09:19:50 +00:00
2021-02-02 11:58:31 +00:00
protected ModSelectOverlay()
2017-02-23 02:16:23 +00:00
{
Waves.FirstWaveColour = Color4Extensions.FromHex(@"19b0e2");
Waves.SecondWaveColour = Color4Extensions.FromHex(@"2280a2");
Waves.ThirdWaveColour = Color4Extensions.FromHex(@"005774");
Waves.FourthWaveColour = Color4Extensions.FromHex(@"003a4e");
2018-04-13 09:19:50 +00:00
RelativeSizeAxes = Axes.X;
Height = HEIGHT;
2019-01-25 05:10:59 +00:00
Padding = new MarginPadding { Horizontal = -OsuScreen.HORIZONTAL_OVERFLOW_PADDING };
2017-02-23 02:16:23 +00:00
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = new Color4(36, 50, 68, 255)
},
new Triangles
{
TriangleScale = 5,
RelativeSizeAxes = Axes.Both,
2017-02-23 02:16:23 +00:00
ColourLight = new Color4(53, 66, 82, 255),
ColourDark = new Color4(41, 54, 70, 255),
},
},
},
new GridContainer
2017-02-23 02:16:23 +00:00
{
RelativeSizeAxes = Axes.Both,
2017-02-23 02:16:23 +00:00
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RowDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 90),
2021-07-05 15:52:39 +00:00
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
2017-02-23 02:16:23 +00:00
{
new Drawable[]
2017-02-23 02:16:23 +00:00
{
new Container
2017-02-23 02:16:23 +00:00
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Children = new Drawable[]
2017-02-23 02:16:23 +00:00
{
new Box
2017-02-23 02:16:23 +00:00
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(10).Opacity(100),
2017-02-23 02:16:23 +00:00
},
new FillFlowContainer
2017-02-23 02:16:23 +00:00
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Width = content_width,
2019-01-25 05:10:59 +00:00
Padding = new MarginPadding { Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING },
Children = new Drawable[]
2017-02-23 02:16:23 +00:00
{
new OsuSpriteText
2017-02-23 02:16:23 +00:00
{
Text = @"Gameplay Mods",
Font = OsuFont.GetFont(size: 22, weight: FontWeight.Bold),
Shadow = true,
Margin = new MarginPadding
{
Bottom = 4,
},
},
new OsuTextFlowContainer(text =>
{
text.Font = text.Font.With(size: 18);
text.Shadow = true;
})
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Text = "Mods provide different ways to enjoy gameplay. Some have an effect on the score you can achieve during ranked play.\nOthers are just for fun.",
2017-02-23 02:16:23 +00:00
},
},
},
},
},
},
new Drawable[]
2017-02-23 02:16:23 +00:00
{
new Container
2017-03-06 16:19:38 +00:00
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
2017-03-06 16:19:38 +00:00
{
// Body
new OsuScrollContainer
{
ScrollbarVisible = false,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Vertical = 10,
Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING
},
Children = new Drawable[]
{
ModSectionsContainer = new FillFlowContainer<ModSection>
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0f, 10f),
Width = content_width,
LayoutDuration = 200,
LayoutEasing = Easing.OutQuint,
Children = new[]
{
CreateModSection(ModType.DifficultyReduction).With(s =>
{
s.ToggleKeys = new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P };
s.Action = modButtonPressed;
}),
CreateModSection(ModType.DifficultyIncrease).With(s =>
{
s.ToggleKeys = new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L };
s.Action = modButtonPressed;
}),
CreateModSection(ModType.Automation).With(s =>
{
s.ToggleKeys = new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M };
s.Action = modButtonPressed;
}),
CreateModSection(ModType.Conversion).With(s =>
{
s.Action = modButtonPressed;
}),
CreateModSection(ModType.Fun).With(s =>
{
s.Action = modButtonPressed;
}),
}
},
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Padding = new MarginPadding(30),
Width = 0.3f,
Children = new Drawable[]
{
ModSettingsContainer = new ModSettingsContainer
{
Alpha = 0,
SelectedMods = { BindTarget = SelectedMods },
},
}
},
}
},
2017-02-23 02:16:23 +00:00
},
new Drawable[]
2017-02-23 02:16:23 +00:00
{
new Container
2017-02-23 02:16:23 +00:00
{
Name = "Footer content",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Children = new Drawable[]
2017-02-23 02:16:23 +00:00
{
new Box
2017-02-23 02:16:23 +00:00
{
RelativeSizeAxes = Axes.Both,
Colour = new Color4(172, 20, 116, 255),
Alpha = 0.5f,
2017-02-23 02:16:23 +00:00
},
FooterContainer = new FillFlowContainer
2017-02-23 02:16:23 +00:00
{
Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
RelativePositionAxes = Axes.X,
Width = content_width,
Spacing = new Vector2(footer_button_spacing, footer_button_spacing / 2),
Padding = new MarginPadding
{
Vertical = 15,
2019-01-25 05:10:59 +00:00
Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING
},
Children = new[]
2017-02-23 02:16:23 +00:00
{
DeselectAllButton = new TriangleButton
{
Width = 180,
Text = "Deselect All",
Action = deselectAll,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
},
2019-12-06 09:57:11 +00:00
CustomiseButton = new TriangleButton
{
Width = 180,
2019-12-11 04:19:13 +00:00
Text = "Customisation",
Action = () => ModSettingsContainer.ToggleVisibility(),
Enabled = { Value = false },
2021-02-10 10:56:59 +00:00
Alpha = AllowConfiguration ? 1 : 0,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
},
CloseButton = new TriangleButton
{
Width = 180,
Text = CommonStrings.ButtonsClose,
Action = Hide,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
},
}
}
},
}
2017-02-23 02:16:23 +00:00
},
},
},
};
((IBindable<bool>)CustomiseButton.Enabled).BindTo(ModSettingsContainer.HasSettingsForSelection);
2017-02-23 02:16:23 +00:00
}
2019-06-07 06:58:24 +00:00
[BackgroundDependencyLoader(true)]
2021-06-25 07:36:31 +00:00
private void load(AudioManager audio, OsuGameBase osu)
2019-06-07 06:58:24 +00:00
{
availableMods = osu.AvailableMods.GetBoundCopy();
2019-06-07 06:58:24 +00:00
sampleOn = audio.Samples.Get(@"UI/check-on");
sampleOff = audio.Samples.Get(@"UI/check-off");
}
private void deselectAll()
2019-06-07 06:58:24 +00:00
{
foreach (var section in ModSectionsContainer.Children)
section.DeselectAll();
refreshSelectedMods();
}
protected override void LoadComplete()
{
base.LoadComplete();
2021-02-02 11:47:50 +00:00
availableMods.BindValueChanged(_ => updateAvailableMods(), true);
// intentionally bound after the above line to avoid a potential update feedback cycle.
// i haven't actually observed this happening but as updateAvailableMods() changes the selection it is plausible.
SelectedMods.BindValueChanged(_ => updateSelectedButtons());
2019-06-07 06:58:24 +00:00
}
protected override void PopOut()
{
base.PopOut();
2021-02-04 10:12:37 +00:00
foreach (var section in ModSectionsContainer)
{
section.FlushAnimation();
}
FooterContainer.MoveToX(content_width, WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
FooterContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
2019-06-07 06:58:24 +00:00
foreach (var section in ModSectionsContainer.Children)
{
section.ButtonsContainer.TransformSpacingTo(new Vector2(100f, 0f), WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
section.ButtonsContainer.MoveToX(100f, WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
section.ButtonsContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
}
}
protected override void PopIn()
{
base.PopIn();
FooterContainer.MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint);
FooterContainer.FadeIn(WaveContainer.APPEAR_DURATION, Easing.OutQuint);
2019-06-07 06:58:24 +00:00
foreach (var section in ModSectionsContainer.Children)
{
section.ButtonsContainer.TransformSpacingTo(new Vector2(50f, 0f), WaveContainer.APPEAR_DURATION, Easing.OutQuint);
section.ButtonsContainer.MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint);
section.ButtonsContainer.FadeIn(WaveContainer.APPEAR_DURATION, Easing.OutQuint);
}
}
protected override bool OnKeyDown(KeyDownEvent e)
{
// don't absorb control as ToolbarRulesetSelector uses control + number to navigate
if (e.ControlPressed) return false;
2019-06-07 06:58:24 +00:00
switch (e.Key)
{
case Key.Number1:
2021-08-04 08:27:44 +00:00
DeselectAllButton.TriggerClick();
2019-06-07 06:58:24 +00:00
return true;
case Key.Number2:
2021-08-04 08:27:44 +00:00
CloseButton.TriggerClick();
2019-06-07 06:58:24 +00:00
return true;
}
return base.OnKeyDown(e);
}
2021-09-16 09:26:12 +00:00
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e) => false; // handled by back button
2020-07-15 04:17:22 +00:00
2021-02-02 11:47:50 +00:00
private void updateAvailableMods()
2019-06-07 06:58:24 +00:00
{
2021-02-02 11:47:50 +00:00
if (availableMods?.Value == null)
return;
2019-06-07 06:58:24 +00:00
foreach (var section in ModSectionsContainer.Children)
2021-02-02 11:27:41 +00:00
{
2021-02-02 11:35:31 +00:00
IEnumerable<Mod> modEnumeration = availableMods.Value[section.ModType];
if (!Stacked)
modEnumeration = ModUtils.FlattenMods(modEnumeration);
section.Mods = modEnumeration.Select(getValidModOrNull).Where(m => m != null).Select(m => m.DeepClone());
2021-02-02 11:27:41 +00:00
}
updateSelectedButtons();
2021-02-22 06:47:47 +00:00
OnAvailableModsChanged();
2019-06-07 06:58:24 +00:00
}
2021-02-02 11:47:50 +00:00
/// <summary>
/// Returns a valid form of a given <see cref="Mod"/> if possible, or null otherwise.
/// </summary>
/// <remarks>
/// This is a recursive process during which any invalid mods are culled while preserving <see cref="MultiMod"/> structures where possible.
/// </remarks>
/// <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 getValidModOrNull([NotNull] Mod mod)
{
if (!(mod is MultiMod multi))
return IsValidMod(mod) ? mod : null;
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);
2019-06-07 06:58:24 +00:00
}
private void updateSelectedButtons()
2019-06-07 06:58:24 +00:00
{
// Enumeration below may update the bindable list.
var selectedMods = SelectedMods.Value.ToList();
2019-06-07 06:58:24 +00:00
foreach (var section in ModSectionsContainer.Children)
section.UpdateSelectedButtons(selectedMods);
2019-06-07 06:58:24 +00:00
}
private void modButtonPressed(Mod selectedMod)
{
if (selectedMod != null)
{
2021-02-02 12:20:16 +00:00
if (State.Value == Visibility.Visible)
2021-02-04 08:06:11 +00:00
Scheduler.AddOnce(playSelectedSound);
2020-01-21 04:30:11 +00:00
OnModSelected(selectedMod);
2020-01-21 04:30:11 +00:00
2021-02-10 10:56:59 +00:00
if (selectedMod.RequiresConfiguration && AllowConfiguration)
ModSettingsContainer.Show();
2019-06-07 06:58:24 +00:00
}
else
{
2021-02-02 12:20:16 +00:00
if (State.Value == Visibility.Visible)
2021-02-04 08:06:11 +00:00
Scheduler.AddOnce(playDeselectedSound);
2019-06-07 06:58:24 +00:00
}
refreshSelectedMods();
}
2021-02-04 08:06:11 +00:00
private void playSelectedSound() => sampleOn?.Play();
private void playDeselectedSound() => sampleOff?.Play();
2021-02-22 06:47:47 +00:00
/// <summary>
/// Invoked after <see cref="availableMods"/> has changed.
/// </summary>
protected virtual void OnAvailableModsChanged()
{
}
/// <summary>
/// Invoked when a new <see cref="Mod"/> has been selected.
/// </summary>
/// <param name="mod">The <see cref="Mod"/> that has been selected.</param>
protected virtual void OnModSelected(Mod mod)
{
}
2019-06-07 06:58:24 +00:00
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);
2019-06-07 06:58:24 +00:00
#region Disposal
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
availableMods?.UnbindAll();
SelectedMods?.UnbindAll();
2019-06-07 06:58:24 +00:00
}
#endregion
2017-02-23 02:16:23 +00:00
}
}