Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add combo colour override control to editor #31473

Merged
merged 4 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;

Expand Down Expand Up @@ -72,7 +71,7 @@ private void load()

protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider);

protected override IEnumerable<DrawableTernaryButton> CreateTernaryButtons()
protected override IEnumerable<Drawable> CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Concat(DistanceSnapProvider.CreateTernaryButtons());

Expand Down
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ protected override DrawableRuleset<OsuHitObject> CreateDrawableRuleset(Ruleset r

protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();

protected override IEnumerable<DrawableTernaryButton> CreateTernaryButtons()
protected override IEnumerable<Drawable> CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Append(new DrawableTernaryButton
{
Expand Down
5 changes: 3 additions & 2 deletions osu.Game/Rulesets/Edit/HitObjectComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
Expand Down Expand Up @@ -370,7 +371,7 @@ protected override void Update()
/// <summary>
/// Create all ternary states required to be displayed to the user.
/// </summary>
protected virtual IEnumerable<DrawableTernaryButton> CreateTernaryButtons() => BlueprintContainer.MainTernaryStates;
protected virtual IEnumerable<Drawable> CreateTernaryButtons() => BlueprintContainer.MainTernaryStates;

/// <summary>
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
Expand Down Expand Up @@ -429,7 +430,7 @@ protected override bool OnKeyDown(KeyDownEvent e)
}
else
{
if (togglesCollection.ElementAtOrDefault(rightIndex) is DrawableTernaryButton button)
if (togglesCollection.ChildrenOfType<DrawableTernaryButton>().ElementAtOrDefault(rightIndex) is DrawableTernaryButton button)
{
button.Toggle();
return true;
Expand Down
3 changes: 3 additions & 0 deletions osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public interface IHasComboInformation : IHasCombo
/// </summary>
new bool NewCombo { get; set; }

/// <inheritdoc cref="IHasCombo.ComboOffset"/>
new int ComboOffset { get; set; }

/// <summary>
/// Bindable exposure of <see cref="LastInCombo"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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,
Child = new DrawableTernaryButton
{
Current = Current,
Description = "New combo",
CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA },
},
},
pickerButton = new ColourPickerButton
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Alpha = 0,
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()
{
Enabled.Value = SelectedHitObject.Value != null;

if (SelectedHitObject.Value == null || SelectedHitObject.Value.ComboOffset == 0)
{
BackgroundColour = colourProvider.Background3;
icon.Colour = BackgroundColour.Darken(0.5f);
icon.Blending = BlendingParameters.Additive;
}
else
{
BackgroundColour = ComboColours[comboIndexFor(SelectedHitObject.Value, ComboColours)];
icon.Colour = OsuColour.ForegroundTextColourFor(BackgroundColour);
icon.Blending = BlendingParameters.Inherit;
}
}

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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -237,22 +237,17 @@ private void additionBankChanged(string bankName, TernaryState state)
/// <summary>
/// A collection of states which will be displayed to the user in the toolbox.
/// </summary>
public DrawableTernaryButton[] MainTernaryStates { get; private set; }
public Drawable[] MainTernaryStates { get; private set; }

public SampleBankTernaryButton[] SampleBankTernaryStates { get; private set; }

/// <summary>
/// Create all ternary states required to be displayed to the user.
/// </summary>
protected virtual IEnumerable<DrawableTernaryButton> CreateTernaryButtons()
protected virtual IEnumerable<Drawable> CreateTernaryButtons()
{
//TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects.
yield return new DrawableTernaryButton
{
Current = NewCombo,
Description = "New combo",
CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA },
};
yield return new NewComboTernaryButton { Current = NewCombo };

foreach (var kvp in SelectionHandler.SelectionSampleStates)
{
Expand Down
Loading