diff --git a/.globalconfig b/.globalconfig index a098f1948..bb8093586 100644 --- a/.globalconfig +++ b/.globalconfig @@ -16,7 +16,7 @@ dotnet_diagnostic.IDE0003.severity = warning dotnet_diagnostic.IDE0004.severity = warning # IDE0005: Remove unnecessary imports -dotnet_diagnostic.IDE0005.severity = warning +dotnet_diagnostic.IDE0005.severity = suggestion # IDE0034: Simplify default literal dotnet_diagnostic.IDE0034.severity = warning diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneCircleChaining.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneCircleChaining.cs new file mode 100644 index 000000000..644bc1a00 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneCircleChaining.cs @@ -0,0 +1,104 @@ +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides; +using osu.Game.Rulesets.Sentakki.UI; +using osu.Game.Rulesets.Sentakki.UI.Components; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Sentakki.Tests.Objects.Slides +{ + [TestFixture] + public partial class TestSceneCircleChaining : OsuTestScene + { + protected override Ruleset CreateRuleset() => new SentakkiRuleset(); + + private bool mirrored; + + private readonly SlideVisual slide; + private readonly Container nodes; + + [Cached] + private readonly DrawablePool chevronPool = null!; + + [Cached] + private readonly SlideFanChevrons fanChevrons = null!; + + public TestSceneCircleChaining() + { + Add(chevronPool = new DrawablePool(62)); + Add(fanChevrons = new SlideFanChevrons()); + + Add(new SentakkiRing + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(SentakkiPlayfield.RINGSIZE) + }); + + Add(slide = new SlideVisual()); + + AddToggleStep("Mirrored second part", b => + { + mirrored = b; + RefreshSlide(); + }); + + AddStep("Perform entry animation", () => slide.PerformEntryAnimation(1000)); + AddWaitStep("Wait for transforms", 5); + + AddStep("Perform exit animation", () => slide.PerformExitAnimation(1000)); + AddWaitStep("Wait for transforms", 5); + + Add(nodes = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + + protected SentakkiSlidePath CreatePattern() + { + var pathParameters = new[]{ + new SlideBodyPart(SlidePaths.PathShapes.Circle, endOffset: 4, false), + new SlideBodyPart(SlidePaths.PathShapes.Circle, endOffset: 4, mirrored), + }; + + return SlidePaths.CreateSlidePath(pathParameters); + } + protected override void LoadComplete() + { + base.LoadComplete(); + RefreshSlide(); + } + + protected void RefreshSlide() + { + slide.Path = CreatePattern(); + nodes.Clear(); + + foreach (var node in slide.Path.SlideSegments.SelectMany(s => s.ControlPoints)) + { + nodes.Add(new CircularContainer + { + Size = new Vector2(10), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = node.Position, + Masking = true, + Child = new Box + { + Colour = Color4.Green, + RelativeSizeAxes = Axes.Both + } + }); + } + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Objects/SlidePaths.cs b/osu.Game.Rulesets.Sentakki/Objects/SlidePaths.cs index abae8e909..798de3a07 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/SlidePaths.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/SlidePaths.cs @@ -85,8 +85,10 @@ public static SentakkiSlidePath CreateSlidePath(int startOffset, params SlideBod { List slideSegments = new List(); - foreach (var path in pathParameters) + for (int i = 0; i < pathParameters.Length; ++i) { + var path = pathParameters[i]; + switch (path.Shape) { case PathShapes.Straight: @@ -98,7 +100,22 @@ public static SentakkiSlidePath CreateSlidePath(int startOffset, params SlideBod break; case PathShapes.Circle: - slideSegments.Add(generateCirclePattern(startOffset, path.EndOffset, path.Mirrored ? RotationDirection.Counterclockwise : RotationDirection.Clockwise)); + + var newSegment = generateCirclePattern(startOffset, path.EndOffset, path.Mirrored ? RotationDirection.Counterclockwise : RotationDirection.Clockwise); + + // Combine Circle paths in the same direction + if (i > 0) + { + var prevPath = pathParameters[i - 1]; + + if (prevPath.Shape == PathShapes.Circle && prevPath.Mirrored == path.Mirrored) + { + slideSegments[^1].ControlPoints.AddRange(newSegment.ControlPoints); + break; + } + } + + slideSegments.Add(newSegment); break; case PathShapes.V: @@ -123,6 +140,7 @@ public static SentakkiSlidePath CreateSlidePath(int startOffset, params SlideBod } startOffset += path.EndOffset; + } return new SentakkiSlidePath(slideSegments.ToArray(), startOffset, pathParameters[^1].Shape == PathShapes.Fan); @@ -243,16 +261,41 @@ private static IEnumerable generateLPattern(int offset, int end, boo // DX Circle Pattern private static SliderPath generateCirclePattern(int offset, int end, RotationDirection direction = RotationDirection.Clockwise) { - float centre = ((offset.GetRotationForLane() + (end + offset).GetRotationForLane()) / 2) + (direction == RotationDirection.Counterclockwise ? 180 : 0); - Vector2 centreNode = SentakkiExtensions.GetCircularPosition(SentakkiPlayfield.INTERSECTDISTANCE, centre == offset.GetRotationForLane() ? centre + 180 : centre); + bool isFullCircle = end.NormalizePath() == 0; + bool isCounterClockwise = direction == RotationDirection.Counterclockwise; + + float startAngle = offset.GetRotationForLane(); + float endAngle = (end + offset).GetRotationForLane(); + + float centreAngle; + + if (isFullCircle) + { + // If it is a full circle, we simply put the centre node across that start point + centreAngle = (offset + 4).GetRotationForLane(); + } + else + { + // Find the angle between the start and end points + centreAngle = (startAngle + endAngle) / 2; + + // If the direction is Counterclockwise, then we flip the centre to the otherside; + if (isCounterClockwise) + centreAngle += 180; + } + + // This is a slight angle tweak to help SliderPath determine which direction the path goes + float angleTweak = 0.1f * (isCounterClockwise ? -1 : 1); + + Vector2 node0Pos = SentakkiExtensions.GetCircularPosition(SentakkiPlayfield.INTERSECTDISTANCE, startAngle + angleTweak); + Vector2 node1Pos = SentakkiExtensions.GetCircularPosition(SentakkiPlayfield.INTERSECTDISTANCE, centreAngle); + Vector2 node2Pos = SentakkiExtensions.GetCircularPosition(SentakkiPlayfield.INTERSECTDISTANCE, endAngle); return new SliderPath(new[] { - new PathControlPoint( - SentakkiExtensions.GetCircularPosition(SentakkiPlayfield.INTERSECTDISTANCE, offset.GetRotationForLane() + (direction == RotationDirection.Counterclockwise ? -.5f : .5f)), - PathType.PerfectCurve), - new PathControlPoint(centreNode), - new PathControlPoint(SentakkiExtensions.GetPositionAlongLane(SentakkiPlayfield.INTERSECTDISTANCE, end + offset), PathType.PerfectCurve) + new PathControlPoint(node0Pos, PathType.PerfectCurve), + new PathControlPoint(node1Pos), + new PathControlPoint(node2Pos, PathType.PerfectCurve) }); }