diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 050c2f625d..9b779d3370 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -2,14 +2,23 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; 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; @@ -17,6 +26,14 @@ namespace osu.Game.Rulesets.Catch.Edit { public class CatchHitObjectComposer : HitObjectComposer { + private const float distance_snap_radius = 50; + + private CatchDistanceSnapGrid distanceSnapGrid; + + private readonly Bindable distanceSnapToggle = new Bindable(); + + private InputManager inputManager; + public CatchHitObjectComposer(CatchRuleset ruleset) : base(ruleset) { @@ -30,6 +47,27 @@ private void load() RelativeSizeAxes = Axes.Both, PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners } }); + + LayerBelowRuleset.Add(distanceSnapGrid = new CatchDistanceSnapGrid(new[] + { + 0.0, + Catcher.BASE_SPEED, -Catcher.BASE_SPEED, + Catcher.BASE_SPEED / 2, -Catcher.BASE_SPEED / 2, + })); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + inputManager = GetContainingInputManager(); + } + + protected override void Update() + { + base.Update(); + + updateDistanceSnapGrid(); } protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) => @@ -42,14 +80,90 @@ protected override DrawableRuleset CreateDrawableRuleset(Ruleset new BananaShowerCompositionTool() }; + protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[] + { + new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }) + }); + public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) { var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition); - // TODO: implement position snap result.ScreenSpacePosition.X = screenSpacePosition.X; + + if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult && + Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius) + { + result = snapResult; + } + return result; } protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this); + + [CanBeNull] + private PalpableCatchHitObject getPreviousHitObject(double time) + { + var hitObject = EditorBeatmap.HitObjects.OfType().LastOrDefault(h => h.GetEndTime() < time && !(h is BananaShower)); + + switch (hitObject) + { + case Fruit fruit: + return fruit; + + case JuiceStream juiceStream: + return juiceStream.NestedHitObjects.OfType().LastOrDefault(h => !(h is TinyDroplet)); + + default: + return null; + } + } + + [CanBeNull] + private PalpableCatchHitObject getDistanceSnapGridSourceHitObject() + { + switch (BlueprintContainer.CurrentTool) + { + case SelectTool _: + if (EditorBeatmap.SelectedHitObjects.Count == 0) + return null; + + double minTime = EditorBeatmap.SelectedHitObjects.Min(hitObject => hitObject.StartTime); + return getPreviousHitObject(minTime); + + case FruitCompositionTool _: + case JuiceStreamCompositionTool _: + if (!CursorInPlacementArea) + return null; + + if (EditorBeatmap.PlacementObject.Value is JuiceStream) + { + // Juice stream path is not subject to snapping. + return null; + } + + double timeAtCursor = ((CatchPlayfield)Playfield).TimeAtScreenSpacePosition(inputManager.CurrentState.Mouse.Position); + return getPreviousHitObject(timeAtCursor); + + default: + return null; + } + } + + private void updateDistanceSnapGrid() + { + var sourceHitObject = getDistanceSnapGridSourceHitObject(); + + if (distanceSnapToggle.Value != TernaryState.True || sourceHitObject == null) + { + distanceSnapGrid.Hide(); + } + else + { + distanceSnapGrid.Show(); + distanceSnapGrid.StartTime = sourceHitObject.GetEndTime(); + distanceSnapGrid.StartX = sourceHitObject.EffectiveX; + } + } } }