mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 09:27:29 +08:00
Merge pull request #18156 from ekrctb/catch-editor-per-object-sv
Automatically adjust per-hit object slider velocity in osu!catch editor
This commit is contained in:
commit
77b4bd97f8
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
{
|
{
|
||||||
public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene
|
public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene
|
||||||
{
|
{
|
||||||
private const double velocity = 0.5;
|
private const double velocity_factor = 0.5;
|
||||||
|
|
||||||
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
|
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
{
|
{
|
||||||
var playable = base.GetPlayableBeatmap();
|
var playable = base.GetPlayableBeatmap();
|
||||||
playable.Difficulty.SliderTickRate = 5;
|
playable.Difficulty.SliderTickRate = 5;
|
||||||
playable.Difficulty.SliderMultiplier = velocity * 10;
|
playable.Difficulty.SliderMultiplier = velocity_factor * 10;
|
||||||
return playable;
|
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("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
|
||||||
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
||||||
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
|
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
|
||||||
|
AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -66,28 +67,21 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestVelocityLimit()
|
public void TestSliderVelocityChange()
|
||||||
{
|
{
|
||||||
double[] times = { 100, 300 };
|
double[] times = { 100, 300 };
|
||||||
float[] positions = { 200, 500 };
|
float[] positions = { 200, 500 };
|
||||||
addPlacementSteps(times, positions);
|
addPlacementSteps(times, positions);
|
||||||
addPathCheckStep(times, new float[] { 200, 300 });
|
addPathCheckStep(times, positions);
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestClampedPositionIsRestored()
|
public void TestClampedPositionIsRestored()
|
||||||
{
|
{
|
||||||
double[] times = { 100, 300, 500 };
|
double[] times = { 100, 300, 500 };
|
||||||
float[] positions = { 200, 200, 0, 250 };
|
float[] positions = { 200, 200, -3000, 250 };
|
||||||
|
|
||||||
addMoveAndClickSteps(times[0], positions[0]);
|
addMoveAndClickSteps(times[0], positions[0]);
|
||||||
addMoveAndClickSteps(times[1], positions[1]);
|
addMoveAndClickSteps(times[1], positions[1]);
|
||||||
@ -97,15 +91,6 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
addPathCheckStep(times, new float[] { 200, 200, 250 });
|
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]
|
[Test]
|
||||||
public void TestOutOfOrder()
|
public void TestOutOfOrder()
|
||||||
{
|
{
|
||||||
|
@ -101,31 +101,16 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestClampedPositionIsRestored()
|
public void TestSliderVelocityChange()
|
||||||
{
|
{
|
||||||
const double velocity = 0.25;
|
double[] times = { 100, 300 };
|
||||||
double[] times = { 100, 500, 700 };
|
float[] positions = { 200, 300 };
|
||||||
float[] positions = { 100, 100, 100 };
|
addBlueprintStep(times, positions);
|
||||||
addBlueprintStep(times, positions, velocity);
|
AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||||
|
|
||||||
addDragStartStep(times[1], positions[1]);
|
addDragStartStep(times[1], positions[1]);
|
||||||
|
AddMouseMoveStep(times[1], 400);
|
||||||
AddMouseMoveStep(times[1], 200);
|
AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -174,7 +159,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
addAddVertexSteps(500, 150);
|
addAddVertexSteps(500, 150);
|
||||||
addVertexCheckStep(3, 1, 500, 150);
|
addVertexCheckStep(3, 1, 500, 150);
|
||||||
|
|
||||||
addAddVertexSteps(90, 220);
|
addAddVertexSteps(90, 200);
|
||||||
addVertexCheckStep(4, 1, times[0], positions[0]);
|
addVertexCheckStep(4, 1, times[0], positions[0]);
|
||||||
|
|
||||||
addAddVertexSteps(750, 180);
|
addAddVertexSteps(750, 180);
|
||||||
@ -234,10 +219,10 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
{
|
{
|
||||||
var path = new JuiceStreamPath();
|
var path = new JuiceStreamPath();
|
||||||
for (int i = 1; i < times.Length; i++)
|
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();
|
var sliderPath = new SliderPath();
|
||||||
path.ConvertToSliderPath(sliderPath, 0);
|
path.ConvertToSliderPath(sliderPath, 0, velocity);
|
||||||
addBlueprintStep(times[0], positions[0], sliderPath, velocity);
|
addBlueprintStep(times[0], positions[0], sliderPath, velocity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,11 +230,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}", () =>
|
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;
|
float expectedX = x - hitObject.OriginalX;
|
||||||
var vertices = getVertices();
|
var vertices = getVertices();
|
||||||
return vertices.Count == count &&
|
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);
|
Precision.AlmostEquals(vertices[index].X, expectedX);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
@ -37,14 +36,14 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
{
|
{
|
||||||
double distance = rng.NextDouble() * scale * 2 - scale;
|
double time = rng.NextDouble() * scale * 2 - scale;
|
||||||
if (integralValues)
|
if (integralValues)
|
||||||
distance = Math.Round(distance);
|
time = Math.Round(time);
|
||||||
|
|
||||||
float oldX = path.PositionAtDistance(distance);
|
float oldX = path.PositionAtTime(time);
|
||||||
int index = path.InsertVertex(distance);
|
int index = path.InsertVertex(time);
|
||||||
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1));
|
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));
|
Assert.That(path.Vertices[index].X, Is.EqualTo(oldX));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -52,20 +51,20 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
case 1:
|
case 1:
|
||||||
{
|
{
|
||||||
int index = rng.Next(path.Vertices.Count);
|
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);
|
float newX = (float)(rng.NextDouble() * scale * 2 - scale);
|
||||||
if (integralValues)
|
if (integralValues)
|
||||||
newX = MathF.Round(newX);
|
newX = MathF.Round(newX);
|
||||||
|
|
||||||
path.SetVertexPosition(index, newX);
|
path.SetVertexPosition(index, newX);
|
||||||
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount));
|
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));
|
Assert.That(path.Vertices[index].X, Is.EqualTo(newX));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assertInvariants(path.Vertices, checkSlope);
|
assertInvariants(path.Vertices);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
path.Add(10, 5);
|
path.Add(10, 5);
|
||||||
path.Add(20, -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(removeCount, Is.EqualTo(1));
|
||||||
Assert.That(path.Vertices, Is.EqualTo(new[]
|
Assert.That(path.Vertices, Is.EqualTo(new[]
|
||||||
{
|
{
|
||||||
@ -131,8 +130,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase(10)]
|
||||||
public void TestRandomConvertFromSliderPath()
|
[TestCase(0.1)]
|
||||||
|
public void TestRandomConvertFromSliderPath(double velocity)
|
||||||
{
|
{
|
||||||
var rng = new Random(1);
|
var rng = new Random(1);
|
||||||
var path = new JuiceStreamPath();
|
var path = new JuiceStreamPath();
|
||||||
@ -162,28 +162,28 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
else
|
else
|
||||||
sliderPath.ExpectedDistance.Value = null;
|
sliderPath.ExpectedDistance.Value = null;
|
||||||
|
|
||||||
path.ConvertFromSliderPath(sliderPath);
|
path.ConvertFromSliderPath(sliderPath, velocity);
|
||||||
Assert.That(path.Vertices[0].Distance, Is.EqualTo(0));
|
Assert.That(path.Vertices[0].Time, Is.EqualTo(0));
|
||||||
Assert.That(path.Distance, Is.EqualTo(sliderPath.Distance).Within(1e-3));
|
Assert.That(path.Duration * velocity, Is.EqualTo(sliderPath.Distance).Within(1e-3));
|
||||||
assertInvariants(path.Vertices, true);
|
assertInvariants(path.Vertices);
|
||||||
|
|
||||||
double[] sampleDistances = Enumerable.Range(0, 10)
|
double[] sampleTimes = Enumerable.Range(0, 10)
|
||||||
.Select(_ => rng.NextDouble() * sliderPath.Distance)
|
.Select(_ => rng.NextDouble() * sliderPath.Distance / velocity)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
foreach (double distance in sampleDistances)
|
foreach (double time in sampleTimes)
|
||||||
{
|
{
|
||||||
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
|
float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
|
||||||
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
|
Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
|
||||||
}
|
}
|
||||||
|
|
||||||
path.ResampleVertices(sampleDistances);
|
path.ResampleVertices(sampleTimes);
|
||||||
assertInvariants(path.Vertices, true);
|
assertInvariants(path.Vertices);
|
||||||
|
|
||||||
foreach (double distance in sampleDistances)
|
foreach (double time in sampleTimes)
|
||||||
{
|
{
|
||||||
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
|
float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
|
||||||
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
|
Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -201,17 +201,17 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
double distance = rng.NextDouble() * 1e3;
|
double time = rng.NextDouble() * 1e3;
|
||||||
float x = (float)(rng.NextDouble() * 1e3);
|
float x = (float)(rng.NextDouble() * 1e3);
|
||||||
path.Add(distance, x);
|
path.Add(time, x);
|
||||||
} while (rng.Next(5) != 0);
|
} while (rng.Next(5) != 0);
|
||||||
|
|
||||||
float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT);
|
float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT);
|
||||||
|
|
||||||
path.ConvertToSliderPath(sliderPath, sliderStartY);
|
double requiredVelocity = path.ComputeRequiredVelocity();
|
||||||
Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3));
|
double velocity = Math.Clamp(requiredVelocity, 1, 100);
|
||||||
Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
|
|
||||||
assertInvariants(path.Vertices, true);
|
path.ConvertToSliderPath(sliderPath, sliderStartY, velocity);
|
||||||
|
|
||||||
foreach (var point in sliderPath.ControlPoints)
|
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(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++)
|
for (int i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
double distance = rng.NextDouble() * path.Distance;
|
double time = rng.NextDouble() * path.Duration;
|
||||||
float expected = path.PositionAtDistance(distance);
|
float expected = path.PositionAtTime(time);
|
||||||
Assert.That(sliderPath.PositionAt(distance / sliderPath.Distance).X, Is.EqualTo(expected).Within(1e-3));
|
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);
|
path.Add(20, 0);
|
||||||
checkNewId();
|
checkNewId();
|
||||||
|
|
||||||
path.RemoveVertices((v, _) => v.Distance == 20);
|
path.RemoveVertices((v, _) => v.Time == 20);
|
||||||
checkNewId();
|
checkNewId();
|
||||||
|
|
||||||
path.ResampleVertices(new double[] { 5, 10, 15 });
|
path.ResampleVertices(new double[] { 5, 10, 15 });
|
||||||
@ -253,7 +260,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
path.Clear();
|
path.Clear();
|
||||||
checkNewId();
|
checkNewId();
|
||||||
|
|
||||||
path.ConvertFromSliderPath(new SliderPath());
|
path.ConvertFromSliderPath(new SliderPath(), 1);
|
||||||
checkNewId();
|
checkNewId();
|
||||||
|
|
||||||
void checkNewId()
|
void checkNewId()
|
||||||
@ -263,25 +270,19 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices, bool checkSlope)
|
private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices)
|
||||||
{
|
{
|
||||||
Assert.That(vertices, Is.Not.Empty);
|
Assert.That(vertices, Is.Not.Empty);
|
||||||
|
|
||||||
for (int i = 0; i < vertices.Count; i++)
|
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));
|
Assert.That(float.IsFinite(vertices[i].X));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 1; i < vertices.Count; i++)
|
for (int i = 1; i < vertices.Count; i++)
|
||||||
{
|
{
|
||||||
Assert.That(vertices[i].Distance, Is.GreaterThanOrEqualTo(vertices[i - 1].Distance));
|
Assert.That(vertices[i].Time, Is.GreaterThanOrEqualTo(vertices[i - 1].Time));
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
|
|
||||||
public int VertexCount => path.Vertices.Count;
|
public int VertexCount => path.Vertices.Count;
|
||||||
|
|
||||||
protected readonly Func<float, double> PositionToDistance;
|
protected readonly Func<float, double> PositionToTime;
|
||||||
|
|
||||||
protected IReadOnlyList<VertexState> VertexStates => vertexStates;
|
protected IReadOnlyList<VertexState> VertexStates => vertexStates;
|
||||||
|
|
||||||
@ -44,9 +44,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
private IBeatSnapProvider beatSnapProvider { get; set; }
|
private IBeatSnapProvider beatSnapProvider { get; set; }
|
||||||
|
|
||||||
protected EditablePath(Func<float, double> positionToDistance)
|
protected EditablePath(Func<float, double> positionToTime)
|
||||||
{
|
{
|
||||||
PositionToDistance = positionToDistance;
|
PositionToTime = positionToTime;
|
||||||
|
|
||||||
Anchor = Anchor.BottomLeft;
|
Anchor = Anchor.BottomLeft;
|
||||||
}
|
}
|
||||||
@ -59,13 +59,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
while (InternalChildren.Count < path.Vertices.Count)
|
while (InternalChildren.Count < path.Vertices.Count)
|
||||||
AddInternal(new VertexPiece());
|
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++)
|
for (int i = 0; i < VertexCount; i++)
|
||||||
{
|
{
|
||||||
var piece = (VertexPiece)InternalChildren[i];
|
var piece = (VertexPiece)InternalChildren[i];
|
||||||
var vertex = path.Vertices[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]);
|
piece.UpdateFrom(vertexStates[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,14 +73,14 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
public void InitializeFromHitObject(JuiceStream hitObject)
|
public void InitializeFromHitObject(JuiceStream hitObject)
|
||||||
{
|
{
|
||||||
var sliderPath = hitObject.Path;
|
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 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))
|
if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear))
|
||||||
{
|
{
|
||||||
path.ResampleVertices(hitObject.NestedHitObjects
|
path.ResampleVertices(hitObject.NestedHitObjects
|
||||||
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.
|
.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();
|
vertexStates.Clear();
|
||||||
@ -92,11 +92,26 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
|
|
||||||
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||||
{
|
{
|
||||||
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY);
|
// 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);
|
||||||
|
|
||||||
|
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity);
|
||||||
|
|
||||||
if (beatSnapProvider == null) return;
|
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);
|
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
|
||||||
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
|
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
|
||||||
}
|
}
|
||||||
@ -108,9 +123,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
|
|
||||||
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
|
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);
|
path.SetVertexPosition(index, x);
|
||||||
vertexStates.Insert(index, new VertexState());
|
vertexStates.Insert(index, new VertexState());
|
||||||
|
|
||||||
@ -138,9 +153,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
return true;
|
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.Clear();
|
||||||
previousVertexStates.AddRange(vertexStates);
|
previousVertexStates.AddRange(vertexStates);
|
||||||
|
|
||||||
@ -152,11 +167,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
for (int i = 1; i < vertexCount; i++)
|
for (int i = 1; i < vertexCount; i++)
|
||||||
{
|
{
|
||||||
var state = previousVertexStates[i];
|
var state = previousVertexStates[i];
|
||||||
double distance = state.VertexBeforeChange.Distance;
|
double time = state.VertexBeforeChange.Time;
|
||||||
if (state.IsSelected)
|
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);
|
vertexStates.Insert(newIndex, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,15 +15,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private JuiceStreamPathVertex lastVertex;
|
private JuiceStreamPathVertex lastVertex;
|
||||||
|
|
||||||
public PlacementEditablePath(Func<float, double> positionToDistance)
|
public PlacementEditablePath(Func<float, double> positionToTime)
|
||||||
: base(positionToDistance)
|
: base(positionToTime)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddNewVertex()
|
public void AddNewVertex()
|
||||||
{
|
{
|
||||||
var endVertex = Vertices[^1];
|
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++)
|
for (int i = 0; i < VertexCount; i++)
|
||||||
{
|
{
|
||||||
@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
public void MoveLastVertex(Vector2 screenSpacePosition)
|
public void MoveLastVertex(Vector2 screenSpacePosition)
|
||||||
{
|
{
|
||||||
Vector2 position = ToRelativePosition(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;
|
float xDelta = position.X - lastVertex.X;
|
||||||
MoveSelectedVertices(distanceDelta, xDelta);
|
MoveSelectedVertices(timeDelta, xDelta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
{
|
{
|
||||||
private readonly Path drawablePath;
|
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()
|
public ScrollingPath()
|
||||||
{
|
{
|
||||||
@ -35,16 +35,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
|
|
||||||
public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
|
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
|
drawablePath.Vertices = vertices
|
||||||
.Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor)))
|
.Select(v => new Vector2(v.X, (float)(v.Time * timeToYFactor)))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
|
drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void computeDistanceXs(JuiceStream hitObject)
|
private void computeTimeXs(JuiceStream hitObject)
|
||||||
{
|
{
|
||||||
vertices.Clear();
|
vertices.Clear();
|
||||||
|
|
||||||
@ -54,17 +54,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
if (sliderVertices.Count == 0)
|
if (sliderVertices.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
double distance = 0;
|
double time = 0;
|
||||||
Vector2 lastPosition = Vector2.Zero;
|
Vector2 lastPosition = Vector2.Zero;
|
||||||
|
|
||||||
for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
|
for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
|
||||||
{
|
{
|
||||||
foreach (var position in sliderVertices)
|
foreach (var position in sliderVertices)
|
||||||
{
|
{
|
||||||
distance += Vector2.Distance(lastPosition, position);
|
time += Vector2.Distance(lastPosition, position) / hitObject.Velocity;
|
||||||
lastPosition = position;
|
lastPosition = position;
|
||||||
|
|
||||||
vertices.Add((distance, position.X));
|
vertices.Add((time, position.X));
|
||||||
}
|
}
|
||||||
|
|
||||||
sliderVertices.Reverse();
|
sliderVertices.Reverse();
|
||||||
|
@ -27,15 +27,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
private IEditorChangeHandler changeHandler { get; set; }
|
private IEditorChangeHandler changeHandler { get; set; }
|
||||||
|
|
||||||
public SelectionEditablePath(Func<float, double> positionToDistance)
|
public SelectionEditablePath(Func<float, double> positionToTime)
|
||||||
: base(positionToDistance)
|
: base(positionToTime)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddVertex(Vector2 relativePosition)
|
public void AddVertex(Vector2 relativePosition)
|
||||||
{
|
{
|
||||||
double distance = Math.Max(0, PositionToDistance(relativePosition.Y));
|
double time = Math.Max(0, PositionToTime(relativePosition.Y));
|
||||||
int index = AddVertex(distance, relativePosition.X);
|
int index = AddVertex(time, relativePosition.X);
|
||||||
selectOnly(index);
|
selectOnly(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
protected override void OnDrag(DragEvent e)
|
protected override void OnDrag(DragEvent e)
|
||||||
{
|
{
|
||||||
Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition);
|
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;
|
float xDelta = mousePosition.X - dragStartPosition.X;
|
||||||
MoveSelectedVertices(distanceDelta, xDelta);
|
MoveSelectedVertices(timeDelta, xDelta);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnDragEnd(DragEndEvent e)
|
protected override void OnDragEnd(DragEndEvent e)
|
||||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
scrollingPath = new ScrollingPath(),
|
scrollingPath = new ScrollingPath(),
|
||||||
nestedOutlineContainer = new NestedOutlineContainer(),
|
nestedOutlineContainer = new NestedOutlineContainer(),
|
||||||
editablePath = new PlacementEditablePath(positionToDistance)
|
editablePath = new PlacementEditablePath(positionToTime)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,10 +121,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
lastEditablePathId = editablePath.PathId;
|
lastEditablePathId = editablePath.PathId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double positionToDistance(float relativeYPosition)
|
private double positionToTime(float relativeYPosition)
|
||||||
{
|
{
|
||||||
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
|
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
|
||||||
return (time - HitObject.StartTime) * HitObject.Velocity;
|
return time - HitObject.StartTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
scrollingPath = new ScrollingPath(),
|
scrollingPath = new ScrollingPath(),
|
||||||
nestedOutlineContainer = new NestedOutlineContainer(),
|
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);
|
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);
|
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
|
||||||
return (time - HitObject.StartTime) * HitObject.Velocity;
|
return time - HitObject.StartTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeJuiceStreamPath()
|
private void initializeJuiceStreamPath()
|
||||||
|
@ -27,10 +27,16 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
public int RepeatCount { get; set; }
|
public int RepeatCount { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public double Velocity { get; private set; }
|
private double velocityFactor;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public double TickDistance { get; private set; }
|
private double tickDistanceFactor;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public double Velocity => velocityFactor * DifficultyControlPoint.SliderVelocity;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The length of one span of this <see cref="JuiceStream"/>.
|
/// The length of one span of this <see cref="JuiceStream"/>.
|
||||||
@ -43,10 +49,8 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
|
|
||||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||||
|
|
||||||
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
|
velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength;
|
||||||
|
tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate;
|
||||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
|
||||||
TickDistance = scoringDistance / difficulty.SliderTickRate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
|
@ -20,11 +20,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
/// However, the <see cref="SliderPath"/> representation is difficult to work with.
|
/// However, the <see cref="SliderPath"/> representation is difficult to work with.
|
||||||
/// This <see cref="JuiceStreamPath"/> represents the path in a more convenient way, a polyline connecting list of <see cref="JuiceStreamPathVertex"/>s.
|
/// This <see cref="JuiceStreamPath"/> represents the path in a more convenient way, a polyline connecting list of <see cref="JuiceStreamPathVertex"/>s.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
|
||||||
/// The path can be regarded as a function from the closed interval <c>[Vertices[0].Distance, Vertices[^1].Distance]</c> to the x position, given by <see cref="PositionAtDistance"/>.
|
|
||||||
/// To ensure the path is convertible to a <see cref="SliderPath"/>, the slope of the function must not be more than <c>1</c> everywhere,
|
|
||||||
/// and this slope condition is always maintained as an invariant.
|
|
||||||
/// </para>
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class JuiceStreamPath
|
public class JuiceStreamPath
|
||||||
{
|
{
|
||||||
@ -46,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
public int InvalidationID { get; private set; } = 1;
|
public int InvalidationID { get; private set; } = 1;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The difference between first vertex's <see cref="JuiceStreamPathVertex.Distance"/> and last vertex's <see cref="JuiceStreamPathVertex.Distance"/>.
|
/// The difference between first vertex's <see cref="JuiceStreamPathVertex.Time"/> and last vertex's <see cref="JuiceStreamPathVertex.Time"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double Distance => vertices[^1].Distance - vertices[0].Distance;
|
public double Duration => vertices[^1].Time - vertices[0].Time;
|
||||||
|
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This list should always be non-empty.
|
/// This list should always be non-empty.
|
||||||
@ -59,15 +54,15 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compute the x-position of the path at the given <paramref name="distance"/>.
|
/// Compute the x-position of the path at the given <paramref name="time"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// 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,
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public float PositionAtDistance(double distance)
|
public float PositionAtTime(double time)
|
||||||
{
|
{
|
||||||
int index = vertexIndexAtDistance(distance);
|
int index = vertexIndexAtTime(time);
|
||||||
return positionAtDistance(distance, index);
|
return positionAtTime(time, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -81,19 +76,19 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Insert a vertex at given <paramref name="distance"/>.
|
/// Insert a vertex at given <paramref name="time"/>.
|
||||||
/// The <see cref="PositionAtDistance"/> is used as the position of the new vertex.
|
/// The <see cref="PositionAtTime"/> 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).
|
/// Thus, the set of points of the path is not changed (up to floating-point precision).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The index of the new vertex.</returns>
|
/// <returns>The index of the new vertex.</returns>
|
||||||
public int InsertVertex(double distance)
|
public int InsertVertex(double time)
|
||||||
{
|
{
|
||||||
if (!double.IsFinite(distance))
|
if (!double.IsFinite(time))
|
||||||
throw new ArgumentOutOfRangeException(nameof(distance));
|
throw new ArgumentOutOfRangeException(nameof(time));
|
||||||
|
|
||||||
int index = vertexIndexAtDistance(distance);
|
int index = vertexIndexAtTime(time);
|
||||||
float x = positionAtDistance(distance, index);
|
float x = positionAtTime(time, index);
|
||||||
vertices.Insert(index, new JuiceStreamPathVertex(distance, x));
|
vertices.Insert(index, new JuiceStreamPathVertex(time, x));
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
return index;
|
return index;
|
||||||
@ -101,7 +96,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Move the vertex of given <paramref name="index"/> to the given position <paramref name="newX"/>.
|
/// Move the vertex of given <paramref name="index"/> to the given position <paramref name="newX"/>.
|
||||||
/// When the distances between vertices are too small for the new vertex positions, the adjacent vertices are moved towards <paramref name="newX"/>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SetVertexPosition(int index, float newX)
|
public void SetVertexPosition(int index, float newX)
|
||||||
{
|
{
|
||||||
@ -111,32 +105,17 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
if (!float.IsFinite(newX))
|
if (!float.IsFinite(newX))
|
||||||
throw new ArgumentOutOfRangeException(nameof(newX));
|
throw new ArgumentOutOfRangeException(nameof(newX));
|
||||||
|
|
||||||
var newVertex = new JuiceStreamPathVertex(vertices[index].Distance, newX);
|
vertices[index] = new JuiceStreamPathVertex(vertices[index].Time, 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;
|
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a new vertex at given <paramref name="distance"/> and position.
|
/// Add a new vertex at given <paramref name="time"/> and position.
|
||||||
/// Adjacent vertices are moved when necessary in the same way as <see cref="SetVertexPosition"/>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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);
|
SetVertexPosition(index, x);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,22 +142,22 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recreate this path by using difference set of vertices at given distances.
|
/// Recreate this path by using difference set of vertices at given time points.
|
||||||
/// In addition to the given <paramref name="sampleDistances"/>, the first vertex and the last vertex are always added to the new path.
|
/// In addition to the given <paramref name="sampleTimes"/>, the first vertex and the last vertex are always added to the new path.
|
||||||
/// New vertices use the positions on the original path. Thus, <see cref="PositionAtDistance"/>s at <paramref name="sampleDistances"/> are preserved.
|
/// New vertices use the positions on the original path. Thus, <see cref="PositionAtTime"/>s at <paramref name="sampleTimes"/> are preserved.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ResampleVertices(IEnumerable<double> sampleDistances)
|
public void ResampleVertices(IEnumerable<double> sampleTimes)
|
||||||
{
|
{
|
||||||
var sampledVertices = new List<JuiceStreamPathVertex>();
|
var sampledVertices = new List<JuiceStreamPathVertex>();
|
||||||
|
|
||||||
foreach (double distance in sampleDistances)
|
foreach (double time in sampleTimes)
|
||||||
{
|
{
|
||||||
if (!double.IsFinite(distance))
|
if (!double.IsFinite(time))
|
||||||
throw new ArgumentOutOfRangeException(nameof(sampleDistances));
|
throw new ArgumentOutOfRangeException(nameof(sampleTimes));
|
||||||
|
|
||||||
double clampedDistance = Math.Clamp(distance, vertices[0].Distance, vertices[^1].Distance);
|
double clampedTime = Math.Clamp(time, vertices[0].Time, vertices[^1].Time);
|
||||||
float x = PositionAtDistance(clampedDistance);
|
float x = PositionAtTime(clampedTime);
|
||||||
sampledVertices.Add(new JuiceStreamPathVertex(clampedDistance, x));
|
sampledVertices.Add(new JuiceStreamPathVertex(clampedTime, x));
|
||||||
}
|
}
|
||||||
|
|
||||||
sampledVertices.Sort();
|
sampledVertices.Sort();
|
||||||
@ -196,37 +175,62 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Duplicated vertices are automatically removed.
|
/// Duplicated vertices are automatically removed.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public void ConvertFromSliderPath(SliderPath sliderPath)
|
public void ConvertFromSliderPath(SliderPath sliderPath, double velocity)
|
||||||
{
|
{
|
||||||
var sliderPathVertices = new List<Vector2>();
|
var sliderPathVertices = new List<Vector2>();
|
||||||
sliderPath.GetPathToProgress(sliderPathVertices, 0, 1);
|
sliderPath.GetPathToProgress(sliderPathVertices, 0, 1);
|
||||||
|
|
||||||
double distance = 0;
|
double time = 0;
|
||||||
|
|
||||||
vertices.Clear();
|
vertices.Clear();
|
||||||
vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X));
|
vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X));
|
||||||
|
|
||||||
for (int i = 1; i < sliderPathVertices.Count; i++)
|
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))
|
if (!Precision.AlmostEquals(vertices[^1].Time, time))
|
||||||
vertices.Add(new JuiceStreamPathVertex(distance, sliderPathVertices[i].X));
|
Add(time, sliderPathVertices[i].X);
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the minimum slider velocity required to convert this path to a <see cref="SliderPath"/>.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return maximumSlope;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert the path of this <see cref="JuiceStreamPath"/> to a <see cref="SliderPath"/> and write the result to <paramref name="sliderPath"/>.
|
/// Convert the path of this <see cref="JuiceStreamPath"/> to a <see cref="SliderPath"/> and write the result to <paramref name="sliderPath"/>.
|
||||||
/// The resulting slider is "folded" to make it vertically contained in the playfield `(0..<see cref="OSU_PLAYFIELD_HEIGHT"/>)` assuming the slider start position is <paramref name="sliderStartY"/>.
|
/// The resulting slider is "folded" to make it vertically contained in the playfield `(0..<see cref="OSU_PLAYFIELD_HEIGHT"/>)` assuming the slider start position is <paramref name="sliderStartY"/>.
|
||||||
|
///
|
||||||
|
/// The velocity of the converted slider is assumed to be <paramref name="velocity"/>.
|
||||||
|
/// To preserve the path, <paramref name="velocity"/> should be at least the value returned by <see cref="ComputeRequiredVelocity"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY)
|
public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY, double velocity)
|
||||||
{
|
{
|
||||||
const float margin = 1;
|
const float margin = 1;
|
||||||
|
|
||||||
// Note: these two variables and `sliderPath` are modified by the local functions.
|
// 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);
|
Vector2 lastPosition = new Vector2(vertices[0].X, 0);
|
||||||
|
|
||||||
sliderPath.ControlPoints.Clear();
|
sliderPath.ControlPoints.Clear();
|
||||||
@ -237,10 +241,10 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
sliderPath.ControlPoints[^1].Type = PathType.Linear;
|
sliderPath.ControlPoints[^1].Type = PathType.Linear;
|
||||||
|
|
||||||
float deltaX = vertices[i].X - lastPosition.X;
|
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`.
|
// 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));
|
double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX));
|
||||||
|
|
||||||
// When `deltaY` is small, one segment is always enough.
|
// When `deltaY` is small, one segment is always enough.
|
||||||
@ -280,59 +284,38 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
{
|
{
|
||||||
Vector2 nextPosition = new Vector2(nextX, nextY);
|
Vector2 nextPosition = new Vector2(nextX, nextY);
|
||||||
sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition));
|
sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition));
|
||||||
currentDistance += Vector2.Distance(lastPosition, nextPosition);
|
currentTime += Vector2.Distance(lastPosition, nextPosition) / velocity;
|
||||||
lastPosition = nextPosition;
|
lastPosition = nextPosition;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Find the index at which a new vertex with <paramref name="distance"/> can be inserted.
|
/// Find the index at which a new vertex with <paramref name="time"/> can be inserted.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private int vertexIndexAtDistance(double distance)
|
private int vertexIndexAtTime(double time)
|
||||||
{
|
{
|
||||||
// The position of `(distance, Infinity)` is uniquely determined because infinite positions are not allowed.
|
// The position of `(time, Infinity)` is uniquely determined because infinite positions are not allowed.
|
||||||
int i = vertices.BinarySearch(new JuiceStreamPathVertex(distance, float.PositiveInfinity));
|
int i = vertices.BinarySearch(new JuiceStreamPathVertex(time, float.PositiveInfinity));
|
||||||
return i < 0 ? ~i : i;
|
return i < 0 ? ~i : i;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compute the position at the given <paramref name="distance"/>, assuming <paramref name="index"/> is the vertex index returned by <see cref="vertexIndexAtDistance"/>.
|
/// Compute the position at the given <paramref name="time"/>, assuming <paramref name="index"/> is the vertex index returned by <see cref="vertexIndexAtTime"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private float positionAtDistance(double distance, int index)
|
private float positionAtTime(double time, int index)
|
||||||
{
|
{
|
||||||
if (index <= 0)
|
if (index <= 0)
|
||||||
return vertices[0].X;
|
return vertices[0].X;
|
||||||
if (index >= vertices.Count)
|
if (index >= vertices.Count)
|
||||||
return vertices[^1].X;
|
return vertices[^1].X;
|
||||||
|
|
||||||
double length = vertices[index].Distance - vertices[index - 1].Distance;
|
double duration = vertices[index].Time - vertices[index - 1].Time;
|
||||||
if (Precision.AlmostEquals(length, 0))
|
if (Precision.AlmostEquals(duration, 0))
|
||||||
return vertices[index].X;
|
return vertices[index].X;
|
||||||
|
|
||||||
float deltaX = vertices[index].X - vertices[index - 1].X;
|
float deltaX = vertices[index].X - vertices[index - 1].X;
|
||||||
|
|
||||||
return (float)(vertices[index - 1].X + deltaX * ((distance - vertices[index - 1].Distance) / length));
|
return (float)(vertices[index - 1].X + deltaX * ((time - vertices[index - 1].Time) / duration));
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Check the two vertices can connected directly while satisfying the slope condition.
|
|
||||||
/// </summary>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Move the position of <paramref name="movableVertex"/> towards the position of <paramref name="fixedVertex"/>
|
|
||||||
/// until the vertex pair satisfies the condition <see cref="canConnect"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The resulting position of <paramref name="movableVertex"/>.</returns>
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void invalidate() => InvalidationID++;
|
private void invalidate() => InvalidationID++;
|
||||||
|
@ -12,22 +12,22 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly struct JuiceStreamPathVertex : IComparable<JuiceStreamPathVertex>
|
public readonly struct JuiceStreamPathVertex : IComparable<JuiceStreamPathVertex>
|
||||||
{
|
{
|
||||||
public readonly double Distance;
|
public readonly double Time;
|
||||||
|
|
||||||
public readonly float X;
|
public readonly float X;
|
||||||
|
|
||||||
public JuiceStreamPathVertex(double distance, float x)
|
public JuiceStreamPathVertex(double time, float x)
|
||||||
{
|
{
|
||||||
Distance = distance;
|
Time = time;
|
||||||
X = x;
|
X = x;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int CompareTo(JuiceStreamPathVertex other)
|
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);
|
return c != 0 ? c : X.CompareTo(other.X);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => $"({Distance}, {X})";
|
public override string ToString() => $"({Time}, {X})";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user