diff --git a/osu.Game.Rulesets.Sentakki.Tests/TestSceneSentakkiEditor.cs b/osu.Game.Rulesets.Sentakki.Tests/TestSceneSentakkiEditor.cs new file mode 100644 index 000000000..279f7c109 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki.Tests/TestSceneSentakkiEditor.cs @@ -0,0 +1,11 @@ +using NUnit.Framework; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Sentakki.Tests +{ + [TestFixture] + public partial class TestSceneSentakkiEditor : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new SentakkiRuleset(); + } +} diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/SentakkiBeatmapConverterOld.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/SentakkiBeatmapConverterOld.cs index 7cec144cc..5cc306c89 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/SentakkiBeatmapConverterOld.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/SentakkiBeatmapConverterOld.cs @@ -251,6 +251,7 @@ private SentakkiHitObject createHoldNote(HitObject original, IList ring.ReceivePositionalInputAt(screenSpacePos); + public override Quad ScreenSpaceDrawQuad => ring.ScreenSpaceDrawQuad; + + private readonly RingPiece ring; + + public HoldHighlight() + { + Anchor = Origin = Anchor.Centre; + Colour = Color4.YellowGreen; + Alpha = 0.5f; + InternalChildren = new Drawable[] + { + Note = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(-75 / 2f), + Child = ring = new RingPiece() + }, + new DotPiece(squared: true) + { + Anchor = Anchor.TopCentre, + Rotation = 45, + }, + new DotPiece(squared: true) + { + Anchor = Anchor.BottomCentre, + Rotation = 45, + }, + } + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Holds/HoldPlacementBlueprint.cs b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Holds/HoldPlacementBlueprint.cs new file mode 100644 index 000000000..b4dfb870b --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Holds/HoldPlacementBlueprint.cs @@ -0,0 +1,80 @@ +using System; +using osu.Framework.Allocation; +using osu.Framework.Input.Events; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Rulesets.Sentakki.UI; +using osu.Game.Screens.Edit; +using osuTK.Input; + +namespace osu.Game.Rulesets.Sentakki.Edit.Blueprints.Holds +{ + public partial class HoldPlacementBlueprint : SentakkiPlacementBlueprint + { + private readonly HoldHighlight highlight; + + public HoldPlacementBlueprint() + { + InternalChild = highlight = new HoldHighlight(); + highlight.Note.Y = -SentakkiPlayfield.INTERSECTDISTANCE; + } + + [Resolved] + private SentakkiSnapProvider snapProvider { get; set; } = null!; + + protected override void Update() + { + highlight.Rotation = HitObject.Lane.GetRotationForLane(); + highlight.Note.Y = -snapProvider.GetDistanceRelativeToCurrentTime(HitObject.StartTime, SentakkiPlayfield.NOTESTARTDISTANCE); + highlight.Note.Height = -snapProvider.GetDistanceRelativeToCurrentTime(HitObject.EndTime, SentakkiPlayfield.NOTESTARTDISTANCE) - highlight.Note.Y; + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button != MouseButton.Left) + return false; + + if (PlacementActive == PlacementState.Active) + return false; + + BeginPlacement(true); + EditorClock.SeekSmoothlyTo(HitObject.StartTime); + + return true; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + if (e.Button != MouseButton.Right) + return; + + if (PlacementActive == PlacementState.Active) + EndPlacement(HitObject.Duration > 0); + } + + private double originalStartTime; + + public override void UpdateTimeAndPosition(SnapResult result) + { + base.UpdateTimeAndPosition(result); + + if (result is not SentakkiLanedSnapResult senRes) + return; + + if (PlacementActive == PlacementState.Active) + { + if (result.Time is double endTime) + { + HitObject.StartTime = endTime < originalStartTime ? endTime : originalStartTime; + HitObject.Duration = Math.Abs(endTime - originalStartTime); + } + } + else + { + HitObject.Lane = senRes.Lane; + if (result.Time is double startTime) + originalStartTime = HitObject.StartTime = startTime; + } + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Holds/HoldSelectionBlueprint.cs b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Holds/HoldSelectionBlueprint.cs new file mode 100644 index 000000000..236a9bc8a --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Holds/HoldSelectionBlueprint.cs @@ -0,0 +1,41 @@ +using osu.Framework.Allocation; +using osu.Framework.Graphics.Primitives; +using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Rulesets.Sentakki.Objects.Drawables; +using osu.Game.Rulesets.Sentakki.UI; +using osuTK; + +namespace osu.Game.Rulesets.Sentakki.Edit.Blueprints.Holds +{ + public partial class HoldSelectionBlueprint : SentakkiSelectionBlueprint + { + public new DrawableHold DrawableObject => (DrawableHold)base.DrawableObject; + + private readonly HoldHighlight highlight; + + public HoldSelectionBlueprint(Hold hitObject) + : base(hitObject) + { + InternalChild = highlight = new HoldHighlight(); + } + + [Resolved] + private SentakkiSnapProvider snapProvider { get; set; } = null!; + + protected override void Update() + { + base.Update(); + + highlight.Rotation = DrawableObject.HitObject.Lane.GetRotationForLane(); + highlight.Note.Y = -snapProvider.GetDistanceRelativeToCurrentTime(HitObject.StartTime, SentakkiPlayfield.NOTESTARTDISTANCE); + highlight.Note.Height = -snapProvider.GetDistanceRelativeToCurrentTime(HitObject.EndTime, SentakkiPlayfield.NOTESTARTDISTANCE) - highlight.Note.Y; + highlight.Note.Scale = DrawableObject.NoteBody.Scale; + } + + public override Vector2 ScreenSpaceSelectionPoint => highlight.ScreenSpaceDrawQuad.Centre; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => highlight.ReceivePositionalInputAt(screenSpacePos); + + public override Quad SelectionQuad => highlight.ScreenSpaceDrawQuad; + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Blueprints/SentakkiPlacementBlueprint.cs b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/SentakkiPlacementBlueprint.cs new file mode 100644 index 000000000..472ef2723 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/SentakkiPlacementBlueprint.cs @@ -0,0 +1,20 @@ +using osu.Framework.Graphics; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osuTK; + +namespace osu.Game.Rulesets.Sentakki.Edit.Blueprints +{ + public partial class SentakkiPlacementBlueprint : PlacementBlueprint where T : HitObject, new() + { + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + + public new T HitObject => (T)base.HitObject; + + public SentakkiPlacementBlueprint() + : base(new T()) + { + Anchor = Origin = Anchor.Centre; + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Blueprints/SentakkiSelectionBlueprint.cs b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/SentakkiSelectionBlueprint.cs new file mode 100644 index 000000000..7b1fa05bb --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/SentakkiSelectionBlueprint.cs @@ -0,0 +1,15 @@ +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Sentakki.Objects; + +namespace osu.Game.Rulesets.Sentakki.Edit.Blueprints +{ + public partial class SentakkiSelectionBlueprint : HitObjectSelectionBlueprint where T : SentakkiHitObject + { + protected override bool AlwaysShowWhenSelected => true; + + public SentakkiSelectionBlueprint(T hitObject) + : base(hitObject) + { + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Slides/SlideBodyHighlight.cs b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Slides/SlideBodyHighlight.cs new file mode 100644 index 000000000..977b0a480 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Slides/SlideBodyHighlight.cs @@ -0,0 +1,59 @@ +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Rulesets.Sentakki.Objects.Drawables; +using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Sentakki.Edit.Blueprints.Slides +{ + public partial class SlideBodyHighlight : CompositeDrawable + { + private readonly Container stars; + + private readonly SlideVisual slideBody; + + private readonly SlideBodyInfo slideBodyInfo; + + // This drawable is zero width + // We should use the quad of the note container + //public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SlideTapPiece.ReceivePositionalInputAt(screenSpacePos); + //public override Quad ScreenSpaceDrawQuad => SlideTapPiece.ScreenSpaceDrawQuad; + + public SlideBodyHighlight(SlideBodyInfo slideBodyInfo) + { + Anchor = Origin = Anchor.Centre; + Colour = Color4.YellowGreen; + Alpha = 1; + + InternalChildren = new Drawable[] + { + slideBody = new SlideVisual(), + stars = new Container(), + }; + + const int number_of_stars = 3; + + for (int i = 0; i < number_of_stars; ++i) + stars.Add(new StarPiece()); + + this.slideBodyInfo = slideBodyInfo; + } + + public void OnSelected() => slideBody.Path = slideBodyInfo.SlidePath; + + public void OnDeselected() => slideBody.Free(); + + public void UpdateFrom(DrawableSlideBody body) + { + for (int i = 0; i < body.SlideStars.Count; ++i) + { + stars[i].Position = body.SlideStars[i].Position; + stars[i].Rotation = body.SlideStars[i].Rotation; + stars[i].Scale = body.SlideStars[i].Scale; + stars[i].Alpha = body.SlideStars[i].Alpha; + } + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Slides/SlidePlacementBlueprint.cs b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Slides/SlidePlacementBlueprint.cs new file mode 100644 index 000000000..728fae4f2 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Slides/SlidePlacementBlueprint.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides; +using osu.Game.Rulesets.Sentakki.UI; +using osu.Game.Screens.Edit; +using osuTK; +using osuTK.Graphics; +using osuTK.Input; + +namespace osu.Game.Rulesets.Sentakki.Edit.Blueprints.Slides +{ + public partial class SlidePlacementBlueprint : SentakkiPlacementBlueprint + { + private readonly SlideTapHighlight highlight; + + private readonly SlideVisual bodyHighlight; + private readonly SlideVisual commited; + + [Resolved] + private SlideEditorToolboxGroup slidePlacementToolbox { get; set; } = null!; + + public SlidePlacementBlueprint() + { + AddRangeInternal(new Drawable[] + { + highlight = new SlideTapHighlight(), + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Rotation = -22.5f, + Children = new Drawable[] + { + commited = new SlideVisual + { + Colour = Color4.LimeGreen, + Alpha = 0.5f, + }, + bodyHighlight = new SlideVisual + { + Colour = Color4.GreenYellow, + Alpha = 0f, + }, + } + }, + }); + + highlight.SlideTapPiece.Y = -SentakkiPlayfield.INTERSECTDISTANCE; + highlight.SlideTapPiece.Scale = Vector2.One; + } + + [Resolved] + private SentakkiSnapProvider snapProvider { get; set; } = null!; + + protected override void Update() + { + highlight.Rotation = HitObject.Lane.GetRotationForLane(); + highlight.SlideTapPiece.Y = -snapProvider.GetDistanceRelativeToCurrentTime(HitObject.StartTime, SentakkiPlayfield.NOTESTARTDISTANCE); + } + + private SlideBodyInfo commitedSlideBodyInfo = null!; + private SlideBodyInfo previewSlideBodyInfo = null!; + private int currentLaneOffset; + + private Bindable currentPart = new Bindable(); + + protected override void LoadComplete() + { + currentPart.BindTo(slidePlacementToolbox.CurrentPartBindable); + currentPart.BindValueChanged(v => + { + previewSlideBodyInfo = new SlideBodyInfo + { + SlidePathParts = new[] { v.NewValue } + }; + bodyHighlight.Path = previewSlideBodyInfo.SlidePath; + }, true); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button != MouseButton.Left) + return false; + + if (PlacementActive != PlacementState.Active) + { + BeginPlacement(true); + + EditorClock.SeekSmoothlyTo(HitObject.StartTime); + + HitObject.SlideInfoList.Add(commitedSlideBodyInfo = new SlideBodyInfo()); + + commited.Rotation = HitObject.Lane.GetRotationForLane(); + bodyHighlight.Rotation = HitObject.Lane.GetRotationForLane(); + bodyHighlight.Alpha = 0.8f; + } + else + { + commitCurrentPart(); + } + + return true; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + if (e.Button != MouseButton.Right) + return; + + if (PlacementActive == PlacementState.Active) + EndPlacement(bodyParts.Count > 0 && commitedSlideBodyInfo.Duration > 0); + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + switch (e.Key) + { + case Key.BackSpace: + uncommitLastPart(); + break; + } + + return base.OnKeyDown(e); + } + + private readonly List bodyParts = new List(); + + // Only used to revert the lane offsets after uncommit + private readonly Stack laneOffsets = new Stack(); + + // The path offset of the lane the player is pointing at + private int targetPathOffset; + + private double originalStartTime; + + public override void UpdateTimeAndPosition(SnapResult result) + { + if (result is not SentakkiLanedSnapResult senRes) + return; + + if (PlacementActive == PlacementState.Active) + { + double endTime = EditorClock.CurrentTime; + + HitObject.StartTime = endTime < originalStartTime ? endTime : originalStartTime; + commitedSlideBodyInfo.Duration = Math.Abs(endTime - originalStartTime); + + var localSpacePointerCoord = ToLocalSpace(result.ScreenSpacePosition); + + if ((localSpacePointerCoord - OriginPosition).LengthSquared > 400 * 400) + return; + + int newPo = (senRes.Lane - currentLaneOffset - HitObject.Lane).NormalizePath(); + + if (targetPathOffset != newPo) + { + slidePlacementToolbox.RequestLaneChange(newPo, true); + targetPathOffset = newPo; + } + } + else + { + base.UpdateTimeAndPosition(result); + + HitObject.Lane = senRes.Lane; + + if (result.Time is double startTime) + originalStartTime = HitObject.StartTime = startTime; + } + } + + private void commitCurrentPart() + { + laneOffsets.Push(slidePlacementToolbox.CurrentPart.EndOffset); + bodyParts.Add(slidePlacementToolbox.CurrentPart); + + currentLaneOffset += slidePlacementToolbox.CurrentPart.EndOffset; + commitedSlideBodyInfo.SlidePathParts = bodyParts.ToArray(); + commited.Path = commitedSlideBodyInfo.SlidePath; + + bodyHighlight.Rotation = (HitObject.Lane + currentLaneOffset).GetRotationForLane(); + } + + private void uncommitLastPart() + { + if (laneOffsets.Count == 0) + return; + + currentLaneOffset -= laneOffsets.Pop(); + bodyParts.RemoveAt(bodyParts.Count - 1); + + commitedSlideBodyInfo.SlidePathParts = bodyParts.ToArray(); + commited.Path = commitedSlideBodyInfo.SlidePath; + + bodyHighlight.Rotation = (HitObject.Lane + currentLaneOffset).GetRotationForLane(); + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Slides/SlideSelectionBlueprint.cs b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Slides/SlideSelectionBlueprint.cs new file mode 100644 index 000000000..f1e7df892 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Slides/SlideSelectionBlueprint.cs @@ -0,0 +1,94 @@ +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Rulesets.Sentakki.Objects.Drawables; +using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides; +using osu.Game.Rulesets.Sentakki.UI; +using osuTK; + +namespace osu.Game.Rulesets.Sentakki.Edit.Blueprints.Slides +{ + public partial class SlideSelectionBlueprint : SentakkiSelectionBlueprint + { + public new DrawableSlide DrawableObject => (DrawableSlide)base.DrawableObject; + + private readonly SlideTapHighlight tapHighlight; + + private readonly Container slideBodyHighlights; + + public SlideSelectionBlueprint(Slide hitObject) + : base(hitObject) + { + InternalChildren = new Drawable[] + { + slideBodyHighlights = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Rotation = -22.5f, + }, + tapHighlight = new SlideTapHighlight(), + }; + + foreach (var body in hitObject.SlideBodies) + slideBodyHighlights.Add(new SlideBodyHighlight(body.SlideBodyInfo)); + } + + protected override void OnDeselected() + { + foreach (var sb in slideBodyHighlights) + sb.OnDeselected(); + + base.OnDeselected(); + } + + protected override void OnSelected() + { + foreach (var sb in slideBodyHighlights) + sb.OnSelected(); + + base.OnSelected(); + } + + protected override void Update() + { + base.Update(); + + updateTapHighlight(); + updateSlideBodyHighlights(); + } + + [Resolved] + private SentakkiSnapProvider snapProvider { get; set; } = null!; + + private void updateTapHighlight() + { + var slideTap = DrawableObject.SlideTaps.Child; + + tapHighlight.SlideTapPiece.Scale = slideTap.TapVisual.Scale; + tapHighlight.SlideTapPiece.Stars.Rotation = ((SlideTapPiece)slideTap.TapVisual).Stars.Rotation; + tapHighlight.SlideTapPiece.SecondStar.Alpha = ((SlideTapPiece)slideTap.TapVisual).SecondStar.Alpha; + tapHighlight.Rotation = DrawableObject.HitObject.Lane.GetRotationForLane(); + tapHighlight.SlideTapPiece.Y = -snapProvider.GetDistanceRelativeToCurrentTime(DrawableObject.HitObject.StartTime, SentakkiPlayfield.NOTESTARTDISTANCE); + } + + private void updateSlideBodyHighlights() + { + for (int i = 0; i < DrawableObject.SlideBodies.Count; ++i) + { + var slideBody = DrawableObject.SlideBodies[i]; + + slideBodyHighlights[i].UpdateFrom(slideBody); + slideBodyHighlights[i].Rotation = DrawableObject.HitObject.Lane.GetRotationForLane(); + } + } + + public override Vector2 ScreenSpaceSelectionPoint => tapHighlight.ScreenSpaceDrawQuad.Centre; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => tapHighlight.ReceivePositionalInputAt(screenSpacePos); + + public override Quad SelectionQuad => tapHighlight.ScreenSpaceDrawQuad; + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Slides/SlideTapHighlight.cs b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Slides/SlideTapHighlight.cs new file mode 100644 index 000000000..f71bdd868 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Slides/SlideTapHighlight.cs @@ -0,0 +1,27 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Sentakki.Edit.Blueprints.Slides +{ + public partial class SlideTapHighlight : CompositeDrawable + { + public readonly SlideTapPiece SlideTapPiece; + + // This drawable is zero width + // We should use the quad of the note container + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SlideTapPiece.ReceivePositionalInputAt(screenSpacePos); + public override Quad ScreenSpaceDrawQuad => SlideTapPiece.ScreenSpaceDrawQuad; + + public SlideTapHighlight() + { + Anchor = Origin = Anchor.Centre; + Colour = Color4.YellowGreen; + Alpha = 0.5f; + InternalChild = SlideTapPiece = new SlideTapPiece(); + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Taps/TapHighlight.cs b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Taps/TapHighlight.cs new file mode 100644 index 000000000..e4a05de9e --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Taps/TapHighlight.cs @@ -0,0 +1,40 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Sentakki.Edit.Blueprints.Taps +{ + public partial class TapHighlight : CompositeDrawable + { + public readonly Container Note; + + // This drawable is zero width + // We should use the quad of the note container + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Note.ReceivePositionalInputAt(screenSpacePos); + public override Quad ScreenSpaceDrawQuad => Note.ScreenSpaceDrawQuad; + + public TapHighlight() + { + Anchor = Origin = Anchor.Centre; + Colour = Color4.YellowGreen; + Alpha = 0.5f; + InternalChildren = new Drawable[] + { + Note = new Container + { + Size = new Vector2(75), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new RingPiece(), + new DotPiece(), + } + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Taps/TapPlacementBlueprint.cs b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Taps/TapPlacementBlueprint.cs new file mode 100644 index 000000000..fb7d77b4a --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Taps/TapPlacementBlueprint.cs @@ -0,0 +1,44 @@ +using osu.Framework.Input.Events; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Rulesets.Sentakki.UI; +using osuTK.Input; + +namespace osu.Game.Rulesets.Sentakki.Edit.Blueprints.Taps +{ + public partial class TapPlacementBlueprint : SentakkiPlacementBlueprint + { + private readonly TapHighlight highlight; + + public TapPlacementBlueprint() + { + InternalChild = highlight = new TapHighlight(); + highlight.Note.Y = -SentakkiPlayfield.INTERSECTDISTANCE; + } + + protected override void Update() + { + highlight.Rotation = HitObject.Lane.GetRotationForLane(); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button != MouseButton.Left) return false; + + EndPlacement(true); + EditorClock.SeekSmoothlyTo(HitObject.StartTime); + return true; + } + + public override void UpdateTimeAndPosition(SnapResult result) + { + base.UpdateTimeAndPosition(result); + + if (result is not SentakkiLanedSnapResult senRes) + return; + + HitObject.Lane = senRes.Lane; + highlight.Note.Y = -senRes.YPos; + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Taps/TapSelectionBlueprint.cs b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Taps/TapSelectionBlueprint.cs new file mode 100644 index 000000000..57002a078 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Taps/TapSelectionBlueprint.cs @@ -0,0 +1,40 @@ +using osu.Framework.Allocation; +using osu.Framework.Graphics.Primitives; +using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Rulesets.Sentakki.Objects.Drawables; +using osu.Game.Rulesets.Sentakki.UI; +using osuTK; + +namespace osu.Game.Rulesets.Sentakki.Edit.Blueprints.Taps +{ + public partial class TapSelectionBlueprint : SentakkiSelectionBlueprint + { + public new DrawableTap DrawableObject => (DrawableTap)base.DrawableObject; + + private readonly TapHighlight highlight; + + public TapSelectionBlueprint(Tap hitObject) + : base(hitObject) + { + InternalChild = highlight = new TapHighlight(); + } + + [Resolved] + private SentakkiSnapProvider snapProvider { get; set; } = null!; + + protected override void Update() + { + base.Update(); + + highlight.Rotation = DrawableObject.HitObject.Lane.GetRotationForLane(); + highlight.Note.Y = -snapProvider.GetDistanceRelativeToCurrentTime(DrawableObject.HitObject.StartTime, SentakkiPlayfield.NOTESTARTDISTANCE); + highlight.Note.Scale = DrawableObject.TapVisual.Scale; + } + + public override Vector2 ScreenSpaceSelectionPoint => highlight.ScreenSpaceDrawQuad.Centre; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => highlight.ReceivePositionalInputAt(screenSpacePos); + + public override Quad SelectionQuad => highlight.ScreenSpaceDrawQuad; + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Blueprints/TouchHolds/TouchHoldHighlight.cs b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/TouchHolds/TouchHoldHighlight.cs new file mode 100644 index 000000000..5df110475 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/TouchHolds/TouchHoldHighlight.cs @@ -0,0 +1,19 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; +using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.TouchHolds; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Sentakki.Edit.Blueprints.TouchHolds +{ + public partial class TouchHoldHighlight : TouchHoldBody + { + public override Quad ScreenSpaceDrawQuad => ProgressPiece.ScreenSpaceDrawQuad; + + public TouchHoldHighlight() + { + Anchor = Origin = Anchor.Centre; + Colour = Color4.YellowGreen; + Alpha = 0.5f; + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Blueprints/TouchHolds/TouchHoldPlacementBlueprint.cs b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/TouchHolds/TouchHoldPlacementBlueprint.cs new file mode 100644 index 000000000..1fdadf0ba --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/TouchHolds/TouchHoldPlacementBlueprint.cs @@ -0,0 +1,57 @@ +using System; +using osu.Framework.Input.Events; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Sentakki.Objects; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Sentakki.Edit.Blueprints.TouchHolds +{ + public partial class TouchHoldPlacementBlueprint : SentakkiPlacementBlueprint + { + public TouchHoldPlacementBlueprint() + { + InternalChild = new TouchHoldHighlight(); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button != MouseButton.Left) + return false; + + if (PlacementActive == PlacementState.Active) + return false; + + BeginPlacement(true); + return true; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + if (e.Button != MouseButton.Right) + return; + + if (PlacementActive == PlacementState.Active) + EndPlacement(HitObject.Duration > 0); + } + + private double originalStartTime; + + public override void UpdateTimeAndPosition(SnapResult result) + { + if (PlacementActive == PlacementState.Active) + { + if (EditorClock.CurrentTime is double endTime) + { + HitObject.StartTime = endTime < originalStartTime ? endTime : originalStartTime; + HitObject.Duration = Math.Abs(endTime - originalStartTime); + } + } + else + { + if (EditorClock.CurrentTime is double startTime) + originalStartTime = HitObject.StartTime = startTime; + } + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Blueprints/TouchHolds/TouchHoldSelectionBlueprint.cs b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/TouchHolds/TouchHoldSelectionBlueprint.cs new file mode 100644 index 000000000..36f5f4ef5 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/TouchHolds/TouchHoldSelectionBlueprint.cs @@ -0,0 +1,35 @@ +using osu.Framework.Graphics.Primitives; +using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Rulesets.Sentakki.Objects.Drawables; +using osuTK; + +namespace osu.Game.Rulesets.Sentakki.Edit.Blueprints.TouchHolds +{ + public partial class TouchHoldSelectionBlueprint : SentakkiSelectionBlueprint + { + public new DrawableTouchHold DrawableObject => (DrawableTouchHold)base.DrawableObject; + + private readonly TouchHoldHighlight highlight; + + public TouchHoldSelectionBlueprint(TouchHold hitObject) + : base(hitObject) + { + InternalChild = highlight = new TouchHoldHighlight(); + } + + protected override void Update() + { + base.Update(); + + highlight.Position = DrawableObject.Position; + highlight.Scale = DrawableObject.Scale; + highlight.ProgressPiece.ProgressBindable.Value = DrawableObject.TouchHoldBody.ProgressPiece.ProgressBindable.Value; + } + + public override Vector2 ScreenSpaceSelectionPoint => highlight.ScreenSpaceDrawQuad.Centre; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => highlight.ReceivePositionalInputAt(screenSpacePos); + + public override Quad SelectionQuad => highlight.ScreenSpaceDrawQuad; + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Touches/TouchHighlight.cs b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Touches/TouchHighlight.cs new file mode 100644 index 000000000..406f356a8 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Touches/TouchHighlight.cs @@ -0,0 +1,16 @@ +using osu.Framework.Graphics; +using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Touches; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Sentakki.Edit.Blueprints.Touches +{ + public partial class TouchHighlight : TouchBody + { + public TouchHighlight() + { + Anchor = Origin = Anchor.Centre; + Colour = Color4.YellowGreen; + Alpha = 0.5f; + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Touches/TouchPlacementBlueprint.cs b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Touches/TouchPlacementBlueprint.cs new file mode 100644 index 000000000..cf8fcc11f --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Touches/TouchPlacementBlueprint.cs @@ -0,0 +1,47 @@ +using osu.Framework.Input.Events; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Sentakki.Objects; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Sentakki.Edit.Blueprints.Touches +{ + public partial class TouchPlacementBlueprint : SentakkiPlacementBlueprint + { + private readonly TouchHighlight highlight; + + public TouchPlacementBlueprint() + { + InternalChild = highlight = new TouchHighlight(); + } + + protected override void Update() + { + highlight.Position = HitObject.Position; + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button != MouseButton.Left) + return false; + + EndPlacement(true); + return true; + } + + public override void UpdateTimeAndPosition(SnapResult result) + { + base.UpdateTimeAndPosition(new SnapResult(Vector2.Zero, null)); + + var newPosition = ToLocalSpace(result.ScreenSpacePosition) - OriginPosition; + + if (Vector2.Distance(Vector2.Zero, newPosition) > 250) + { + float angle = Vector2.Zero.GetDegreesFromPosition(newPosition); + newPosition = SentakkiExtensions.GetCircularPosition(250, angle); + } + + HitObject.Position = newPosition; + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Touches/TouchSelectionBlueprint.cs b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Touches/TouchSelectionBlueprint.cs new file mode 100644 index 000000000..b81a8233d --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Blueprints/Touches/TouchSelectionBlueprint.cs @@ -0,0 +1,35 @@ +using osu.Framework.Graphics.Primitives; +using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Rulesets.Sentakki.Objects.Drawables; +using osuTK; + +namespace osu.Game.Rulesets.Sentakki.Edit.Blueprints.Touches +{ + public partial class TouchSelectionBlueprint : SentakkiSelectionBlueprint + { + public new DrawableTouch DrawableObject => (DrawableTouch)base.DrawableObject; + + private readonly TouchHighlight highlight; + + public TouchSelectionBlueprint(Touch hitObject) + : base(hitObject) + { + InternalChild = highlight = new TouchHighlight(); + } + + protected override void Update() + { + base.Update(); + + highlight.Position = DrawableObject.Position; + highlight.Size = DrawableObject.TouchBody.Size; + highlight.BorderContainer.Alpha = DrawableObject.TouchBody.BorderContainer.Alpha; + } + + public override Vector2 ScreenSpaceSelectionPoint => highlight.ScreenSpaceDrawQuad.Centre; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => highlight.ReceivePositionalInputAt(screenSpacePos); + + public override Quad SelectionQuad => highlight.ScreenSpaceDrawQuad; + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/HoldCompositionTool.cs b/osu.Game.Rulesets.Sentakki/Edit/HoldCompositionTool.cs new file mode 100644 index 000000000..0042475cb --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/HoldCompositionTool.cs @@ -0,0 +1,21 @@ +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Sentakki.Edit.Blueprints.Holds; +using osu.Game.Rulesets.Sentakki.Objects; + +namespace osu.Game.Rulesets.Sentakki.Edit +{ + public class HoldCompositionTool : HitObjectCompositionTool + { + public HoldCompositionTool() + : base(nameof(Hold)) + { + } + + public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); + + public override PlacementBlueprint CreatePlacementBlueprint() => new HoldPlacementBlueprint(); + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/SentakkiBlueprintContainer.cs b/osu.Game.Rulesets.Sentakki/Edit/SentakkiBlueprintContainer.cs new file mode 100644 index 000000000..71b7dc231 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/SentakkiBlueprintContainer.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Sentakki.Edit.Blueprints.Holds; +using osu.Game.Rulesets.Sentakki.Edit.Blueprints.Slides; +using osu.Game.Rulesets.Sentakki.Edit.Blueprints.Taps; +using osu.Game.Rulesets.Sentakki.Edit.Blueprints.Touches; +using osu.Game.Rulesets.Sentakki.Edit.Blueprints.TouchHolds; +using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; + +namespace osu.Game.Rulesets.Sentakki.Edit +{ + public partial class SentakkiBlueprintContainer : ComposeBlueprintContainer + { + public SentakkiBlueprintContainer(HitObjectComposer composer) + : base(composer) + { + AddInternal(chevronPool = new DrawablePool(100)); + AddInternal(fanChevrons = new SlideFanChevrons()); + } + + [Cached] + private DrawablePool chevronPool; + + [Cached] + private SlideFanChevrons fanChevrons; + + protected override SelectionHandler CreateSelectionHandler() => new SentakkiSelectionHandler(); + + protected override bool ApplySnapResult(SelectionBlueprint[] blueprints, SnapResult result) + { + if (!base.ApplySnapResult(blueprints, result)) + return false; + + if (blueprints.All(b => b.Item is SentakkiLanedHitObject)) + { + SentakkiLanedSnapResult senSnapResult = (SentakkiLanedSnapResult)result; + + int offset = senSnapResult.Lane - ((SentakkiLanedHitObject)blueprints.First().Item).Lane; + if (offset != 0) + { + Beatmap.PerformOnSelection(delegate (HitObject ho) + { + var lho = (SentakkiLanedHitObject)ho; + + lho.Lane = (lho.Lane + offset).NormalizePath(); + Beatmap.Update(ho); + }); + } + } + + return true; + } + + public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) + { + switch (hitObject) + { + case Tap t: + return new TapSelectionBlueprint(t); + + case Hold h: + return new HoldSelectionBlueprint(h); + + case Touch t: + return new TouchSelectionBlueprint(t); + + case TouchHold th: + return new TouchHoldSelectionBlueprint(th); + + case Slide s: + return new SlideSelectionBlueprint(s); + } + + return base.CreateHitObjectBlueprintFor(hitObject); + } + + private Vector2 currentMousePosition => InputManager.CurrentState.Mouse.Position; + + protected override IEnumerable> SortForMovement(IReadOnlyList> blueprints) + => blueprints.OrderBy(b => Vector2.DistanceSquared(b.ScreenSpaceSelectionPoint, currentMousePosition)); + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/SentakkiHitObjectComposer.cs b/osu.Game.Rulesets.Sentakki/Edit/SentakkiHitObjectComposer.cs new file mode 100644 index 000000000..93a23286d --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/SentakkiHitObjectComposer.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Edit.Components.TernaryButtons; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; + +namespace osu.Game.Rulesets.Sentakki.Edit +{ + public partial class SentakkiHitObjectComposer : HitObjectComposer + { + [Cached] + private SentakkiSnapProvider snapProvider { get; set; } = new SentakkiSnapProvider(); + + public SentakkiHitObjectComposer(SentakkiRuleset ruleset) + : base(ruleset) + { + } + + private DrawableRulesetDependencies dependencies = null!; + + [Cached] + private SlideEditorToolboxGroup slideEditorToolboxGroup = new SlideEditorToolboxGroup(); + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + => dependencies = new DrawableRulesetDependencies(Ruleset, base.CreateChildDependencies(parent)); + + protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] + { + new TapCompositionTool(), + new HoldCompositionTool(), + new TouchCompositionTool(), + new TouchHoldCompositionTool(), + new SlideCompositionTool(), + }; + + protected override IEnumerable CreateTernaryButtons() + => base.CreateTernaryButtons() + .Skip(1) + .Concat(snapProvider.CreateTernaryButtons()); + + + public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) + => snapProvider.GetSnapResult(screenSpacePosition); + + + protected override ComposeBlueprintContainer CreateBlueprintContainer() => new SentakkiBlueprintContainer(this); + + private BindableList selectedHitObjects = null!; + + + [BackgroundDependencyLoader] + private void load() + { + RightToolbox.Add(slideEditorToolboxGroup); + LayerBelowRuleset.Add(snapProvider); + + selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy(); + selectedHitObjects.CollectionChanged += (_, _) => updateSnapProvider(); + } + + protected override void Update() + { + base.Update(); + + if (BlueprintContainer.CurrentTool != lastTool) + { + lastTool = BlueprintContainer.CurrentTool; + updateSnapProvider(); + } + } + + private HitObjectCompositionTool? lastTool = null; + + public void updateSnapProvider() + { + lastTool = BlueprintContainer.CurrentTool; + if (BlueprintContainer.CurrentTool is SelectTool) + { + if (selectedHitObjects.Count == 0) + snapProvider.SwitchModes(SentakkiSnapProvider.SnapMode.Off); + else if (selectedHitObjects.All(h => h is Touch)) + snapProvider.SwitchModes(SentakkiSnapProvider.SnapMode.Touch); + else if (selectedHitObjects.All(h => h is SentakkiLanedHitObject)) + snapProvider.SwitchModes(SentakkiSnapProvider.SnapMode.Laned); + else + snapProvider.SwitchModes(SentakkiSnapProvider.SnapMode.Off); + return; + } + + snapProvider.SwitchModes(BlueprintContainer.CurrentTool switch + { + TouchCompositionTool => SentakkiSnapProvider.SnapMode.Touch, + TouchHoldCompositionTool => SentakkiSnapProvider.SnapMode.Off, + _ => SentakkiSnapProvider.SnapMode.Laned, + }); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + dependencies?.Dispose(); + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/SentakkiLanedSnapGrid.cs b/osu.Game.Rulesets.Sentakki/Edit/SentakkiLanedSnapGrid.cs new file mode 100644 index 000000000..44a917baf --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/SentakkiLanedSnapGrid.cs @@ -0,0 +1,212 @@ +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Sentakki.Configuration; +using osu.Game.Rulesets.Sentakki.UI; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Components.TernaryButtons; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Sentakki.Edit; + +public partial class SentakkiSnapGrid : CompositeDrawable +{ + private Bindable enabled = new Bindable(TernaryState.True); + + private DrawablePool linePool = null!; + + private Bindable animationSpeed = new Bindable(2); + + private Container linesContainer = null!; + + [Resolved] + private EditorClock editorClock { get; set; } = null!; + + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + [Resolved] + private IBeatSnapProvider beatSnapProvider { get; set; } = null!; + + [Resolved] + private Bindable working { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public TernaryButton CreateTernaryButton() => new TernaryButton(enabled, "Lane beat snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }); + + [BackgroundDependencyLoader] + private void load(SentakkiRulesetConfigManager configManager) + { + Anchor = Origin = Anchor.Centre; + AddRangeInternal(new Drawable[]{ + linePool = new DrawablePool(10), + linesContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, + }); + + configManager.BindWith(SentakkiRulesetSettings.AnimationSpeed, animationSpeed); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + enabled.BindValueChanged(toggleVisibility); + } + + private void toggleVisibility(ValueChangedEvent v) + { + if (v.NewValue is TernaryState.True) + { + linesContainer.Show(); + return; + } + + linesContainer.Hide(); + } + + public SnapResult GetSnapResult(Vector2 screenSpacePosition) + { + var localPosition = ToLocalSpace(screenSpacePosition); + int lane = Vector2.Zero.GetDegreesFromPosition(localPosition).GetNoteLaneFromDegrees(); + + if (enabled.Value is not TernaryState.True) + return new SentakkiLanedSnapResult(screenSpacePosition, lane, null) { YPos = SentakkiPlayfield.INTERSECTDISTANCE }; + + float length = localPosition.Length; + + var closestLine = linesContainer.MinBy(l => MathF.Abs(length - (l.DrawWidth / 2))); + + return new SentakkiLanedSnapResult(screenSpacePosition, lane, closestLine?.SnappingTime) + { + YPos = closestLine is not null ? closestLine.DrawWidth / 2 : SentakkiPlayfield.INTERSECTDISTANCE, + }; + } + + private void recreateLines() + { + linesContainer.Clear(false); + double time = editorClock.CurrentTime; + double animationDuration = DrawableSentakkiRuleset.ComputeLaneNoteEntryTime(animationSpeed.Value); + + + double maximumVisibleTime = editorClock.CurrentTime + (animationDuration * 0.5f); + double minimumVisibleTime = editorClock.CurrentTime - (animationDuration * 0.5f); + + for (int i = 0; i < editorBeatmap.ControlPointInfo.TimingPoints.Count; ++i) + { + var timingPoint = editorBeatmap.ControlPointInfo.TimingPoints[i]; + + double nextTimingPointTime = i + 1 == editorBeatmap.ControlPointInfo.TimingPoints.Count ? working.Value.Track.Length : editorBeatmap.ControlPointInfo.TimingPoints[i + 1].Time; + + // This timing point isn't visible from the current time (too late), subsequent timing points are later, so no need to consider them as well. + if (timingPoint.Time > maximumVisibleTime) + return; + + // The current timing point isn't visible from the current time as it is too early + if (nextTimingPointTime <= minimumVisibleTime) + continue; + + double beatLength = timingPoint.BeatLength / beatSnapProvider.BeatDivisor; + + for (int beatIndex = 0; timingPoint.Time + (beatIndex * beatLength) < nextTimingPointTime; ++beatIndex) + { + double beatTime = timingPoint.Time + (beatIndex * beatLength); + + if (beatTime > maximumVisibleTime) + return; + + if (beatTime < minimumVisibleTime) + continue; + + float circleRadius = GetDistanceRelativeToCurrentTime(beatTime); + + BeatSnapGridLine line; + linesContainer.Add(line = linePool.Get()); + + int divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex, beatSnapProvider.BeatDivisor); + + float thickness = getWidthForDivisor(divisor); + + line.Size = new Vector2(circleRadius * 2); + line.BorderThickness = thickness; + line.Colour = BindableBeatDivisor.GetColourFor(divisor, colours); + line.SnappingTime = beatTime; + } + } + } + + private int getWidthForDivisor(int divisor) => divisor switch + { + 1 or 2 => 5, + 3 or 4 => 4, + 6 or 8 => 3, + _ => 2, + }; + + protected override void Update() + { + recreateLines(); + base.Update(); + } + + public float GetDistanceRelativeToCurrentTime(double time, float min = float.MinValue, float max = float.MaxValue) + { + double animationDuration = DrawableSentakkiRuleset.ComputeLaneNoteEntryTime(animationSpeed.Value); + + double offsetRatio = (time - editorClock.CurrentTime) / (animationDuration * 0.5f); + + float distance = (float)Interpolation.Lerp(SentakkiPlayfield.INTERSECTDISTANCE, SentakkiPlayfield.NOTESTARTDISTANCE, offsetRatio); + + return (float)Math.Clamp(distance, min, max); + } + + private partial class BeatSnapGridLine : PoolableDrawable + { + public override bool RemoveCompletedTransforms => false; + + public double SnappingTime { get; set; } + public new float BorderThickness + { + get => ring.BorderThickness; + set => ring.BorderThickness = value; + } + + private CircularContainer ring = null!; + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Origin = Anchor.Centre; + + AddInternal(ring = new CircularContainer + { + Masking = true, + RelativeSizeAxes = Axes.Both, + BorderColour = Color4.White, + BorderThickness = 2, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + }); + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/SentakkiLanedSnapResult.cs b/osu.Game.Rulesets.Sentakki/Edit/SentakkiLanedSnapResult.cs new file mode 100644 index 000000000..a7014dda8 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/SentakkiLanedSnapResult.cs @@ -0,0 +1,16 @@ +using osu.Game.Rulesets.Edit; +using osuTK; + +namespace osu.Game.Rulesets.Sentakki.Edit; + +public class SentakkiLanedSnapResult : SnapResult +{ + public SentakkiLanedSnapResult(Vector2 screenSpacePosition, int lane, double? time) : base(screenSpacePosition, time, null) + { + Lane = lane; + } + + public int Lane { get; set; } + + public float YPos { get; set; } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/SentakkiSelectionHandler.cs b/osu.Game.Rulesets.Sentakki/Edit/SentakkiSelectionHandler.cs new file mode 100644 index 000000000..49141c72f --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/SentakkiSelectionHandler.cs @@ -0,0 +1,290 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Extensions; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; + +namespace osu.Game.Rulesets.Sentakki.Edit +{ + public partial class SentakkiSelectionHandler : EditorSelectionHandler + { + private readonly Bindable selectionBreakState = new Bindable(); + private readonly Bindable selectionExState = new Bindable(); + private readonly Bindable selectionSlideBodyBreakState = new Bindable(); + + public SentakkiSelectionHandler() + { + selectionBreakState.ValueChanged += s => + { + switch (s.NewValue) + { + case TernaryState.True: + case TernaryState.False: + setBreakState(s.NewValue == TernaryState.True); + return; + } + }; + + selectionSlideBodyBreakState.ValueChanged += s => + { + switch (s.NewValue) + { + case TernaryState.True: + case TernaryState.False: + setSlideBodyBreakState(s.NewValue == TernaryState.True); + return; + } + }; + + selectionExState.ValueChanged += s => + { + switch (s.NewValue) + { + case TernaryState.True: + case TernaryState.False: + setExState(s.NewValue == TernaryState.True); + return; + } + }; + } + + public override bool HandleMovement(MoveSelectionEvent moveEvent) + { + if (SelectedBlueprints.All(bp => bp.Item is SentakkiLanedHitObject)) + return true; + + if (SelectedBlueprints.All(bp => bp.Item is Touch)) + { + // Special movement handling to ensure that all touch notes are within 250 units from the playfield centre + moveTouchNotes(this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta)); + return true; + } + + return false; + } + + private void setBreakState(bool state) + { + var lhos = EditorBeatmap.SelectedHitObjects.OfType(); + + EditorBeatmap.BeginChange(); + + foreach (var lho in lhos) + { + if (lho.Break == state) + continue; + + lho.Break = state; + EditorBeatmap.Update(lho); + } + + EditorBeatmap.EndChange(); + } + + private void setSlideBodyBreakState(bool state) + { + var lhos = EditorBeatmap.SelectedHitObjects.OfType(); + + EditorBeatmap.BeginChange(); + + foreach (var slide in lhos) + { + bool adjusted = false; + + foreach (var body in slide.SlideInfoList) + { + if (body.Break == state) + continue; + + body.Break = state; + adjusted = true; + } + + if (adjusted) + EditorBeatmap.Update(slide); + } + + EditorBeatmap.EndChange(); + } + + private void setExState(bool state) + { + var shos = EditorBeatmap.SelectedHitObjects.OfType().Where(s => s is not TouchHold); + + EditorBeatmap.BeginChange(); + foreach (var sho in shos) + { + if (sho.Ex == state) + continue; + + sho.Ex = state; + EditorBeatmap.Update(sho); + } + EditorBeatmap.EndChange(); + } + + protected override void UpdateTernaryStates() + { + base.UpdateTernaryStates(); + + selectionBreakState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType(), h => h.Break); + selectionSlideBodyBreakState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType().SelectMany(h => h.SlideInfoList), s => s.Break); + selectionExState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType().Where(s => s is not TouchHold), s => s.Ex); + } + + protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) + { + if (selection.Any(s => s.Item is SentakkiLanedHitObject)) + yield return new TernaryStateToggleMenuItem("Break") { State = { BindTarget = selectionBreakState } }; + + if (selection.Any(s => s.Item is not TouchHold)) + yield return new TernaryStateToggleMenuItem("Ex") { State = { BindTarget = selectionExState } }; + + var slides = selection.Where(bp => bp.Item is Slide).Select(bp => (Slide)bp.Item).OrderBy(s => s.StartTime).ToList(); + + if (slides.Count > 0) + yield return new TernaryStateToggleMenuItem("Slide Break") { State = { BindTarget = selectionSlideBodyBreakState } }; + + if (canMergeSlides(slides)) + yield return new OsuMenuItem("Merge slides", MenuItemType.Destructive, () => mergeSlides(slides)); + + if (canUnmerge(slides)) + yield return new OsuMenuItem("Unmerge slides", MenuItemType.Destructive, () => unmergeSlides(slides)); + + foreach (var item in base.GetContextMenuItemsForSelection(selection)) + yield return item; + } + + private void moveTouchNotes(Vector2 dragDelta) + { + const float boundary_radius = 250; + + float dragDistance(Vector2 origin, Vector2 destination) + => MathF.Min((destination - origin).Length, circleIntersectionDistance(origin, destination - origin)); + + float circleIntersectionDistance(Vector2 centre, Vector2 direction) + { + direction.Normalize(); + float b = (direction.X * centre.X) + (direction.Y * centre.Y); + float c = centre.LengthSquared - (boundary_radius * boundary_radius); + return MathF.Sqrt((b * b) - c) - b; + } + + var touches = SelectedBlueprints.Select(bp => (Touch)bp.Item).ToList(); + Vector2 centre = touches.Aggregate(Vector2.Zero, (a, b) => a + b.Position) / touches.Count; + float cappedDragDelta = touches.Min(t => dragDistance(t.Position - centre, t.Position + dragDelta)); + + if (!(cappedDragDelta >= 0)) return; // No movement or invalid movement occurred + + foreach (var touch in touches) + touch.Position = touch.Position - centre + (cappedDragDelta * (dragDelta + centre).Normalized()); + } + + private bool canMergeSlides(List slides) => slides.Count > 1 && slides.GroupBy(s => s.Lane).Count() == 1; + private bool canUnmerge(List hitObjects) => hitObjects.Any(s => s.SlideInfoList.Count > 1); + + private void mergeSlides(List slides) + { + var controlPointInfo = EditorBeatmap.ControlPointInfo; + var firstHitObject = slides[0]; + var mergedSlide = new Slide + { + StartTime = firstHitObject.StartTime, + SlideInfoList = firstHitObject.SlideInfoList, + Samples = firstHitObject.Samples, + Lane = firstHitObject.Lane, + Break = firstHitObject.Break + }; + + double beatLength = controlPointInfo.TimingPointAt(mergedSlide.StartTime).BeatLength; + + int findSimilarBody(SlideBodyInfo other) + { + for (int i = 0; i < mergedSlide!.SlideInfoList.Count; ++i) + if (mergedSlide.SlideInfoList[i].ShapeEquals(other)) + return i; + + return -1; + } + + for (int i = 1; i < slides.Count; ++i) + { + var slide = slides[i]; + double bodyBeatLength = controlPointInfo.TimingPointAt(slide.StartTime).BeatLength; + + foreach (var slideBody in slide.SlideInfoList) + { + double bodyOffset = (bodyBeatLength * slideBody.ShootDelay); + double startTimeDelta = slide.StartTime - firstHitObject.StartTime; + double newBodyOffset = startTimeDelta + bodyOffset; + slideBody.ShootDelay = (float)(newBodyOffset / beatLength); + slideBody.Duration += startTimeDelta; + + int similarBodyIndex = findSimilarBody(slideBody); + + if (similarBodyIndex != -1) + { + mergedSlide.SlideInfoList[similarBodyIndex].Duration = slideBody.Duration; + continue; + } + + mergedSlide.SlideInfoList.Add(slideBody); + } + } + + EditorBeatmap.BeginChange(); + + foreach (var slide in slides) + EditorBeatmap.Remove(slide); + + EditorBeatmap.Add(mergedSlide); + SelectedItems.Add(mergedSlide); + + EditorBeatmap.EndChange(); + } + + private void unmergeSlides(List slides) + { + EditorBeatmap.BeginChange(); + + foreach (var slide in slides) + { + if (slide.SlideInfoList.Count <= 1) + continue; + + for (int i = 0; i < slide.SlideInfoList.Count; ++i) + { + var slideInfo = slide.SlideInfoList[i]; + var cpi = EditorBeatmap.ControlPointInfo; + double beatLengthOriginal = cpi.TimingPointAt(slide.StartTime).BeatLength; + double newSt = slide.StartTime + ((slideInfo.ShootDelay - 1) * beatLengthOriginal); + + slideInfo.ShootDelay = 1; + slideInfo.Duration -= newSt - slide.Duration; + + var newSlide = new Slide + { + StartTime = newSt, + SlideInfoList = new List { slideInfo }, + Samples = slide.Samples, + Lane = slide.Lane, + Break = slide.Break + }; + + EditorBeatmap.Add(newSlide); + } + + EditorBeatmap.Remove(slide); + } + + EditorBeatmap.EndChange(); + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/SentakkiSnapProvider.cs b/osu.Game.Rulesets.Sentakki/Edit/SentakkiSnapProvider.cs new file mode 100644 index 000000000..093a48cf3 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/SentakkiSnapProvider.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit.Components.TernaryButtons; +using osuTK; + +namespace osu.Game.Rulesets.Sentakki.Edit; + +public partial class SentakkiSnapProvider : CompositeDrawable +{ + public enum SnapMode + { + Off, + Laned, + Touch, + } + + private SnapMode activeMode = SnapMode.Off; + + private SentakkiSnapGrid lanedSnapGrid = null!; + private SentakkiTouchSnapGrid touchSnapGrid = null!; + + public IEnumerable CreateTernaryButtons() + { + yield return lanedSnapGrid.CreateTernaryButton(); + yield return touchSnapGrid.CreateTernaryButton(); + } + + public SentakkiSnapProvider() + { + Anchor = Origin = Anchor.Centre; + + AddRangeInternal(new Drawable[]{ + lanedSnapGrid = new SentakkiSnapGrid(), + touchSnapGrid = new SentakkiTouchSnapGrid(), + }); + + SwitchModes(SnapMode.Off); + } + + public SnapResult GetSnapResult(Vector2 screenSpacePosition) + { + return activeMode switch + { + SnapMode.Laned => lanedSnapGrid.GetSnapResult(screenSpacePosition), + SnapMode.Touch => touchSnapGrid.GetSnapResult(screenSpacePosition), + _ => new SnapResult(screenSpacePosition, null), + }; + } + + public void SwitchModes(SnapMode mode) + { + activeMode = mode; + switch (mode) + { + case SnapMode.Off: + lanedSnapGrid.Hide(); + touchSnapGrid.Hide(); + break; + + case SnapMode.Laned: + lanedSnapGrid.Show(); + touchSnapGrid.Hide(); + break; + + case SnapMode.Touch: + lanedSnapGrid.Hide(); + touchSnapGrid.Show(); + break; + } + } + + public float GetDistanceRelativeToCurrentTime(double time, float min = float.MinValue, float max = float.MaxValue) => lanedSnapGrid.GetDistanceRelativeToCurrentTime(time, min, max); +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/SentakkiTouchSnapGrid.cs b/osu.Game.Rulesets.Sentakki/Edit/SentakkiTouchSnapGrid.cs new file mode 100644 index 000000000..a6ef5eb25 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/SentakkiTouchSnapGrid.cs @@ -0,0 +1,74 @@ +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Sentakki.Beatmaps; +using osu.Game.Screens.Edit.Components.TernaryButtons; +using osuTK; + +namespace osu.Game.Rulesets.Sentakki.Edit; + +public partial class SentakkiTouchSnapGrid : CompositeDrawable +{ + private Bindable enabled = new Bindable(TernaryState.True); + + public TernaryButton CreateTernaryButton() => new TernaryButton(enabled, "Touch Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Thumbtack }); + + private Container dotContainer = null!; + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Origin = Anchor.Centre; + AddInternal(dotContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + foreach (var point in SentakkiBeatmapConverterOld.VALID_TOUCH_POSITIONS) + { + dotContainer.Add(new Circle + { + Size = new Vector2(10), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = point + }); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + enabled.BindValueChanged(toggleVisibility); + } + + private void toggleVisibility(ValueChangedEvent v) + { + if (v.NewValue is TernaryState.True) + { + dotContainer.Show(); + return; + } + + dotContainer.Hide(); + } + + public SnapResult GetSnapResult(Vector2 screenSpacePosition) + { + if (enabled.Value is not TernaryState.True) + return new SnapResult(screenSpacePosition, null); + + var localPosition = ToLocalSpace(screenSpacePosition); + + var closestPoint = SentakkiBeatmapConverterOld.VALID_TOUCH_POSITIONS.MinBy(v => Vector2.DistanceSquared(v, localPosition)); + + return new SnapResult(ToScreenSpace(closestPoint), null); + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/SlideCompositionTool.cs b/osu.Game.Rulesets.Sentakki/Edit/SlideCompositionTool.cs new file mode 100644 index 000000000..664d52241 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/SlideCompositionTool.cs @@ -0,0 +1,24 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Sentakki.Edit.Blueprints.Slides; +using osu.Game.Rulesets.Sentakki.Objects; + +namespace osu.Game.Rulesets.Sentakki.Edit +{ + public class SlideCompositionTool : HitObjectCompositionTool + { + public SlideCompositionTool() + : base(nameof(Slide)) + { + } + + public override Drawable CreateIcon() => new SpriteIcon + { + Icon = FontAwesome.Regular.Star, + }; + + public override PlacementBlueprint CreatePlacementBlueprint() => new SlidePlacementBlueprint(); + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/SlideEditorToolboxGroup.cs b/osu.Game.Rulesets.Sentakki/Edit/SlideEditorToolboxGroup.cs new file mode 100644 index 000000000..f4a68996b --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/SlideEditorToolboxGroup.cs @@ -0,0 +1,144 @@ +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Sentakki.Edit.Blueprints.Slides; +using osu.Game.Rulesets.Sentakki.Edit.Toolbox; +using osu.Game.Rulesets.Sentakki.Objects; +using osuTK.Input; + +namespace osu.Game.Rulesets.Sentakki.Edit; + +[Cached] +public partial class SlideEditorToolboxGroup : EditorToolboxGroup +{ + // Slide info + private Bindable shapeBindable = new Bindable(); // This is locked + public readonly Bindable LaneOffset = new Bindable(4); + private Bindable mirrored = new Bindable(); // This is locked + + private Bindable shootDelay = new Bindable(); + + public readonly Bindable CurrentPartBindable = new Bindable(new SlideBodyPart(SlidePaths.PathShapes.Straight, 4, false)); + + public SlideBodyPart CurrentPart => CurrentPartBindable.Value; + + public SlideEditorToolboxGroup() : base("slide") + { + Children = new Drawable[]{ + new ExpandableMenu("Shape"){ + Current = shapeBindable + }, + new LaneOffsetCounter{ + Current = LaneOffset + }, + new ShootDelayCounter(){ + Current = shootDelay + }, + new ExpandableCheckbox("Mirrored"){ + Current = mirrored + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + shapeBindable.BindValueChanged(e => RequestLaneChange(LaneOffset.Value)); + mirrored.BindValueChanged(e => RequestLaneChange(LaneOffset.Value)); + } + + [Resolved] + private IBeatSnapProvider beatSnapProvider { get; set; } = null!; + + public void ChangeTarget(SlidePlacementBlueprint? slideBlueprint) + { + if (slideBlueprint is null) + Hide(); + + Show(); + } + + public void RequestLaneChange(int newLane, bool findClosestMatch = false) + { + int oldOffset = LaneOffset.Value; + + int rotationFactor = newLane - oldOffset >= 0 ? 1 : -1; + + for (int i = 0; i < 8; ++i) + { + var newPart = new SlideBodyPart(shapeBindable.Value, (newLane + (i * rotationFactor)).NormalizePath(), mirrored.Value); + + if (SlidePaths.CheckSlideValidity(newPart)) + { + CurrentPartBindable.Value = newPart; + LaneOffset.Value = newPart.EndOffset; + return; + } + if (findClosestMatch) + rotationFactor *= -1; + } + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + switch (e.Key) + { + case Key.Plus: + shootDelay.Value += 1f / beatSnapProvider.BeatDivisor; + break; + + case Key.Minus: + shootDelay.Value = Math.Max(0, shootDelay.Value - (1f / beatSnapProvider.BeatDivisor)); + break; + + case Key.Number0: + shootDelay.Value = 1; + break; + + case Key.BackSlash: + mirrored.Value = !mirrored.Value; + return true; + + case Key.BracketRight: + shapeBindable.Value = (SlidePaths.PathShapes)((int)(shapeBindable.Value + 1) % 8); + return true; + + case Key.BracketLeft: + shapeBindable.Value = (SlidePaths.PathShapes)(((int)(shapeBindable.Value + 7)) % 8); + return true; + } + + return base.OnKeyDown(e); + } + + private partial class LaneOffsetCounter : ExpandableCounter + { + public LaneOffsetCounter() : base("Lane offset") + { + } + + [Resolved] + private SlideEditorToolboxGroup slideEditorToolboxGroup { get; set; } = null!; + + protected override void OnLeftButtonPressed() => slideEditorToolboxGroup.RequestLaneChange(Current.Value - 1); + protected override void OnRightButtonPressed() => slideEditorToolboxGroup.RequestLaneChange(Current.Value + 1); + } + + private partial class ShootDelayCounter : ExpandableCounter + { + [Resolved] + private IBeatSnapProvider beatSnapProvider { get; set; } = null!; + + public ShootDelayCounter() : base("Shoot delay", @"{0:0.##} beats") + { + } + + protected override void OnLeftButtonPressed() => Current.Value = Math.Max(0, Current.Value - (1f / beatSnapProvider.BeatDivisor)); + + protected override void OnRightButtonPressed() => Current.Value += 1f / beatSnapProvider.BeatDivisor; + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/TapCompositionTool.cs b/osu.Game.Rulesets.Sentakki/Edit/TapCompositionTool.cs new file mode 100644 index 000000000..adf216cec --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/TapCompositionTool.cs @@ -0,0 +1,21 @@ +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Sentakki.Edit.Blueprints.Taps; +using osu.Game.Rulesets.Sentakki.Objects; + +namespace osu.Game.Rulesets.Sentakki.Edit +{ + public class TapCompositionTool : HitObjectCompositionTool + { + public TapCompositionTool() + : base(nameof(Tap)) + { + } + + public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles); + + public override PlacementBlueprint CreatePlacementBlueprint() => new TapPlacementBlueprint(); + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Toolbox/ExpandableCheckbox.cs b/osu.Game.Rulesets.Sentakki/Edit/Toolbox/ExpandableCheckbox.cs new file mode 100644 index 000000000..7705207a6 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Toolbox/ExpandableCheckbox.cs @@ -0,0 +1,77 @@ +using osuTK; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Framework.Allocation; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Rulesets.Sentakki.Edit.Toolbox; +public partial class ExpandableCheckbox : CompositeDrawable, IExpandable, IHasCurrentValue +{ + public override bool HandlePositionalInput => true; + + public BindableBool Expanded { get; } = new BindableBool(); + + public Bindable Current { get; set; } = new Bindable(); + + private string expandedLabelText; + private string unexpandedLabeltext = ""; + + private OsuSpriteText label; + private OsuCheckbox checkbox; + + public ExpandableCheckbox(string labelText) + { + expandedLabelText = labelText; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 10f), + Children = new Drawable[] + { + checkbox = new OsuCheckbox(){ + LabelText = labelText + }, + label = new OsuSpriteText(){ + Text = labelText + }, + } + }; + } + + [Resolved] + private IExpandingContainer? expandingContainer { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + expandingContainer?.Expanded.BindValueChanged(containerExpanded => + { + Expanded.Value = containerExpanded.NewValue; + }, true); + + checkbox.Current = Current; + + Current.BindValueChanged(v => + { + unexpandedLabeltext = $"{expandedLabelText}: {v.NewValue}"; + label.Text = unexpandedLabeltext; + }, true); + + Expanded.BindValueChanged(v => + { + checkbox.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); + label.FadeTo(v.NewValue ? 0f : 1f, 500, Easing.OutQuint); + checkbox.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; + label.BypassAutoSizeAxes = v.NewValue ? Axes.Y : Axes.None; + }, true); + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Toolbox/ExpandableCounter.cs b/osu.Game.Rulesets.Sentakki/Edit/Toolbox/ExpandableCounter.cs new file mode 100644 index 000000000..99c30a948 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Toolbox/ExpandableCounter.cs @@ -0,0 +1,119 @@ +using osuTK; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.Sprites; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using System; +using System.Globalization; +using System.Numerics; + +using Vector2 = osuTK.Vector2; + +namespace osu.Game.Rulesets.Sentakki.Edit.Toolbox; +public partial class ExpandableCounter : CompositeDrawable, IExpandable, IHasCurrentValue + where T : struct, IComparable, IConvertible, IEquatable, INumber, IMinMaxValue +{ + public override bool HandlePositionalInput => true; + + public BindableBool Expanded { get; } = new BindableBool(); + + public Bindable Current { get; set; } = new BindableNumber(default); + + private OsuSpriteText label; + private OsuSpriteText counter; + + private string labelText; + private string unexpandedLabeltext = ""; + private GridContainer grid; + + private string labelFormat; + + public ExpandableCounter(string label, string format = @"{0:0.##}") + { + labelFormat = format; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + labelText = label; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 10f), + Children = new Drawable[] + { + this.label = new OsuSpriteText(){ + Text = label + }, + grid = new GridContainer() + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize)}, + ColumnDimensions = new[]{ new Dimension(GridSizeMode.Distributed), new Dimension(GridSizeMode.Distributed), new Dimension(GridSizeMode.Distributed) }, + Content = new Drawable[][] + { + new Drawable[]{ + new IconButton(){ + RelativeSizeAxes = Axes.X, + Width = 1, + Icon = FontAwesome.Solid.ChevronLeft, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = OnLeftButtonPressed + }, + counter = new OsuSpriteText(){ + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new IconButton(){ + RelativeSizeAxes = Axes.X, + Width = 1, + Icon = FontAwesome.Solid.ChevronRight, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = OnRightButtonPressed + }, + } + } + } + } + }; + } + + [Resolved] + private IExpandingContainer? expandingContainer { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(v => + { + counter.Text = string.Format(labelFormat, v.NewValue.ToSingle(NumberFormatInfo.InvariantInfo)); + unexpandedLabeltext = $"{labelText}: {counter.Text}"; + if (!Expanded.Value) + label.Text = unexpandedLabeltext; + }, true); + + expandingContainer?.Expanded.BindValueChanged(containerExpanded => + { + Expanded.Value = containerExpanded.NewValue; + }, true); + + Expanded.BindValueChanged(v => + { + label.Text = v.NewValue ? labelText : unexpandedLabeltext; + grid.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); + grid.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; + }, true); + } + + protected virtual void OnLeftButtonPressed() { } + protected virtual void OnRightButtonPressed() { } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/Toolbox/ExpandableMenu.cs b/osu.Game.Rulesets.Sentakki/Edit/Toolbox/ExpandableMenu.cs new file mode 100644 index 000000000..d579d957e --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/Toolbox/ExpandableMenu.cs @@ -0,0 +1,95 @@ +using System; +using osuTK; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Framework.Allocation; +using osu.Game.Graphics.UserInterface; +using osu.Framework.Input.Events; + +namespace osu.Game.Rulesets.Sentakki.Edit.Toolbox; +public partial class ExpandableMenu : CompositeDrawable, IExpandable, IHasCurrentValue + where T : struct, Enum +{ + public override bool HandlePositionalInput => true; + + public BindableBool Expanded { get; } = new BindableBool(); + + public Bindable Current { get; set; } = new Bindable(); + + private OsuDropdown menu; + + private string expandedLabelText; + private string unexpandedLabeltext = ""; + + private OsuSpriteText label; + + public ExpandableMenu(string labelText) + { + expandedLabelText = labelText; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 10f), + Children = new Drawable[] + { + label = new OsuSpriteText(){ + Text = labelText + }, + menu = new NonBlockingDropdown + { + RelativeSizeAxes = Axes.X, + }, + } + }; + } + + [Resolved] + private IExpandingContainer? expandingContainer { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + expandingContainer?.Expanded.BindValueChanged(containerExpanded => + { + Expanded.Value = containerExpanded.NewValue; + }, true); + + menu.Current = Current; + Current.BindValueChanged(v => + { + unexpandedLabeltext = $"{expandedLabelText}: {v.NewValue}"; + if (!Expanded.Value) + label.Text = unexpandedLabeltext; + }, true); + + Expanded.BindValueChanged(v => + { + label.Text = v.NewValue ? expandedLabelText : unexpandedLabeltext; + menu.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); + menu.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; + }, true); + } + + private partial class NonBlockingDropdown : OsuEnumDropdown + { + protected override DropdownMenu CreateMenu() => new NonBlockingMenu(); + + private partial class NonBlockingMenu : OsuDropdownMenu + { + protected override bool OnHover(HoverEvent e) + { + base.OnHover(e); + return false; + } + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/TouchCompositionTool.cs b/osu.Game.Rulesets.Sentakki/Edit/TouchCompositionTool.cs new file mode 100644 index 000000000..c97c644fc --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/TouchCompositionTool.cs @@ -0,0 +1,24 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Sentakki.Edit.Blueprints.Touches; +using osu.Game.Rulesets.Sentakki.Objects; + +namespace osu.Game.Rulesets.Sentakki.Edit +{ + public class TouchCompositionTool : HitObjectCompositionTool + { + public TouchCompositionTool() + : base(nameof(Touch)) + { + } + + public override Drawable CreateIcon() => new SpriteIcon + { + Icon = FontAwesome.Regular.HandPointRight, + }; + + public override PlacementBlueprint CreatePlacementBlueprint() => new TouchPlacementBlueprint(); + } +} diff --git a/osu.Game.Rulesets.Sentakki/Edit/TouchHoldCompositionTool.cs b/osu.Game.Rulesets.Sentakki/Edit/TouchHoldCompositionTool.cs new file mode 100644 index 000000000..abe0d630f --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Edit/TouchHoldCompositionTool.cs @@ -0,0 +1,21 @@ +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Sentakki.Edit.Blueprints.TouchHolds; +using osu.Game.Rulesets.Sentakki.Objects; + +namespace osu.Game.Rulesets.Sentakki.Edit +{ + public class TouchHoldCompositionTool : HitObjectCompositionTool + { + public TouchHoldCompositionTool() + : base(nameof(TouchHold)) + { + } + + public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners); + + public override PlacementBlueprint CreatePlacementBlueprint() => new TouchHoldPlacementBlueprint(); + } +} diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSlide.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSlide.cs index efe396d21..d1da535ca 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSlide.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSlide.cs @@ -9,13 +9,13 @@ namespace osu.Game.Rulesets.Sentakki.Objects.Drawables { public partial class DrawableSlide : DrawableSentakkiHitObject { + public new Slide HitObject => (Slide)base.HitObject; + public override bool DisplayResult => false; public Container SlideBodies = null!; public Container SlideTaps = null!; - public new Slide HitObject => (Slide)base.HitObject; - public DrawableSlide() : this(null) { diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSlideTap.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSlideTap.cs index 84728ff7a..b1faae9f1 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSlideTap.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSlideTap.cs @@ -41,7 +41,8 @@ protected override void UpdateInitialTransforms() if (ParentHitObject is DrawableSlide slide) spinDuration += slide.HitObject.SlideInfoList.FirstOrDefault()?.Duration ?? 1000; - note.Stars.Spin(spinDuration, RotationDirection.Counterclockwise).Loop(); + if (spinDuration != 0) + note.Stars.Spin(spinDuration, RotationDirection.Counterclockwise).Loop(); } } } diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTouch.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTouch.cs index ad0d6e004..e7cb60718 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTouch.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTouch.cs @@ -27,6 +27,8 @@ public partial class DrawableTouch : DrawableSentakkiHitObject private SentakkiInputManager sentakkiActionInputManager = null!; internal SentakkiInputManager SentakkiActionInputManager => sentakkiActionInputManager ??= (SentakkiInputManager)GetContainingInputManager(); + private readonly IBindable positionBindable = new Bindable(); + public DrawableTouch() : this(null) { @@ -58,17 +60,20 @@ private void load() UpdateResult(true); }); + + positionBindable.BindValueChanged(p => Position = p.NewValue); } protected override void OnApply() { base.OnApply(); - Position = HitObject.Position; + positionBindable.BindTo(HitObject.PositionBindable); } protected override void OnFree() { base.OnFree(); + positionBindable.UnbindFrom(HitObject.PositionBindable); for (int i = 0; i < 11; ++i) PointInteractionState[i] = false; } diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideTapPiece.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideTapPiece.cs index bf139b990..f6f14cb4e 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideTapPiece.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideTapPiece.cs @@ -45,9 +45,12 @@ public SlideTapPiece() private readonly IBindable accentColour = new Bindable(); [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableObject) + private void load(DrawableHitObject? drawableObject) { - accentColour.BindTo(drawableObject.AccentColour); + if (drawableObject is null) + return; + + accentColour.BindTo(drawableObject?.AccentColour); accentColour.BindValueChanged(colour => { Stars.Colour = colour.NewValue; diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Touches/TouchBody.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Touches/TouchBody.cs index 43cda5546..d57d97fa2 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Touches/TouchBody.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Touches/TouchBody.cs @@ -58,9 +58,11 @@ public TouchBody() private readonly IBindable accentColour = new Bindable(); - [BackgroundDependencyLoader] + [BackgroundDependencyLoader(true)] private void load(DrawableHitObject drawableObject) { + if (drawableObject is null) return; + accentColour.BindTo(drawableObject.AccentColour); accentColour.BindValueChanged(colour => PieceContainer.Colour = colour.NewValue, true); } diff --git a/osu.Game.Rulesets.Sentakki/Objects/Hold.cs b/osu.Game.Rulesets.Sentakki/Objects/Hold.cs index b4052e71a..bd7709c49 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Hold.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Hold.cs @@ -21,6 +21,9 @@ public IList> NodeSamples get => nodeSamples; set { + if (!value.Any()) + return; + Samples = value.Last(); nodeSamples = value; } @@ -43,7 +46,7 @@ protected override void CreateNestedHitObjects(CancellationToken cancellationTok Break = Break, StartTime = StartTime, Lane = Lane, - Samples = nodeSamples.Any() ? nodeSamples.First() : new List(), + Samples = NodeSamples.Any() ? NodeSamples.First() : Samples, ColourBindable = ColourBindable.GetBoundCopy(), Ex = Ex }); diff --git a/osu.Game.Rulesets.Sentakki/Objects/SentakkiHitObject.cs b/osu.Game.Rulesets.Sentakki/Objects/SentakkiHitObject.cs index 1c3efbd97..b42875180 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/SentakkiHitObject.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/SentakkiHitObject.cs @@ -6,21 +6,32 @@ using osu.Game.Audio; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Sentakki.Judgements; using osu.Game.Rulesets.Sentakki.Scoring; using osu.Game.Utils; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Sentakki.Objects { - public abstract class SentakkiHitObject : HitObject + public abstract class SentakkiHitObject : HitObject, IHasPosition, IHasDisplayColour { + // TODO: + // No-op IHasPosition properties are added to allow work on editor. Remove ASAP + + public virtual Vector2 Position { get; set; } + public float X => Position.X; + public float Y => Position.Y; + protected SentakkiHitObject() { // We initialize the note colour to the default value first for test scenes // The colours during gameplay will be set during beatmap post-process ColourBindable.Value = DefaultNoteColour; + + DisplayColour = new Bindable(DefaultNoteColour); } public override Judgement CreateJudgement() => new SentakkiJudgement(); @@ -28,6 +39,10 @@ protected SentakkiHitObject() [JsonIgnore] public Bindable ColourBindable = new Bindable(); + // This colour is used to differentiate notes in the editor timeline, and is initialized to the base colour + [JsonIgnore] + public Bindable DisplayColour { get; private set; } + [JsonIgnore] public Color4 NoteColour { diff --git a/osu.Game.Rulesets.Sentakki/Objects/Slide.cs b/osu.Game.Rulesets.Sentakki/Objects/Slide.cs index d58d19edc..9ee2a5762 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Slide.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Slide.cs @@ -24,7 +24,13 @@ public double Duration return max; } - set => throw new NotSupportedException(); + set + { + double ratio = value / Duration; + + foreach (var slide in SlideInfoList) + slide.Duration *= ratio; + } } public double EndTime => StartTime + Duration; diff --git a/osu.Game.Rulesets.Sentakki/Objects/SlideBodyInfo.cs b/osu.Game.Rulesets.Sentakki/Objects/SlideBodyInfo.cs index 3cf2c16dd..d09b45f31 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/SlideBodyInfo.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/SlideBodyInfo.cs @@ -5,6 +5,11 @@ namespace osu.Game.Rulesets.Sentakki.Objects { public class SlideBodyInfo : IEquatable { + private static readonly SentakkiSlidePath empty_path = SlidePaths.CreateSlidePath(new[] + { + new SlideBodyPart(SlidePaths.PathShapes.Straight, endOffset: 0, false) + }); + private SlideBodyPart[] slidePathParts = null!; public SlideBodyPart[] SlidePathParts @@ -17,7 +22,7 @@ public SlideBodyPart[] SlidePathParts } } - public SentakkiSlidePath SlidePath { get; private set; } = null!; + public SentakkiSlidePath SlidePath { get; private set; } = empty_path; // Duration of the slide public double Duration; @@ -29,7 +34,7 @@ public SlideBodyPart[] SlidePathParts // Whether the slide body should have a break modifier applied to them. public bool Break; - public void UpdatePaths() => SlidePath = SlidePaths.CreateSlidePath(slidePathParts); + public void UpdatePaths() => SlidePath = (slidePathParts.Length > 0) ? SlidePaths.CreateSlidePath(slidePathParts) : empty_path; public override bool Equals(object? obj) => obj is not null && obj is SlideBodyInfo other && Equals(other); diff --git a/osu.Game.Rulesets.Sentakki/Objects/SlideCheckpoint.cs b/osu.Game.Rulesets.Sentakki/Objects/SlideCheckpoint.cs index e687b306a..6f04331d0 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/SlideCheckpoint.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/SlideCheckpoint.cs @@ -32,7 +32,7 @@ public CheckpointNode(Vector2 position) Position = position; } - public readonly Vector2 Position; + //public readonly Vector2 Position; protected override HitWindows CreateHitWindows() => HitWindows.Empty; public override Judgement CreateJudgement() => new IgnoreJudgement(); diff --git a/osu.Game.Rulesets.Sentakki/Objects/Touch.cs b/osu.Game.Rulesets.Sentakki/Objects/Touch.cs index a0851f2e2..bd9297946 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Touch.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Touch.cs @@ -1,3 +1,4 @@ +using osu.Framework.Bindables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Sentakki.Scoring; using osuTK; @@ -9,7 +10,13 @@ public class Touch : SentakkiHitObject { public override Color4 DefaultNoteColour => Color4.Aqua; - public Vector2 Position { get; set; } + public Bindable PositionBindable = new Bindable(); + + public override Vector2 Position + { + get => PositionBindable.Value; + set => PositionBindable.Value = value; + } protected override HitWindows CreateHitWindows() => new SentakkiTouchHitWindows(); } diff --git a/osu.Game.Rulesets.Sentakki/SentakkiExtensions.cs b/osu.Game.Rulesets.Sentakki/SentakkiExtensions.cs index 1c195dd05..15231de7d 100644 --- a/osu.Game.Rulesets.Sentakki/SentakkiExtensions.cs +++ b/osu.Game.Rulesets.Sentakki/SentakkiExtensions.cs @@ -124,5 +124,16 @@ public static string GetDisplayNameForSentakkiResult(this HitResult result) return result.GetDescription(); } } + + public static int GetNoteLaneFromDegrees(this float degrees) + { + if (degrees < 0) degrees += 360; + if (degrees >= 360) degrees %= 360; + + int lane = (int)MathF.Round((degrees - 22.5f) / 45f); + if (lane >= 8) lane -= 8; + + return lane; + } } } diff --git a/osu.Game.Rulesets.Sentakki/SentakkiRuleset.cs b/osu.Game.Rulesets.Sentakki/SentakkiRuleset.cs index f1f2cfb63..0a8e665b5 100644 --- a/osu.Game.Rulesets.Sentakki/SentakkiRuleset.cs +++ b/osu.Game.Rulesets.Sentakki/SentakkiRuleset.cs @@ -17,12 +17,14 @@ using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Sentakki.Beatmaps; using osu.Game.Rulesets.Sentakki.Configuration; using osu.Game.Rulesets.Sentakki.Difficulty; +using osu.Game.Rulesets.Sentakki.Edit; using osu.Game.Rulesets.Sentakki.Localisation; using osu.Game.Rulesets.Sentakki.Mods; using osu.Game.Rulesets.Sentakki.Objects; @@ -68,6 +70,8 @@ public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new SentakkiReplayFrame(); + public override HitObjectComposer CreateHitObjectComposer() => new SentakkiHitObjectComposer(this); + public override PerformanceCalculator CreatePerformanceCalculator() => new SentakkiPerformanceCalculator(this); public override IEnumerable GetModsFor(ModType type)