2022-03-27 05:43:17 +08:00
// 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.
2022-03-28 04:55:52 +08:00
#nullable enable
2022-03-27 05:43:17 +08:00
using System ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.Linq ;
using osu.Framework.Allocation ;
using osu.Framework.Bindables ;
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
using osu.Framework.Input.Events ;
using osu.Framework.Layout ;
2022-04-25 01:13:19 +08:00
using osu.Framework.Utils ;
2022-03-27 05:43:17 +08:00
using osu.Game.Configuration ;
2022-04-25 01:13:19 +08:00
using osu.Game.Graphics ;
2022-03-27 05:43:17 +08:00
using osu.Game.Graphics.Containers ;
using osu.Game.Graphics.UserInterface ;
using osu.Game.Rulesets.Mods ;
using osuTK ;
using osuTK.Input ;
namespace osu.Game.Overlays.Mods
{
2022-04-20 22:17:04 +08:00
public abstract class ModSelectScreen : ShearedOverlayContainer
2022-03-27 05:43:17 +08:00
{
2022-04-20 14:05:07 +08:00
protected override OverlayColourScheme ColourScheme = > OverlayColourScheme . Green ;
2022-03-27 05:43:17 +08:00
[Cached]
2022-03-27 06:21:17 +08:00
public Bindable < IReadOnlyList < Mod > > SelectedMods { get ; private set ; } = new Bindable < IReadOnlyList < Mod > > ( Array . Empty < Mod > ( ) ) ;
2022-03-27 05:43:17 +08:00
2022-03-28 04:55:52 +08:00
private Func < Mod , bool > isValidMod = m = > true ;
public Func < Mod , bool > IsValidMod
{
get = > isValidMod ;
set
{
isValidMod = value ? ? throw new ArgumentNullException ( nameof ( value ) ) ;
if ( IsLoaded )
updateAvailableMods ( ) ;
}
}
/// <summary>
/// Whether configurable <see cref="Mod"/>s can be configured by the local user.
/// </summary>
2022-04-20 22:17:29 +08:00
protected virtual bool AllowCustomisation = > true ;
2022-03-28 04:55:52 +08:00
/// <summary>
/// Whether the total score multiplier calculated from the current selected set of mods should be shown.
/// </summary>
protected virtual bool ShowTotalMultiplier = > true ;
protected virtual ModColumn CreateModColumn ( ModType modType , Key [ ] ? toggleKeys = null ) = > new ModColumn ( modType , false , toggleKeys ) ;
2022-03-27 05:43:17 +08:00
private readonly BindableBool customisationVisible = new BindableBool ( ) ;
2022-03-28 04:55:52 +08:00
private DifficultyMultiplierDisplay ? multiplierDisplay ;
private ModSettingsArea modSettingsArea = null ! ;
2022-04-27 04:35:18 +08:00
private ColumnScrollContainer columnScroll = null ! ;
2022-04-25 01:13:19 +08:00
private ColumnFlowContainer columnFlow = null ! ;
2022-04-04 14:45:44 +08:00
2022-03-27 05:43:17 +08:00
[BackgroundDependencyLoader]
private void load ( )
{
2022-04-20 15:08:00 +08:00
Header . Title = "Mod Select" ;
Header . Description = "Mods provide different ways to enjoy gameplay. Some have an effect on the score you can achieve during ranked play. Others are just for fun." ;
2022-03-27 05:43:17 +08:00
2022-04-20 14:57:45 +08:00
AddRange ( new Drawable [ ]
2022-03-27 05:43:17 +08:00
{
2022-04-20 14:57:45 +08:00
new ClickToReturnContainer
{
RelativeSizeAxes = Axes . Both ,
HandleMouse = { BindTarget = customisationVisible } ,
OnClicked = ( ) = > customisationVisible . Value = false
} ,
modSettingsArea = new ModSettingsArea
2022-03-27 05:43:17 +08:00
{
Anchor = Anchor . BottomCentre ,
2022-04-20 14:57:45 +08:00
Origin = Anchor . BottomCentre ,
Height = 0
}
} ) ;
2022-04-20 14:05:07 +08:00
MainAreaContent . AddRange ( new Drawable [ ]
2022-03-27 05:43:17 +08:00
{
2022-04-20 14:57:45 +08:00
new Container
{
2022-04-20 15:28:52 +08:00
Padding = new MarginPadding
2022-04-20 14:57:45 +08:00
{
2022-04-20 22:17:29 +08:00
Top = ( ShowTotalMultiplier ? DifficultyMultiplierDisplay . HEIGHT : 0 ) + PADDING ,
2022-04-20 14:57:45 +08:00
} ,
2022-03-27 05:43:17 +08:00
RelativeSizeAxes = Axes . Both ,
2022-04-20 14:57:45 +08:00
RelativePositionAxes = Axes . Both ,
2022-03-27 05:43:17 +08:00
Children = new Drawable [ ]
{
2022-04-27 04:35:18 +08:00
columnScroll = new ColumnScrollContainer
2022-03-27 05:43:17 +08:00
{
2022-04-27 03:57:19 +08:00
RelativeSizeAxes = Axes . Both ,
Masking = false ,
ClampExtension = 100 ,
ScrollbarOverlapsContent = false ,
Child = columnFlow = new ColumnFlowContainer
2022-03-27 05:43:17 +08:00
{
2022-04-20 14:57:45 +08:00
Direction = FillDirection . Horizontal ,
2022-04-20 15:30:58 +08:00
Shear = new Vector2 ( SHEAR , 0 ) ,
2022-04-20 14:57:45 +08:00
RelativeSizeAxes = Axes . Y ,
AutoSizeAxes = Axes . X ,
Spacing = new Vector2 ( 10 , 0 ) ,
2022-04-25 01:27:27 +08:00
Margin = new MarginPadding { Horizontal = 70 } ,
2022-04-20 14:57:45 +08:00
Children = new [ ]
2022-03-27 05:43:17 +08:00
{
2022-04-27 03:57:19 +08:00
createModColumnContent ( ModType . DifficultyReduction , new [ ] { Key . Q , Key . W , Key . E , Key . R , Key . T , Key . Y , Key . U , Key . I , Key . O , Key . P } ) ,
createModColumnContent ( ModType . DifficultyIncrease , new [ ] { Key . A , Key . S , Key . D , Key . F , Key . G , Key . H , Key . J , Key . K , Key . L } ) ,
createModColumnContent ( ModType . Automation , new [ ] { Key . Z , Key . X , Key . C , Key . V , Key . B , Key . N , Key . M } ) ,
createModColumnContent ( ModType . Conversion ) ,
createModColumnContent ( ModType . Fun )
2022-04-20 14:57:45 +08:00
}
2022-04-27 03:57:19 +08:00
}
}
2022-03-27 05:43:17 +08:00
}
}
2022-04-20 14:05:07 +08:00
} ) ;
2022-04-18 05:26:25 +08:00
2022-04-20 22:17:29 +08:00
if ( ShowTotalMultiplier )
2022-03-28 04:55:52 +08:00
{
2022-04-20 22:17:29 +08:00
MainAreaContent . Add ( new Container
{
Anchor = Anchor . TopRight ,
Origin = Anchor . TopRight ,
AutoSizeAxes = Axes . X ,
Height = DifficultyMultiplierDisplay . HEIGHT ,
Margin = new MarginPadding { Horizontal = 100 } ,
Child = multiplierDisplay = new DifficultyMultiplierDisplay
{
Anchor = Anchor . Centre ,
Origin = Anchor . Centre
} ,
} ) ;
}
if ( AllowCustomisation )
{
Footer . Add ( new ShearedToggleButton ( 200 )
{
Anchor = Anchor . BottomLeft ,
Origin = Anchor . BottomLeft ,
Margin = new MarginPadding { Vertical = PADDING , Left = 70 } ,
Text = "Mod Customisation" ,
Active = { BindTarget = customisationVisible }
} ) ;
}
2022-03-27 05:43:17 +08:00
}
2022-04-27 03:57:19 +08:00
private ColumnDimContainer createModColumnContent ( ModType modType , Key [ ] ? toggleKeys = null )
= > new ColumnDimContainer ( CreateModColumn ( modType , toggleKeys ) )
2022-04-25 01:13:19 +08:00
{
AutoSizeAxes = Axes . X ,
2022-04-27 03:57:19 +08:00
RelativeSizeAxes = Axes . Y ,
2022-04-28 19:03:54 +08:00
RequestScroll = column = > columnScroll . ScrollIntoView ( column , extraScroll : 140 )
2022-04-25 01:13:19 +08:00
} ;
2022-03-27 05:43:17 +08:00
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2022-03-27 06:21:17 +08:00
( ( IBindable < IReadOnlyList < Mod > > ) modSettingsArea . SelectedMods ) . BindTo ( SelectedMods ) ;
2022-03-28 06:16:10 +08:00
2022-03-27 06:21:17 +08:00
SelectedMods . BindValueChanged ( val = >
2022-03-27 05:43:17 +08:00
{
updateMultiplier ( ) ;
updateCustomisation ( val ) ;
2022-03-28 06:16:10 +08:00
updateSelectionFromBindable ( ) ;
2022-03-27 05:43:17 +08:00
} , true ) ;
2022-03-28 06:16:10 +08:00
2022-04-25 01:13:19 +08:00
foreach ( var column in columnFlow . Columns )
2022-03-28 06:16:10 +08:00
{
2022-04-21 05:34:43 +08:00
column . SelectedMods . BindValueChanged ( updateBindableFromSelection ) ;
2022-03-28 06:16:10 +08:00
}
2022-03-27 05:43:17 +08:00
customisationVisible . BindValueChanged ( _ = > updateCustomisationVisualState ( ) , true ) ;
2022-03-28 04:55:52 +08:00
updateAvailableMods ( ) ;
2022-03-27 05:43:17 +08:00
}
private void updateMultiplier ( )
{
2022-03-28 04:55:52 +08:00
if ( multiplierDisplay = = null )
return ;
2022-03-27 05:43:17 +08:00
double multiplier = 1.0 ;
2022-03-27 06:21:17 +08:00
foreach ( var mod in SelectedMods . Value )
2022-03-27 05:43:17 +08:00
multiplier * = mod . ScoreMultiplier ;
multiplierDisplay . Current . Value = multiplier ;
}
2022-03-28 04:55:52 +08:00
private void updateAvailableMods ( )
{
2022-04-25 01:13:19 +08:00
foreach ( var column in columnFlow . Columns )
2022-03-28 04:55:52 +08:00
column . Filter = isValidMod ;
}
2022-03-27 05:43:17 +08:00
private void updateCustomisation ( ValueChangedEvent < IReadOnlyList < Mod > > valueChangedEvent )
{
2022-04-20 22:17:29 +08:00
if ( ! AllowCustomisation )
2022-03-28 04:55:52 +08:00
return ;
2022-03-27 05:43:17 +08:00
bool anyCustomisableMod = false ;
bool anyModWithRequiredCustomisationAdded = false ;
2022-03-27 06:21:17 +08:00
foreach ( var mod in SelectedMods . Value )
2022-03-27 05:43:17 +08:00
{
anyCustomisableMod | = mod . GetSettingsSourceProperties ( ) . Any ( ) ;
anyModWithRequiredCustomisationAdded | = ! valueChangedEvent . OldValue . Contains ( mod ) & & mod . RequiresConfiguration ;
}
if ( anyCustomisableMod )
{
customisationVisible . Disabled = false ;
if ( anyModWithRequiredCustomisationAdded & & ! customisationVisible . Value )
customisationVisible . Value = true ;
}
else
{
if ( customisationVisible . Value )
customisationVisible . Value = false ;
customisationVisible . Disabled = true ;
}
}
private void updateCustomisationVisualState ( )
{
2022-04-05 15:56:24 +08:00
const double transition_duration = 300 ;
2022-04-04 14:50:40 +08:00
2022-04-20 14:57:45 +08:00
MainAreaContent . FadeColour ( customisationVisible . Value ? Colour4 . Gray : Colour4 . White , transition_duration , Easing . InOutCubic ) ;
2022-03-27 05:43:17 +08:00
float modAreaHeight = customisationVisible . Value ? ModSettingsArea . HEIGHT : 0 ;
2022-04-04 14:50:40 +08:00
modSettingsArea . ResizeHeightTo ( modAreaHeight , transition_duration , Easing . InOutCubic ) ;
2022-04-20 14:05:07 +08:00
TopLevelContent . MoveToY ( - modAreaHeight , transition_duration , Easing . InOutCubic ) ;
2022-03-27 05:43:17 +08:00
}
2022-03-28 06:16:10 +08:00
private void updateSelectionFromBindable ( )
{
2022-04-21 05:34:43 +08:00
// note that selectionBindableSyncInProgress is purposefully not checked here.
// this is because in the case of mod selection in solo gameplay, a user selection of a mod can actually lead to deselection of other incompatible mods.
// to synchronise state correctly, updateBindableFromSelection() computes the final mods (including incompatibility rules) and updates SelectedMods,
// and this method then runs unconditionally again to make sure the new visual selection accurately reflects the final set of selected mods.
// selectionBindableSyncInProgress ensures that mutual infinite recursion does not happen after that unconditional call.
2022-04-25 01:13:19 +08:00
foreach ( var column in columnFlow . Columns )
2022-03-28 06:16:10 +08:00
column . SelectedMods . Value = SelectedMods . Value . Where ( mod = > mod . Type = = column . ModType ) . ToArray ( ) ;
}
2022-04-21 05:34:43 +08:00
private bool selectionBindableSyncInProgress ;
private void updateBindableFromSelection ( ValueChangedEvent < IReadOnlyList < Mod > > modSelectionChange )
2022-03-28 06:16:10 +08:00
{
if ( selectionBindableSyncInProgress )
return ;
selectionBindableSyncInProgress = true ;
2022-04-21 05:34:43 +08:00
SelectedMods . Value = ComputeNewModsFromSelection (
modSelectionChange . NewValue . Except ( modSelectionChange . OldValue ) ,
modSelectionChange . OldValue . Except ( modSelectionChange . NewValue ) ) ;
2022-03-28 06:16:10 +08:00
selectionBindableSyncInProgress = false ;
}
2022-04-21 05:34:43 +08:00
protected virtual IReadOnlyList < Mod > ComputeNewModsFromSelection ( IEnumerable < Mod > addedMods , IEnumerable < Mod > removedMods )
2022-04-25 01:13:19 +08:00
= > columnFlow . Columns . SelectMany ( column = > column . SelectedMods . Value ) . ToArray ( ) ;
2022-04-21 05:34:43 +08:00
2022-03-27 05:43:17 +08:00
protected override void PopIn ( )
{
2022-04-05 17:38:31 +08:00
const double fade_in_duration = 400 ;
2022-04-04 14:45:44 +08:00
2022-03-27 05:43:17 +08:00
base . PopIn ( ) ;
2022-04-04 14:45:44 +08:00
2022-03-28 04:55:52 +08:00
multiplierDisplay ?
2022-04-05 17:38:31 +08:00
. Delay ( fade_in_duration * 0.65f )
. FadeIn ( fade_in_duration / 2 , Easing . OutQuint )
2022-04-05 17:25:27 +08:00
. ScaleTo ( 1 , fade_in_duration , Easing . OutElastic ) ;
2022-04-05 17:27:34 +08:00
for ( int i = 0 ; i < columnFlow . Count ; i + + )
{
2022-04-25 01:13:19 +08:00
columnFlow [ i ] . Column
. TopLevelContent
2022-04-05 17:27:34 +08:00
. Delay ( i * 30 )
2022-04-05 17:38:31 +08:00
. MoveToY ( 0 , fade_in_duration , Easing . OutQuint )
. FadeIn ( fade_in_duration , Easing . OutQuint ) ;
2022-04-05 17:27:34 +08:00
}
2022-03-27 05:43:17 +08:00
}
protected override void PopOut ( )
{
2022-04-04 14:45:44 +08:00
const double fade_out_duration = 500 ;
2022-03-27 05:43:17 +08:00
base . PopOut ( ) ;
2022-04-04 14:45:44 +08:00
2022-03-28 04:55:52 +08:00
multiplierDisplay ?
2022-04-05 17:38:31 +08:00
. FadeOut ( fade_out_duration / 2 , Easing . OutQuint )
2022-04-05 17:25:27 +08:00
. ScaleTo ( 0.75f , fade_out_duration , Easing . OutQuint ) ;
2022-04-05 17:27:34 +08:00
for ( int i = 0 ; i < columnFlow . Count ; i + + )
{
const float distance = 700 ;
2022-04-25 01:13:19 +08:00
columnFlow [ i ] . Column
. TopLevelContent
2022-04-05 17:38:31 +08:00
. MoveToY ( i % 2 = = 0 ? - distance : distance , fade_out_duration , Easing . OutQuint )
. FadeOut ( fade_out_duration , Easing . OutQuint ) ;
2022-04-05 17:27:34 +08:00
}
2022-03-27 05:43:17 +08:00
}
2022-04-27 16:10:27 +08:00
internal class ColumnScrollContainer : OsuScrollContainer < ColumnFlowContainer >
2022-04-27 03:57:19 +08:00
{
2022-04-27 04:35:18 +08:00
public ColumnScrollContainer ( )
: base ( Direction . Horizontal )
{
}
2022-04-27 04:15:58 +08:00
2022-04-27 04:35:18 +08:00
protected override void Update ( )
2022-04-27 03:57:19 +08:00
{
2022-04-27 04:35:18 +08:00
base . Update ( ) ;
// the bounds below represent the horizontal range of scroll items to be considered fully visible/active, in the scroll's internal coordinate space.
// note that clamping is applied to the left scroll bound to ensure scrolling past extents does not change the set of active columns.
2022-04-27 04:43:58 +08:00
float leftVisibleBound = Math . Clamp ( Current , 0 , ScrollableExtent ) ;
float rightVisibleBound = leftVisibleBound + DrawWidth ;
// if a movement is occurring at this time, the bounds below represent the full range of columns that the scroll movement will encompass.
// this will be used to ensure that columns do not change state from active to inactive back and forth until they are fully scrolled past.
float leftMovementBound = Math . Min ( Current , Target ) ;
float rightMovementBound = Math . Max ( Current , Target ) + DrawWidth ;
2022-04-27 04:35:18 +08:00
foreach ( var column in Child )
{
// DrawWidth/DrawPosition do not include shear effects, and we want to know the full extents of the columns post-shear,
// so we have to manually compensate.
2022-04-27 04:54:54 +08:00
var topLeft = column . ToSpaceOfOtherDrawable ( Vector2 . Zero , ScrollContent ) ;
var bottomRight = column . ToSpaceOfOtherDrawable ( new Vector2 ( column . DrawWidth - column . DrawHeight * SHEAR , 0 ) , ScrollContent ) ;
2022-04-27 04:35:18 +08:00
2022-04-27 04:43:58 +08:00
bool isCurrentlyVisible = Precision . AlmostBigger ( topLeft . X , leftVisibleBound )
2022-04-27 04:54:54 +08:00
& & Precision . DefinitelyBigger ( rightVisibleBound , bottomRight . X ) ;
2022-04-27 04:43:58 +08:00
bool isBeingScrolledToward = Precision . AlmostBigger ( topLeft . X , leftMovementBound )
2022-04-27 04:54:54 +08:00
& & Precision . DefinitelyBigger ( rightMovementBound , bottomRight . X ) ;
2022-04-27 04:43:58 +08:00
column . Active . Value = isCurrentlyVisible | | isBeingScrolledToward ;
2022-04-27 04:35:18 +08:00
}
2022-04-27 03:57:19 +08:00
}
}
2022-04-27 16:10:27 +08:00
internal class ColumnFlowContainer : FillFlowContainer < ColumnDimContainer >
2022-03-27 05:43:17 +08:00
{
2022-04-25 01:13:19 +08:00
public IEnumerable < ModColumn > Columns = > Children . Select ( dimWrapper = > dimWrapper . Column ) ;
2022-03-27 05:43:17 +08:00
private readonly LayoutValue drawSizeLayout = new LayoutValue ( Invalidation . DrawSize ) ;
2022-04-25 01:13:19 +08:00
public ColumnFlowContainer ( )
2022-03-27 05:43:17 +08:00
{
AddLayout ( drawSizeLayout ) ;
}
2022-04-25 01:13:19 +08:00
public override void Add ( ColumnDimContainer dimContainer )
2022-03-27 05:43:17 +08:00
{
2022-04-25 01:13:19 +08:00
base . Add ( dimContainer ) ;
2022-03-27 05:43:17 +08:00
2022-04-25 01:13:19 +08:00
Debug . Assert ( dimContainer ! = null ) ;
dimContainer . Column . Shear = Vector2 . Zero ;
2022-03-27 05:43:17 +08:00
}
protected override void Update ( )
{
base . Update ( ) ;
if ( ! drawSizeLayout . IsValid )
{
Padding = new MarginPadding
{
2022-04-20 15:30:58 +08:00
Left = DrawHeight * SHEAR ,
2022-03-27 05:43:17 +08:00
Bottom = 10
} ;
drawSizeLayout . Validate ( ) ;
}
}
}
2022-04-27 05:11:38 +08:00
internal class ColumnDimContainer : Container
2022-04-25 01:13:19 +08:00
{
public ModColumn Column { get ; }
2022-04-27 03:57:19 +08:00
public readonly Bindable < bool > Active = new BindableBool ( ) ;
public Action < ColumnDimContainer > ? RequestScroll { get ; set ; }
2022-04-25 01:13:19 +08:00
[Resolved]
private OsuColour colours { get ; set ; } = null ! ;
2022-04-27 03:57:19 +08:00
public ColumnDimContainer ( ModColumn column )
2022-04-25 01:13:19 +08:00
{
Child = Column = column ;
2022-04-27 03:57:19 +08:00
column . Active . BindTo ( Active ) ;
2022-04-25 01:13:19 +08:00
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2022-04-27 03:57:19 +08:00
Active . BindValueChanged ( _ = > updateDim ( ) , true ) ;
2022-04-25 01:13:19 +08:00
FinishTransforms ( ) ;
}
private void updateDim ( )
{
Colour4 targetColour ;
2022-04-27 03:57:19 +08:00
if ( Active . Value )
2022-04-25 01:13:19 +08:00
targetColour = Colour4 . White ;
else
targetColour = IsHovered ? colours . GrayC : colours . Gray8 ;
2022-04-28 13:59:39 +08:00
this . FadeColour ( targetColour , 800 , Easing . OutQuint ) ;
2022-04-25 01:13:19 +08:00
}
2022-04-27 04:01:24 +08:00
protected override bool OnClick ( ClickEvent e )
2022-04-25 01:13:19 +08:00
{
2022-04-27 03:57:19 +08:00
if ( ! Active . Value )
RequestScroll ? . Invoke ( this ) ;
2022-04-25 01:13:19 +08:00
return true ;
}
protected override bool OnHover ( HoverEvent e )
{
base . OnHover ( e ) ;
updateDim ( ) ;
2022-04-27 03:57:19 +08:00
return Active . Value ;
2022-04-25 01:13:19 +08:00
}
protected override void OnHoverLost ( HoverLostEvent e )
{
base . OnHoverLost ( e ) ;
updateDim ( ) ;
}
}
2022-03-27 05:43:17 +08:00
private class ClickToReturnContainer : Container
{
public BindableBool HandleMouse { get ; } = new BindableBool ( ) ;
2022-03-28 04:55:52 +08:00
public Action ? OnClicked { get ; set ; }
2022-03-27 05:43:17 +08:00
protected override bool Handle ( UIEvent e )
{
if ( ! HandleMouse . Value )
return base . Handle ( e ) ;
switch ( e )
{
case ClickEvent _ :
OnClicked ? . Invoke ( ) ;
return true ;
case MouseEvent _ :
return true ;
}
return base . Handle ( e ) ;
}
}
}
}