-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add combo colour override control to editor
Closes #25608. Logic mostly matching stable. All operations are done on `ComboOffset` which still makes overridden combo colours weirdly relatively dependent on each other rather than them be an "absolute" choice, but alas... As per stable, two consecutive new combos can use the same colour only if they are separated by a break: https://github.com/peppy/osu-stable-reference/blob/52f3f75ed7efd7b9eb56e1e45c95bb91504337be/osu!/GameModes/Edit/Modes/EditorModeCompose.cs#L4564-L4571 This control is only available once the user has changed the combo colours from defaults; additionally, only a single new combo object must be selected for the colour selector to show up.
- Loading branch information
Showing
6 changed files
with
289 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
278 changes: 278 additions & 0 deletions
278
osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,278 @@ | ||
// 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. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.Linq; | ||
using osu.Framework.Allocation; | ||
using osu.Framework.Bindables; | ||
using osu.Framework.Extensions; | ||
using osu.Framework.Extensions.Color4Extensions; | ||
using osu.Framework.Extensions.ObjectExtensions; | ||
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.UserInterface; | ||
using osu.Game.Graphics; | ||
using osu.Game.Graphics.Containers; | ||
using osu.Game.Graphics.UserInterface; | ||
using osu.Game.Graphics.UserInterfaceV2; | ||
using osu.Game.Overlays; | ||
using osu.Game.Rulesets.Objects; | ||
using osu.Game.Rulesets.Objects.Types; | ||
using osuTK; | ||
|
||
namespace osu.Game.Screens.Edit.Components.TernaryButtons | ||
{ | ||
public partial class NewComboTernaryButton : CompositeDrawable, IHasCurrentValue<TernaryState> | ||
{ | ||
public Bindable<TernaryState> Current | ||
{ | ||
get => current.Current; | ||
set => current.Current = value; | ||
} | ||
|
||
private readonly BindableWithCurrent<TernaryState> current = new BindableWithCurrent<TernaryState>(); | ||
|
||
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>(); | ||
private readonly BindableList<Colour4> comboColours = new BindableList<Colour4>(); | ||
|
||
private Container mainButtonContainer = null!; | ||
private ColourPickerButton pickerButton = null!; | ||
|
||
[BackgroundDependencyLoader] | ||
private void load(EditorBeatmap editorBeatmap) | ||
{ | ||
RelativeSizeAxes = Axes.X; | ||
AutoSizeAxes = Axes.Y; | ||
InternalChildren = new Drawable[] | ||
{ | ||
mainButtonContainer = new Container | ||
{ | ||
RelativeSizeAxes = Axes.X, | ||
AutoSizeAxes = Axes.Y, | ||
Padding = new MarginPadding { Right = 30 }, | ||
Child = new DrawableTernaryButton | ||
{ | ||
Current = Current, | ||
Description = "New combo", | ||
CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA }, | ||
}, | ||
}, | ||
pickerButton = new ColourPickerButton | ||
{ | ||
Anchor = Anchor.CentreRight, | ||
Origin = Anchor.CentreRight, | ||
Width = 25, | ||
ComboColours = { BindTarget = comboColours } | ||
} | ||
}; | ||
|
||
selectedHitObjects.BindTo(editorBeatmap.SelectedHitObjects); | ||
if (editorBeatmap.BeatmapSkin != null) | ||
comboColours.BindTo(editorBeatmap.BeatmapSkin.ComboColours); | ||
} | ||
|
||
protected override void LoadComplete() | ||
{ | ||
base.LoadComplete(); | ||
|
||
selectedHitObjects.BindCollectionChanged((_, _) => updateState()); | ||
comboColours.BindCollectionChanged((_, _) => updateState()); | ||
Current.BindValueChanged(_ => updateState(), true); | ||
} | ||
|
||
private void updateState() | ||
{ | ||
if (Current.Value == TernaryState.True && selectedHitObjects.Count == 1 && selectedHitObjects.Single() is IHasComboInformation hasCombo && comboColours.Count > 1) | ||
{ | ||
mainButtonContainer.Padding = new MarginPadding { Right = 30 }; | ||
pickerButton.SelectedHitObject.Value = hasCombo; | ||
pickerButton.Alpha = 1; | ||
} | ||
else | ||
{ | ||
mainButtonContainer.Padding = new MarginPadding(); | ||
pickerButton.Alpha = 0; | ||
} | ||
} | ||
|
||
private partial class ColourPickerButton : OsuButton, IHasPopover | ||
{ | ||
public BindableList<Colour4> ComboColours { get; } = new BindableList<Colour4>(); | ||
public Bindable<IHasComboInformation?> SelectedHitObject { get; } = new Bindable<IHasComboInformation?>(); | ||
|
||
[Resolved] | ||
private EditorBeatmap editorBeatmap { get; set; } = null!; | ||
|
||
[Resolved] | ||
private OverlayColourProvider colourProvider { get; set; } = null!; | ||
|
||
private SpriteIcon icon = null!; | ||
|
||
[BackgroundDependencyLoader] | ||
private void load() | ||
{ | ||
Add(icon = new SpriteIcon | ||
{ | ||
Icon = FontAwesome.Solid.Palette, | ||
Size = new Vector2(16), | ||
Anchor = Anchor.Centre, | ||
Origin = Anchor.Centre, | ||
}); | ||
|
||
Action = this.ShowPopover; | ||
} | ||
|
||
protected override void LoadComplete() | ||
{ | ||
base.LoadComplete(); | ||
ComboColours.BindCollectionChanged((_, _) => updateState()); | ||
SelectedHitObject.BindValueChanged(val => | ||
{ | ||
if (val.OldValue != null) | ||
val.OldValue.ComboIndexWithOffsetsBindable.ValueChanged -= onComboIndexChanged; | ||
|
||
updateState(); | ||
|
||
if (val.NewValue != null) | ||
val.NewValue.ComboIndexWithOffsetsBindable.ValueChanged += onComboIndexChanged; | ||
}, true); | ||
} | ||
|
||
private void onComboIndexChanged(ValueChangedEvent<int> _) => updateState(); | ||
|
||
private void updateState() | ||
{ | ||
if (SelectedHitObject.Value == null) | ||
{ | ||
BackgroundColour = colourProvider.Background3; | ||
icon.Colour = BackgroundColour.Darken(0.5f); | ||
icon.Blending = BlendingParameters.Additive; | ||
Enabled.Value = false; | ||
} | ||
else | ||
{ | ||
BackgroundColour = ComboColours[comboIndexFor(SelectedHitObject.Value, ComboColours)]; | ||
icon.Colour = OsuColour.ForegroundTextColourFor(BackgroundColour); | ||
icon.Blending = BlendingParameters.Inherit; | ||
Enabled.Value = true; | ||
} | ||
} | ||
|
||
public Popover GetPopover() => new ComboColourPalettePopover(ComboColours, SelectedHitObject.Value.AsNonNull(), editorBeatmap); | ||
} | ||
|
||
private partial class ComboColourPalettePopover : OsuPopover | ||
{ | ||
private readonly IReadOnlyList<Colour4> comboColours; | ||
private readonly IHasComboInformation hasComboInformation; | ||
private readonly EditorBeatmap editorBeatmap; | ||
|
||
public ComboColourPalettePopover(IReadOnlyList<Colour4> comboColours, IHasComboInformation hasComboInformation, EditorBeatmap editorBeatmap) | ||
{ | ||
this.comboColours = comboColours; | ||
this.hasComboInformation = hasComboInformation; | ||
this.editorBeatmap = editorBeatmap; | ||
|
||
AllowableAnchors = [Anchor.CentreRight]; | ||
} | ||
|
||
[BackgroundDependencyLoader] | ||
private void load() | ||
{ | ||
Debug.Assert(comboColours.Count > 0); | ||
var hitObject = hasComboInformation as HitObject; | ||
Debug.Assert(hitObject != null); | ||
|
||
FillFlowContainer container; | ||
|
||
Child = container = new FillFlowContainer | ||
{ | ||
Width = 230, | ||
AutoSizeAxes = Axes.Y, | ||
Spacing = new Vector2(10), | ||
}; | ||
|
||
int selectedColourIndex = comboIndexFor(hasComboInformation, comboColours); | ||
|
||
for (int i = 0; i < comboColours.Count; i++) | ||
{ | ||
int index = i; | ||
|
||
if (getPreviousHitObjectWithCombo(editorBeatmap, hitObject) is IHasComboInformation previousHasCombo | ||
&& index == comboIndexFor(previousHasCombo, comboColours) | ||
&& !canReuseLastComboColour(editorBeatmap, hitObject)) | ||
{ | ||
continue; | ||
} | ||
|
||
container.Add(new OsuClickableContainer | ||
{ | ||
Size = new Vector2(50), | ||
Masking = true, | ||
CornerRadius = 25, | ||
Children = new[] | ||
{ | ||
new Box | ||
{ | ||
RelativeSizeAxes = Axes.Both, | ||
Colour = comboColours[index], | ||
}, | ||
selectedColourIndex == index | ||
? new SpriteIcon | ||
{ | ||
Icon = FontAwesome.Solid.Check, | ||
Size = new Vector2(24), | ||
Anchor = Anchor.Centre, | ||
Origin = Anchor.Centre, | ||
Colour = OsuColour.ForegroundTextColourFor(comboColours[index]), | ||
} | ||
: Empty() | ||
}, | ||
Action = () => | ||
{ | ||
int comboDifference = index - selectedColourIndex; | ||
if (comboDifference == 0) | ||
return; | ||
|
||
int newOffset = hasComboInformation.ComboOffset + comboDifference; | ||
// `newOffset` must be positive to serialise correctly - this implements the true math "modulus" rather than the built-in "remainder" % op | ||
// which can return negative results when the first operand is negative | ||
newOffset -= (int)Math.Floor((double)newOffset / comboColours.Count) * comboColours.Count; | ||
|
||
hasComboInformation.ComboOffset = newOffset; | ||
editorBeatmap.BeginChange(); | ||
editorBeatmap.Update((HitObject)hasComboInformation); | ||
editorBeatmap.EndChange(); | ||
this.HidePopover(); | ||
} | ||
}); | ||
} | ||
} | ||
|
||
private static IHasComboInformation? getPreviousHitObjectWithCombo(EditorBeatmap editorBeatmap, HitObject hitObject) | ||
=> editorBeatmap.HitObjects.TakeWhile(ho => ho != hitObject).LastOrDefault() as IHasComboInformation; | ||
|
||
private static bool canReuseLastComboColour(EditorBeatmap editorBeatmap, HitObject hitObject) | ||
{ | ||
double? closestBreakEnd = editorBeatmap.Breaks.Select(b => b.EndTime) | ||
.Where(t => t <= hitObject.StartTime) | ||
.OrderBy(t => t) | ||
.LastOrDefault(); | ||
|
||
if (closestBreakEnd == null) | ||
return false; | ||
|
||
return editorBeatmap.HitObjects.FirstOrDefault(ho => ho.StartTime >= closestBreakEnd) == hitObject; | ||
} | ||
} | ||
|
||
// compare `EditorBeatmapSkin.updateColours()` et al. for reasoning behind the off-by-one index rotation | ||
private static int comboIndexFor(IHasComboInformation hasComboInformation, IReadOnlyCollection<Colour4> comboColours) | ||
=> (hasComboInformation.ComboIndexWithOffsets + comboColours.Count - 1) % comboColours.Count; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters