From 4e0155fa4bac0dd8a8316f4cf1e80b610a465e17 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sun, 8 May 2022 18:32:01 +0900 Subject: [PATCH 1/7] Make `JuiceStreamPath` time based instead of distance based. And remove the "slope limit" feature. TODO: for a juice stream with a large slope, the slider velocity of the hit object should be changed. --- .../TestSceneJuiceStreamSelectionBlueprint.cs | 8 +- .../JuiceStreamPathTest.cs | 95 +++++------ .../Blueprints/Components/EditablePath.cs | 30 ++-- .../Components/PlacementEditablePath.cs | 10 +- .../Components/SelectionEditablePath.cs | 12 +- .../JuiceStreamPlacementBlueprint.cs | 6 +- .../JuiceStreamSelectionBlueprint.cs | 6 +- .../Objects/JuiceStreamPath.cs | 158 ++++++++---------- .../Objects/JuiceStreamPathVertex.cs | 10 +- 9 files changed, 157 insertions(+), 178 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs index fb77fb1efd..123316f461 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs @@ -234,10 +234,10 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor { var path = new JuiceStreamPath(); for (int i = 1; i < times.Length; i++) - path.Add((times[i] - times[0]) * velocity, positions[i] - positions[0]); + path.Add(times[i] - times[0], positions[i] - positions[0]); var sliderPath = new SliderPath(); - path.ConvertToSliderPath(sliderPath, 0); + path.ConvertToSliderPath(sliderPath, 0, velocity); addBlueprintStep(times[0], positions[0], sliderPath, velocity); } @@ -245,11 +245,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor private void addVertexCheckStep(int count, int index, double time, float x) => AddAssert($"vertex {index} of {count} at {time}, {x}", () => { - double expectedDistance = (time - hitObject.StartTime) * hitObject.Velocity; + double expectedTime = time - hitObject.StartTime; float expectedX = x - hitObject.OriginalX; var vertices = getVertices(); return vertices.Count == count && - Precision.AlmostEquals(vertices[index].Distance, expectedDistance, 1e-3) && + Precision.AlmostEquals(vertices[index].Time, expectedTime, 1e-3) && Precision.AlmostEquals(vertices[index].X, expectedX); }); diff --git a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs index 8fa96fb8c9..5248d5a96a 100644 --- a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -37,14 +36,14 @@ namespace osu.Game.Rulesets.Catch.Tests { case 0: { - double distance = rng.NextDouble() * scale * 2 - scale; + double time = rng.NextDouble() * scale * 2 - scale; if (integralValues) - distance = Math.Round(distance); + time = Math.Round(time); - float oldX = path.PositionAtDistance(distance); - int index = path.InsertVertex(distance); + float oldX = path.PositionAtTime(time); + int index = path.InsertVertex(time); Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1)); - Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance)); + Assert.That(path.Vertices[index].Time, Is.EqualTo(time)); Assert.That(path.Vertices[index].X, Is.EqualTo(oldX)); break; } @@ -52,20 +51,20 @@ namespace osu.Game.Rulesets.Catch.Tests case 1: { int index = rng.Next(path.Vertices.Count); - double distance = path.Vertices[index].Distance; + double time = path.Vertices[index].Time; float newX = (float)(rng.NextDouble() * scale * 2 - scale); if (integralValues) newX = MathF.Round(newX); path.SetVertexPosition(index, newX); Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount)); - Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance)); + Assert.That(path.Vertices[index].Time, Is.EqualTo(time)); Assert.That(path.Vertices[index].X, Is.EqualTo(newX)); break; } } - assertInvariants(path.Vertices, checkSlope); + assertInvariants(path.Vertices); } } @@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Tests path.Add(10, 5); path.Add(20, -5); - int removeCount = path.RemoveVertices((v, i) => v.Distance == 10 && i == 1); + int removeCount = path.RemoveVertices((v, i) => v.Time == 10 && i == 1); Assert.That(removeCount, Is.EqualTo(1)); Assert.That(path.Vertices, Is.EqualTo(new[] { @@ -131,8 +130,9 @@ namespace osu.Game.Rulesets.Catch.Tests })); } - [Test] - public void TestRandomConvertFromSliderPath() + [TestCase(10)] + [TestCase(0.1)] + public void TestRandomConvertFromSliderPath(double velocity) { var rng = new Random(1); var path = new JuiceStreamPath(); @@ -162,28 +162,28 @@ namespace osu.Game.Rulesets.Catch.Tests else sliderPath.ExpectedDistance.Value = null; - path.ConvertFromSliderPath(sliderPath); - Assert.That(path.Vertices[0].Distance, Is.EqualTo(0)); - Assert.That(path.Distance, Is.EqualTo(sliderPath.Distance).Within(1e-3)); - assertInvariants(path.Vertices, true); + path.ConvertFromSliderPath(sliderPath, velocity); + Assert.That(path.Vertices[0].Time, Is.EqualTo(0)); + Assert.That(path.Duration * velocity, Is.EqualTo(sliderPath.Distance).Within(1e-3)); + assertInvariants(path.Vertices); - double[] sampleDistances = Enumerable.Range(0, 10) - .Select(_ => rng.NextDouble() * sliderPath.Distance) - .ToArray(); + double[] sampleTimes = Enumerable.Range(0, 10) + .Select(_ => rng.NextDouble() * sliderPath.Distance / velocity) + .ToArray(); - foreach (double distance in sampleDistances) + foreach (double time in sampleTimes) { - float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X; - Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3)); + float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X; + Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3)); } - path.ResampleVertices(sampleDistances); - assertInvariants(path.Vertices, true); + path.ResampleVertices(sampleTimes); + assertInvariants(path.Vertices); - foreach (double distance in sampleDistances) + foreach (double time in sampleTimes) { - float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X; - Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3)); + float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X; + Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3)); } } } @@ -201,17 +201,17 @@ namespace osu.Game.Rulesets.Catch.Tests do { - double distance = rng.NextDouble() * 1e3; + double time = rng.NextDouble() * 1e3; float x = (float)(rng.NextDouble() * 1e3); - path.Add(distance, x); + path.Add(time, x); } while (rng.Next(5) != 0); float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT); - path.ConvertToSliderPath(sliderPath, sliderStartY); - Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3)); - Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X)); - assertInvariants(path.Vertices, true); + double requiredVelocity = path.ComputeRequiredVelocity(); + double velocity = Math.Clamp(requiredVelocity, 1, 100); + + path.ConvertToSliderPath(sliderPath, sliderStartY, velocity); foreach (var point in sliderPath.ControlPoints) { @@ -219,11 +219,18 @@ namespace osu.Game.Rulesets.Catch.Tests Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT)); } + Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X)); + + // The path is preserved only if required velocity is used. + if (velocity < requiredVelocity) continue; + + Assert.That(sliderPath.Distance / velocity, Is.EqualTo(path.Duration).Within(1e-3)); + for (int i = 0; i < 10; i++) { - double distance = rng.NextDouble() * path.Distance; - float expected = path.PositionAtDistance(distance); - Assert.That(sliderPath.PositionAt(distance / sliderPath.Distance).X, Is.EqualTo(expected).Within(1e-3)); + double time = rng.NextDouble() * path.Duration; + float expected = path.PositionAtTime(time); + Assert.That(sliderPath.PositionAt(time * velocity / sliderPath.Distance).X, Is.EqualTo(expected).Within(3e-3)); } } } @@ -244,7 +251,7 @@ namespace osu.Game.Rulesets.Catch.Tests path.Add(20, 0); checkNewId(); - path.RemoveVertices((v, _) => v.Distance == 20); + path.RemoveVertices((v, _) => v.Time == 20); checkNewId(); path.ResampleVertices(new double[] { 5, 10, 15 }); @@ -253,7 +260,7 @@ namespace osu.Game.Rulesets.Catch.Tests path.Clear(); checkNewId(); - path.ConvertFromSliderPath(new SliderPath()); + path.ConvertFromSliderPath(new SliderPath(), 1); checkNewId(); void checkNewId() @@ -263,25 +270,19 @@ namespace osu.Game.Rulesets.Catch.Tests } } - private void assertInvariants(IReadOnlyList vertices, bool checkSlope) + private void assertInvariants(IReadOnlyList vertices) { Assert.That(vertices, Is.Not.Empty); for (int i = 0; i < vertices.Count; i++) { - Assert.That(double.IsFinite(vertices[i].Distance)); + Assert.That(double.IsFinite(vertices[i].Time)); Assert.That(float.IsFinite(vertices[i].X)); } for (int i = 1; i < vertices.Count; i++) { - Assert.That(vertices[i].Distance, Is.GreaterThanOrEqualTo(vertices[i - 1].Distance)); - - if (!checkSlope) continue; - - float xDiff = Math.Abs(vertices[i].X - vertices[i - 1].X); - double distanceDiff = vertices[i].Distance - vertices[i - 1].Distance; - Assert.That(xDiff, Is.LessThanOrEqualTo(distanceDiff).Within(Precision.FLOAT_EPSILON)); + Assert.That(vertices[i].Time, Is.GreaterThanOrEqualTo(vertices[i - 1].Time)); } } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs index 1a43a10c81..3004d3644c 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public int VertexCount => path.Vertices.Count; - protected readonly Func PositionToDistance; + protected readonly Func PositionToTime; protected IReadOnlyList VertexStates => vertexStates; @@ -44,9 +44,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components [CanBeNull] private IBeatSnapProvider beatSnapProvider { get; set; } - protected EditablePath(Func positionToDistance) + protected EditablePath(Func positionToTime) { - PositionToDistance = positionToDistance; + PositionToTime = positionToTime; Anchor = Anchor.BottomLeft; } @@ -59,13 +59,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components while (InternalChildren.Count < path.Vertices.Count) AddInternal(new VertexPiece()); - double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity); + double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1); for (int i = 0; i < VertexCount; i++) { var piece = (VertexPiece)InternalChildren[i]; var vertex = path.Vertices[i]; - piece.Position = new Vector2(vertex.X, (float)(vertex.Distance * distanceToYFactor)); + piece.Position = new Vector2(vertex.X, (float)(vertex.Time * timeToYFactor)); piece.UpdateFrom(vertexStates[i]); } } @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public void InitializeFromHitObject(JuiceStream hitObject) { var sliderPath = hitObject.Path; - path.ConvertFromSliderPath(sliderPath); + path.ConvertFromSliderPath(sliderPath, hitObject.Velocity); // If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices. if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear)) @@ -92,11 +92,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public void UpdateHitObjectFromPath(JuiceStream hitObject) { - path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY); + path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity); if (beatSnapProvider == null) return; - double endTime = hitObject.StartTime + path.Distance / hitObject.Velocity; + double endTime = hitObject.StartTime + path.Duration; double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime); hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity; } @@ -108,9 +108,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; - protected int AddVertex(double distance, float x) + protected int AddVertex(double time, float x) { - int index = path.InsertVertex(distance); + int index = path.InsertVertex(time); path.SetVertexPosition(index, x); vertexStates.Insert(index, new VertexState()); @@ -138,9 +138,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components return true; } - protected void MoveSelectedVertices(double distanceDelta, float xDelta) + protected void MoveSelectedVertices(double timeDelta, float xDelta) { - // Because the vertex list may be reordered due to distance change, the state list must be reordered as well. + // Because the vertex list may be reordered due to time change, the state list must be reordered as well. previousVertexStates.Clear(); previousVertexStates.AddRange(vertexStates); @@ -152,11 +152,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components for (int i = 1; i < vertexCount; i++) { var state = previousVertexStates[i]; - double distance = state.VertexBeforeChange.Distance; + double time = state.VertexBeforeChange.Time; if (state.IsSelected) - distance += distanceDelta; + time += timeDelta; - int newIndex = path.InsertVertex(Math.Max(0, distance)); + int newIndex = path.InsertVertex(Math.Max(0, time)); vertexStates.Insert(newIndex, state); } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/PlacementEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/PlacementEditablePath.cs index 158872fbab..511aec5e5d 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/PlacementEditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/PlacementEditablePath.cs @@ -15,15 +15,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components /// private JuiceStreamPathVertex lastVertex; - public PlacementEditablePath(Func positionToDistance) - : base(positionToDistance) + public PlacementEditablePath(Func positionToTime) + : base(positionToTime) { } public void AddNewVertex() { var endVertex = Vertices[^1]; - int index = AddVertex(endVertex.Distance, endVertex.X); + int index = AddVertex(endVertex.Time, endVertex.X); for (int i = 0; i < VertexCount; i++) { @@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public void MoveLastVertex(Vector2 screenSpacePosition) { Vector2 position = ToRelativePosition(screenSpacePosition); - double distanceDelta = PositionToDistance(position.Y) - lastVertex.Distance; + double timeDelta = PositionToTime(position.Y) - lastVertex.Time; float xDelta = position.X - lastVertex.X; - MoveSelectedVertices(distanceDelta, xDelta); + MoveSelectedVertices(timeDelta, xDelta); } } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs index 8c7314d0b6..b4c353313c 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs @@ -27,15 +27,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components [CanBeNull] private IEditorChangeHandler changeHandler { get; set; } - public SelectionEditablePath(Func positionToDistance) - : base(positionToDistance) + public SelectionEditablePath(Func positionToTime) + : base(positionToTime) { } public void AddVertex(Vector2 relativePosition) { - double distance = Math.Max(0, PositionToDistance(relativePosition.Y)); - int index = AddVertex(distance, relativePosition.X); + double time = Math.Max(0, PositionToTime(relativePosition.Y)); + int index = AddVertex(time, relativePosition.X); selectOnly(index); } @@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components protected override void OnDrag(DragEvent e) { Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition); - double distanceDelta = PositionToDistance(mousePosition.Y) - PositionToDistance(dragStartPosition.Y); + double timeDelta = PositionToTime(mousePosition.Y) - PositionToTime(dragStartPosition.Y); float xDelta = mousePosition.X - dragStartPosition.X; - MoveSelectedVertices(distanceDelta, xDelta); + MoveSelectedVertices(timeDelta, xDelta); } protected override void OnDragEnd(DragEndEvent e) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs index cff5bc2417..4a5a1d8160 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints { scrollingPath = new ScrollingPath(), nestedOutlineContainer = new NestedOutlineContainer(), - editablePath = new PlacementEditablePath(positionToDistance) + editablePath = new PlacementEditablePath(positionToTime) }; } @@ -119,10 +119,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints lastEditablePathId = editablePath.PathId; } - private double positionToDistance(float relativeYPosition) + private double positionToTime(float relativeYPosition) { double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime); - return (time - HitObject.StartTime) * HitObject.Velocity; + return time - HitObject.StartTime; } } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs index 890d059d19..12054a1d16 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints { scrollingPath = new ScrollingPath(), nestedOutlineContainer = new NestedOutlineContainer(), - editablePath = new SelectionEditablePath(positionToDistance) + editablePath = new SelectionEditablePath(positionToTime) }; } @@ -145,10 +145,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius); } - private double positionToDistance(float relativeYPosition) + private double positionToTime(float relativeYPosition) { double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime); - return (time - HitObject.StartTime) * HitObject.Velocity; + return time - HitObject.StartTime; } private void initializeJuiceStreamPath() diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs index 7207833fe6..d8cea3945c 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs @@ -20,11 +20,6 @@ namespace osu.Game.Rulesets.Catch.Objects /// However, the representation is difficult to work with. /// This represents the path in a more convenient way, a polyline connecting list of s. /// - /// - /// The path can be regarded as a function from the closed interval [Vertices[0].Distance, Vertices[^1].Distance] to the x position, given by . - /// To ensure the path is convertible to a , the slope of the function must not be more than 1 everywhere, - /// and this slope condition is always maintained as an invariant. - /// /// public class JuiceStreamPath { @@ -46,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Objects public int InvalidationID { get; private set; } = 1; /// - /// The difference between first vertex's and last vertex's . + /// The difference between first vertex's and last vertex's . /// - public double Distance => vertices[^1].Distance - vertices[0].Distance; + public double Duration => vertices[^1].Time - vertices[0].Time; /// /// This list should always be non-empty. @@ -59,15 +54,15 @@ namespace osu.Game.Rulesets.Catch.Objects }; /// - /// Compute the x-position of the path at the given . + /// Compute the x-position of the path at the given . /// /// - /// When the given distance is outside of the path, the x position at the corresponding endpoint is returned, + /// When the given time is outside of the path, the x position at the corresponding endpoint is returned, /// - public float PositionAtDistance(double distance) + public float PositionAtTime(double time) { - int index = vertexIndexAtDistance(distance); - return positionAtDistance(distance, index); + int index = vertexIndexAtTime(time); + return positionAtTime(time, index); } /// @@ -81,19 +76,19 @@ namespace osu.Game.Rulesets.Catch.Objects } /// - /// Insert a vertex at given . - /// The is used as the position of the new vertex. + /// Insert a vertex at given . + /// The is used as the position of the new vertex. /// Thus, the set of points of the path is not changed (up to floating-point precision). /// /// The index of the new vertex. - public int InsertVertex(double distance) + public int InsertVertex(double time) { - if (!double.IsFinite(distance)) - throw new ArgumentOutOfRangeException(nameof(distance)); + if (!double.IsFinite(time)) + throw new ArgumentOutOfRangeException(nameof(time)); - int index = vertexIndexAtDistance(distance); - float x = positionAtDistance(distance, index); - vertices.Insert(index, new JuiceStreamPathVertex(distance, x)); + int index = vertexIndexAtTime(time); + float x = positionAtTime(time, index); + vertices.Insert(index, new JuiceStreamPathVertex(time, x)); invalidate(); return index; @@ -101,7 +96,6 @@ namespace osu.Game.Rulesets.Catch.Objects /// /// Move the vertex of given to the given position . - /// When the distances between vertices are too small for the new vertex positions, the adjacent vertices are moved towards . /// public void SetVertexPosition(int index, float newX) { @@ -111,32 +105,17 @@ namespace osu.Game.Rulesets.Catch.Objects if (!float.IsFinite(newX)) throw new ArgumentOutOfRangeException(nameof(newX)); - var newVertex = new JuiceStreamPathVertex(vertices[index].Distance, newX); - - for (int i = index - 1; i >= 0 && !canConnect(vertices[i], newVertex); i--) - { - float clampedX = clampToConnectablePosition(newVertex, vertices[i]); - vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX); - } - - for (int i = index + 1; i < vertices.Count; i++) - { - float clampedX = clampToConnectablePosition(newVertex, vertices[i]); - vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX); - } - - vertices[index] = newVertex; + vertices[index] = new JuiceStreamPathVertex(vertices[index].Time, newX); invalidate(); } /// - /// Add a new vertex at given and position. - /// Adjacent vertices are moved when necessary in the same way as . + /// Add a new vertex at given and position. /// - public void Add(double distance, float x) + public void Add(double time, float x) { - int index = InsertVertex(distance); + int index = InsertVertex(time); SetVertexPosition(index, x); } @@ -163,22 +142,22 @@ namespace osu.Game.Rulesets.Catch.Objects } /// - /// Recreate this path by using difference set of vertices at given distances. - /// In addition to the given , the first vertex and the last vertex are always added to the new path. - /// New vertices use the positions on the original path. Thus, s at are preserved. + /// Recreate this path by using difference set of vertices at given time points. + /// In addition to the given , the first vertex and the last vertex are always added to the new path. + /// New vertices use the positions on the original path. Thus, s at are preserved. /// - public void ResampleVertices(IEnumerable sampleDistances) + public void ResampleVertices(IEnumerable sampleTimes) { var sampledVertices = new List(); - foreach (double distance in sampleDistances) + foreach (double time in sampleTimes) { - if (!double.IsFinite(distance)) - throw new ArgumentOutOfRangeException(nameof(sampleDistances)); + if (!double.IsFinite(time)) + throw new ArgumentOutOfRangeException(nameof(sampleTimes)); - double clampedDistance = Math.Clamp(distance, vertices[0].Distance, vertices[^1].Distance); - float x = PositionAtDistance(clampedDistance); - sampledVertices.Add(new JuiceStreamPathVertex(clampedDistance, x)); + double clampedTime = Math.Clamp(time, vertices[0].Time, vertices[^1].Time); + float x = PositionAtTime(clampedTime); + sampledVertices.Add(new JuiceStreamPathVertex(clampedTime, x)); } sampledVertices.Sort(); @@ -196,37 +175,57 @@ namespace osu.Game.Rulesets.Catch.Objects /// /// Duplicated vertices are automatically removed. /// - public void ConvertFromSliderPath(SliderPath sliderPath) + public void ConvertFromSliderPath(SliderPath sliderPath, double velocity) { var sliderPathVertices = new List(); sliderPath.GetPathToProgress(sliderPathVertices, 0, 1); - double distance = 0; + double time = 0; vertices.Clear(); vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X)); for (int i = 1; i < sliderPathVertices.Count; i++) { - distance += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]); + time += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]) / velocity; - if (!Precision.AlmostEquals(vertices[^1].Distance, distance)) - vertices.Add(new JuiceStreamPathVertex(distance, sliderPathVertices[i].X)); + if (!Precision.AlmostEquals(vertices[^1].Time, time)) + Add(time, sliderPathVertices[i].X); } invalidate(); } + /// + /// Computes the minimum slider velocity required to convert this path to a . + /// + public double ComputeRequiredVelocity() + { + double maximumSlope = 0; + + for (int i = 1; i < vertices.Count; i++) + { + double xDifference = Math.Abs((double)vertices[i].X - vertices[i - 1].X); + double timeDifference = vertices[i].Time - vertices[i - 1].Time; + maximumSlope = Math.Max(maximumSlope, xDifference / timeDifference); + } + + return maximumSlope; + } + /// /// Convert the path of this to a and write the result to . /// The resulting slider is "folded" to make it vertically contained in the playfield `(0..)` assuming the slider start position is . + /// + /// The velocity of the converted slider is assumed to be . + /// To preserve the path, should be at least the value returned by . /// - public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY) + public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY, double velocity) { const float margin = 1; // Note: these two variables and `sliderPath` are modified by the local functions. - double currentDistance = 0; + double currentTime = 0; Vector2 lastPosition = new Vector2(vertices[0].X, 0); sliderPath.ControlPoints.Clear(); @@ -237,10 +236,10 @@ namespace osu.Game.Rulesets.Catch.Objects sliderPath.ControlPoints[^1].Type = PathType.Linear; float deltaX = vertices[i].X - lastPosition.X; - double length = vertices[i].Distance - currentDistance; + double length = (vertices[i].Time - currentTime) * velocity; // Should satisfy `deltaX^2 + deltaY^2 = length^2`. - // By invariants, the expression inside the `sqrt` is (almost) non-negative. + // The expression inside the `sqrt` is (almost) non-negative if the slider velocity is large enough. double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX)); // When `deltaY` is small, one segment is always enough. @@ -280,59 +279,38 @@ namespace osu.Game.Rulesets.Catch.Objects { Vector2 nextPosition = new Vector2(nextX, nextY); sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition)); - currentDistance += Vector2.Distance(lastPosition, nextPosition); + currentTime += Vector2.Distance(lastPosition, nextPosition) / velocity; lastPosition = nextPosition; } } /// - /// Find the index at which a new vertex with can be inserted. + /// Find the index at which a new vertex with can be inserted. /// - private int vertexIndexAtDistance(double distance) + private int vertexIndexAtTime(double time) { - // The position of `(distance, Infinity)` is uniquely determined because infinite positions are not allowed. - int i = vertices.BinarySearch(new JuiceStreamPathVertex(distance, float.PositiveInfinity)); + // The position of `(time, Infinity)` is uniquely determined because infinite positions are not allowed. + int i = vertices.BinarySearch(new JuiceStreamPathVertex(time, float.PositiveInfinity)); return i < 0 ? ~i : i; } /// - /// Compute the position at the given , assuming is the vertex index returned by . + /// Compute the position at the given , assuming is the vertex index returned by . /// - private float positionAtDistance(double distance, int index) + private float positionAtTime(double time, int index) { if (index <= 0) return vertices[0].X; if (index >= vertices.Count) return vertices[^1].X; - double length = vertices[index].Distance - vertices[index - 1].Distance; - if (Precision.AlmostEquals(length, 0)) + double duration = vertices[index].Time - vertices[index - 1].Time; + if (Precision.AlmostEquals(duration, 0)) return vertices[index].X; float deltaX = vertices[index].X - vertices[index - 1].X; - return (float)(vertices[index - 1].X + deltaX * ((distance - vertices[index - 1].Distance) / length)); - } - - /// - /// Check the two vertices can connected directly while satisfying the slope condition. - /// - private bool canConnect(JuiceStreamPathVertex vertex1, JuiceStreamPathVertex vertex2, float allowance = 0) - { - double xDistance = Math.Abs((double)vertex2.X - vertex1.X); - float length = (float)Math.Abs(vertex2.Distance - vertex1.Distance); - return xDistance <= length + allowance; - } - - /// - /// Move the position of towards the position of - /// until the vertex pair satisfies the condition . - /// - /// The resulting position of . - private float clampToConnectablePosition(JuiceStreamPathVertex fixedVertex, JuiceStreamPathVertex movableVertex) - { - float length = (float)Math.Abs(movableVertex.Distance - fixedVertex.Distance); - return Math.Clamp(movableVertex.X, fixedVertex.X - length, fixedVertex.X + length); + return (float)(vertices[index - 1].X + deltaX * ((time - vertices[index - 1].Time) / duration)); } private void invalidate() => InvalidationID++; diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs index 58c50603c4..afef2e637f 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs @@ -12,22 +12,22 @@ namespace osu.Game.Rulesets.Catch.Objects /// public readonly struct JuiceStreamPathVertex : IComparable { - public readonly double Distance; + public readonly double Time; public readonly float X; - public JuiceStreamPathVertex(double distance, float x) + public JuiceStreamPathVertex(double time, float x) { - Distance = distance; + Time = time; X = x; } public int CompareTo(JuiceStreamPathVertex other) { - int c = Distance.CompareTo(other.Distance); + int c = Time.CompareTo(other.Time); return c != 0 ? c : X.CompareTo(other.X); } - public override string ToString() => $"({Distance}, {X})"; + public override string ToString() => $"({Time}, {X})"; } } From 9ffa90602bc675ed0a37eac66845b34f36948f8f Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sun, 8 May 2022 21:17:57 +0900 Subject: [PATCH 2/7] Automatically set slider velocity from juice stream path --- .../Blueprints/Components/EditablePath.cs | 22 +++++++++++++++++-- .../Objects/JuiceStreamPath.cs | 5 +++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs index 3004d3644c..20e303dad1 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs @@ -92,13 +92,31 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public void UpdateHitObjectFromPath(JuiceStream hitObject) { - path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity); + // The SV setting may need to be changed for the current path. + var svBindable = hitObject.DifficultyControlPoint.SliderVelocityBindable; + double svToVelocityFactor = hitObject.Velocity / svBindable.Value; + double requiredVelocity = path.ComputeRequiredVelocity(); + + // The value is pre-rounded here because setting it to the bindable will rounded to the nearest value + // but it should be always rounded up to satisfy the required minimum velocity condition. + // + // This is rounded to integers instead of using the precision of the bindable + // because it results in a smaller number of non-redundant control points. + // + // The value is clamped here by the bindable min and max values. + // In case the required velocity is too large, the path is not preserved. + svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor); + + // Calculate the velocity using the resulting SV because `hitObject.Velocity` is not recomputed yet. + double velocity = svBindable.Value * svToVelocityFactor; + + path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, velocity); if (beatSnapProvider == null) return; double endTime = hitObject.StartTime + path.Duration; double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime); - hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity; + hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * velocity; } public Vector2 ToRelativePosition(Vector2 screenSpacePosition) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs index d8cea3945c..61f4c580ae 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs @@ -207,6 +207,11 @@ namespace osu.Game.Rulesets.Catch.Objects { double xDifference = Math.Abs((double)vertices[i].X - vertices[i - 1].X); double timeDifference = vertices[i].Time - vertices[i - 1].Time; + + // A short segment won't affect the resulting path much anyways so ignore it to avoid divide-by-zero. + if (Precision.AlmostEquals(timeDifference, 0)) + continue; + maximumSlope = Math.Max(maximumSlope, xDifference / timeDifference); } From 0e98bb28bda2c7dcedba4d4637493e61c3c28d8e Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sun, 8 May 2022 21:21:51 +0900 Subject: [PATCH 3/7] Fix wrong resampling times are used for juice stream path --- .../Edit/Blueprints/Components/EditablePath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs index 20e303dad1..652d083c4e 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs @@ -80,7 +80,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components { path.ResampleVertices(hitObject.NestedHitObjects .Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used. - .Select(h => (h.StartTime - hitObject.StartTime) * hitObject.Velocity)); + .Select(h => h.StartTime - hitObject.StartTime)); } vertexStates.Clear(); From 670922c8e563c202b6d7e396450f64ce77ecb78d Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sun, 8 May 2022 21:32:05 +0900 Subject: [PATCH 4/7] Use latest slider velocity for juice stream velocity computation. This fixes one-frame glitch in editor when slider velocity is changed. --- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 282afb6343..d34452cdbb 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -27,10 +27,16 @@ namespace osu.Game.Rulesets.Catch.Objects public int RepeatCount { get; set; } [JsonIgnore] - public double Velocity { get; private set; } + private double velocityFactor; [JsonIgnore] - public double TickDistance { get; private set; } + private double tickDistanceFactor; + + [JsonIgnore] + public double Velocity => velocityFactor * DifficultyControlPoint.SliderVelocity; + + [JsonIgnore] + public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity; /// /// The length of one span of this . @@ -43,10 +49,8 @@ namespace osu.Game.Rulesets.Catch.Objects TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; - - Velocity = scoringDistance / timingPoint.BeatLength; - TickDistance = scoringDistance / difficulty.SliderTickRate; + velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength; + tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) From 37c9aac49f836bb49f57c0ea246998dcf6815144 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sun, 8 May 2022 21:35:06 +0900 Subject: [PATCH 5/7] Make `ScrollingPath` use time instead of distance. This is consistent as other components now use time instead of distance. --- .../Edit/Blueprints/Components/ScrollingPath.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs index 109bf61ea5..cfaca2f9a4 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components { private readonly Path drawablePath; - private readonly List<(double Distance, float X)> vertices = new List<(double, float)>(); + private readonly List<(double Time, float X)> vertices = new List<(double, float)>(); public ScrollingPath() { @@ -35,16 +35,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject) { - double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity); + double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1); - computeDistanceXs(hitObject); + computeTimeXs(hitObject); drawablePath.Vertices = vertices - .Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor))) + .Select(v => new Vector2(v.X, (float)(v.Time * timeToYFactor))) .ToArray(); drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero); } - private void computeDistanceXs(JuiceStream hitObject) + private void computeTimeXs(JuiceStream hitObject) { vertices.Clear(); @@ -54,17 +54,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components if (sliderVertices.Count == 0) return; - double distance = 0; + double time = 0; Vector2 lastPosition = Vector2.Zero; for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++) { foreach (var position in sliderVertices) { - distance += Vector2.Distance(lastPosition, position); + time += Vector2.Distance(lastPosition, position) / hitObject.Velocity; lastPosition = position; - vertices.Add((distance, position.X)); + vertices.Add((time, position.X)); } sliderVertices.Reverse(); From 7daa3d8eb79b59756fad96810012c01e3a167dfe Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sun, 8 May 2022 21:54:14 +0900 Subject: [PATCH 6/7] Remove now-redundant velocity calculation Velocity is computed from the up-to-date SV now. --- .../Edit/Blueprints/Components/EditablePath.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs index 652d083c4e..e038562b4b 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs @@ -107,16 +107,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components // In case the required velocity is too large, the path is not preserved. svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor); - // Calculate the velocity using the resulting SV because `hitObject.Velocity` is not recomputed yet. - double velocity = svBindable.Value * svToVelocityFactor; - - path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, velocity); + path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity); if (beatSnapProvider == null) return; double endTime = hitObject.StartTime + path.Duration; double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime); - hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * velocity; + hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity; } public Vector2 ToRelativePosition(Vector2 screenSpacePosition) From d8a4f9d37ddb42a2e0a73f51e5112edab432964a Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sun, 8 May 2022 22:33:16 +0900 Subject: [PATCH 7/7] Update juice stream blueprint tests No "clipping" occur anymore for vertex positions. Instead, clipping may occur when the path is converted to a slider. Add tests for automatic slider velocity change. --- .../TestSceneJuiceStreamPlacementBlueprint.cs | 29 +++++------------ .../TestSceneJuiceStreamSelectionBlueprint.cs | 31 +++++-------------- 2 files changed, 15 insertions(+), 45 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs index 981efc9a13..b1adc4901c 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor { public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene { - private const double velocity = 0.5; + private const double velocity_factor = 0.5; private JuiceStream lastObject => LastObject?.HitObject as JuiceStream; @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor { var playable = base.GetPlayableBeatmap(); playable.Difficulty.SliderTickRate = 5; - playable.Difficulty.SliderMultiplier = velocity * 10; + playable.Difficulty.SliderMultiplier = velocity_factor * 10; return playable; } @@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1])); AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0])); AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1])); + AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); } [Test] @@ -66,28 +67,21 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor } [Test] - public void TestVelocityLimit() + public void TestSliderVelocityChange() { double[] times = { 100, 300 }; float[] positions = { 200, 500 }; addPlacementSteps(times, positions); - addPathCheckStep(times, new float[] { 200, 300 }); - } + addPathCheckStep(times, positions); - [Test] - public void TestPreviousVerticesAreFixed() - { - double[] times = { 100, 300, 500, 700 }; - float[] positions = { 200, 400, 100, 500 }; - addPlacementSteps(times, positions); - addPathCheckStep(times, new float[] { 200, 300, 200, 300 }); + AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); } [Test] public void TestClampedPositionIsRestored() { double[] times = { 100, 300, 500 }; - float[] positions = { 200, 200, 0, 250 }; + float[] positions = { 200, 200, -3000, 250 }; addMoveAndClickSteps(times[0], positions[0]); addMoveAndClickSteps(times[1], positions[1]); @@ -97,15 +91,6 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor addPathCheckStep(times, new float[] { 200, 200, 250 }); } - [Test] - public void TestFirstVertexIsFixed() - { - double[] times = { 100, 200 }; - float[] positions = { 100, 300 }; - addPlacementSteps(times, positions); - addPathCheckStep(times, new float[] { 100, 150 }); - } - [Test] public void TestOutOfOrder() { diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs index 123316f461..22a839d847 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs @@ -101,31 +101,16 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor } [Test] - public void TestClampedPositionIsRestored() + public void TestSliderVelocityChange() { - const double velocity = 0.25; - double[] times = { 100, 500, 700 }; - float[] positions = { 100, 100, 100 }; - addBlueprintStep(times, positions, velocity); + double[] times = { 100, 300 }; + float[] positions = { 200, 300 }; + addBlueprintStep(times, positions); + AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); addDragStartStep(times[1], positions[1]); - - AddMouseMoveStep(times[1], 200); - addVertexCheckStep(3, 1, times[1], 200); - addVertexCheckStep(3, 2, times[2], 150); - - AddMouseMoveStep(times[1], 100); - addVertexCheckStep(3, 1, times[1], 100); - // Stored position is restored. - addVertexCheckStep(3, 2, times[2], positions[2]); - - AddMouseMoveStep(times[1], 300); - addDragEndStep(); - addDragStartStep(times[1], 300); - - AddMouseMoveStep(times[1], 100); - // Position is different because a changed position is committed when the previous drag is ended. - addVertexCheckStep(3, 2, times[2], 250); + AddMouseMoveStep(times[1], 400); + AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); } [Test] @@ -174,7 +159,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor addAddVertexSteps(500, 150); addVertexCheckStep(3, 1, 500, 150); - addAddVertexSteps(90, 220); + addAddVertexSteps(90, 200); addVertexCheckStep(4, 1, times[0], positions[0]); addAddVertexSteps(750, 180);