Merge pull request #13983 from ekrctb/juice-stream-placement

Add initial implementation of juice stream placement
This commit is contained in:
Dean Herbert 2021-07-23 13:52:25 +09:00 committed by GitHub
commit a7d6c682de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 357 additions and 0 deletions

View File

@ -0,0 +1,155 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene
{
private const double velocity = 0.5;
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
[BackgroundDependencyLoader]
private void load()
{
Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderTickRate = 5;
Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity * 10;
}
[Test]
public void TestBasicPlacement()
{
double[] times = { 300, 800 };
float[] positions = { 100, 200 };
addPlacementSteps(times, positions);
AddAssert("juice stream is placed", () => lastObject != null);
AddAssert("start time is correct", () => Precision.AlmostEquals(lastObject.StartTime, times[0]));
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]));
}
[Test]
public void TestEmptyNotCommitted()
{
addMoveAndClickSteps(100, 100);
addMoveAndClickSteps(100, 100);
addMoveAndClickSteps(100, 100, true);
AddAssert("juice stream not placed", () => lastObject == null);
}
[Test]
public void TestMultipleSegments()
{
double[] times = { 100, 300, 500, 700 };
float[] positions = { 100, 150, 100, 100 };
addPlacementSteps(times, positions);
AddAssert("has 4 vertices", () => lastObject.Path.ControlPoints.Count == 4);
addPathCheckStep(times, positions);
}
[Test]
public void TestVelocityLimit()
{
double[] times = { 100, 300 };
float[] positions = { 200, 500 };
addPlacementSteps(times, positions);
addPathCheckStep(times, new float[] { 200, 300 });
}
[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 });
}
[Test]
public void TestClampedPositionIsRestored()
{
double[] times = { 100, 300, 500 };
float[] positions = { 200, 200, 0, 250 };
addMoveAndClickSteps(times[0], positions[0]);
addMoveAndClickSteps(times[1], positions[1]);
AddMoveStep(times[2], positions[2]);
addMoveAndClickSteps(times[2], positions[3], true);
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()
{
double[] times = { 100, 700, 500, 300 };
float[] positions = { 100, 200, 150, 50 };
addPlacementSteps(times, positions);
addPathCheckStep(times, positions);
}
[Test]
public void TestMoveBeforeFirstVertex()
{
double[] times = { 300, 500, 100 };
float[] positions = { 100, 100, 100 };
addPlacementSteps(times, positions);
AddAssert("start time is correct", () => Precision.AlmostEquals(lastObject.StartTime, times[0]));
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1], 1e-3));
}
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableJuiceStream((JuiceStream)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint();
private void addMoveAndClickSteps(double time, float position, bool end = false)
{
AddMoveStep(time, position);
AddClickStep(end ? MouseButton.Right : MouseButton.Left);
}
private void addPlacementSteps(double[] times, float[] positions)
{
for (int i = 0; i < times.Length; i++)
addMoveAndClickSteps(times[i], positions[i], i == times.Length - 1);
}
private void addPathCheckStep(double[] times, float[] positions) => AddStep("assert path is correct", () =>
Assert.That(getPositions(times), Is.EqualTo(positions).Within(Precision.FLOAT_EPSILON)));
private float[] getPositions(IEnumerable<double> times)
{
JuiceStream hitObject = lastObject.AsNonNull();
return times
.Select(time => (time - hitObject.StartTime) * hitObject.Velocity)
.Select(distance => hitObject.EffectiveX + hitObject.Path.PositionAt(distance / hitObject.Distance).X)
.ToArray();
}
}
}

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Rulesets.Catch.Objects;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class PlacementEditablePath : EditablePath
{
/// <summary>
/// The original position of the last added vertex.
/// This is not same as the last vertex of the current path because the vertex ordering can change.
/// </summary>
private JuiceStreamPathVertex lastVertex;
public PlacementEditablePath(Func<float, double> positionToDistance)
: base(positionToDistance)
{
}
public void AddNewVertex()
{
var endVertex = Vertices[^1];
int index = AddVertex(endVertex.Distance, endVertex.X);
for (int i = 0; i < VertexCount; i++)
{
VertexStates[i].IsSelected = i == index;
VertexStates[i].IsFixed = i != index;
VertexStates[i].VertexBeforeChange = Vertices[i];
}
lastVertex = Vertices[index];
}
/// <summary>
/// Move the vertex added by <see cref="AddNewVertex"/> in the last time.
/// </summary>
public void MoveLastVertex(Vector2 screenSpacePosition)
{
Vector2 position = ToRelativePosition(screenSpacePosition);
double distanceDelta = PositionToDistance(position.Y) - lastVertex.Distance;
float xDelta = position.X - lastVertex.X;
MoveSelectedVertices(distanceDelta, xDelta);
}
}
}

View File

@ -0,0 +1,128 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public class JuiceStreamPlacementBlueprint : CatchPlacementBlueprint<JuiceStream>
{
private readonly ScrollingPath scrollingPath;
private readonly NestedOutlineContainer nestedOutlineContainer;
private readonly PlacementEditablePath editablePath;
private int lastEditablePathId = -1;
private InputManager inputManager;
public JuiceStreamPlacementBlueprint()
{
InternalChildren = new Drawable[]
{
scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer(),
editablePath = new PlacementEditablePath(positionToDistance)
};
}
protected override void Update()
{
base.Update();
if (PlacementActive == PlacementState.Active)
editablePath.UpdateFrom(HitObjectContainer, HitObject);
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
protected override bool OnMouseDown(MouseDownEvent e)
{
switch (PlacementActive)
{
case PlacementState.Waiting:
if (e.Button != MouseButton.Left) break;
editablePath.AddNewVertex();
BeginPlacement(true);
return true;
case PlacementState.Active:
switch (e.Button)
{
case MouseButton.Left:
editablePath.AddNewVertex();
return true;
case MouseButton.Right:
EndPlacement(HitObject.Duration > 0);
return true;
}
break;
}
return base.OnMouseDown(e);
}
public override void UpdateTimeAndPosition(SnapResult result)
{
switch (PlacementActive)
{
case PlacementState.Waiting:
if (!(result.Time is double snappedTime)) return;
HitObject.OriginalX = ToLocalSpace(result.ScreenSpacePosition).X;
HitObject.StartTime = snappedTime;
break;
case PlacementState.Active:
Vector2 unsnappedPosition = inputManager.CurrentState.Mouse.Position;
editablePath.MoveLastVertex(unsnappedPosition);
break;
default:
return;
}
// Make sure the up-to-date position is used for outlines.
Vector2 startPosition = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
editablePath.Position = nestedOutlineContainer.Position = scrollingPath.Position = startPosition;
updateHitObjectFromPath();
}
private void updateHitObjectFromPath()
{
if (lastEditablePathId == editablePath.PathId)
return;
editablePath.UpdateHitObjectFromPath(HitObject);
ApplyDefaultsToHitObject();
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
lastEditablePathId = editablePath.PathId;
}
private double positionToDistance(float relativeYPosition)
{
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
return (time - HitObject.StartTime) * HitObject.Velocity;
}
}
}

View File

@ -38,6 +38,7 @@ protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
{
new FruitCompositionTool(),
new JuiceStreamCompositionTool(),
new BananaShowerCompositionTool()
};

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
namespace osu.Game.Rulesets.Catch.Edit
{
public class JuiceStreamCompositionTool : HitObjectCompositionTool
{
public JuiceStreamCompositionTool()
: base(nameof(JuiceStream))
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override PlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint();
}
}