1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-15 10:27:35 +08:00

Merge pull request #29986 from OliBomby/refactor-placement-tool

Refactor placement blueprints to not be limited to hit objects
This commit is contained in:
Dan Balasescu 2024-09-25 17:59:43 +09:00 committed by GitHub
commit 3111d6a74d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 209 additions and 171 deletions

View File

@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
contentContainer.Playfield.HitObjectContainer.Add(hitObject);
}
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
{
var result = base.SnapForBlueprint(blueprint);
result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableBananaShower((BananaShower)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint();
protected override HitObjectPlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint();
protected override void AddHitObject(DrawableHitObject hitObject)
{

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableFruit((Fruit)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint();
protected override HitObjectPlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint();
[Test]
public void TestFruitPlacementPosition()

View File

@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableJuiceStream((JuiceStream)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint();
protected override HitObjectPlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint();
private void addMoveAndClickSteps(double time, float position, bool end = false)
{

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools;
namespace osu.Game.Rulesets.Catch.Edit
{
public class BananaShowerCompositionTool : HitObjectCompositionTool
public class BananaShowerCompositionTool : CompositionTool
{
public BananaShowerCompositionTool()
: base(nameof(BananaShower))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
public override PlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
}
}

View File

@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public partial class CatchPlacementBlueprint<THitObject> : PlacementBlueprint
public partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
where THitObject : CatchHitObject, new()
{
protected new THitObject HitObject => (THitObject)base.HitObject;

View File

@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.Edit
protected override BeatSnapGrid CreateBeatSnapGrid() => new CatchBeatSnapGrid();
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
{
new FruitCompositionTool(),
new JuiceStreamCompositionTool(),

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools;
namespace osu.Game.Rulesets.Catch.Edit
{
public class FruitCompositionTool : HitObjectCompositionTool
public class FruitCompositionTool : CompositionTool
{
public FruitCompositionTool()
: base(nameof(Fruit))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override PlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
}
}

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools;
namespace osu.Game.Rulesets.Catch.Edit
{
public class JuiceStreamCompositionTool : HitObjectCompositionTool
public class JuiceStreamCompositionTool : CompositionTool
{
public JuiceStreamCompositionTool()
: base(nameof(JuiceStream))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override PlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint();
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint();
}
}

View File

@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
});
}
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
{
double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
var pos = column.ScreenSpacePositionAtTime(time);

View File

@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
public partial class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
{
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHoldNote((HoldNote)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new HoldNotePlacementBlueprint();
protected override HitObjectPlacementBlueprint CreateBlueprint() => new HoldNotePlacementBlueprint();
}
}

View File

@ -64,6 +64,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
private Note getNote() => this.ChildrenOfType<DrawableNote>().FirstOrDefault()?.HitObject;
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableNote((Note)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint();
protected override HitObjectPlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint();
}
}

View File

@ -15,7 +15,7 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public abstract partial class ManiaPlacementBlueprint<T> : PlacementBlueprint
public abstract partial class ManiaPlacementBlueprint<T> : HitObjectPlacementBlueprint
where T : ManiaHitObject
{
protected new T HitObject => (T)base.HitObject;

View File

@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mania.Edit.Blueprints;
namespace osu.Game.Rulesets.Mania.Edit
{
public class HoldNoteCompositionTool : HitObjectCompositionTool
public class HoldNoteCompositionTool : CompositionTool
{
public HoldNoteCompositionTool()
: base("Hold")
@ -18,6 +18,6 @@ namespace osu.Game.Rulesets.Mania.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override PlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint();
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint();
}
}

View File

@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override BeatSnapGrid CreateBeatSnapGrid() => new ManiaBeatSnapGrid();
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
{
new NoteCompositionTool(),
new HoldNoteCompositionTool()

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mania.Objects;
namespace osu.Game.Rulesets.Mania.Edit
{
public class NoteCompositionTool : HitObjectCompositionTool
public class NoteCompositionTool : CompositionTool
{
public NoteCompositionTool()
: base(nameof(Note))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Mania.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override PlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint();
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint();
}
}

View File

@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
public partial class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene
{
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint();
protected override HitObjectPlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint();
}
}

View File

@ -514,6 +514,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private Slider? getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null;
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint();
protected override HitObjectPlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint();
}
}

View File

@ -15,6 +15,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSpinner((Spinner)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint();
protected override HitObjectPlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint();
}
}

View File

@ -9,7 +9,7 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
{
public partial class HitCirclePlacementBlueprint : PlacementBlueprint
public partial class HitCirclePlacementBlueprint : HitObjectPlacementBlueprint
{
public new HitCircle HitObject => (HitCircle)base.HitObject;

View File

@ -21,7 +21,7 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
public partial class SliderPlacementBlueprint : PlacementBlueprint
public partial class SliderPlacementBlueprint : HitObjectPlacementBlueprint
{
public new Slider HitObject => (Slider)base.HitObject;

View File

@ -13,7 +13,7 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
{
public partial class SpinnerPlacementBlueprint : PlacementBlueprint
public partial class SpinnerPlacementBlueprint : HitObjectPlacementBlueprint
{
public new Spinner HitObject => (Spinner)base.HitObject;

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit
{
public class HitCircleCompositionTool : HitObjectCompositionTool
public class HitCircleCompositionTool : CompositionTool
{
public HitCircleCompositionTool()
: base(nameof(HitCircle))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Osu.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override PlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint();
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint();
}
}

View File

@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Edit
protected override DrawableRuleset<OsuHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new DrawableOsuEditorRuleset(ruleset, beatmap, mods);
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
{
new HitCircleCompositionTool(),
new SliderCompositionTool(),

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit
{
public class SliderCompositionTool : HitObjectCompositionTool
public class SliderCompositionTool : CompositionTool
{
public SliderCompositionTool()
: base(nameof(Slider))
@ -26,6 +26,6 @@ namespace osu.Game.Rulesets.Osu.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override PlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint();
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint();
}
}

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit
{
public class SpinnerCompositionTool : HitObjectCompositionTool
public class SpinnerCompositionTool : CompositionTool
{
public SpinnerCompositionTool()
: base(nameof(Spinner))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Osu.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
public override PlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint();
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint();
}
}

View File

@ -10,7 +10,7 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
{
public partial class HitPlacementBlueprint : PlacementBlueprint
public partial class HitPlacementBlueprint : HitObjectPlacementBlueprint
{
private readonly HitPiece piece;

View File

@ -17,7 +17,7 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
{
public partial class TaikoSpanPlacementBlueprint : PlacementBlueprint
public partial class TaikoSpanPlacementBlueprint : HitObjectPlacementBlueprint
{
private readonly HitPiece headPiece;
private readonly HitPiece tailPiece;

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Edit
{
public class DrumRollCompositionTool : HitObjectCompositionTool
public class DrumRollCompositionTool : CompositionTool
{
public DrumRollCompositionTool()
: base(nameof(DrumRoll))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override PlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint();
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint();
}
}

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Edit
{
public class HitCompositionTool : HitObjectCompositionTool
public class HitCompositionTool : CompositionTool
{
public HitCompositionTool()
: base(nameof(Hit))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override PlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint();
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint();
}
}

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Edit
{
public class SwellCompositionTool : HitObjectCompositionTool
public class SwellCompositionTool : CompositionTool
{
public SwellCompositionTool()
: base(nameof(Swell))
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
public override PlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint();
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint();
}
}

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
}
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
{
new HitCompositionTool(),
new DrumRollCompositionTool(),

View File

@ -323,7 +323,7 @@ namespace osu.Game.Rulesets.Edit
/// <remarks>
/// A "select" tool is automatically added as the first tool.
/// </remarks>
protected abstract IReadOnlyList<HitObjectCompositionTool> CompositionTools { get; }
protected abstract IReadOnlyList<CompositionTool> CompositionTools { get; }
/// <summary>
/// A collection of states which will be displayed to the user in the toolbox.
@ -466,7 +466,7 @@ namespace osu.Game.Rulesets.Edit
private void setSelectTool() => toolboxCollection.Items.First().Select();
private void toolSelected(HitObjectCompositionTool tool)
private void toolSelected(CompositionTool tool)
{
BlueprintContainer.CurrentTool = tool;

View File

@ -9,9 +9,9 @@ namespace osu.Game.Rulesets.Edit
{
public class HitObjectCompositionToolButton : RadioButton
{
public HitObjectCompositionTool Tool { get; }
public CompositionTool Tool { get; }
public HitObjectCompositionToolButton(HitObjectCompositionTool tool, Action? action)
public HitObjectCompositionToolButton(CompositionTool tool, Action? action)
: base(tool.Name, action, tool.CreateIcon)
{
Tool = tool;

View File

@ -0,0 +1,126 @@
// 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.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose;
namespace osu.Game.Rulesets.Edit
{
/// <summary>
/// A blueprint which governs the creation of a new <see cref="HitObject"/> to actualisation.
/// </summary>
public abstract partial class HitObjectPlacementBlueprint : PlacementBlueprint
{
/// <summary>
/// Whether the sample bank should be taken from the previous hit object.
/// </summary>
public bool AutomaticBankAssignment { get; set; }
/// <summary>
/// The <see cref="HitObject"/> that is being placed.
/// </summary>
public readonly HitObject HitObject;
[Resolved]
protected EditorClock EditorClock { get; private set; } = null!;
[Resolved]
private EditorBeatmap beatmap { get; set; } = null!;
private Bindable<double> startTimeBindable = null!;
private HitObject? getPreviousHitObject() => beatmap.HitObjects.TakeWhile(h => h.StartTime <= startTimeBindable.Value).LastOrDefault();
[Resolved]
private IPlacementHandler placementHandler { get; set; } = null!;
protected HitObjectPlacementBlueprint(HitObject hitObject)
{
HitObject = hitObject;
// adding the default hit sample should be the case regardless of the ruleset.
HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL));
}
[BackgroundDependencyLoader]
private void load()
{
startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true);
}
protected override void BeginPlacement(bool commitStart = false)
{
base.BeginPlacement(commitStart);
placementHandler.BeginPlacement(HitObject);
}
public override void EndPlacement(bool commit)
{
base.EndPlacement(commit);
placementHandler.EndPlacement(HitObject, IsValidForPlacement && commit);
}
/// <summary>
/// Updates the time and position of this <see cref="HitObjectPlacementBlueprint"/> based on the provided snap information.
/// </summary>
/// <param name="result">The snap result information.</param>
public override void UpdateTimeAndPosition(SnapResult result)
{
if (PlacementActive == PlacementState.Waiting)
{
HitObject.StartTime = result.Time ?? EditorClock.CurrentTime;
if (HitObject is IHasComboInformation comboInformation)
comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation);
}
var lastHitObject = getPreviousHitObject();
if (AutomaticBankAssignment)
{
// Create samples based on the sample settings of the previous hit object
if (lastHitObject != null)
{
for (int i = 0; i < HitObject.Samples.Count; i++)
HitObject.Samples[i] = lastHitObject.CreateHitSampleInfo(HitObject.Samples[i].Name);
}
}
else
{
var lastHitNormal = lastHitObject?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
if (lastHitNormal != null)
{
// Only inherit the volume from the previous hit object
for (int i = 0; i < HitObject.Samples.Count; i++)
HitObject.Samples[i] = HitObject.Samples[i].With(newVolume: lastHitNormal.Volume);
}
}
if (HitObject is IHasRepeats hasRepeats)
{
// Make sure all the node samples are identical to the hit object's samples
for (int i = 0; i < hasRepeats.NodeSamples.Count; i++)
hasRepeats.NodeSamples[i] = HitObject.Samples.Select(o => o.With()).ToList();
}
}
/// <summary>
/// Invokes <see cref="Objects.HitObject.ApplyDefaults(ControlPointInfo,IBeatmapDifficultyInfo,CancellationToken)"/>,
/// refreshing <see cref="Objects.HitObject.NestedHitObjects"/> and parameters for the <see cref="HitObject"/>.
/// </summary>
protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
}
}

View File

@ -1,29 +1,19 @@
// 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.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Edit
{
/// <summary>
/// A blueprint which governs the creation of a new <see cref="HitObject"/> to actualisation.
/// A blueprint which governs the placement of something.
/// </summary>
public abstract partial class PlacementBlueprint : CompositeDrawable, IKeyBindingHandler<GlobalAction>
{
@ -32,29 +22,6 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
public PlacementState PlacementActive { get; private set; }
/// <summary>
/// Whether the sample bank should be taken from the previous hit object.
/// </summary>
public bool AutomaticBankAssignment { get; set; }
/// <summary>
/// The <see cref="HitObject"/> that is being placed.
/// </summary>
public readonly HitObject HitObject;
[Resolved]
protected EditorClock EditorClock { get; private set; } = null!;
[Resolved]
private EditorBeatmap beatmap { get; set; } = null!;
private Bindable<double> startTimeBindable = null!;
private HitObject? getPreviousHitObject() => beatmap.HitObjects.TakeWhile(h => h.StartTime <= startTimeBindable.Value).LastOrDefault();
[Resolved]
private IPlacementHandler placementHandler { get; set; } = null!;
/// <summary>
/// Whether this blueprint is currently in a state that can be committed.
/// </summary>
@ -64,13 +31,8 @@ namespace osu.Game.Rulesets.Edit
/// </remarks>
protected virtual bool IsValidForPlacement => true;
protected PlacementBlueprint(HitObject hitObject)
protected PlacementBlueprint()
{
HitObject = hitObject;
// adding the default hit sample should be the case regardless of the ruleset.
HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL));
RelativeSizeAxes = Axes.Both;
// This is required to allow the blueprint's position to be updated via OnMouseMove/Handle
@ -78,30 +40,22 @@ namespace osu.Game.Rulesets.Edit
AlwaysPresent = true;
}
[BackgroundDependencyLoader]
private void load()
{
startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true);
}
/// <summary>
/// Signals that the placement of <see cref="HitObject"/> has started.
/// Signals that the placement has started.
/// </summary>
/// <param name="commitStart">Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments.</param>
protected void BeginPlacement(bool commitStart = false)
/// <param name="commitStart">Whether this call is committing a value and continuing with further adjustments.</param>
protected virtual void BeginPlacement(bool commitStart = false)
{
placementHandler.BeginPlacement(HitObject);
if (commitStart)
PlacementActive = PlacementState.Active;
}
/// <summary>
/// Signals that the placement of <see cref="HitObject"/> has finished.
/// This will destroy this <see cref="PlacementBlueprint"/>, and add the HitObject.StartTime to the <see cref="Beatmap"/>.
/// This will destroy this <see cref="PlacementBlueprint"/>, and commit the changes.
/// </summary>
/// <param name="commit">Whether the object should be committed. Note that a commit may fail if <see cref="IsValidForPlacement"/> is <c>false</c>.</param>
public void EndPlacement(bool commit)
/// <param name="commit">Whether the changes should be committed. Note that a commit may fail if <see cref="IsValidForPlacement"/> is <c>false</c>.</param>
public virtual void EndPlacement(bool commit)
{
switch (PlacementActive)
{
@ -114,10 +68,17 @@ namespace osu.Game.Rulesets.Edit
break;
}
placementHandler.EndPlacement(HitObject, IsValidForPlacement && commit);
PlacementActive = PlacementState.Finished;
}
/// <summary>
/// Updates the time and position of this <see cref="PlacementBlueprint"/> based on the provided snap information.
/// </summary>
/// <param name="result">The snap result information.</param>
public virtual void UpdateTimeAndPosition(SnapResult result)
{
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (PlacementActive == PlacementState.Waiting)
@ -138,57 +99,6 @@ namespace osu.Game.Rulesets.Edit
{
}
/// <summary>
/// Updates the time and position of this <see cref="PlacementBlueprint"/> based on the provided snap information.
/// </summary>
/// <param name="result">The snap result information.</param>
public virtual void UpdateTimeAndPosition(SnapResult result)
{
if (PlacementActive == PlacementState.Waiting)
{
HitObject.StartTime = result.Time ?? EditorClock.CurrentTime;
if (HitObject is IHasComboInformation comboInformation)
comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation);
}
var lastHitObject = getPreviousHitObject();
if (AutomaticBankAssignment)
{
// Create samples based on the sample settings of the previous hit object
if (lastHitObject != null)
{
for (int i = 0; i < HitObject.Samples.Count; i++)
HitObject.Samples[i] = lastHitObject.CreateHitSampleInfo(HitObject.Samples[i].Name);
}
}
else
{
var lastHitNormal = lastHitObject?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
if (lastHitNormal != null)
{
// Only inherit the volume from the previous hit object
for (int i = 0; i < HitObject.Samples.Count; i++)
HitObject.Samples[i] = HitObject.Samples[i].With(newVolume: lastHitNormal.Volume);
}
}
if (HitObject is IHasRepeats hasRepeats)
{
// Make sure all the node samples are identical to the hit object's samples
for (int i = 0; i < hasRepeats.NodeSamples.Count; i++)
hasRepeats.NodeSamples[i] = HitObject.Samples.Select(o => o.With()).ToList();
}
}
/// <summary>
/// Invokes <see cref="Objects.HitObject.ApplyDefaults(ControlPointInfo,IBeatmapDifficultyInfo,CancellationToken)"/>,
/// refreshing <see cref="Objects.HitObject.NestedHitObjects"/> and parameters for the <see cref="HitObject"/>.
/// </summary>
protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
protected override bool Handle(UIEvent e)

View File

@ -6,13 +6,13 @@ using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Edit.Tools
{
public abstract class HitObjectCompositionTool
public abstract class CompositionTool
{
public readonly string Name;
public LocalisableString TooltipText { get; init; }
protected HitObjectCompositionTool(string name)
protected CompositionTool(string name)
{
Name = name;
}

View File

@ -9,7 +9,7 @@ using osu.Game.Graphics;
namespace osu.Game.Rulesets.Edit.Tools
{
public class SelectTool : HitObjectCompositionTool
public class SelectTool : CompositionTool
{
public SelectTool()
: base("Select")
@ -18,6 +18,6 @@ namespace osu.Game.Rulesets.Edit.Tools
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorSelect };
public override PlacementBlueprint CreatePlacementBlueprint() => null;
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => null;
}
}

View File

@ -41,6 +41,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
public PlacementBlueprint CurrentPlacement { get; private set; }
public HitObjectPlacementBlueprint CurrentHitObjectPlacement => CurrentPlacement as HitObjectPlacementBlueprint;
[Resolved(canBeNull: true)]
private EditorScreenWithTimeline editorScreen { get; set; }
@ -164,13 +166,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updatePlacementNewCombo()
{
if (CurrentPlacement?.HitObject is IHasComboInformation c)
if (CurrentHitObjectPlacement?.HitObject is IHasComboInformation c)
c.NewCombo = NewCombo.Value == TernaryState.True;
}
private void updatePlacementSamples()
{
if (CurrentPlacement == null) return;
if (CurrentHitObjectPlacement == null) return;
foreach (var kvp in SelectionHandler.SelectionSampleStates)
sampleChanged(kvp.Key, kvp.Value.Value);
@ -181,9 +183,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void sampleChanged(string sampleName, TernaryState state)
{
if (CurrentPlacement == null) return;
if (CurrentHitObjectPlacement == null) return;
var samples = CurrentPlacement.HitObject.Samples;
var samples = CurrentHitObjectPlacement.HitObject.Samples;
var existingSample = samples.FirstOrDefault(s => s.Name == sampleName);
@ -196,19 +198,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
case TernaryState.True:
if (existingSample == null)
samples.Add(CurrentPlacement.HitObject.CreateHitSampleInfo(sampleName));
samples.Add(CurrentHitObjectPlacement.HitObject.CreateHitSampleInfo(sampleName));
break;
}
}
private void bankChanged(string bankName, TernaryState state)
{
if (CurrentPlacement == null) return;
if (CurrentHitObjectPlacement == null) return;
if (bankName == EditorSelectionHandler.HIT_BANK_AUTO)
CurrentPlacement.AutomaticBankAssignment = state == TernaryState.True;
CurrentHitObjectPlacement.AutomaticBankAssignment = state == TernaryState.True;
else if (state == TernaryState.True)
CurrentPlacement.HitObject.Samples = CurrentPlacement.HitObject.Samples.Select(s => s.With(newBank: bankName)).ToList();
CurrentHitObjectPlacement.HitObject.Samples = CurrentHitObjectPlacement.HitObject.Samples.Select(s => s.With(newBank: bankName)).ToList();
}
public readonly Bindable<TernaryState> NewCombo = new Bindable<TernaryState> { Description = "New Combo" };
@ -386,12 +388,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
CurrentPlacement = null;
}
private HitObjectCompositionTool currentTool;
private CompositionTool currentTool;
/// <summary>
/// The current placement tool.
/// </summary>
public HitObjectCompositionTool CurrentTool
public CompositionTool CurrentTool
{
get => currentTool;

View File

@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual
public abstract partial class PlacementBlueprintTestScene : OsuManualInputManagerTestScene, IPlacementHandler
{
protected readonly Container HitObjectContainer;
protected PlacementBlueprint CurrentBlueprint { get; private set; }
protected HitObjectPlacementBlueprint CurrentBlueprint { get; private set; }
protected PlacementBlueprintTestScene()
{
@ -87,14 +87,14 @@ namespace osu.Game.Tests.Visual
CurrentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(CurrentBlueprint));
}
protected virtual SnapResult SnapForBlueprint(PlacementBlueprint blueprint) =>
protected virtual SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) =>
new SnapResult(InputManager.CurrentState.Mouse.Position, null);
public override void Add(Drawable drawable)
{
base.Add(drawable);
if (drawable is PlacementBlueprint blueprint)
if (drawable is HitObjectPlacementBlueprint blueprint)
{
blueprint.Show();
blueprint.UpdateTimeAndPosition(SnapForBlueprint(blueprint));
@ -106,6 +106,6 @@ namespace osu.Game.Tests.Visual
protected virtual Container CreateHitObjectContainer() => new Container { RelativeSizeAxes = Axes.Both };
protected abstract DrawableHitObject CreateHitObject(HitObject hitObject);
protected abstract PlacementBlueprint CreateBlueprint();
protected abstract HitObjectPlacementBlueprint CreateBlueprint();
}
}