mirror of
https://github.com/osukey/osukey.git
synced 2025-07-01 16:29:58 +09:00
Merge branch 'master' into lounge-redesign
This commit is contained in:
@ -27,10 +27,10 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ppy.localisationanalyser.tools": {
|
"ppy.localisationanalyser.tools": {
|
||||||
"version": "2021.705.0",
|
"version": "2021.725.0",
|
||||||
"commands": [
|
"commands": [
|
||||||
"localisation"
|
"localisation"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,3 +190,5 @@ dotnet_diagnostic.CA2225.severity = none
|
|||||||
|
|
||||||
# Banned APIs
|
# Banned APIs
|
||||||
dotnet_diagnostic.RS0030.severity = error
|
dotnet_diagnostic.RS0030.severity = error
|
||||||
|
|
||||||
|
dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text.
|
||||||
|
7
.run/Dual client test.run.xml
Normal file
7
.run/Dual client test.run.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Dual client test" type="CompoundRunConfigurationType">
|
||||||
|
<toRun name="osu!" type="DotNetProject" />
|
||||||
|
<toRun name="osu! (Second Client)" type="DotNetProject" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
20
.run/osu! (Second Client).run.xml
Normal file
20
.run/osu! (Second Client).run.xml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="osu! (Second Client)" type="DotNetProject" factoryName=".NET Project" folderName="osu!" activateToolWindowBeforeRun="false">
|
||||||
|
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net5.0/osu!.dll" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="--debug-client-id=1" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net5.0" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="USE_MONO" value="0" />
|
||||||
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/osu.Desktop/osu.Desktop.csproj" />
|
||||||
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
|
<option name="PROJECT_TFM" value="net5.0" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Build" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
@ -3,6 +3,7 @@ M:System.Object.Equals(System.Object)~System.Boolean;Don't use object.Equals. Us
|
|||||||
M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(Fallbacks to ValueType). Use IEquatable<T> or EqualityComparer<T>.Default instead.
|
M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(Fallbacks to ValueType). Use IEquatable<T> or EqualityComparer<T>.Default instead.
|
||||||
M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead.
|
M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead.
|
||||||
T:System.IComparable;Don't use non-generic IComparable. Use generic version instead.
|
T:System.IComparable;Don't use non-generic IComparable. Use generic version instead.
|
||||||
|
T:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable<T> instead.
|
||||||
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
|
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
|
||||||
M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
|
M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
|
||||||
T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
|
T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
|
||||||
|
@ -23,7 +23,7 @@ We are accepting bug reports (please report with as much detail as possible and
|
|||||||
|
|
||||||
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
|
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
|
||||||
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
|
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
|
||||||
- Read peppy's [latest blog post](https://blog.ppy.sh/a-definitive-lazer-faq/) exploring where the project is currently and the roadmap going forward.
|
- Read peppy's [blog post](https://blog.ppy.sh/a-definitive-lazer-faq/) exploring where the project is currently and the roadmap going forward.
|
||||||
|
|
||||||
## Running osu!
|
## Running osu!
|
||||||
|
|
||||||
|
@ -51,8 +51,8 @@
|
|||||||
<Reference Include="Java.Interop" />
|
<Reference Include="Java.Interop" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.706.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.803.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.713.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.803.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||||
|
@ -64,7 +64,7 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Xamarin.Essentials" Version="1.6.1" />
|
<PackageReference Include="Xamarin.Essentials" Version="1.7.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
||||||
</Project>
|
</Project>
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using osu.Framework;
|
using osu.Framework;
|
||||||
@ -17,13 +16,43 @@ namespace osu.Desktop
|
|||||||
{
|
{
|
||||||
public static class Program
|
public static class Program
|
||||||
{
|
{
|
||||||
|
private const string base_game_name = @"osu";
|
||||||
|
|
||||||
[STAThread]
|
[STAThread]
|
||||||
public static int Main(string[] args)
|
public static int Main(string[] args)
|
||||||
{
|
{
|
||||||
// Back up the cwd before DesktopGameHost changes it
|
// Back up the cwd before DesktopGameHost changes it
|
||||||
var cwd = Environment.CurrentDirectory;
|
var cwd = Environment.CurrentDirectory;
|
||||||
|
|
||||||
using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
|
string gameName = base_game_name;
|
||||||
|
bool tournamentClient = false;
|
||||||
|
|
||||||
|
foreach (var arg in args)
|
||||||
|
{
|
||||||
|
var split = arg.Split('=');
|
||||||
|
|
||||||
|
var key = split[0];
|
||||||
|
var val = split.Length > 1 ? split[1] : string.Empty;
|
||||||
|
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case "--tournament":
|
||||||
|
tournamentClient = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--debug-client-id":
|
||||||
|
if (!DebugUtils.IsDebugBuild)
|
||||||
|
throw new InvalidOperationException("Cannot use this argument in a non-debug build.");
|
||||||
|
|
||||||
|
if (!int.TryParse(val, out int clientID))
|
||||||
|
throw new ArgumentException("Provided client ID must be an integer.");
|
||||||
|
|
||||||
|
gameName = $"{base_game_name}-{clientID}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (DesktopGameHost host = Host.GetSuitableHost(gameName, true))
|
||||||
{
|
{
|
||||||
host.ExceptionThrown += handleException;
|
host.ExceptionThrown += handleException;
|
||||||
|
|
||||||
@ -48,16 +77,10 @@ namespace osu.Desktop
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (args.FirstOrDefault() ?? string.Empty)
|
if (tournamentClient)
|
||||||
{
|
host.Run(new TournamentGame());
|
||||||
default:
|
else
|
||||||
host.Run(new OsuGameDesktop(args));
|
host.Run(new OsuGameDesktop(args));
|
||||||
break;
|
|
||||||
|
|
||||||
case "--tournament":
|
|
||||||
host.Run(new TournamentGame());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<Description>A free-to-win rhythm game. Rhythm is just a *click* away!</Description>
|
<Description>A free-to-win rhythm game. Rhythm is just a *click* away!</Description>
|
||||||
<AssemblyName>osu!</AssemblyName>
|
<AssemblyName>osu!</AssemblyName>
|
||||||
<Title>osu!</Title>
|
<Title>osu!</Title>
|
||||||
<Product>osu!</Product>
|
<Product>osu!(lazer)</Product>
|
||||||
<ApplicationIcon>lazer.ico</ApplicationIcon>
|
<ApplicationIcon>lazer.ico</ApplicationIcon>
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<Version>0.0.0</Version>
|
<Version>0.0.0</Version>
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Tests.Editor
|
namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||||
{
|
{
|
||||||
@ -14,11 +21,52 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
|
|
||||||
protected override Container<Drawable> Content => contentContainer;
|
protected override Container<Drawable> Content => contentContainer;
|
||||||
|
|
||||||
|
[Cached(typeof(EditorBeatmap))]
|
||||||
|
[Cached(typeof(IBeatSnapProvider))]
|
||||||
|
protected readonly EditorBeatmap EditorBeatmap;
|
||||||
|
|
||||||
private readonly CatchEditorTestSceneContainer contentContainer;
|
private readonly CatchEditorTestSceneContainer contentContainer;
|
||||||
|
|
||||||
protected CatchSelectionBlueprintTestScene()
|
protected CatchSelectionBlueprintTestScene()
|
||||||
{
|
{
|
||||||
base.Content.Add(contentContainer = new CatchEditorTestSceneContainer());
|
EditorBeatmap = new EditorBeatmap(new CatchBeatmap());
|
||||||
|
EditorBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = 0;
|
||||||
|
EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint
|
||||||
|
{
|
||||||
|
BeatLength = 100
|
||||||
|
});
|
||||||
|
|
||||||
|
base.Content.Add(new EditorBeatmapDependencyContainer(EditorBeatmap, new BindableBeatDivisor())
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
EditorBeatmap,
|
||||||
|
contentContainer = new CatchEditorTestSceneContainer()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AddMouseMoveStep(double time, float x) => AddStep($"move to time={time}, x={x}", () =>
|
||||||
|
{
|
||||||
|
float y = HitObjectContainer.PositionAtTime(time);
|
||||||
|
Vector2 pos = HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
|
||||||
|
InputManager.MoveMouseTo(pos);
|
||||||
|
});
|
||||||
|
|
||||||
|
private class EditorBeatmapDependencyContainer : Container
|
||||||
|
{
|
||||||
|
[Cached]
|
||||||
|
private readonly EditorClock editorClock;
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private readonly BindableBeatDivisor beatDivisor;
|
||||||
|
|
||||||
|
public EditorBeatmapDependencyContainer(IBeatmap beatmap, BindableBeatDivisor beatDivisor)
|
||||||
|
{
|
||||||
|
editorClock = new EditorClock(beatmap, beatDivisor);
|
||||||
|
this.beatDivisor = beatDivisor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,38 +1,286 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Game.Beatmaps;
|
using System.Collections.Generic;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||||
|
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||||
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;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Tests.Editor
|
namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||||
{
|
{
|
||||||
public class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
|
public class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
|
||||||
{
|
{
|
||||||
public TestSceneJuiceStreamSelectionBlueprint()
|
private JuiceStream hitObject;
|
||||||
|
|
||||||
|
private readonly ManualClock manualClock = new ManualClock();
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp() => Schedule(() =>
|
||||||
{
|
{
|
||||||
var hitObject = new JuiceStream
|
EditorBeatmap.Clear();
|
||||||
|
Content.Clear();
|
||||||
|
|
||||||
|
manualClock.CurrentTime = 0;
|
||||||
|
Content.Clock = new FramedClock(manualClock);
|
||||||
|
|
||||||
|
InputManager.ReleaseButton(MouseButton.Left);
|
||||||
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBasicComponentLayout()
|
||||||
|
{
|
||||||
|
double[] times = { 100, 300, 500 };
|
||||||
|
float[] positions = { 100, 200, 100 };
|
||||||
|
addBlueprintStep(times, positions);
|
||||||
|
|
||||||
|
for (int i = 0; i < times.Length; i++)
|
||||||
|
addVertexCheckStep(times.Length, i, times[i], positions[i]);
|
||||||
|
|
||||||
|
AddAssert("correct outline count", () =>
|
||||||
{
|
{
|
||||||
OriginalX = 100,
|
var expected = hitObject.NestedHitObjects.Count(h => !(h is TinyDroplet));
|
||||||
StartTime = 100,
|
return this.ChildrenOfType<FruitOutline>().Count() == expected;
|
||||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
});
|
||||||
|
AddAssert("correct vertex piece count", () =>
|
||||||
|
this.ChildrenOfType<VertexPiece>().Count() == times.Length);
|
||||||
|
|
||||||
|
AddAssert("first vertex is semitransparent", () =>
|
||||||
|
Precision.DefinitelyBigger(1, this.ChildrenOfType<VertexPiece>().First().Alpha));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestVertexDrag()
|
||||||
|
{
|
||||||
|
double[] times = { 100, 400, 700 };
|
||||||
|
float[] positions = { 100, 100, 100 };
|
||||||
|
addBlueprintStep(times, positions);
|
||||||
|
|
||||||
|
addDragStartStep(times[1], positions[1]);
|
||||||
|
|
||||||
|
AddMouseMoveStep(500, 150);
|
||||||
|
addVertexCheckStep(3, 1, 500, 150);
|
||||||
|
|
||||||
|
addDragEndStep();
|
||||||
|
addDragStartStep(times[2], positions[2]);
|
||||||
|
|
||||||
|
AddMouseMoveStep(300, 50);
|
||||||
|
addVertexCheckStep(3, 1, 300, 50);
|
||||||
|
addVertexCheckStep(3, 2, 500, 150);
|
||||||
|
|
||||||
|
AddMouseMoveStep(-100, 100);
|
||||||
|
addVertexCheckStep(3, 1, times[0], positions[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultipleDrag()
|
||||||
|
{
|
||||||
|
double[] times = { 100, 300, 500, 700 };
|
||||||
|
float[] positions = { 100, 100, 100, 100 };
|
||||||
|
addBlueprintStep(times, positions);
|
||||||
|
|
||||||
|
AddMouseMoveStep(times[1], positions[1]);
|
||||||
|
AddStep("press left", () => InputManager.PressButton(MouseButton.Left));
|
||||||
|
AddStep("release left", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft));
|
||||||
|
addDragStartStep(times[2], positions[2]);
|
||||||
|
|
||||||
|
AddMouseMoveStep(times[2] - 50, positions[2] - 50);
|
||||||
|
addVertexCheckStep(4, 1, times[1] - 50, positions[1] - 50);
|
||||||
|
addVertexCheckStep(4, 2, times[2] - 50, positions[2] - 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestClampedPositionIsRestored()
|
||||||
|
{
|
||||||
|
const double velocity = 0.25;
|
||||||
|
double[] times = { 100, 500, 700 };
|
||||||
|
float[] positions = { 100, 100, 100 };
|
||||||
|
addBlueprintStep(times, positions, velocity);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScrollWhileDrag()
|
||||||
|
{
|
||||||
|
double[] times = { 300, 500 };
|
||||||
|
float[] positions = { 100, 100 };
|
||||||
|
addBlueprintStep(times, positions);
|
||||||
|
|
||||||
|
addDragStartStep(times[1], positions[1]);
|
||||||
|
// This mouse move is necessary to start drag and capture the input.
|
||||||
|
AddMouseMoveStep(times[1], positions[1] + 50);
|
||||||
|
|
||||||
|
AddStep("scroll playfield", () => manualClock.CurrentTime += 200);
|
||||||
|
AddMouseMoveStep(times[1] + 200, positions[1] + 100);
|
||||||
|
addVertexCheckStep(2, 1, times[1] + 200, positions[1] + 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUpdateFromHitObject()
|
||||||
|
{
|
||||||
|
double[] times = { 100, 300 };
|
||||||
|
float[] positions = { 200, 200 };
|
||||||
|
addBlueprintStep(times, positions);
|
||||||
|
|
||||||
|
AddStep("update hit object path", () =>
|
||||||
|
{
|
||||||
|
hitObject.Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||||
{
|
{
|
||||||
Vector2.Zero,
|
Vector2.Zero,
|
||||||
new Vector2(200, 100),
|
new Vector2(100, 100),
|
||||||
new Vector2(0, 200),
|
new Vector2(0, 200),
|
||||||
}),
|
});
|
||||||
};
|
EditorBeatmap.Update(hitObject);
|
||||||
var controlPoint = new ControlPointInfo();
|
|
||||||
controlPoint.Add(0, new TimingControlPoint
|
|
||||||
{
|
|
||||||
BeatLength = 100
|
|
||||||
});
|
});
|
||||||
hitObject.ApplyDefaults(controlPoint, new BeatmapDifficulty { CircleSize = 0 });
|
AddAssert("path is updated", () => getVertices().Count > 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddVertex()
|
||||||
|
{
|
||||||
|
double[] times = { 100, 700 };
|
||||||
|
float[] positions = { 200, 200 };
|
||||||
|
addBlueprintStep(times, positions, 0.2);
|
||||||
|
|
||||||
|
addAddVertexSteps(500, 150);
|
||||||
|
addVertexCheckStep(3, 1, 500, 150);
|
||||||
|
|
||||||
|
addAddVertexSteps(90, 220);
|
||||||
|
addVertexCheckStep(4, 1, times[0], positions[0]);
|
||||||
|
|
||||||
|
addAddVertexSteps(750, 180);
|
||||||
|
addVertexCheckStep(5, 4, 750, 180);
|
||||||
|
AddAssert("duration is changed", () => Precision.AlmostEquals(hitObject.Duration, 800 - times[0], 1e-3));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDeleteVertex()
|
||||||
|
{
|
||||||
|
double[] times = { 100, 300, 500 };
|
||||||
|
float[] positions = { 100, 200, 150 };
|
||||||
|
addBlueprintStep(times, positions);
|
||||||
|
|
||||||
|
addDeleteVertexSteps(times[1], positions[1]);
|
||||||
|
addVertexCheckStep(2, 1, times[2], positions[2]);
|
||||||
|
|
||||||
|
// The first vertex cannot be deleted.
|
||||||
|
addDeleteVertexSteps(times[0], positions[0]);
|
||||||
|
addVertexCheckStep(2, 0, times[0], positions[0]);
|
||||||
|
|
||||||
|
addDeleteVertexSteps(times[2], positions[2]);
|
||||||
|
addVertexCheckStep(1, 0, times[0], positions[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestVertexResampling()
|
||||||
|
{
|
||||||
|
addBlueprintStep(100, 100, new SliderPath(PathType.PerfectCurve, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(100, 100),
|
||||||
|
new Vector2(50, 200),
|
||||||
|
}), 0.5);
|
||||||
|
AddAssert("1 vertex per 1 nested HO", () => getVertices().Count == hitObject.NestedHitObjects.Count);
|
||||||
|
AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type.Value == PathType.PerfectCurve);
|
||||||
|
addAddVertexSteps(150, 150);
|
||||||
|
AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type.Value == PathType.Linear);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addBlueprintStep(double time, float x, SliderPath sliderPath, double velocity) => AddStep("add selection blueprint", () =>
|
||||||
|
{
|
||||||
|
hitObject = new JuiceStream
|
||||||
|
{
|
||||||
|
StartTime = time,
|
||||||
|
X = x,
|
||||||
|
Path = sliderPath,
|
||||||
|
};
|
||||||
|
EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity;
|
||||||
|
EditorBeatmap.Add(hitObject);
|
||||||
|
EditorBeatmap.Update(hitObject);
|
||||||
|
Assert.That(hitObject.Velocity, Is.EqualTo(velocity));
|
||||||
AddBlueprint(new JuiceStreamSelectionBlueprint(hitObject));
|
AddBlueprint(new JuiceStreamSelectionBlueprint(hitObject));
|
||||||
|
});
|
||||||
|
|
||||||
|
private void addBlueprintStep(double[] times, float[] positions, double velocity = 0.5)
|
||||||
|
{
|
||||||
|
var path = new JuiceStreamPath();
|
||||||
|
for (int i = 1; i < times.Length; i++)
|
||||||
|
path.Add((times[i] - times[0]) * velocity, positions[i] - positions[0]);
|
||||||
|
|
||||||
|
var sliderPath = new SliderPath();
|
||||||
|
path.ConvertToSliderPath(sliderPath, 0);
|
||||||
|
addBlueprintStep(times[0], positions[0], sliderPath, velocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<JuiceStreamPathVertex> getVertices() => this.ChildrenOfType<EditablePath>().Single().Vertices;
|
||||||
|
|
||||||
|
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;
|
||||||
|
float expectedX = x - hitObject.OriginalX;
|
||||||
|
var vertices = getVertices();
|
||||||
|
return vertices.Count == count &&
|
||||||
|
Precision.AlmostEquals(vertices[index].Distance, expectedDistance, 1e-3) &&
|
||||||
|
Precision.AlmostEquals(vertices[index].X, expectedX);
|
||||||
|
});
|
||||||
|
|
||||||
|
private void addDragStartStep(double time, float x)
|
||||||
|
{
|
||||||
|
AddMouseMoveStep(time, x);
|
||||||
|
AddStep("start dragging", () => InputManager.PressButton(MouseButton.Left));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addDragEndStep() => AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
|
||||||
|
private void addAddVertexSteps(double time, float x)
|
||||||
|
{
|
||||||
|
AddMouseMoveStep(time, x);
|
||||||
|
AddStep("add vertex", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addDeleteVertexSteps(double time, float x)
|
||||||
|
{
|
||||||
|
AddMouseMoveStep(time, x);
|
||||||
|
AddStep("delete vertex", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ShiftLeft);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
288
osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs
Normal file
288
osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
// 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 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;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class JuiceStreamPathTest
|
||||||
|
{
|
||||||
|
[TestCase(1e3, true, false)]
|
||||||
|
// When the coordinates are large, the slope invariant fails within the specified absolute allowance due to the floating-number precision.
|
||||||
|
[TestCase(1e9, false, false)]
|
||||||
|
// Using discrete values sometimes discover more edge cases.
|
||||||
|
[TestCase(10, true, true)]
|
||||||
|
public void TestRandomInsertSetPosition(double scale, bool checkSlope, bool integralValues)
|
||||||
|
{
|
||||||
|
var rng = new Random(1);
|
||||||
|
var path = new JuiceStreamPath();
|
||||||
|
|
||||||
|
for (int iteration = 0; iteration < 100000; iteration++)
|
||||||
|
{
|
||||||
|
if (rng.Next(10) == 0)
|
||||||
|
path.Clear();
|
||||||
|
|
||||||
|
int vertexCount = path.Vertices.Count;
|
||||||
|
|
||||||
|
switch (rng.Next(2))
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
{
|
||||||
|
double distance = rng.NextDouble() * scale * 2 - scale;
|
||||||
|
if (integralValues)
|
||||||
|
distance = Math.Round(distance);
|
||||||
|
|
||||||
|
float oldX = path.PositionAtDistance(distance);
|
||||||
|
int index = path.InsertVertex(distance);
|
||||||
|
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1));
|
||||||
|
Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance));
|
||||||
|
Assert.That(path.Vertices[index].X, Is.EqualTo(oldX));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
{
|
||||||
|
int index = rng.Next(path.Vertices.Count);
|
||||||
|
double distance = path.Vertices[index].Distance;
|
||||||
|
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].X, Is.EqualTo(newX));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertInvariants(path.Vertices, checkSlope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRemoveVertices()
|
||||||
|
{
|
||||||
|
var path = new JuiceStreamPath();
|
||||||
|
path.Add(10, 5);
|
||||||
|
path.Add(20, -5);
|
||||||
|
|
||||||
|
int removeCount = path.RemoveVertices((v, i) => v.Distance == 10 && i == 1);
|
||||||
|
Assert.That(removeCount, Is.EqualTo(1));
|
||||||
|
Assert.That(path.Vertices, Is.EqualTo(new[]
|
||||||
|
{
|
||||||
|
new JuiceStreamPathVertex(0, 0),
|
||||||
|
new JuiceStreamPathVertex(20, -5)
|
||||||
|
}));
|
||||||
|
|
||||||
|
removeCount = path.RemoveVertices((_, i) => i == 0);
|
||||||
|
Assert.That(removeCount, Is.EqualTo(1));
|
||||||
|
Assert.That(path.Vertices, Is.EqualTo(new[]
|
||||||
|
{
|
||||||
|
new JuiceStreamPathVertex(20, -5)
|
||||||
|
}));
|
||||||
|
|
||||||
|
removeCount = path.RemoveVertices((_, i) => true);
|
||||||
|
Assert.That(removeCount, Is.EqualTo(1));
|
||||||
|
Assert.That(path.Vertices, Is.EqualTo(new[]
|
||||||
|
{
|
||||||
|
new JuiceStreamPathVertex()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestResampleVertices()
|
||||||
|
{
|
||||||
|
var path = new JuiceStreamPath();
|
||||||
|
path.Add(-100, -10);
|
||||||
|
path.Add(100, 50);
|
||||||
|
path.ResampleVertices(new double[]
|
||||||
|
{
|
||||||
|
-50,
|
||||||
|
0,
|
||||||
|
70,
|
||||||
|
120
|
||||||
|
});
|
||||||
|
Assert.That(path.Vertices, Is.EqualTo(new[]
|
||||||
|
{
|
||||||
|
new JuiceStreamPathVertex(-100, -10),
|
||||||
|
new JuiceStreamPathVertex(-50, -5),
|
||||||
|
new JuiceStreamPathVertex(0, 0),
|
||||||
|
new JuiceStreamPathVertex(70, 35),
|
||||||
|
new JuiceStreamPathVertex(100, 50),
|
||||||
|
new JuiceStreamPathVertex(100, 50),
|
||||||
|
}));
|
||||||
|
|
||||||
|
path.Clear();
|
||||||
|
path.SetVertexPosition(0, 10);
|
||||||
|
path.ResampleVertices(Array.Empty<double>());
|
||||||
|
Assert.That(path.Vertices, Is.EqualTo(new[]
|
||||||
|
{
|
||||||
|
new JuiceStreamPathVertex(0, 10)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRandomConvertFromSliderPath()
|
||||||
|
{
|
||||||
|
var rng = new Random(1);
|
||||||
|
var path = new JuiceStreamPath();
|
||||||
|
var sliderPath = new SliderPath();
|
||||||
|
|
||||||
|
for (int iteration = 0; iteration < 10000; iteration++)
|
||||||
|
{
|
||||||
|
sliderPath.ControlPoints.Clear();
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
int start = sliderPath.ControlPoints.Count;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
float x = (float)(rng.NextDouble() * 1e3);
|
||||||
|
float y = (float)(rng.NextDouble() * 1e3);
|
||||||
|
sliderPath.ControlPoints.Add(new PathControlPoint(new Vector2(x, y)));
|
||||||
|
} while (rng.Next(2) != 0);
|
||||||
|
|
||||||
|
int length = sliderPath.ControlPoints.Count - start + 1;
|
||||||
|
sliderPath.ControlPoints[start].Type.Value = length <= 2 ? PathType.Linear : length == 3 ? PathType.PerfectCurve : PathType.Bezier;
|
||||||
|
} while (rng.Next(3) != 0);
|
||||||
|
|
||||||
|
if (rng.Next(5) == 0)
|
||||||
|
sliderPath.ExpectedDistance.Value = rng.NextDouble() * 3e3;
|
||||||
|
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);
|
||||||
|
|
||||||
|
double[] sampleDistances = Enumerable.Range(0, 10)
|
||||||
|
.Select(_ => rng.NextDouble() * sliderPath.Distance)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (double distance in sampleDistances)
|
||||||
|
{
|
||||||
|
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
|
||||||
|
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
|
||||||
|
}
|
||||||
|
|
||||||
|
path.ResampleVertices(sampleDistances);
|
||||||
|
assertInvariants(path.Vertices, true);
|
||||||
|
|
||||||
|
foreach (double distance in sampleDistances)
|
||||||
|
{
|
||||||
|
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
|
||||||
|
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRandomConvertToSliderPath()
|
||||||
|
{
|
||||||
|
var rng = new Random(1);
|
||||||
|
var path = new JuiceStreamPath();
|
||||||
|
var sliderPath = new SliderPath();
|
||||||
|
|
||||||
|
for (int iteration = 0; iteration < 10000; iteration++)
|
||||||
|
{
|
||||||
|
path.Clear();
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
double distance = rng.NextDouble() * 1e3;
|
||||||
|
float x = (float)(rng.NextDouble() * 1e3);
|
||||||
|
path.Add(distance, 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.Value.X, Is.EqualTo(path.Vertices[0].X));
|
||||||
|
assertInvariants(path.Vertices, true);
|
||||||
|
|
||||||
|
foreach (var point in sliderPath.ControlPoints)
|
||||||
|
{
|
||||||
|
Assert.That(point.Type.Value, Is.EqualTo(PathType.Linear).Or.Null);
|
||||||
|
Assert.That(sliderStartY + point.Position.Value.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInvalidation()
|
||||||
|
{
|
||||||
|
var path = new JuiceStreamPath();
|
||||||
|
Assert.That(path.InvalidationID, Is.EqualTo(1));
|
||||||
|
int previousId = path.InvalidationID;
|
||||||
|
|
||||||
|
path.InsertVertex(10);
|
||||||
|
checkNewId();
|
||||||
|
|
||||||
|
path.SetVertexPosition(1, 5);
|
||||||
|
checkNewId();
|
||||||
|
|
||||||
|
path.Add(20, 0);
|
||||||
|
checkNewId();
|
||||||
|
|
||||||
|
path.RemoveVertices((v, _) => v.Distance == 20);
|
||||||
|
checkNewId();
|
||||||
|
|
||||||
|
path.ResampleVertices(new double[] { 5, 10, 15 });
|
||||||
|
checkNewId();
|
||||||
|
|
||||||
|
path.Clear();
|
||||||
|
checkNewId();
|
||||||
|
|
||||||
|
path.ConvertFromSliderPath(new SliderPath());
|
||||||
|
checkNewId();
|
||||||
|
|
||||||
|
void checkNewId()
|
||||||
|
{
|
||||||
|
Assert.That(path.InvalidationID, Is.Not.EqualTo(previousId));
|
||||||
|
previousId = path.InvalidationID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices, bool checkSlope)
|
||||||
|
{
|
||||||
|
Assert.That(vertices, Is.Not.Empty);
|
||||||
|
|
||||||
|
for (int i = 0; i < vertices.Count; i++)
|
||||||
|
{
|
||||||
|
Assert.That(double.IsFinite(vertices[i].Distance));
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -24,16 +23,12 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
public class TestSceneCatchSkinConfiguration : OsuTestScene
|
public class TestSceneCatchSkinConfiguration : OsuTestScene
|
||||||
{
|
{
|
||||||
[Cached]
|
|
||||||
private readonly DroppedObjectContainer droppedObjectContainer;
|
|
||||||
|
|
||||||
private Catcher catcher;
|
private Catcher catcher;
|
||||||
|
|
||||||
private readonly Container container;
|
private readonly Container container;
|
||||||
|
|
||||||
public TestSceneCatchSkinConfiguration()
|
public TestSceneCatchSkinConfiguration()
|
||||||
{
|
{
|
||||||
Add(droppedObjectContainer = new DroppedObjectContainer());
|
|
||||||
Add(container = new Container { RelativeSizeAxes = Axes.Both });
|
Add(container = new Container { RelativeSizeAxes = Axes.Both });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
var skin = new TestSkin { FlipCatcherPlate = flip };
|
var skin = new TestSkin { FlipCatcherPlate = flip };
|
||||||
container.Child = new SkinProvidingContainer(skin)
|
container.Child = new SkinProvidingContainer(skin)
|
||||||
{
|
{
|
||||||
Child = catcher = new Catcher(new Container())
|
Child = catcher = new Catcher(new DroppedObjectContainer())
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre
|
Anchor = Anchor.Centre
|
||||||
}
|
}
|
||||||
|
@ -31,23 +31,10 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuConfigManager config { get; set; }
|
private OsuConfigManager config { get; set; }
|
||||||
|
|
||||||
[Cached]
|
private DroppedObjectContainer droppedObjectContainer;
|
||||||
private readonly DroppedObjectContainer droppedObjectContainer;
|
|
||||||
|
|
||||||
private readonly Container trailContainer;
|
|
||||||
|
|
||||||
private TestCatcher catcher;
|
private TestCatcher catcher;
|
||||||
|
|
||||||
public TestSceneCatcher()
|
|
||||||
{
|
|
||||||
Add(trailContainer = new Container
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Depth = -1
|
|
||||||
});
|
|
||||||
Add(droppedObjectContainer = new DroppedObjectContainer());
|
|
||||||
}
|
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp() => Schedule(() =>
|
public void SetUp() => Schedule(() =>
|
||||||
{
|
{
|
||||||
@ -56,13 +43,17 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
CircleSize = 0,
|
CircleSize = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (catcher != null)
|
droppedObjectContainer = new DroppedObjectContainer();
|
||||||
Remove(catcher);
|
|
||||||
|
|
||||||
Add(catcher = new TestCatcher(trailContainer, difficulty)
|
Child = new Container
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre
|
Anchor = Anchor.Centre,
|
||||||
});
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
droppedObjectContainer,
|
||||||
|
catcher = new TestCatcher(droppedObjectContainer, difficulty),
|
||||||
|
}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -299,8 +290,8 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>();
|
public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>();
|
||||||
|
|
||||||
public TestCatcher(Container trailsTarget, BeatmapDifficulty difficulty)
|
public TestCatcher(DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty)
|
||||||
: base(trailsTarget, difficulty)
|
: base(droppedObjectTarget, difficulty)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
area.OnNewResult(drawable, new CatchJudgementResult(fruit, new CatchJudgement())
|
area.OnNewResult(drawable, new CatchJudgementResult(fruit, new CatchJudgement())
|
||||||
{
|
{
|
||||||
Type = area.MovableCatcher.CanCatch(fruit) ? HitResult.Great : HitResult.Miss
|
Type = area.Catcher.CanCatch(fruit) ? HitResult.Great : HitResult.Miss
|
||||||
});
|
});
|
||||||
|
|
||||||
drawable.Expire();
|
drawable.Expire();
|
||||||
@ -119,16 +119,18 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
|
|
||||||
private class TestCatcherArea : CatcherArea
|
private class TestCatcherArea : CatcherArea
|
||||||
{
|
{
|
||||||
[Cached]
|
|
||||||
private readonly DroppedObjectContainer droppedObjectContainer;
|
|
||||||
|
|
||||||
public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
|
public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
|
||||||
: base(beatmapDifficulty)
|
|
||||||
{
|
{
|
||||||
AddInternal(droppedObjectContainer = new DroppedObjectContainer());
|
var droppedObjectContainer = new DroppedObjectContainer();
|
||||||
|
Add(droppedObjectContainer);
|
||||||
|
|
||||||
|
Catcher = new Catcher(droppedObjectContainer, beatmapDifficulty)
|
||||||
|
{
|
||||||
|
X = CatchPlayfield.CENTER_X
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperDashState(status ? 2 : 1);
|
public void ToggleHyperDash(bool status) => Catcher.SetHyperDashState(status ? 2 : 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
|
|
||||||
private bool playfieldIsEmpty => !((CatchPlayfield)drawableRuleset.Playfield).AllHitObjects.Any(h => h.IsAlive);
|
private bool playfieldIsEmpty => !((CatchPlayfield)drawableRuleset.Playfield).AllHitObjects.Any(h => h.IsAlive);
|
||||||
|
|
||||||
private CatcherAnimationState catcherState => ((CatchPlayfield)drawableRuleset.Playfield).CatcherArea.MovableCatcher.CurrentState;
|
private CatcherAnimationState catcherState => ((CatchPlayfield)drawableRuleset.Playfield).Catcher.CurrentState;
|
||||||
|
|
||||||
private void spawnFruits(bool hit = false)
|
private void spawnFruits(bool hit = false)
|
||||||
{
|
{
|
||||||
@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
float xCoords = CatchPlayfield.CENTER_X;
|
float xCoords = CatchPlayfield.CENTER_X;
|
||||||
|
|
||||||
if (drawableRuleset.Playfield is CatchPlayfield catchPlayfield)
|
if (drawableRuleset.Playfield is CatchPlayfield catchPlayfield)
|
||||||
catchPlayfield.CatcherArea.MovableCatcher.X = xCoords - x_offset;
|
catchPlayfield.Catcher.X = xCoords - x_offset;
|
||||||
|
|
||||||
if (hit)
|
if (hit)
|
||||||
xCoords -= x_offset;
|
xCoords -= x_offset;
|
||||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
// this needs to be done within the frame stable context due to how quickly hyperdash state changes occur.
|
// this needs to be done within the frame stable context due to how quickly hyperdash state changes occur.
|
||||||
Player.DrawableRuleset.FrameStableComponents.OnUpdate += d =>
|
Player.DrawableRuleset.FrameStableComponents.OnUpdate += d =>
|
||||||
{
|
{
|
||||||
var catcher = Player.ChildrenOfType<CatcherArea>().FirstOrDefault()?.MovableCatcher;
|
var catcher = Player.ChildrenOfType<Catcher>().FirstOrDefault();
|
||||||
|
|
||||||
if (catcher == null)
|
if (catcher == null)
|
||||||
return;
|
return;
|
||||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestCustomEndGlowColour()
|
public void TestCustomAfterImageColour()
|
||||||
{
|
{
|
||||||
var skin = new TestSkin
|
var skin = new TestSkin
|
||||||
{
|
{
|
||||||
@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestCustomEndGlowColourPriority()
|
public void TestCustomAfterImageColourPriority()
|
||||||
{
|
{
|
||||||
var skin = new TestSkin
|
var skin = new TestSkin
|
||||||
{
|
{
|
||||||
@ -111,38 +111,45 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
checkHyperDashFruitColour(skin, skin.HyperDashColour);
|
checkHyperDashFruitColour(skin, skin.HyperDashColour);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedEndGlowColour = null)
|
private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedAfterImageColour = null)
|
||||||
{
|
{
|
||||||
CatcherArea catcherArea = null;
|
|
||||||
CatcherTrailDisplay trails = null;
|
CatcherTrailDisplay trails = null;
|
||||||
|
Catcher catcher = null;
|
||||||
|
|
||||||
AddStep("create hyper-dashing catcher", () =>
|
AddStep("create hyper-dashing catcher", () =>
|
||||||
{
|
{
|
||||||
Child = setupSkinHierarchy(catcherArea = new TestCatcherArea
|
CatcherArea catcherArea;
|
||||||
|
Child = setupSkinHierarchy(new Container
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre
|
Child = catcherArea = new CatcherArea
|
||||||
|
{
|
||||||
|
Catcher = catcher = new Catcher(new DroppedObjectContainer())
|
||||||
|
{
|
||||||
|
Scale = new Vector2(4)
|
||||||
|
}
|
||||||
|
}
|
||||||
}, skin);
|
}, skin);
|
||||||
|
trails = catcherArea.ChildrenOfType<CatcherTrailDisplay>().Single();
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("get trails container", () =>
|
AddStep("start hyper-dash", () =>
|
||||||
{
|
{
|
||||||
trails = catcherArea.OfType<CatcherTrailDisplay>().Single();
|
catcher.SetHyperDashState(2);
|
||||||
catcherArea.MovableCatcher.SetHyperDashState(2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("catcher colour is correct", () => catcherArea.MovableCatcher.Colour == expectedCatcherColour);
|
AddUntilStep("catcher colour is correct", () => catcher.Colour == expectedCatcherColour);
|
||||||
|
|
||||||
AddAssert("catcher trails colours are correct", () => trails.HyperDashTrailsColour == expectedCatcherColour);
|
AddAssert("catcher trails colours are correct", () => trails.HyperDashTrailsColour == expectedCatcherColour);
|
||||||
AddAssert("catcher end-glow colours are correct", () => trails.EndGlowSpritesColour == (expectedEndGlowColour ?? expectedCatcherColour));
|
AddAssert("catcher after-image colours are correct", () => trails.HyperDashAfterImageColour == (expectedAfterImageColour ?? expectedCatcherColour));
|
||||||
|
|
||||||
AddStep("finish hyper-dashing", () =>
|
AddStep("finish hyper-dashing", () =>
|
||||||
{
|
{
|
||||||
catcherArea.MovableCatcher.SetHyperDashState();
|
catcher.SetHyperDashState();
|
||||||
catcherArea.MovableCatcher.FinishTransforms();
|
catcher.FinishTransforms();
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("catcher colour returned to white", () => catcherArea.MovableCatcher.Colour == Color4.White);
|
AddAssert("catcher colour returned to white", () => catcher.Colour == Color4.White);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkHyperDashFruitColour(ISkin skin, Color4 expectedColour)
|
private void checkHyperDashFruitColour(ISkin skin, Color4 expectedColour)
|
||||||
@ -205,18 +212,5 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestCatcherArea : CatcherArea
|
|
||||||
{
|
|
||||||
[Cached]
|
|
||||||
private readonly DroppedObjectContainer droppedObjectContainer;
|
|
||||||
|
|
||||||
public TestCatcherArea()
|
|
||||||
{
|
|
||||||
Scale = new Vector2(4f);
|
|
||||||
|
|
||||||
AddInternal(droppedObjectContainer = new DroppedObjectContainer());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
|||||||
|
|
||||||
protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken)
|
protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var positionData = obj as IHasXPosition;
|
var xPositionData = obj as IHasXPosition;
|
||||||
|
var yPositionData = obj as IHasYPosition;
|
||||||
var comboData = obj as IHasCombo;
|
var comboData = obj as IHasCombo;
|
||||||
|
|
||||||
switch (obj)
|
switch (obj)
|
||||||
@ -36,10 +37,11 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
|||||||
Path = curveData.Path,
|
Path = curveData.Path,
|
||||||
NodeSamples = curveData.NodeSamples,
|
NodeSamples = curveData.NodeSamples,
|
||||||
RepeatCount = curveData.RepeatCount,
|
RepeatCount = curveData.RepeatCount,
|
||||||
X = positionData?.X ?? 0,
|
X = xPositionData?.X ?? 0,
|
||||||
NewCombo = comboData?.NewCombo ?? false,
|
NewCombo = comboData?.NewCombo ?? false,
|
||||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||||
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0
|
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
|
||||||
|
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y
|
||||||
}.Yield();
|
}.Yield();
|
||||||
|
|
||||||
case IHasDuration endTime:
|
case IHasDuration endTime:
|
||||||
@ -59,7 +61,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
|||||||
Samples = obj.Samples,
|
Samples = obj.Samples,
|
||||||
NewCombo = comboData?.NewCombo ?? false,
|
NewCombo = comboData?.NewCombo ?? false,
|
||||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||||
X = positionData?.X ?? 0
|
X = xPositionData?.X ?? 0,
|
||||||
|
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y
|
||||||
}.Yield();
|
}.Yield();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,7 +130,8 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
return new Mod[]
|
return new Mod[]
|
||||||
{
|
{
|
||||||
new MultiMod(new ModWindUp(), new ModWindDown()),
|
new MultiMod(new ModWindUp(), new ModWindDown()),
|
||||||
new CatchModFloatingFruits()
|
new CatchModFloatingFruits(),
|
||||||
|
new CatchModMuted(),
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Extensions;
|
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@ -33,11 +32,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
{
|
{
|
||||||
mods = Score.Mods;
|
mods = Score.Mods;
|
||||||
|
|
||||||
fruitsHit = Score.Statistics.GetOrDefault(HitResult.Great);
|
fruitsHit = Score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||||
ticksHit = Score.Statistics.GetOrDefault(HitResult.LargeTickHit);
|
ticksHit = Score.Statistics.GetValueOrDefault(HitResult.LargeTickHit);
|
||||||
tinyTicksHit = Score.Statistics.GetOrDefault(HitResult.SmallTickHit);
|
tinyTicksHit = Score.Statistics.GetValueOrDefault(HitResult.SmallTickHit);
|
||||||
tinyTicksMissed = Score.Statistics.GetOrDefault(HitResult.SmallTickMiss);
|
tinyTicksMissed = Score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss);
|
||||||
misses = Score.Statistics.GetOrDefault(HitResult.Miss);
|
misses = Score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||||
|
|
||||||
// We are heavily relying on aim in catch the beat
|
// We are heavily relying on aim in catch the beat
|
||||||
double value = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
|
double value = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
|
||||||
|
@ -19,9 +19,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
float x = HitObject.OriginalX;
|
Vector2 position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
|
||||||
float y = HitObjectContainer.PositionAtTime(HitObject.StartTime);
|
return HitObjectContainer.ToScreenSpace(position + new Vector2(0, HitObjectContainer.DrawHeight));
|
||||||
return HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,190 @@
|
|||||||
|
// 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 System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||||
|
{
|
||||||
|
public abstract class EditablePath : CompositeDrawable
|
||||||
|
{
|
||||||
|
public int PathId => path.InvalidationID;
|
||||||
|
|
||||||
|
public IReadOnlyList<JuiceStreamPathVertex> Vertices => path.Vertices;
|
||||||
|
|
||||||
|
public int VertexCount => path.Vertices.Count;
|
||||||
|
|
||||||
|
protected readonly Func<float, double> PositionToDistance;
|
||||||
|
|
||||||
|
protected IReadOnlyList<VertexState> VertexStates => vertexStates;
|
||||||
|
|
||||||
|
private readonly JuiceStreamPath path = new JuiceStreamPath();
|
||||||
|
|
||||||
|
// Invariant: `path.Vertices.Count == vertexStates.Count`
|
||||||
|
private readonly List<VertexState> vertexStates = new List<VertexState>
|
||||||
|
{
|
||||||
|
new VertexState { IsFixed = true }
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly List<VertexState> previousVertexStates = new List<VertexState>();
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
[CanBeNull]
|
||||||
|
private IBeatSnapProvider beatSnapProvider { get; set; }
|
||||||
|
|
||||||
|
protected EditablePath(Func<float, double> positionToDistance)
|
||||||
|
{
|
||||||
|
PositionToDistance = positionToDistance;
|
||||||
|
|
||||||
|
Anchor = Anchor.BottomLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
|
||||||
|
{
|
||||||
|
while (path.Vertices.Count < InternalChildren.Count)
|
||||||
|
RemoveInternal(InternalChildren[^1]);
|
||||||
|
|
||||||
|
while (InternalChildren.Count < path.Vertices.Count)
|
||||||
|
AddInternal(new VertexPiece());
|
||||||
|
|
||||||
|
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
|
||||||
|
|
||||||
|
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.UpdateFrom(vertexStates[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InitializeFromHitObject(JuiceStream hitObject)
|
||||||
|
{
|
||||||
|
var sliderPath = hitObject.Path;
|
||||||
|
path.ConvertFromSliderPath(sliderPath);
|
||||||
|
|
||||||
|
// 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.Value != null && p.Type.Value != PathType.Linear))
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
vertexStates.Clear();
|
||||||
|
vertexStates.AddRange(path.Vertices.Select((_, i) => new VertexState
|
||||||
|
{
|
||||||
|
IsFixed = i == 0
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||||
|
{
|
||||||
|
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY);
|
||||||
|
|
||||||
|
if (beatSnapProvider == null) return;
|
||||||
|
|
||||||
|
double endTime = hitObject.StartTime + path.Distance / hitObject.Velocity;
|
||||||
|
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
|
||||||
|
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 ToRelativePosition(Vector2 screenSpacePosition)
|
||||||
|
{
|
||||||
|
return ToLocalSpace(screenSpacePosition) - new Vector2(0, DrawHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
|
||||||
|
|
||||||
|
protected int AddVertex(double distance, float x)
|
||||||
|
{
|
||||||
|
int index = path.InsertVertex(distance);
|
||||||
|
path.SetVertexPosition(index, x);
|
||||||
|
vertexStates.Insert(index, new VertexState());
|
||||||
|
|
||||||
|
correctFixedVertexPositions();
|
||||||
|
|
||||||
|
Debug.Assert(vertexStates.Count == VertexCount);
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool RemoveVertex(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= path.Vertices.Count)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (vertexStates[index].IsFixed)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
path.RemoveVertices((_, i) => i == index);
|
||||||
|
|
||||||
|
vertexStates.RemoveAt(index);
|
||||||
|
if (vertexStates.Count == 0)
|
||||||
|
vertexStates.Add(new VertexState());
|
||||||
|
|
||||||
|
Debug.Assert(vertexStates.Count == VertexCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void MoveSelectedVertices(double distanceDelta, float xDelta)
|
||||||
|
{
|
||||||
|
// Because the vertex list may be reordered due to distance change, the state list must be reordered as well.
|
||||||
|
previousVertexStates.Clear();
|
||||||
|
previousVertexStates.AddRange(vertexStates);
|
||||||
|
|
||||||
|
// We will recreate the path from scratch. Note that `Clear` leaves the first vertex.
|
||||||
|
int vertexCount = VertexCount;
|
||||||
|
path.Clear();
|
||||||
|
vertexStates.RemoveRange(1, vertexCount - 1);
|
||||||
|
|
||||||
|
for (int i = 1; i < vertexCount; i++)
|
||||||
|
{
|
||||||
|
var state = previousVertexStates[i];
|
||||||
|
double distance = state.VertexBeforeChange.Distance;
|
||||||
|
if (state.IsSelected)
|
||||||
|
distance += distanceDelta;
|
||||||
|
|
||||||
|
int newIndex = path.InsertVertex(Math.Max(0, distance));
|
||||||
|
vertexStates.Insert(newIndex, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, restore positions of the non-selected vertices.
|
||||||
|
for (int i = 0; i < vertexCount; i++)
|
||||||
|
{
|
||||||
|
if (!vertexStates[i].IsSelected && !vertexStates[i].IsFixed)
|
||||||
|
path.SetVertexPosition(i, vertexStates[i].VertexBeforeChange.X);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, move the selected vertices.
|
||||||
|
for (int i = 0; i < vertexCount; i++)
|
||||||
|
{
|
||||||
|
if (vertexStates[i].IsSelected && !vertexStates[i].IsFixed)
|
||||||
|
path.SetVertexPosition(i, vertexStates[i].VertexBeforeChange.X + xDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, correct the position of fixed vertices.
|
||||||
|
correctFixedVertexPositions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void correctFixedVertexPositions()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < VertexCount; i++)
|
||||||
|
{
|
||||||
|
if (vertexStates[i].IsFixed)
|
||||||
|
path.SetVertexPosition(i, vertexStates[i].VertexBeforeChange.X);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,12 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||||
@ -28,10 +26,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
Colour = osuColour.Yellow;
|
Colour = osuColour.Yellow;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject, [CanBeNull] CatchHitObject parent = null)
|
public void UpdateFrom(CatchHitObject hitObject)
|
||||||
{
|
{
|
||||||
X = hitObject.EffectiveX - (parent?.OriginalX ?? 0);
|
|
||||||
Y = hitObjectContainer.PositionAtTime(hitObject.StartTime, parent?.StartTime ?? hitObjectContainer.Time.Current);
|
|
||||||
Scale = new Vector2(hitObject.Scale);
|
Scale = new Vector2(hitObject.Scale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,12 +20,6 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
Anchor = Anchor.BottomLeft;
|
Anchor = Anchor.BottomLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdatePositionFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
|
|
||||||
{
|
|
||||||
X = parentHitObject.OriginalX;
|
|
||||||
Y = hitObjectContainer.PositionAtTime(parentHitObject.StartTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateNestedObjectsFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
|
public void UpdateNestedObjectsFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
|
||||||
{
|
{
|
||||||
nestedHitObjects.Clear();
|
nestedHitObjects.Clear();
|
||||||
@ -43,7 +37,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
{
|
{
|
||||||
var hitObject = nestedHitObjects[i];
|
var hitObject = nestedHitObjects[i];
|
||||||
var outline = (FruitOutline)InternalChildren[i];
|
var outline = (FruitOutline)InternalChildren[i];
|
||||||
outline.UpdateFrom(hitObjectContainer, hitObject, parentHitObject);
|
outline.Position = CatchHitObjectUtils.GetStartPosition(hitObjectContainer, hitObject) - Position;
|
||||||
|
outline.UpdateFrom(hitObject);
|
||||||
outline.Scale *= hitObject is Droplet ? 0.5f : 1;
|
outline.Scale *= hitObject is Droplet ? 0.5f : 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -33,12 +33,6 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdatePositionFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
|
|
||||||
{
|
|
||||||
X = hitObject.OriginalX;
|
|
||||||
Y = hitObjectContainer.PositionAtTime(hitObject.StartTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
|
||||||
|
@ -0,0 +1,130 @@
|
|||||||
|
// 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 System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||||
|
{
|
||||||
|
public class SelectionEditablePath : EditablePath, IHasContextMenu
|
||||||
|
{
|
||||||
|
public MenuItem[] ContextMenuItems => getContextMenuItems().ToArray();
|
||||||
|
|
||||||
|
// To handle when the editor is scrolled while dragging.
|
||||||
|
private Vector2 dragStartPosition;
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
[CanBeNull]
|
||||||
|
private IEditorChangeHandler changeHandler { get; set; }
|
||||||
|
|
||||||
|
public SelectionEditablePath(Func<float, double> positionToDistance)
|
||||||
|
: base(positionToDistance)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddVertex(Vector2 relativePosition)
|
||||||
|
{
|
||||||
|
double distance = Math.Max(0, PositionToDistance(relativePosition.Y));
|
||||||
|
int index = AddVertex(distance, relativePosition.X);
|
||||||
|
selectOnly(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => InternalChildren.Any(d => d.ReceivePositionalInputAt(screenSpacePos));
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
|
{
|
||||||
|
int index = getMouseTargetVertex(e.ScreenSpaceMouseDownPosition);
|
||||||
|
if (index == -1 || VertexStates[index].IsFixed)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (e.Button == MouseButton.Left && e.ShiftPressed)
|
||||||
|
{
|
||||||
|
RemoveVertex(index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.ControlPressed)
|
||||||
|
VertexStates[index].IsSelected = !VertexStates[index].IsSelected;
|
||||||
|
else if (!VertexStates[index].IsSelected)
|
||||||
|
selectOnly(index);
|
||||||
|
|
||||||
|
// Don't inhibit right click, to show the context menu
|
||||||
|
return e.Button != MouseButton.Right;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnDragStart(DragStartEvent e)
|
||||||
|
{
|
||||||
|
int index = getMouseTargetVertex(e.ScreenSpaceMouseDownPosition);
|
||||||
|
if (index == -1 || VertexStates[index].IsFixed)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (e.Button != MouseButton.Left)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
dragStartPosition = ToRelativePosition(e.ScreenSpaceMouseDownPosition);
|
||||||
|
|
||||||
|
for (int i = 0; i < VertexCount; i++)
|
||||||
|
VertexStates[i].VertexBeforeChange = Vertices[i];
|
||||||
|
|
||||||
|
changeHandler?.BeginChange();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDrag(DragEvent e)
|
||||||
|
{
|
||||||
|
Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition);
|
||||||
|
double distanceDelta = PositionToDistance(mousePosition.Y) - PositionToDistance(dragStartPosition.Y);
|
||||||
|
float xDelta = mousePosition.X - dragStartPosition.X;
|
||||||
|
MoveSelectedVertices(distanceDelta, xDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDragEnd(DragEndEvent e)
|
||||||
|
{
|
||||||
|
changeHandler?.EndChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getMouseTargetVertex(Vector2 screenSpacePosition)
|
||||||
|
{
|
||||||
|
for (int i = InternalChildren.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (i < VertexCount && InternalChildren[i].ReceivePositionalInputAt(screenSpacePosition))
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<MenuItem> getContextMenuItems()
|
||||||
|
{
|
||||||
|
int selectedCount = VertexStates.Count(state => state.IsSelected);
|
||||||
|
|
||||||
|
if (selectedCount != 0)
|
||||||
|
yield return new OsuMenuItem($"Delete selected {(selectedCount == 1 ? "vertex" : $"{selectedCount} vertices")}", MenuItemType.Destructive, deleteSelectedVertices);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectOnly(int index)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < VertexCount; i++)
|
||||||
|
VertexStates[i].IsSelected = i == index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteSelectedVertices()
|
||||||
|
{
|
||||||
|
for (int i = VertexCount - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (VertexStates[i].IsSelected)
|
||||||
|
RemoveVertex(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
// 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.Allocation;
|
||||||
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||||
|
{
|
||||||
|
public class VertexPiece : Circle
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private OsuColour osuColour { get; set; }
|
||||||
|
|
||||||
|
public VertexPiece()
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomLeft;
|
||||||
|
Origin = Anchor.Centre;
|
||||||
|
Size = new Vector2(15);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateFrom(VertexState state)
|
||||||
|
{
|
||||||
|
Colour = state.IsSelected ? osuColour.Yellow.Lighten(1) : osuColour.Yellow;
|
||||||
|
Alpha = state.IsFixed ? 0.5f : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
// 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.Game.Rulesets.Catch.Objects;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Holds the state of a vertex in the path of a <see cref="EditablePath"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class VertexState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the vertex is selected.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSelected { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the vertex can be moved or deleted.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsFixed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The position of the vertex before a vertex moving operation starts.
|
||||||
|
/// This is used to implement "memory-less" moving operations (only the final position matters) to improve UX.
|
||||||
|
/// </summary>
|
||||||
|
public JuiceStreamPathVertex VertexBeforeChange { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
outline.UpdateFrom(HitObjectContainer, HitObject);
|
outline.Position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
|
||||||
|
outline.UpdateFrom(HitObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
|
@ -20,8 +20,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
if (IsSelected)
|
if (!IsSelected) return;
|
||||||
outline.UpdateFrom(HitObjectContainer, HitObject);
|
|
||||||
|
outline.Position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
|
||||||
|
outline.UpdateFrom(HitObject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,22 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Caching;
|
using osu.Framework.Caching;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||||
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.Screens.Edit;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||||
{
|
{
|
||||||
@ -17,6 +24,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
public override Quad SelectionQuad => HitObjectContainer.ToScreenSpace(getBoundingBox().Offset(new Vector2(0, HitObjectContainer.DrawHeight)));
|
public override Quad SelectionQuad => HitObjectContainer.ToScreenSpace(getBoundingBox().Offset(new Vector2(0, HitObjectContainer.DrawHeight)));
|
||||||
|
|
||||||
|
public override MenuItem[] ContextMenuItems => getContextMenuItems().ToArray();
|
||||||
|
|
||||||
private float minNestedX;
|
private float minNestedX;
|
||||||
private float maxNestedX;
|
private float maxNestedX;
|
||||||
|
|
||||||
@ -26,13 +35,34 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
|
|
||||||
private readonly Cached pathCache = new Cached();
|
private readonly Cached pathCache = new Cached();
|
||||||
|
|
||||||
|
private readonly SelectionEditablePath editablePath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="JuiceStreamPath.InvalidationID"/> of the <see cref="JuiceStreamPath"/> corresponding the current <see cref="SliderPath"/> of the hit object.
|
||||||
|
/// When the path is edited, the change is detected and the <see cref="SliderPath"/> of the hit object is updated.
|
||||||
|
/// </summary>
|
||||||
|
private int lastEditablePathId = -1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="SliderPath.Version"/> of the current <see cref="SliderPath"/> of the hit object.
|
||||||
|
/// When the <see cref="SliderPath"/> of the hit object is changed by external means, the change is detected and the <see cref="JuiceStreamPath"/> is re-initialized.
|
||||||
|
/// </summary>
|
||||||
|
private int lastSliderPathVersion = -1;
|
||||||
|
|
||||||
|
private Vector2 rightMouseDownPosition;
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
[CanBeNull]
|
||||||
|
private EditorBeatmap editorBeatmap { get; set; }
|
||||||
|
|
||||||
public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
|
public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
{
|
{
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
scrollingPath = new ScrollingPath(),
|
scrollingPath = new ScrollingPath(),
|
||||||
nestedOutlineContainer = new NestedOutlineContainer()
|
nestedOutlineContainer = new NestedOutlineContainer(),
|
||||||
|
editablePath = new SelectionEditablePath(positionToDistance)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,8 +79,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
|
|
||||||
if (!IsSelected) return;
|
if (!IsSelected) return;
|
||||||
|
|
||||||
scrollingPath.UpdatePositionFrom(HitObjectContainer, HitObject);
|
if (editablePath.PathId != lastEditablePathId)
|
||||||
nestedOutlineContainer.UpdatePositionFrom(HitObjectContainer, HitObject);
|
updateHitObjectFromPath();
|
||||||
|
|
||||||
|
Vector2 startPosition = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
|
||||||
|
editablePath.Position = nestedOutlineContainer.Position = scrollingPath.Position = startPosition;
|
||||||
|
|
||||||
|
editablePath.UpdateFrom(HitObjectContainer, HitObject);
|
||||||
|
|
||||||
if (pathCache.IsValid) return;
|
if (pathCache.IsValid) return;
|
||||||
|
|
||||||
@ -60,10 +95,38 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
pathCache.Validate();
|
pathCache.Validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnSelected()
|
||||||
|
{
|
||||||
|
initializeJuiceStreamPath();
|
||||||
|
base.OnSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
|
{
|
||||||
|
if (!IsSelected) return base.OnMouseDown(e);
|
||||||
|
|
||||||
|
switch (e.Button)
|
||||||
|
{
|
||||||
|
case MouseButton.Left when e.ControlPressed:
|
||||||
|
editablePath.AddVertex(editablePath.ToRelativePosition(e.ScreenSpaceMouseDownPosition));
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MouseButton.Right:
|
||||||
|
// Record the mouse position to be used in the "add vertex" action.
|
||||||
|
rightMouseDownPosition = editablePath.ToRelativePosition(e.ScreenSpaceMouseDownPosition);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnMouseDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
private void onDefaultsApplied(HitObject _)
|
private void onDefaultsApplied(HitObject _)
|
||||||
{
|
{
|
||||||
computeObjectBounds();
|
computeObjectBounds();
|
||||||
pathCache.Invalidate();
|
pathCache.Invalidate();
|
||||||
|
|
||||||
|
if (lastSliderPathVersion != HitObject.Path.Version.Value)
|
||||||
|
initializeJuiceStreamPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void computeObjectBounds()
|
private void computeObjectBounds()
|
||||||
@ -82,6 +145,38 @@ 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)
|
||||||
|
{
|
||||||
|
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
|
||||||
|
return (time - HitObject.StartTime) * HitObject.Velocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeJuiceStreamPath()
|
||||||
|
{
|
||||||
|
editablePath.InitializeFromHitObject(HitObject);
|
||||||
|
|
||||||
|
// Record the current ID to update the hit object only when a change is made to the path.
|
||||||
|
lastEditablePathId = editablePath.PathId;
|
||||||
|
lastSliderPathVersion = HitObject.Path.Version.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateHitObjectFromPath()
|
||||||
|
{
|
||||||
|
editablePath.UpdateHitObjectFromPath(HitObject);
|
||||||
|
editorBeatmap?.Update(HitObject);
|
||||||
|
|
||||||
|
lastEditablePathId = editablePath.PathId;
|
||||||
|
lastSliderPathVersion = HitObject.Path.Version.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<MenuItem> getContextMenuItems()
|
||||||
|
{
|
||||||
|
yield return new OsuMenuItem("Add vertex", MenuItemType.Standard, () =>
|
||||||
|
{
|
||||||
|
editablePath.AddVertex(rightMouseDownPosition);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
// TODO: honor "hit animation" setting?
|
// TODO: honor "hit animation" setting?
|
||||||
CatcherArea.MovableCatcher.CatchFruitOnPlate = false;
|
Catcher.CatchFruitOnPlate = false;
|
||||||
|
|
||||||
// TODO: disable hit lighting as well
|
// TODO: disable hit lighting as well
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
||||||
{
|
{
|
||||||
new FruitCompositionTool(),
|
new FruitCompositionTool(),
|
||||||
|
new JuiceStreamCompositionTool(),
|
||||||
new BananaShowerCompositionTool()
|
new BananaShowerCompositionTool()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
63
osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs
Normal file
63
osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// 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 osu.Game.Rulesets.Catch.Objects;
|
||||||
|
using osu.Game.Rulesets.Catch.UI;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Edit
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Utility functions used by the editor.
|
||||||
|
/// </summary>
|
||||||
|
public static class CatchHitObjectUtils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get the position of the hit object in the playfield based on <see cref="CatchHitObject.OriginalX"/> and <see cref="HitObject.StartTime"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static Vector2 GetStartPosition(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
|
||||||
|
{
|
||||||
|
return new Vector2(hitObject.OriginalX, hitObjectContainer.PositionAtTime(hitObject.StartTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the range of horizontal position occupied by the hit object.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <see cref="TinyDroplet"/>s are excluded and returns <see cref="PositionRange.EMPTY"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public static PositionRange GetPositionRange(HitObject hitObject)
|
||||||
|
{
|
||||||
|
switch (hitObject)
|
||||||
|
{
|
||||||
|
case Fruit fruit:
|
||||||
|
return new PositionRange(fruit.OriginalX);
|
||||||
|
|
||||||
|
case Droplet droplet:
|
||||||
|
return droplet is TinyDroplet ? PositionRange.EMPTY : new PositionRange(droplet.OriginalX);
|
||||||
|
|
||||||
|
case JuiceStream _:
|
||||||
|
return GetPositionRange(hitObject.NestedHitObjects);
|
||||||
|
|
||||||
|
case BananaShower _:
|
||||||
|
// A banana shower occupies the whole screen width.
|
||||||
|
return new PositionRange(0, CatchPlayfield.WIDTH);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return PositionRange.EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the range of horizontal position occupied by the hit objects.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <see cref="TinyDroplet"/>s are excluded.
|
||||||
|
/// </remarks>
|
||||||
|
public static PositionRange GetPositionRange(IEnumerable<HitObject> hitObjects) => hitObjects.Select(GetPositionRange).Aggregate(PositionRange.EMPTY, PositionRange.Union);
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@ using osu.Game.Rulesets.UI;
|
|||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using Direction = osu.Framework.Graphics.Direction;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit
|
namespace osu.Game.Rulesets.Catch.Edit
|
||||||
{
|
{
|
||||||
@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
Vector2 targetPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta);
|
Vector2 targetPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta);
|
||||||
|
|
||||||
float deltaX = targetPosition.X - originalPosition.X;
|
float deltaX = targetPosition.X - originalPosition.X;
|
||||||
deltaX = limitMovement(deltaX, EditorBeatmap.SelectedHitObjects);
|
deltaX = limitMovement(deltaX, SelectedItems);
|
||||||
|
|
||||||
if (deltaX == 0)
|
if (deltaX == 0)
|
||||||
{
|
{
|
||||||
@ -39,18 +40,60 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
|
|
||||||
EditorBeatmap.PerformOnSelection(h =>
|
EditorBeatmap.PerformOnSelection(h =>
|
||||||
{
|
{
|
||||||
if (!(h is CatchHitObject hitObject)) return;
|
if (!(h is CatchHitObject catchObject)) return;
|
||||||
|
|
||||||
hitObject.OriginalX += deltaX;
|
catchObject.OriginalX += deltaX;
|
||||||
|
|
||||||
// Move the nested hit objects to give an instant result before nested objects are recreated.
|
// Move the nested hit objects to give an instant result before nested objects are recreated.
|
||||||
foreach (var nested in hitObject.NestedHitObjects.OfType<CatchHitObject>())
|
foreach (var nested in catchObject.NestedHitObjects.OfType<CatchHitObject>())
|
||||||
nested.OriginalX += deltaX;
|
nested.OriginalX += deltaX;
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool HandleFlip(Direction direction)
|
||||||
|
{
|
||||||
|
var selectionRange = CatchHitObjectUtils.GetPositionRange(SelectedItems);
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
EditorBeatmap.PerformOnSelection(h =>
|
||||||
|
{
|
||||||
|
if (h is CatchHitObject catchObject)
|
||||||
|
changed |= handleFlip(selectionRange, catchObject);
|
||||||
|
});
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool HandleReverse()
|
||||||
|
{
|
||||||
|
double selectionStartTime = SelectedItems.Min(h => h.StartTime);
|
||||||
|
double selectionEndTime = SelectedItems.Max(h => h.GetEndTime());
|
||||||
|
|
||||||
|
EditorBeatmap.PerformOnSelection(hitObject =>
|
||||||
|
{
|
||||||
|
hitObject.StartTime = selectionEndTime - (hitObject.GetEndTime() - selectionStartTime);
|
||||||
|
|
||||||
|
if (hitObject is JuiceStream juiceStream)
|
||||||
|
{
|
||||||
|
juiceStream.Path.Reverse(out Vector2 positionalOffset);
|
||||||
|
juiceStream.OriginalX += positionalOffset.X;
|
||||||
|
juiceStream.LegacyConvertedY += positionalOffset.Y;
|
||||||
|
EditorBeatmap.Update(juiceStream);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnSelectionChanged()
|
||||||
|
{
|
||||||
|
base.OnSelectionChanged();
|
||||||
|
|
||||||
|
var selectionRange = CatchHitObjectUtils.GetPositionRange(SelectedItems);
|
||||||
|
SelectionBox.CanFlipX = selectionRange.Length > 0 && SelectedItems.Any(h => h is CatchHitObject && !(h is BananaShower));
|
||||||
|
SelectionBox.CanReverse = SelectedItems.Count > 1 || SelectedItems.Any(h => h is JuiceStream);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Limit positional movement of the objects by the constraint that moved objects should stay in bounds.
|
/// Limit positional movement of the objects by the constraint that moved objects should stay in bounds.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -59,20 +102,12 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
/// <returns>The positional movement with the restriction applied.</returns>
|
/// <returns>The positional movement with the restriction applied.</returns>
|
||||||
private float limitMovement(float deltaX, IEnumerable<HitObject> movingObjects)
|
private float limitMovement(float deltaX, IEnumerable<HitObject> movingObjects)
|
||||||
{
|
{
|
||||||
float minX = float.PositiveInfinity;
|
var range = CatchHitObjectUtils.GetPositionRange(movingObjects);
|
||||||
float maxX = float.NegativeInfinity;
|
|
||||||
|
|
||||||
foreach (float x in movingObjects.SelectMany(getOriginalPositions))
|
|
||||||
{
|
|
||||||
minX = Math.Min(minX, x);
|
|
||||||
maxX = Math.Max(maxX, x);
|
|
||||||
}
|
|
||||||
|
|
||||||
// To make an object with position `x` stay in bounds after `deltaX` movement, `0 <= x + deltaX <= WIDTH` should be satisfied.
|
// To make an object with position `x` stay in bounds after `deltaX` movement, `0 <= x + deltaX <= WIDTH` should be satisfied.
|
||||||
// Subtracting `x`, we get `-x <= deltaX <= WIDTH - x`.
|
// Subtracting `x`, we get `-x <= deltaX <= WIDTH - x`.
|
||||||
// We only need to apply the inequality to extreme values of `x`.
|
// We only need to apply the inequality to extreme values of `x`.
|
||||||
float lowerBound = -minX;
|
float lowerBound = -range.Min;
|
||||||
float upperBound = CatchPlayfield.WIDTH - maxX;
|
float upperBound = CatchPlayfield.WIDTH - range.Max;
|
||||||
// The inequality may be unsatisfiable if the objects were already out of bounds.
|
// The inequality may be unsatisfiable if the objects were already out of bounds.
|
||||||
// In that case, don't move objects at all.
|
// In that case, don't move objects at all.
|
||||||
if (lowerBound > upperBound)
|
if (lowerBound > upperBound)
|
||||||
@ -81,35 +116,25 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
return Math.Clamp(deltaX, lowerBound, upperBound);
|
return Math.Clamp(deltaX, lowerBound, upperBound);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private bool handleFlip(PositionRange selectionRange, CatchHitObject hitObject)
|
||||||
/// Enumerate X positions that should be contained in-bounds after move offset is applied.
|
|
||||||
/// </summary>
|
|
||||||
private IEnumerable<float> getOriginalPositions(HitObject hitObject)
|
|
||||||
{
|
{
|
||||||
switch (hitObject)
|
switch (hitObject)
|
||||||
{
|
{
|
||||||
case Fruit fruit:
|
case BananaShower _:
|
||||||
yield return fruit.OriginalX;
|
return false;
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case JuiceStream juiceStream:
|
case JuiceStream juiceStream:
|
||||||
foreach (var nested in juiceStream.NestedHitObjects.OfType<CatchHitObject>())
|
juiceStream.OriginalX = selectionRange.GetFlippedPosition(juiceStream.OriginalX);
|
||||||
{
|
|
||||||
// Even if `OriginalX` is outside the playfield, tiny droplets can be moved inside the playfield after the random offset application.
|
|
||||||
if (!(nested is TinyDroplet))
|
|
||||||
yield return nested.OriginalX;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
foreach (var point in juiceStream.Path.ControlPoints)
|
||||||
|
point.Position.Value *= new Vector2(-1, 1);
|
||||||
|
|
||||||
case BananaShower _:
|
EditorBeatmap.Update(juiceStream);
|
||||||
// A banana shower occupies the whole screen width.
|
return true;
|
||||||
// If the selection contains a banana shower, the selection cannot be moved horizontally.
|
|
||||||
yield return 0;
|
|
||||||
yield return CatchPlayfield.WIDTH;
|
|
||||||
|
|
||||||
break;
|
default:
|
||||||
|
hitObject.OriginalX = selectionRange.GetFlippedPosition(hitObject.OriginalX);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
osu.Game.Rulesets.Catch/Edit/JuiceStreamCompositionTool.cs
Normal file
24
osu.Game.Rulesets.Catch/Edit/JuiceStreamCompositionTool.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
42
osu.Game.Rulesets.Catch/Edit/PositionRange.cs
Normal file
42
osu.Game.Rulesets.Catch/Edit/PositionRange.cs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Edit
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents either the empty range or a closed interval of horizontal positions in the playfield.
|
||||||
|
/// A <see cref="PositionRange"/> represents a closed interval if it is <see cref="Min"/> <= <see cref="Max"/>, and represents the empty range otherwise.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct PositionRange
|
||||||
|
{
|
||||||
|
public readonly float Min;
|
||||||
|
public readonly float Max;
|
||||||
|
|
||||||
|
public float Length => Math.Max(0, Max - Min);
|
||||||
|
|
||||||
|
public PositionRange(float value)
|
||||||
|
: this(value, value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public PositionRange(float min, float max)
|
||||||
|
{
|
||||||
|
Min = min;
|
||||||
|
Max = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PositionRange Union(PositionRange a, PositionRange b) => new PositionRange(Math.Min(a.Min, b.Min), Math.Max(a.Max, b.Max));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the given position flipped (mirrored) for the axis at the center of this range.
|
||||||
|
/// Returns the given position unchanged if the range was empty.
|
||||||
|
/// </summary>
|
||||||
|
public float GetFlippedPosition(float x) => Min <= Max ? Max - (x - Min) : x;
|
||||||
|
|
||||||
|
public static readonly PositionRange EMPTY = new PositionRange(float.PositiveInfinity, float.NegativeInfinity);
|
||||||
|
}
|
||||||
|
}
|
@ -41,9 +41,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
|||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
var catcherArea = playfield.CatcherArea;
|
FlashlightPosition = playfield.CatcherArea.ToSpaceOfOtherDrawable(playfield.Catcher.DrawPosition, this);
|
||||||
|
|
||||||
FlashlightPosition = catcherArea.ToSpaceOfOtherDrawable(catcherArea.MovableCatcher.DrawPosition, this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private float getSizeFor(int combo)
|
private float getSizeFor(int combo)
|
||||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
|||||||
var drawableCatchRuleset = (DrawableCatchRuleset)drawableRuleset;
|
var drawableCatchRuleset = (DrawableCatchRuleset)drawableRuleset;
|
||||||
var catchPlayfield = (CatchPlayfield)drawableCatchRuleset.Playfield;
|
var catchPlayfield = (CatchPlayfield)drawableCatchRuleset.Playfield;
|
||||||
|
|
||||||
catchPlayfield.CatcherArea.MovableCatcher.CatchFruitOnPlate = false;
|
catchPlayfield.Catcher.CatchFruitOnPlate = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||||
|
12
osu.Game.Rulesets.Catch/Mods/CatchModMuted.cs
Normal file
12
osu.Game.Rulesets.Catch/Mods/CatchModMuted.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// 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.Game.Rulesets.Catch.Objects;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Mods
|
||||||
|
{
|
||||||
|
public class CatchModMuted : ModMuted<CatchHitObject>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ using osu.Game.Audio;
|
|||||||
using osu.Game.Rulesets.Catch.Judgements;
|
using osu.Game.Rulesets.Catch.Judgements;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Skinning;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -31,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
}
|
}
|
||||||
|
|
||||||
// override any external colour changes with banananana
|
// override any external colour changes with banananana
|
||||||
Color4 IHasComboInformation.GetComboColour(IReadOnlyList<Color4> comboColours) => getBananaColour();
|
Color4 IHasComboInformation.GetComboColour(ISkin skin) => getBananaColour();
|
||||||
|
|
||||||
private Color4 getBananaColour()
|
private Color4 getBananaColour()
|
||||||
{
|
{
|
||||||
|
@ -9,10 +9,11 @@ using osu.Game.Rulesets.Catch.UI;
|
|||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Objects
|
namespace osu.Game.Rulesets.Catch.Objects
|
||||||
{
|
{
|
||||||
public abstract class CatchHitObject : HitObject, IHasXPosition, IHasComboInformation
|
public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation
|
||||||
{
|
{
|
||||||
public const float OBJECT_RADIUS = 64;
|
public const float OBJECT_RADIUS = 64;
|
||||||
|
|
||||||
@ -31,8 +32,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
set => OriginalXBindable.Value = value;
|
set => OriginalXBindable.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
float IHasXPosition.X => OriginalXBindable.Value;
|
|
||||||
|
|
||||||
public readonly Bindable<float> XOffsetBindable = new Bindable<float>();
|
public readonly Bindable<float> XOffsetBindable = new Bindable<float>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -96,6 +95,14 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
set => ComboIndexBindable.Value = value;
|
set => ComboIndexBindable.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Bindable<int> ComboIndexWithOffsetsBindable { get; } = new Bindable<int>();
|
||||||
|
|
||||||
|
public int ComboIndexWithOffsets
|
||||||
|
{
|
||||||
|
get => ComboIndexWithOffsetsBindable.Value;
|
||||||
|
set => ComboIndexWithOffsetsBindable.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>();
|
public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -131,5 +138,24 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||||
|
|
||||||
|
#region Hit object conversion
|
||||||
|
|
||||||
|
// The half of the height of the osu! playfield.
|
||||||
|
public const float DEFAULT_LEGACY_CONVERT_Y = 192;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Y position of the hit object is not used in the normal osu!catch gameplay.
|
||||||
|
/// It is preserved to maximize the backward compatibility with the legacy editor, in which the mappers use the Y position to organize the patterns.
|
||||||
|
/// </summary>
|
||||||
|
public float LegacyConvertedY { get; set; } = DEFAULT_LEGACY_CONVERT_Y;
|
||||||
|
|
||||||
|
float IHasXPosition.X => OriginalX;
|
||||||
|
|
||||||
|
float IHasYPosition.Y => LegacyConvertedY;
|
||||||
|
|
||||||
|
Vector2 IHasPosition.Position => new Vector2(OriginalX, LegacyConvertedY);
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
340
osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs
Normal file
340
osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
// 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 System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the path of a juice stream.
|
||||||
|
/// <para>
|
||||||
|
/// A <see cref="JuiceStream"/> holds a legacy <see cref="SliderPath"/> as the representation of the path.
|
||||||
|
/// 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.
|
||||||
|
/// </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>
|
||||||
|
public class JuiceStreamPath
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The height of legacy osu!standard playfield.
|
||||||
|
/// The sliders converted by <see cref="ConvertToSliderPath"/> are vertically contained in this height.
|
||||||
|
/// </summary>
|
||||||
|
internal const float OSU_PLAYFIELD_HEIGHT = 384;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of vertices of the path, which is represented as a polyline connecting the vertices.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<JuiceStreamPathVertex> Vertices => vertices;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current version number.
|
||||||
|
/// This starts from <c>1</c> and incremented whenever this <see cref="JuiceStreamPath"/> is modified.
|
||||||
|
/// </summary>
|
||||||
|
public int InvalidationID { get; private set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The difference between first vertex's <see cref="JuiceStreamPathVertex.Distance"/> and last vertex's <see cref="JuiceStreamPathVertex.Distance"/>.
|
||||||
|
/// </summary>
|
||||||
|
public double Distance => vertices[^1].Distance - vertices[0].Distance;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// This list should always be non-empty.
|
||||||
|
/// </remarks>
|
||||||
|
private readonly List<JuiceStreamPathVertex> vertices = new List<JuiceStreamPathVertex>
|
||||||
|
{
|
||||||
|
new JuiceStreamPathVertex()
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compute the x-position of the path at the given <paramref name="distance"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// When the given distance is outside of the path, the x position at the corresponding endpoint is returned,
|
||||||
|
/// </remarks>
|
||||||
|
public float PositionAtDistance(double distance)
|
||||||
|
{
|
||||||
|
int index = vertexIndexAtDistance(distance);
|
||||||
|
return positionAtDistance(distance, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove all vertices of this path, then add a new vertex <c>(0, 0)</c>.
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
vertices.Clear();
|
||||||
|
vertices.Add(new JuiceStreamPathVertex());
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Insert a vertex at given <paramref name="distance"/>.
|
||||||
|
/// The <see cref="PositionAtDistance"/> 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).
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The index of the new vertex.</returns>
|
||||||
|
public int InsertVertex(double distance)
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(distance))
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(distance));
|
||||||
|
|
||||||
|
int index = vertexIndexAtDistance(distance);
|
||||||
|
float x = positionAtDistance(distance, index);
|
||||||
|
vertices.Insert(index, new JuiceStreamPathVertex(distance, x));
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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>
|
||||||
|
public void SetVertexPosition(int index, float newX)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= vertices.Count)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a new vertex at given <paramref name="distance"/> and position.
|
||||||
|
/// Adjacent vertices are moved when necessary in the same way as <see cref="SetVertexPosition"/>.
|
||||||
|
/// </summary>
|
||||||
|
public void Add(double distance, float x)
|
||||||
|
{
|
||||||
|
int index = InsertVertex(distance);
|
||||||
|
SetVertexPosition(index, x);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove all vertices that satisfy the given <paramref name="predicate"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// If all vertices are removed, a new vertex <c>(0, 0)</c> is added.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="predicate">The predicate to determine whether a vertex should be removed given the vertex and its index in the path.</param>
|
||||||
|
/// <returns>The number of removed vertices.</returns>
|
||||||
|
public int RemoveVertices(Func<JuiceStreamPathVertex, int, bool> predicate)
|
||||||
|
{
|
||||||
|
int index = 0;
|
||||||
|
int removeCount = vertices.RemoveAll(vertex => predicate(vertex, index++));
|
||||||
|
|
||||||
|
if (vertices.Count == 0)
|
||||||
|
vertices.Add(new JuiceStreamPathVertex());
|
||||||
|
|
||||||
|
if (removeCount != 0)
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
return removeCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recreate this path by using difference set of vertices at given distances.
|
||||||
|
/// In addition to the given <paramref name="sampleDistances"/>, 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.
|
||||||
|
/// </summary>
|
||||||
|
public void ResampleVertices(IEnumerable<double> sampleDistances)
|
||||||
|
{
|
||||||
|
var sampledVertices = new List<JuiceStreamPathVertex>();
|
||||||
|
|
||||||
|
foreach (double distance in sampleDistances)
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(distance))
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(sampleDistances));
|
||||||
|
|
||||||
|
double clampedDistance = Math.Clamp(distance, vertices[0].Distance, vertices[^1].Distance);
|
||||||
|
float x = PositionAtDistance(clampedDistance);
|
||||||
|
sampledVertices.Add(new JuiceStreamPathVertex(clampedDistance, x));
|
||||||
|
}
|
||||||
|
|
||||||
|
sampledVertices.Sort();
|
||||||
|
|
||||||
|
// The first vertex and the last vertex are always used in the result.
|
||||||
|
vertices.RemoveRange(1, vertices.Count - (vertices.Count == 1 ? 1 : 2));
|
||||||
|
vertices.InsertRange(1, sampledVertices);
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a <see cref="SliderPath"/> to list of vertices and write the result to this <see cref="JuiceStreamPath"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Duplicated vertices are automatically removed.
|
||||||
|
/// </remarks>
|
||||||
|
public void ConvertFromSliderPath(SliderPath sliderPath)
|
||||||
|
{
|
||||||
|
var sliderPathVertices = new List<Vector2>();
|
||||||
|
sliderPath.GetPathToProgress(sliderPathVertices, 0, 1);
|
||||||
|
|
||||||
|
double distance = 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]);
|
||||||
|
|
||||||
|
if (!Precision.AlmostEquals(vertices[^1].Distance, distance))
|
||||||
|
vertices.Add(new JuiceStreamPathVertex(distance, sliderPathVertices[i].X));
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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"/>.
|
||||||
|
/// </summary>
|
||||||
|
public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY)
|
||||||
|
{
|
||||||
|
const float margin = 1;
|
||||||
|
|
||||||
|
// Note: these two variables and `sliderPath` are modified by the local functions.
|
||||||
|
double currentDistance = 0;
|
||||||
|
Vector2 lastPosition = new Vector2(vertices[0].X, 0);
|
||||||
|
|
||||||
|
sliderPath.ControlPoints.Clear();
|
||||||
|
sliderPath.ControlPoints.Add(new PathControlPoint(lastPosition));
|
||||||
|
|
||||||
|
for (int i = 1; i < vertices.Count; i++)
|
||||||
|
{
|
||||||
|
sliderPath.ControlPoints[^1].Type.Value = PathType.Linear;
|
||||||
|
|
||||||
|
float deltaX = vertices[i].X - lastPosition.X;
|
||||||
|
double length = vertices[i].Distance - currentDistance;
|
||||||
|
|
||||||
|
// Should satisfy `deltaX^2 + deltaY^2 = length^2`.
|
||||||
|
// By invariants, the expression inside the `sqrt` is (almost) non-negative.
|
||||||
|
double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX));
|
||||||
|
|
||||||
|
// When `deltaY` is small, one segment is always enough.
|
||||||
|
// This case is handled separately to prevent divide-by-zero.
|
||||||
|
if (deltaY <= OSU_PLAYFIELD_HEIGHT / 2 - margin)
|
||||||
|
{
|
||||||
|
float nextX = vertices[i].X;
|
||||||
|
float nextY = (float)(lastPosition.Y + getYDirection() * deltaY);
|
||||||
|
addControlPoint(nextX, nextY);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When `deltaY` is large or when the slider velocity is fast, the segment must be partitioned to subsegments to stay in bounds.
|
||||||
|
for (double currentProgress = 0; currentProgress < deltaY;)
|
||||||
|
{
|
||||||
|
double nextProgress = Math.Min(currentProgress + getMaxDeltaY(), deltaY);
|
||||||
|
float nextX = (float)(vertices[i - 1].X + nextProgress / deltaY * deltaX);
|
||||||
|
float nextY = (float)(lastPosition.Y + getYDirection() * (nextProgress - currentProgress));
|
||||||
|
addControlPoint(nextX, nextY);
|
||||||
|
currentProgress = nextProgress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int getYDirection()
|
||||||
|
{
|
||||||
|
float lastSliderY = sliderStartY + lastPosition.Y;
|
||||||
|
return lastSliderY < OSU_PLAYFIELD_HEIGHT / 2 ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
float getMaxDeltaY()
|
||||||
|
{
|
||||||
|
float lastSliderY = sliderStartY + lastPosition.Y;
|
||||||
|
return Math.Max(lastSliderY, OSU_PLAYFIELD_HEIGHT - lastSliderY) - margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addControlPoint(float nextX, float nextY)
|
||||||
|
{
|
||||||
|
Vector2 nextPosition = new Vector2(nextX, nextY);
|
||||||
|
sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition));
|
||||||
|
currentDistance += Vector2.Distance(lastPosition, nextPosition);
|
||||||
|
lastPosition = nextPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Find the index at which a new vertex with <paramref name="distance"/> can be inserted.
|
||||||
|
/// </summary>
|
||||||
|
private int vertexIndexAtDistance(double distance)
|
||||||
|
{
|
||||||
|
// The position of `(distance, Infinity)` is uniquely determined because infinite positions are not allowed.
|
||||||
|
int i = vertices.BinarySearch(new JuiceStreamPathVertex(distance, float.PositiveInfinity));
|
||||||
|
return i < 0 ? ~i : i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compute the position at the given <paramref name="distance"/>, assuming <paramref name="index"/> is the vertex index returned by <see cref="vertexIndexAtDistance"/>.
|
||||||
|
/// </summary>
|
||||||
|
private float positionAtDistance(double distance, 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))
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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++;
|
||||||
|
}
|
||||||
|
}
|
33
osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs
Normal file
33
osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A vertex of a <see cref="JuiceStreamPath"/>.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct JuiceStreamPathVertex : IComparable<JuiceStreamPathVertex>
|
||||||
|
{
|
||||||
|
public readonly double Distance;
|
||||||
|
|
||||||
|
public readonly float X;
|
||||||
|
|
||||||
|
public JuiceStreamPathVertex(double distance, float x)
|
||||||
|
{
|
||||||
|
Distance = distance;
|
||||||
|
X = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompareTo(JuiceStreamPathVertex other)
|
||||||
|
{
|
||||||
|
int c = Distance.CompareTo(other.Distance);
|
||||||
|
return c != 0 ? c : X.CompareTo(other.X);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => $"({Distance}, {X})";
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,10 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Skinning;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Objects
|
namespace osu.Game.Rulesets.Catch.Objects
|
||||||
@ -45,6 +45,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Color4 IHasComboInformation.GetComboColour(IReadOnlyList<Color4> comboColours) => comboColours[(IndexInBeatmap + 1) % comboColours.Count];
|
Color4 IHasComboInformation.GetComboColour(ISkin skin) => IHasComboInformation.GetSkinComboColour(this, skin, IndexInBeatmap + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Replays
|
|||||||
bool impossibleJump = speedRequired > movement_speed * 2;
|
bool impossibleJump = speedRequired > movement_speed * 2;
|
||||||
|
|
||||||
// todo: get correct catcher size, based on difficulty CS.
|
// todo: get correct catcher size, based on difficulty CS.
|
||||||
const float catcher_width_half = CatcherArea.CATCHER_SIZE * 0.3f * 0.5f;
|
const float catcher_width_half = Catcher.BASE_SIZE * 0.3f * 0.5f;
|
||||||
|
|
||||||
if (lastPosition - catcher_width_half < h.EffectiveX && lastPosition + catcher_width_half > h.EffectiveX)
|
if (lastPosition - catcher_width_half < h.EffectiveX && lastPosition + catcher_width_half > h.EffectiveX)
|
||||||
{
|
{
|
||||||
|
@ -26,38 +26,47 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const float CENTER_X = WIDTH / 2;
|
public const float CENTER_X = WIDTH / 2;
|
||||||
|
|
||||||
[Cached]
|
|
||||||
private readonly DroppedObjectContainer droppedObjectContainer;
|
|
||||||
|
|
||||||
internal readonly CatcherArea CatcherArea;
|
|
||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||||
// only check the X position; handle all vertical space.
|
// only check the X position; handle all vertical space.
|
||||||
base.ReceivePositionalInputAt(new Vector2(screenSpacePos.X, ScreenSpaceDrawQuad.Centre.Y));
|
base.ReceivePositionalInputAt(new Vector2(screenSpacePos.X, ScreenSpaceDrawQuad.Centre.Y));
|
||||||
|
|
||||||
|
internal Catcher Catcher { get; private set; }
|
||||||
|
|
||||||
|
internal CatcherArea CatcherArea { get; private set; }
|
||||||
|
|
||||||
|
private readonly BeatmapDifficulty difficulty;
|
||||||
|
|
||||||
public CatchPlayfield(BeatmapDifficulty difficulty)
|
public CatchPlayfield(BeatmapDifficulty difficulty)
|
||||||
{
|
{
|
||||||
CatcherArea = new CatcherArea(difficulty)
|
this.difficulty = difficulty;
|
||||||
{
|
|
||||||
Anchor = Anchor.BottomLeft,
|
|
||||||
Origin = Anchor.TopLeft,
|
|
||||||
};
|
|
||||||
|
|
||||||
InternalChildren = new[]
|
|
||||||
{
|
|
||||||
droppedObjectContainer = new DroppedObjectContainer(),
|
|
||||||
CatcherArea.MovableCatcher.CreateProxiedContent(),
|
|
||||||
HitObjectContainer.CreateProxy(),
|
|
||||||
// This ordering (`CatcherArea` before `HitObjectContainer`) is important to
|
|
||||||
// make sure the up-to-date catcher position is used for the catcher catching logic of hit objects.
|
|
||||||
CatcherArea,
|
|
||||||
HitObjectContainer,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
|
var droppedObjectContainer = new DroppedObjectContainer();
|
||||||
|
|
||||||
|
Catcher = new Catcher(droppedObjectContainer, difficulty)
|
||||||
|
{
|
||||||
|
X = CENTER_X
|
||||||
|
};
|
||||||
|
|
||||||
|
AddRangeInternal(new[]
|
||||||
|
{
|
||||||
|
droppedObjectContainer,
|
||||||
|
Catcher.CreateProxiedContent(),
|
||||||
|
HitObjectContainer.CreateProxy(),
|
||||||
|
// This ordering (`CatcherArea` before `HitObjectContainer`) is important to
|
||||||
|
// make sure the up-to-date catcher position is used for the catcher catching logic of hit objects.
|
||||||
|
CatcherArea = new CatcherArea
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.TopLeft,
|
||||||
|
Catcher = Catcher,
|
||||||
|
},
|
||||||
|
HitObjectContainer,
|
||||||
|
});
|
||||||
|
|
||||||
RegisterPool<Droplet, DrawableDroplet>(50);
|
RegisterPool<Droplet, DrawableDroplet>(50);
|
||||||
RegisterPool<TinyDroplet, DrawableTinyDroplet>(50);
|
RegisterPool<TinyDroplet, DrawableTinyDroplet>(50);
|
||||||
RegisterPool<Fruit, DrawableFruit>(100);
|
RegisterPool<Fruit, DrawableFruit>(100);
|
||||||
@ -80,7 +89,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
((DrawableCatchHitObject)d).CheckPosition = checkIfWeCanCatch;
|
((DrawableCatchHitObject)d).CheckPosition = checkIfWeCanCatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool checkIfWeCanCatch(CatchHitObject obj) => CatcherArea.MovableCatcher.CanCatch(obj);
|
private bool checkIfWeCanCatch(CatchHitObject obj) => Catcher.CanCatch(obj);
|
||||||
|
|
||||||
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
||||||
=> CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);
|
=> CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);
|
||||||
|
@ -21,6 +21,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override ReplayFrame HandleFrame(Vector2 mousePosition, List<CatchAction> actions, ReplayFrame previousFrame)
|
protected override ReplayFrame HandleFrame(Vector2 mousePosition, List<CatchAction> actions, ReplayFrame previousFrame)
|
||||||
=> new CatchReplayFrame(Time.Current, playfield.CatcherArea.MovableCatcher.X, actions.Contains(CatchAction.Dash), previousFrame as CatchReplayFrame);
|
=> new CatchReplayFrame(Time.Current, playfield.Catcher.X, actions.Contains(CatchAction.Dash), previousFrame as CatchReplayFrame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,17 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
public class Catcher : SkinReloadableDrawable
|
public class Catcher : SkinReloadableDrawable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default colour used to tint hyper-dash fruit, along with the moving catcher, its trail
|
/// The size of the catcher at 1x scale.
|
||||||
/// and end glow/after-image during a hyper-dash.
|
/// </summary>
|
||||||
|
public const float BASE_SIZE = 106.75f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable.
|
||||||
|
/// </summary>
|
||||||
|
public const float ALLOWED_CATCH_RANGE = 0.8f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default colour used to tint hyper-dash fruit, along with the moving catcher, its trail and after-image during a hyper-dash.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red;
|
public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red;
|
||||||
|
|
||||||
@ -61,11 +70,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private const float caught_fruit_scale_adjust = 0.5f;
|
private const float caught_fruit_scale_adjust = 0.5f;
|
||||||
|
|
||||||
[NotNull]
|
|
||||||
private readonly Container trailsTarget;
|
|
||||||
|
|
||||||
private CatcherTrailDisplay trails;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains caught objects on the plate.
|
/// Contains caught objects on the plate.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -74,40 +78,26 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains objects dropped from the plate.
|
/// Contains objects dropped from the plate.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Resolved]
|
private readonly DroppedObjectContainer droppedObjectTarget;
|
||||||
private DroppedObjectContainer droppedObjectTarget { get; set; }
|
|
||||||
|
|
||||||
public CatcherAnimationState CurrentState
|
public CatcherAnimationState CurrentState
|
||||||
{
|
{
|
||||||
get => Body.AnimationState.Value;
|
get => body.AnimationState.Value;
|
||||||
private set => Body.AnimationState.Value = value;
|
private set => body.AnimationState.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable.
|
/// Whether the catcher is currently dashing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const float ALLOWED_CATCH_RANGE = 0.8f;
|
public bool Dashing { get; set; }
|
||||||
|
|
||||||
private bool dashing;
|
|
||||||
|
|
||||||
public bool Dashing
|
|
||||||
{
|
|
||||||
get => dashing;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value == dashing) return;
|
|
||||||
|
|
||||||
dashing = value;
|
|
||||||
|
|
||||||
updateTrailVisibility();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The currently facing direction.
|
/// The currently facing direction.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Direction VisualDirection { get; set; } = Direction.Right;
|
public Direction VisualDirection { get; set; } = Direction.Right;
|
||||||
|
|
||||||
|
public Vector2 BodyScale => Scale * body.Scale;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
|
/// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -118,10 +108,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly float catchWidth;
|
private readonly float catchWidth;
|
||||||
|
|
||||||
internal readonly SkinnableCatcher Body;
|
private readonly SkinnableCatcher body;
|
||||||
|
|
||||||
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
|
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
|
||||||
private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
|
|
||||||
|
|
||||||
private double hyperDashModifier = 1;
|
private double hyperDashModifier = 1;
|
||||||
private int hyperDashDirection;
|
private int hyperDashDirection;
|
||||||
@ -134,13 +123,13 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
private readonly DrawablePool<CaughtBanana> caughtBananaPool;
|
private readonly DrawablePool<CaughtBanana> caughtBananaPool;
|
||||||
private readonly DrawablePool<CaughtDroplet> caughtDropletPool;
|
private readonly DrawablePool<CaughtDroplet> caughtDropletPool;
|
||||||
|
|
||||||
public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null)
|
public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty = null)
|
||||||
{
|
{
|
||||||
this.trailsTarget = trailsTarget;
|
this.droppedObjectTarget = droppedObjectTarget;
|
||||||
|
|
||||||
Origin = Anchor.TopCentre;
|
Origin = Anchor.TopCentre;
|
||||||
|
|
||||||
Size = new Vector2(CatcherArea.CATCHER_SIZE);
|
Size = new Vector2(BASE_SIZE);
|
||||||
if (difficulty != null)
|
if (difficulty != null)
|
||||||
Scale = calculateScale(difficulty);
|
Scale = calculateScale(difficulty);
|
||||||
|
|
||||||
@ -159,7 +148,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
// offset fruit vertically to better place "above" the plate.
|
// offset fruit vertically to better place "above" the plate.
|
||||||
Y = -5
|
Y = -5
|
||||||
},
|
},
|
||||||
Body = new SkinnableCatcher(),
|
body = new SkinnableCatcher(),
|
||||||
hitExplosionContainer = new HitExplosionContainer
|
hitExplosionContainer = new HitExplosionContainer
|
||||||
{
|
{
|
||||||
Anchor = Anchor.TopCentre,
|
Anchor = Anchor.TopCentre,
|
||||||
@ -172,15 +161,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
private void load(OsuConfigManager config)
|
private void load(OsuConfigManager config)
|
||||||
{
|
{
|
||||||
hitLighting = config.GetBindable<bool>(OsuSetting.HitLighting);
|
hitLighting = config.GetBindable<bool>(OsuSetting.HitLighting);
|
||||||
trails = new CatcherTrailDisplay(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
|
|
||||||
// don't add in above load as we may potentially modify a parent in an unsafe manner.
|
|
||||||
trailsTarget.Add(trails);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -197,7 +177,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
/// Calculates the width of the area used for attempting catches in gameplay.
|
/// Calculates the width of the area used for attempting catches in gameplay.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="scale">The scale of the catcher.</param>
|
/// <param name="scale">The scale of the catcher.</param>
|
||||||
public static float CalculateCatchWidth(Vector2 scale) => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE;
|
public static float CalculateCatchWidth(Vector2 scale) => BASE_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates the width of the area used for attempting catches in gameplay.
|
/// Calculates the width of the area used for attempting catches in gameplay.
|
||||||
@ -213,14 +193,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
if (!(hitObject is PalpableCatchHitObject fruit))
|
if (!(hitObject is PalpableCatchHitObject fruit))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var halfCatchWidth = catchWidth * 0.5f;
|
float halfCatchWidth = catchWidth * 0.5f;
|
||||||
|
return fruit.EffectiveX >= X - halfCatchWidth &&
|
||||||
// this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
|
fruit.EffectiveX <= X + halfCatchWidth;
|
||||||
var catchObjectPosition = fruit.EffectiveX;
|
|
||||||
var catcherPosition = Position.X;
|
|
||||||
|
|
||||||
return catchObjectPosition >= catcherPosition - halfCatchWidth &&
|
|
||||||
catchObjectPosition <= catcherPosition + halfCatchWidth;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnNewResult(DrawableCatchHitObject drawableObject, JudgementResult result)
|
public void OnNewResult(DrawableCatchHitObject drawableObject, JudgementResult result)
|
||||||
@ -307,10 +282,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
hyperDashTargetPosition = targetPosition;
|
hyperDashTargetPosition = targetPosition;
|
||||||
|
|
||||||
if (!wasHyperDashing)
|
if (!wasHyperDashing)
|
||||||
{
|
|
||||||
trails.DisplayEndGlow();
|
|
||||||
runHyperDashStateTransition(true);
|
runHyperDashStateTransition(true);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,13 +298,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
|
|
||||||
private void runHyperDashStateTransition(bool hyperDashing)
|
private void runHyperDashStateTransition(bool hyperDashing)
|
||||||
{
|
{
|
||||||
updateTrailVisibility();
|
|
||||||
|
|
||||||
this.FadeColour(hyperDashing ? hyperDashColour : Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
|
this.FadeColour(hyperDashing ? hyperDashColour : Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing;
|
|
||||||
|
|
||||||
protected override void SkinChanged(ISkinSource skin)
|
protected override void SkinChanged(ISkinSource skin)
|
||||||
{
|
{
|
||||||
base.SkinChanged(skin);
|
base.SkinChanged(skin);
|
||||||
@ -341,13 +309,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ??
|
skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ??
|
||||||
DEFAULT_HYPER_DASH_COLOUR;
|
DEFAULT_HYPER_DASH_COLOUR;
|
||||||
|
|
||||||
hyperDashEndGlowColour =
|
|
||||||
skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value ??
|
|
||||||
hyperDashColour;
|
|
||||||
|
|
||||||
trails.HyperDashTrailsColour = hyperDashColour;
|
|
||||||
trails.EndGlowSpritesColour = hyperDashEndGlowColour;
|
|
||||||
|
|
||||||
flipCatcherPlate = skin.GetConfig<CatchSkinConfiguration, bool>(CatchSkinConfiguration.FlipCatcherPlate)?.Value ?? true;
|
flipCatcherPlate = skin.GetConfig<CatchSkinConfiguration, bool>(CatchSkinConfiguration.FlipCatcherPlate)?.Value ?? true;
|
||||||
|
|
||||||
runHyperDashStateTransition(HyperDashing);
|
runHyperDashStateTransition(HyperDashing);
|
||||||
@ -358,7 +319,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
var scaleFromDirection = new Vector2((int)VisualDirection, 1);
|
var scaleFromDirection = new Vector2((int)VisualDirection, 1);
|
||||||
Body.Scale = scaleFromDirection;
|
body.Scale = scaleFromDirection;
|
||||||
caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
|
caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
|
||||||
|
|
||||||
// Correct overshooting.
|
// Correct overshooting.
|
||||||
|
@ -5,7 +5,6 @@ using System;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Rulesets.Catch.Judgements;
|
using osu.Game.Rulesets.Catch.Judgements;
|
||||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Catch.Replays;
|
using osu.Game.Rulesets.Catch.Replays;
|
||||||
@ -16,13 +15,27 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.UI
|
namespace osu.Game.Rulesets.Catch.UI
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The horizontal band at the bottom of the playfield the catcher is moving on.
|
||||||
|
/// It holds a <see cref="Catcher"/> as a child and translates input to the catcher movement.
|
||||||
|
/// It also holds a combo display that is above the catcher, and judgment results are translated to the catcher and the combo display.
|
||||||
|
/// </summary>
|
||||||
public class CatcherArea : Container, IKeyBindingHandler<CatchAction>
|
public class CatcherArea : Container, IKeyBindingHandler<CatchAction>
|
||||||
{
|
{
|
||||||
public const float CATCHER_SIZE = 106.75f;
|
public Catcher Catcher
|
||||||
|
{
|
||||||
|
get => catcher;
|
||||||
|
set => catcherContainer.Child = catcher = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Container<Catcher> catcherContainer;
|
||||||
|
|
||||||
public readonly Catcher MovableCatcher;
|
|
||||||
private readonly CatchComboDisplay comboDisplay;
|
private readonly CatchComboDisplay comboDisplay;
|
||||||
|
|
||||||
|
private readonly CatcherTrailDisplay catcherTrails;
|
||||||
|
|
||||||
|
private Catcher catcher;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <c>-1</c> when only left button is pressed.
|
/// <c>-1</c> when only left button is pressed.
|
||||||
/// <c>1</c> when only right button is pressed.
|
/// <c>1</c> when only right button is pressed.
|
||||||
@ -30,11 +43,19 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private int currentDirection;
|
private int currentDirection;
|
||||||
|
|
||||||
public CatcherArea(BeatmapDifficulty difficulty = null)
|
// TODO: support replay rewind
|
||||||
|
private bool lastHyperDashState;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// <see cref="Catcher"/> must be set before loading.
|
||||||
|
/// </remarks>
|
||||||
|
public CatcherArea()
|
||||||
{
|
{
|
||||||
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
|
Size = new Vector2(CatchPlayfield.WIDTH, Catcher.BASE_SIZE);
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
|
catcherContainer = new Container<Catcher> { RelativeSizeAxes = Axes.Both },
|
||||||
|
catcherTrails = new CatcherTrailDisplay(),
|
||||||
comboDisplay = new CatchComboDisplay
|
comboDisplay = new CatchComboDisplay
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.None,
|
RelativeSizeAxes = Axes.None,
|
||||||
@ -43,14 +64,13 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Margin = new MarginPadding { Bottom = 350f },
|
Margin = new MarginPadding { Bottom = 350f },
|
||||||
X = CatchPlayfield.CENTER_X
|
X = CatchPlayfield.CENTER_X
|
||||||
},
|
}
|
||||||
MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result)
|
public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result)
|
||||||
{
|
{
|
||||||
MovableCatcher.OnNewResult(hitObject, result);
|
Catcher.OnNewResult(hitObject, result);
|
||||||
|
|
||||||
if (!result.Type.IsScorable())
|
if (!result.Type.IsScorable())
|
||||||
return;
|
return;
|
||||||
@ -58,9 +78,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
if (hitObject.HitObject.LastInCombo)
|
if (hitObject.HitObject.LastInCombo)
|
||||||
{
|
{
|
||||||
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
|
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
|
||||||
MovableCatcher.Explode();
|
Catcher.Explode();
|
||||||
else
|
else
|
||||||
MovableCatcher.Drop();
|
Catcher.Drop();
|
||||||
}
|
}
|
||||||
|
|
||||||
comboDisplay.OnNewResult(hitObject, result);
|
comboDisplay.OnNewResult(hitObject, result);
|
||||||
@ -69,7 +89,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
public void OnRevertResult(DrawableCatchHitObject hitObject, JudgementResult result)
|
public void OnRevertResult(DrawableCatchHitObject hitObject, JudgementResult result)
|
||||||
{
|
{
|
||||||
comboDisplay.OnRevertResult(hitObject, result);
|
comboDisplay.OnRevertResult(hitObject, result);
|
||||||
MovableCatcher.OnRevertResult(hitObject, result);
|
Catcher.OnRevertResult(hitObject, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
@ -80,27 +100,48 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
|
|
||||||
SetCatcherPosition(
|
SetCatcherPosition(
|
||||||
replayState?.CatcherX ??
|
replayState?.CatcherX ??
|
||||||
(float)(MovableCatcher.X + MovableCatcher.Speed * currentDirection * Clock.ElapsedFrameTime));
|
(float)(Catcher.X + Catcher.Speed * currentDirection * Clock.ElapsedFrameTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateAfterChildren()
|
protected override void UpdateAfterChildren()
|
||||||
{
|
{
|
||||||
base.UpdateAfterChildren();
|
base.UpdateAfterChildren();
|
||||||
|
|
||||||
comboDisplay.X = MovableCatcher.X;
|
comboDisplay.X = Catcher.X;
|
||||||
|
|
||||||
|
if (Time.Elapsed <= 0)
|
||||||
|
{
|
||||||
|
// This is probably a wrong value, but currently the true value is not recorded.
|
||||||
|
// Setting `true` will prevent generation of false-positive after-images (with more false-negatives).
|
||||||
|
lastHyperDashState = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lastHyperDashState && Catcher.HyperDashing)
|
||||||
|
displayCatcherTrail(CatcherTrailAnimation.HyperDashAfterImage);
|
||||||
|
|
||||||
|
if (Catcher.Dashing || Catcher.HyperDashing)
|
||||||
|
{
|
||||||
|
double generationInterval = Catcher.HyperDashing ? 25 : 50;
|
||||||
|
|
||||||
|
if (Time.Current - catcherTrails.LastDashTrailTime >= generationInterval)
|
||||||
|
displayCatcherTrail(Catcher.HyperDashing ? CatcherTrailAnimation.HyperDashing : CatcherTrailAnimation.Dashing);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastHyperDashState = Catcher.HyperDashing;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetCatcherPosition(float X)
|
public void SetCatcherPosition(float X)
|
||||||
{
|
{
|
||||||
float lastPosition = MovableCatcher.X;
|
float lastPosition = Catcher.X;
|
||||||
float newPosition = Math.Clamp(X, 0, CatchPlayfield.WIDTH);
|
float newPosition = Math.Clamp(X, 0, CatchPlayfield.WIDTH);
|
||||||
|
|
||||||
MovableCatcher.X = newPosition;
|
Catcher.X = newPosition;
|
||||||
|
|
||||||
if (lastPosition < newPosition)
|
if (lastPosition < newPosition)
|
||||||
MovableCatcher.VisualDirection = Direction.Right;
|
Catcher.VisualDirection = Direction.Right;
|
||||||
else if (lastPosition > newPosition)
|
else if (lastPosition > newPosition)
|
||||||
MovableCatcher.VisualDirection = Direction.Left;
|
Catcher.VisualDirection = Direction.Left;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool OnPressed(CatchAction action)
|
public bool OnPressed(CatchAction action)
|
||||||
@ -116,7 +157,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
case CatchAction.Dash:
|
case CatchAction.Dash:
|
||||||
MovableCatcher.Dashing = true;
|
Catcher.Dashing = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,9 +177,11 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case CatchAction.Dash:
|
case CatchAction.Dash:
|
||||||
MovableCatcher.Dashing = false;
|
Catcher.Dashing = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void displayCatcherTrail(CatcherTrailAnimation animation) => catcherTrails.Add(new CatcherTrailEntry(Time.Current, Catcher.CurrentState, Catcher.X, Catcher.BodyScale, animation));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Pooling;
|
|
||||||
using osu.Framework.Timing;
|
using osu.Framework.Timing;
|
||||||
|
using osu.Game.Rulesets.Objects.Pooling;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.UI
|
namespace osu.Game.Rulesets.Catch.UI
|
||||||
@ -12,18 +12,13 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
/// A trail of the catcher.
|
/// A trail of the catcher.
|
||||||
/// It also represents a hyper dash afterimage.
|
/// It also represents a hyper dash afterimage.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CatcherTrail : PoolableDrawable
|
public class CatcherTrail : PoolableDrawableWithLifetime<CatcherTrailEntry>
|
||||||
{
|
{
|
||||||
public CatcherAnimationState AnimationState
|
|
||||||
{
|
|
||||||
set => body.AnimationState.Value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly SkinnableCatcher body;
|
private readonly SkinnableCatcher body;
|
||||||
|
|
||||||
public CatcherTrail()
|
public CatcherTrail()
|
||||||
{
|
{
|
||||||
Size = new Vector2(CatcherArea.CATCHER_SIZE);
|
Size = new Vector2(Catcher.BASE_SIZE);
|
||||||
Origin = Anchor.TopCentre;
|
Origin = Anchor.TopCentre;
|
||||||
Blending = BlendingParameters.Additive;
|
Blending = BlendingParameters.Additive;
|
||||||
InternalChild = body = new SkinnableCatcher
|
InternalChild = body = new SkinnableCatcher
|
||||||
@ -34,11 +29,40 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void FreeAfterUse()
|
protected override void OnApply(CatcherTrailEntry entry)
|
||||||
{
|
{
|
||||||
|
Position = new Vector2(entry.Position, 0);
|
||||||
|
Scale = entry.Scale;
|
||||||
|
|
||||||
|
body.AnimationState.Value = entry.CatcherState;
|
||||||
|
|
||||||
|
using (BeginAbsoluteSequence(entry.LifetimeStart, false))
|
||||||
|
applyTransforms(entry.Animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnFree(CatcherTrailEntry entry)
|
||||||
|
{
|
||||||
|
ApplyTransformsAt(double.MinValue);
|
||||||
ClearTransforms();
|
ClearTransforms();
|
||||||
Alpha = 1;
|
}
|
||||||
base.FreeAfterUse();
|
|
||||||
|
private void applyTransforms(CatcherTrailAnimation animation)
|
||||||
|
{
|
||||||
|
switch (animation)
|
||||||
|
{
|
||||||
|
case CatcherTrailAnimation.Dashing:
|
||||||
|
case CatcherTrailAnimation.HyperDashing:
|
||||||
|
this.FadeTo(0.4f).FadeOut(800, Easing.OutQuint);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CatcherTrailAnimation.HyperDashAfterImage:
|
||||||
|
this.MoveToOffset(new Vector2(0, -10), 1200, Easing.In);
|
||||||
|
this.ScaleTo(Scale * 0.95f).ScaleTo(Scale * 1.2f, 1200, Easing.In);
|
||||||
|
this.FadeOut(1200);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Expire();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
osu.Game.Rulesets.Catch/UI/CatcherTrailAnimation.cs
Normal file
12
osu.Game.Rulesets.Catch/UI/CatcherTrailAnimation.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.UI
|
||||||
|
{
|
||||||
|
public enum CatcherTrailAnimation
|
||||||
|
{
|
||||||
|
Dashing,
|
||||||
|
HyperDashing,
|
||||||
|
HyperDashAfterImage
|
||||||
|
}
|
||||||
|
}
|
@ -2,11 +2,13 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using JetBrains.Annotations;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Pooling;
|
using osu.Framework.Graphics.Pooling;
|
||||||
using osuTK;
|
using osu.Game.Rulesets.Catch.Skinning;
|
||||||
|
using osu.Game.Rulesets.Objects.Pooling;
|
||||||
|
using osu.Game.Skinning;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.UI
|
namespace osu.Game.Rulesets.Catch.UI
|
||||||
@ -15,70 +17,32 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
/// Represents a component responsible for displaying
|
/// Represents a component responsible for displaying
|
||||||
/// the appropriate catcher trails when requested to.
|
/// the appropriate catcher trails when requested to.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CatcherTrailDisplay : CompositeDrawable
|
public class CatcherTrailDisplay : PooledDrawableWithLifetimeContainer<CatcherTrailEntry, CatcherTrail>
|
||||||
{
|
{
|
||||||
private readonly Catcher catcher;
|
/// <summary>
|
||||||
|
/// The most recent time a dash trail was added to this container.
|
||||||
|
/// Only alive (not faded out) trails are considered.
|
||||||
|
/// Returns <see cref="double.NegativeInfinity"/> if no dash trail is alive.
|
||||||
|
/// </summary>
|
||||||
|
public double LastDashTrailTime => getLastDashTrailTime();
|
||||||
|
|
||||||
|
public Color4 HyperDashTrailsColour => hyperDashTrails.Colour;
|
||||||
|
|
||||||
|
public Color4 HyperDashAfterImageColour => hyperDashAfterImages.Colour;
|
||||||
|
|
||||||
|
protected override bool RemoveRewoundEntry => true;
|
||||||
|
|
||||||
private readonly DrawablePool<CatcherTrail> trailPool;
|
private readonly DrawablePool<CatcherTrail> trailPool;
|
||||||
|
|
||||||
private readonly Container<CatcherTrail> dashTrails;
|
private readonly Container<CatcherTrail> dashTrails;
|
||||||
private readonly Container<CatcherTrail> hyperDashTrails;
|
private readonly Container<CatcherTrail> hyperDashTrails;
|
||||||
private readonly Container<CatcherTrail> endGlowSprites;
|
private readonly Container<CatcherTrail> hyperDashAfterImages;
|
||||||
|
|
||||||
private Color4 hyperDashTrailsColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
|
[Resolved]
|
||||||
|
private ISkinSource skin { get; set; }
|
||||||
|
|
||||||
public Color4 HyperDashTrailsColour
|
public CatcherTrailDisplay()
|
||||||
{
|
{
|
||||||
get => hyperDashTrailsColour;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (hyperDashTrailsColour == value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
hyperDashTrailsColour = value;
|
|
||||||
hyperDashTrails.Colour = hyperDashTrailsColour;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Color4 endGlowSpritesColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
|
|
||||||
|
|
||||||
public Color4 EndGlowSpritesColour
|
|
||||||
{
|
|
||||||
get => endGlowSpritesColour;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (endGlowSpritesColour == value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
endGlowSpritesColour = value;
|
|
||||||
endGlowSprites.Colour = endGlowSpritesColour;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool trail;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether to start displaying trails following the catcher.
|
|
||||||
/// </summary>
|
|
||||||
public bool DisplayTrail
|
|
||||||
{
|
|
||||||
get => trail;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (trail == value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
trail = value;
|
|
||||||
|
|
||||||
if (trail)
|
|
||||||
displayTrail();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public CatcherTrailDisplay([NotNull] Catcher catcher)
|
|
||||||
{
|
|
||||||
this.catcher = catcher ?? throw new ArgumentNullException(nameof(catcher));
|
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
@ -86,47 +50,86 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
trailPool = new DrawablePool<CatcherTrail>(30),
|
trailPool = new DrawablePool<CatcherTrail>(30),
|
||||||
dashTrails = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both },
|
dashTrails = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both },
|
||||||
hyperDashTrails = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
|
hyperDashTrails = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
|
||||||
endGlowSprites = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
|
hyperDashAfterImages = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
protected override void LoadComplete()
|
||||||
/// Displays a single end-glow catcher sprite.
|
|
||||||
/// </summary>
|
|
||||||
public void DisplayEndGlow()
|
|
||||||
{
|
{
|
||||||
var endGlow = createTrailSprite(endGlowSprites);
|
base.LoadComplete();
|
||||||
|
|
||||||
endGlow.MoveToOffset(new Vector2(0, -10), 1200, Easing.In);
|
skin.SourceChanged += skinSourceChanged;
|
||||||
endGlow.ScaleTo(endGlow.Scale * 0.95f).ScaleTo(endGlow.Scale * 1.2f, 1200, Easing.In);
|
skinSourceChanged();
|
||||||
endGlow.FadeOut(1200);
|
|
||||||
endGlow.Expire(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void displayTrail()
|
private void skinSourceChanged()
|
||||||
{
|
{
|
||||||
if (!DisplayTrail)
|
hyperDashTrails.Colour = skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ?? Catcher.DEFAULT_HYPER_DASH_COLOUR;
|
||||||
return;
|
hyperDashAfterImages.Colour = skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value ?? hyperDashTrails.Colour;
|
||||||
|
|
||||||
var sprite = createTrailSprite(catcher.HyperDashing ? hyperDashTrails : dashTrails);
|
|
||||||
|
|
||||||
sprite.FadeTo(0.4f).FadeOut(800, Easing.OutQuint);
|
|
||||||
sprite.Expire(true);
|
|
||||||
|
|
||||||
Scheduler.AddDelayed(displayTrail, catcher.HyperDashing ? 25 : 50);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CatcherTrail createTrailSprite(Container<CatcherTrail> target)
|
protected override void AddDrawable(CatcherTrailEntry entry, CatcherTrail drawable)
|
||||||
{
|
{
|
||||||
CatcherTrail sprite = trailPool.Get();
|
switch (entry.Animation)
|
||||||
|
{
|
||||||
|
case CatcherTrailAnimation.Dashing:
|
||||||
|
dashTrails.Add(drawable);
|
||||||
|
break;
|
||||||
|
|
||||||
sprite.AnimationState = catcher.CurrentState;
|
case CatcherTrailAnimation.HyperDashing:
|
||||||
sprite.Scale = catcher.Scale * catcher.Body.Scale;
|
hyperDashTrails.Add(drawable);
|
||||||
sprite.Position = catcher.Position;
|
break;
|
||||||
|
|
||||||
target.Add(sprite);
|
case CatcherTrailAnimation.HyperDashAfterImage:
|
||||||
|
hyperDashAfterImages.Add(drawable);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return sprite;
|
protected override void RemoveDrawable(CatcherTrailEntry entry, CatcherTrail drawable)
|
||||||
|
{
|
||||||
|
switch (entry.Animation)
|
||||||
|
{
|
||||||
|
case CatcherTrailAnimation.Dashing:
|
||||||
|
dashTrails.Remove(drawable);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CatcherTrailAnimation.HyperDashing:
|
||||||
|
hyperDashTrails.Remove(drawable);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CatcherTrailAnimation.HyperDashAfterImage:
|
||||||
|
hyperDashAfterImages.Remove(drawable);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override CatcherTrail GetDrawable(CatcherTrailEntry entry)
|
||||||
|
{
|
||||||
|
CatcherTrail trail = trailPool.Get();
|
||||||
|
trail.Apply(entry);
|
||||||
|
return trail;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double getLastDashTrailTime()
|
||||||
|
{
|
||||||
|
double maxTime = double.NegativeInfinity;
|
||||||
|
|
||||||
|
foreach (var trail in dashTrails)
|
||||||
|
maxTime = Math.Max(maxTime, trail.LifetimeStart);
|
||||||
|
|
||||||
|
foreach (var trail in hyperDashTrails)
|
||||||
|
maxTime = Math.Max(maxTime, trail.LifetimeStart);
|
||||||
|
|
||||||
|
return maxTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
if (skin != null)
|
||||||
|
skin.SourceChanged -= skinSourceChanged;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
31
osu.Game.Rulesets.Catch/UI/CatcherTrailEntry.cs
Normal file
31
osu.Game.Rulesets.Catch/UI/CatcherTrailEntry.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// 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.Performance;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.UI
|
||||||
|
{
|
||||||
|
public class CatcherTrailEntry : LifetimeEntry
|
||||||
|
{
|
||||||
|
public readonly CatcherAnimationState CatcherState;
|
||||||
|
|
||||||
|
public readonly float Position;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The scaling of the catcher body. It also represents a flipped catcher (negative x component).
|
||||||
|
/// </summary>
|
||||||
|
public readonly Vector2 Scale;
|
||||||
|
|
||||||
|
public readonly CatcherTrailAnimation Animation;
|
||||||
|
|
||||||
|
public CatcherTrailEntry(double startTime, CatcherAnimationState catcherState, float position, Vector2 scale, CatcherTrailAnimation animation)
|
||||||
|
{
|
||||||
|
LifetimeStart = startTime;
|
||||||
|
CatcherState = catcherState;
|
||||||
|
Position = position;
|
||||||
|
Scale = scale;
|
||||||
|
Animation = animation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.TopCentre;
|
Anchor = Anchor.TopCentre;
|
||||||
// Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
|
// Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
|
||||||
OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE;
|
OriginPosition = new Vector2(0.5f, 0.06f) * Catcher.BASE_SIZE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Extensions;
|
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@ -37,12 +36,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
{
|
{
|
||||||
mods = Score.Mods;
|
mods = Score.Mods;
|
||||||
scaledScore = Score.TotalScore;
|
scaledScore = Score.TotalScore;
|
||||||
countPerfect = Score.Statistics.GetOrDefault(HitResult.Perfect);
|
countPerfect = Score.Statistics.GetValueOrDefault(HitResult.Perfect);
|
||||||
countGreat = Score.Statistics.GetOrDefault(HitResult.Great);
|
countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||||
countGood = Score.Statistics.GetOrDefault(HitResult.Good);
|
countGood = Score.Statistics.GetValueOrDefault(HitResult.Good);
|
||||||
countOk = Score.Statistics.GetOrDefault(HitResult.Ok);
|
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||||
countMeh = Score.Statistics.GetOrDefault(HitResult.Meh);
|
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||||
countMiss = Score.Statistics.GetOrDefault(HitResult.Miss);
|
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||||
|
|
||||||
IEnumerable<Mod> scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease);
|
IEnumerable<Mod> scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease);
|
||||||
|
|
||||||
|
@ -253,7 +253,8 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
case ModType.Fun:
|
case ModType.Fun:
|
||||||
return new Mod[]
|
return new Mod[]
|
||||||
{
|
{
|
||||||
new MultiMod(new ModWindUp(), new ModWindDown())
|
new MultiMod(new ModWindUp(), new ModWindDown()),
|
||||||
|
new ManiaModMuted(),
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
{
|
{
|
||||||
public class ManiaSettingsSubsection : RulesetSettingsSubsection
|
public class ManiaSettingsSubsection : RulesetSettingsSubsection
|
||||||
{
|
{
|
||||||
protected override string Header => "osu!mania";
|
protected override LocalisableString Header => "osu!mania";
|
||||||
|
|
||||||
public ManiaSettingsSubsection(ManiaRuleset ruleset)
|
public ManiaSettingsSubsection(ManiaRuleset ruleset)
|
||||||
: base(ruleset)
|
: base(ruleset)
|
||||||
|
@ -10,13 +10,9 @@ using osu.Game.Rulesets.Mania.Beatmaps;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Mods
|
namespace osu.Game.Rulesets.Mania.Mods
|
||||||
{
|
{
|
||||||
public class ManiaModMirror : Mod, IApplicableToBeatmap
|
public class ManiaModMirror : ModMirror, IApplicableToBeatmap
|
||||||
{
|
{
|
||||||
public override string Name => "Mirror";
|
|
||||||
public override string Acronym => "MR";
|
|
||||||
public override ModType Type => ModType.Conversion;
|
|
||||||
public override string Description => "Notes are flipped horizontally.";
|
public override string Description => "Notes are flipped horizontally.";
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
|
|
||||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||||
{
|
{
|
||||||
|
12
osu.Game.Rulesets.Mania/Mods/ManiaModMuted.cs
Normal file
12
osu.Game.Rulesets.Mania/Mods/ManiaModMuted.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// 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.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Mods
|
||||||
|
{
|
||||||
|
public class ManiaModMuted : ModMuted<ManiaHitObject>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
|
|
||||||
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||||
|
|
||||||
|
// Leaving the default (10s) makes hitobjects not appear, as this offset is used for the initial state transforms.
|
||||||
|
// Calculated as DrawableManiaRuleset.MAX_TIME_RANGE + some additional allowance for velocity < 1.
|
||||||
|
protected override double InitialLifetimeOffset => 30000;
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private ManiaPlayfield playfield { get; set; }
|
private ManiaPlayfield playfield { get; set; }
|
||||||
|
|
||||||
|
@ -33,12 +33,12 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The minimum time range. This occurs at a <see cref="relativeTimeRange"/> of 40.
|
/// The minimum time range. This occurs at a <see cref="relativeTimeRange"/> of 40.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const double MIN_TIME_RANGE = 340;
|
public const double MIN_TIME_RANGE = 290;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The maximum time range. This occurs at a <see cref="relativeTimeRange"/> of 1.
|
/// The maximum time range. This occurs at a <see cref="relativeTimeRange"/> of 1.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const double MAX_TIME_RANGE = 13720;
|
public const double MAX_TIME_RANGE = 11485;
|
||||||
|
|
||||||
protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
|
protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
protected override ScrollVisualisationMethod VisualisationMethod => scrollMethod;
|
protected override ScrollVisualisationMethod VisualisationMethod => scrollMethod;
|
||||||
|
|
||||||
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
|
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
|
||||||
private readonly Bindable<double> configTimeRange = new BindableDouble();
|
private readonly BindableDouble configTimeRange = new BindableDouble();
|
||||||
|
|
||||||
// Stores the current speed adjustment active in gameplay.
|
// Stores the current speed adjustment active in gameplay.
|
||||||
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
|
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
|
||||||
@ -103,6 +103,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
|
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
|
||||||
|
|
||||||
Config.BindWith(ManiaRulesetSetting.ScrollTime, configTimeRange);
|
Config.BindWith(ManiaRulesetSetting.ScrollTime, configTimeRange);
|
||||||
|
TimeRange.MinValue = configTimeRange.MinValue;
|
||||||
|
TimeRange.MaxValue = configTimeRange.MaxValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void AdjustScrollSpeed(int amount)
|
protected override void AdjustScrollSpeed(int amount)
|
||||||
|
@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
|
||||||
|
|
||||||
[TestCase(6.9311451172574934d, "diffcalc-test")]
|
[TestCase(6.7568168283591499d, "diffcalc-test")]
|
||||||
[TestCase(1.0736586907780401d, "zero-length-sliders")]
|
[TestCase(1.0348244046058293d, "zero-length-sliders")]
|
||||||
public void Test(double expected, string name)
|
public void Test(double expected, string name)
|
||||||
=> base.Test(expected, name);
|
=> base.Test(expected, name);
|
||||||
|
|
||||||
[TestCase(8.7212283220412345d, "diffcalc-test")]
|
[TestCase(8.4783236764532557d, "diffcalc-test")]
|
||||||
[TestCase(1.3212137158641493d, "zero-length-sliders")]
|
[TestCase(1.2708532136987165d, "zero-length-sliders")]
|
||||||
public void TestClockRateAdjusted(double expected, string name)
|
public void TestClockRateAdjusted(double expected, string name)
|
||||||
=> Test(expected, name, new OsuModDoubleTime());
|
=> Test(expected, name, new OsuModDoubleTime());
|
||||||
|
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osu.Game.Tests.Beatmaps;
|
using osu.Game.Tests.Beatmaps;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
{
|
{
|
||||||
@ -77,23 +82,106 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddAssert("is custom user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours));
|
AddAssert("is custom user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase(true, true)]
|
||||||
|
[TestCase(true, false)]
|
||||||
|
[TestCase(false, true)]
|
||||||
|
[TestCase(false, false)]
|
||||||
|
public void TestComboOffsetWithBeatmapColours(bool userHasCustomColours, bool useBeatmapSkin)
|
||||||
|
{
|
||||||
|
PrepareBeatmap(() => new OsuCustomSkinWorkingBeatmap(audio, true, getHitCirclesWithLegacyOffsets()));
|
||||||
|
ConfigureTest(useBeatmapSkin, true, userHasCustomColours);
|
||||||
|
assertCorrectObjectComboColours("is beatmap skin colours with combo offsets applied",
|
||||||
|
TestBeatmapSkin.Colours,
|
||||||
|
(i, obj) => i + 1 + obj.ComboOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(true)]
|
||||||
|
[TestCase(false)]
|
||||||
|
public void TestComboOffsetWithIgnoredBeatmapColours(bool useBeatmapSkin)
|
||||||
|
{
|
||||||
|
PrepareBeatmap(() => new OsuCustomSkinWorkingBeatmap(audio, true, getHitCirclesWithLegacyOffsets()));
|
||||||
|
ConfigureTest(useBeatmapSkin, false, true);
|
||||||
|
assertCorrectObjectComboColours("is user skin colours without combo offsets applied",
|
||||||
|
TestSkin.Colours,
|
||||||
|
(i, _) => i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertCorrectObjectComboColours(string description, Color4[] expectedColours, Func<int, OsuHitObject, int> nextExpectedComboIndex)
|
||||||
|
{
|
||||||
|
AddUntilStep("wait for objects to become alive", () =>
|
||||||
|
TestPlayer.DrawableRuleset.Playfield.AllHitObjects.Count() == TestPlayer.DrawableRuleset.Objects.Count());
|
||||||
|
|
||||||
|
AddAssert(description, () =>
|
||||||
|
{
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
return TestPlayer.DrawableRuleset.Playfield.AllHitObjects.All(d =>
|
||||||
|
{
|
||||||
|
index = nextExpectedComboIndex(index, (OsuHitObject)d.HitObject);
|
||||||
|
return checkComboColour(d, expectedColours[index % expectedColours.Length]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
static bool checkComboColour(DrawableHitObject drawableHitObject, Color4 expectedColour)
|
||||||
|
{
|
||||||
|
return drawableHitObject.AccentColour.Value == expectedColour &&
|
||||||
|
drawableHitObject.NestedHitObjects.All(n => checkComboColour(n, expectedColour));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<OsuHitObject> getHitCirclesWithLegacyOffsets()
|
||||||
|
{
|
||||||
|
var hitObjects = new List<OsuHitObject>();
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
var hitObject = i % 2 == 0
|
||||||
|
? (OsuHitObject)new HitCircle()
|
||||||
|
: new Slider
|
||||||
|
{
|
||||||
|
Path = new SliderPath(new[]
|
||||||
|
{
|
||||||
|
new PathControlPoint(new Vector2(0, 0)),
|
||||||
|
new PathControlPoint(new Vector2(100, 0)),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
hitObject.StartTime = i;
|
||||||
|
hitObject.Position = new Vector2(256, 192);
|
||||||
|
hitObject.NewCombo = true;
|
||||||
|
hitObject.ComboOffset = i;
|
||||||
|
|
||||||
|
hitObjects.Add(hitObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hitObjects;
|
||||||
|
}
|
||||||
|
|
||||||
private class OsuCustomSkinWorkingBeatmap : CustomSkinWorkingBeatmap
|
private class OsuCustomSkinWorkingBeatmap : CustomSkinWorkingBeatmap
|
||||||
{
|
{
|
||||||
public OsuCustomSkinWorkingBeatmap(AudioManager audio, bool hasColours)
|
public OsuCustomSkinWorkingBeatmap(AudioManager audio, bool hasColours, IEnumerable<OsuHitObject> hitObjects = null)
|
||||||
: base(createBeatmap(), audio, hasColours)
|
: base(createBeatmap(hitObjects), audio, hasColours)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IBeatmap createBeatmap() =>
|
private static IBeatmap createBeatmap(IEnumerable<OsuHitObject> hitObjects)
|
||||||
new Beatmap
|
{
|
||||||
|
var beatmap = new Beatmap
|
||||||
{
|
{
|
||||||
BeatmapInfo =
|
BeatmapInfo =
|
||||||
{
|
{
|
||||||
BeatmapSet = new BeatmapSetInfo(),
|
BeatmapSet = new BeatmapSetInfo(),
|
||||||
Ruleset = new OsuRuleset().RulesetInfo,
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
},
|
},
|
||||||
HitObjects = { new HitCircle { Position = new Vector2(256, 192) } }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
beatmap.HitObjects.AddRange(hitObjects ?? new[]
|
||||||
|
{
|
||||||
|
new HitCircle { Position = new Vector2(256, 192) }
|
||||||
|
});
|
||||||
|
|
||||||
|
return beatmap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Extensions;
|
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
@ -36,10 +35,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
mods = Score.Mods;
|
mods = Score.Mods;
|
||||||
accuracy = Score.Accuracy;
|
accuracy = Score.Accuracy;
|
||||||
scoreMaxCombo = Score.MaxCombo;
|
scoreMaxCombo = Score.MaxCombo;
|
||||||
countGreat = Score.Statistics.GetOrDefault(HitResult.Great);
|
countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||||
countOk = Score.Statistics.GetOrDefault(HitResult.Ok);
|
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||||
countMeh = Score.Statistics.GetOrDefault(HitResult.Meh);
|
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||||
countMiss = Score.Statistics.GetOrDefault(HitResult.Miss);
|
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||||
|
|
||||||
// Custom multipliers for NoFail and SpunOut.
|
// Custom multipliers for NoFail and SpunOut.
|
||||||
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
|
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
|
||||||
@ -98,26 +97,32 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
double approachRateFactor = 0.0;
|
double approachRateFactor = 0.0;
|
||||||
if (Attributes.ApproachRate > 10.33)
|
if (Attributes.ApproachRate > 10.33)
|
||||||
approachRateFactor += 0.4 * (Attributes.ApproachRate - 10.33);
|
approachRateFactor = Attributes.ApproachRate - 10.33;
|
||||||
else if (Attributes.ApproachRate < 8.0)
|
else if (Attributes.ApproachRate < 8.0)
|
||||||
approachRateFactor += 0.01 * (8.0 - Attributes.ApproachRate);
|
approachRateFactor = 0.025 * (8.0 - Attributes.ApproachRate);
|
||||||
|
|
||||||
aimValue *= 1.0 + Math.Min(approachRateFactor, approachRateFactor * (totalHits / 1000.0));
|
double approachRateTotalHitsFactor = 1.0 / (1.0 + Math.Exp(-(0.007 * (totalHits - 400))));
|
||||||
|
|
||||||
|
double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
|
||||||
|
|
||||||
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
||||||
if (mods.Any(h => h is OsuModHidden))
|
if (mods.Any(h => h is OsuModHidden))
|
||||||
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
|
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
|
||||||
|
|
||||||
|
double flashlightBonus = 1.0;
|
||||||
|
|
||||||
if (mods.Any(h => h is OsuModFlashlight))
|
if (mods.Any(h => h is OsuModFlashlight))
|
||||||
{
|
{
|
||||||
// Apply object-based bonus for flashlight.
|
// Apply object-based bonus for flashlight.
|
||||||
aimValue *= 1.0 + 0.35 * Math.Min(1.0, totalHits / 200.0) +
|
flashlightBonus = 1.0 + 0.35 * Math.Min(1.0, totalHits / 200.0) +
|
||||||
(totalHits > 200
|
(totalHits > 200
|
||||||
? 0.3 * Math.Min(1.0, (totalHits - 200) / 300.0) +
|
? 0.3 * Math.Min(1.0, (totalHits - 200) / 300.0) +
|
||||||
(totalHits > 500 ? (totalHits - 500) / 1200.0 : 0.0)
|
(totalHits > 500 ? (totalHits - 500) / 1200.0 : 0.0)
|
||||||
: 0.0);
|
: 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aimValue *= Math.Max(flashlightBonus, approachRateBonus);
|
||||||
|
|
||||||
// Scale the aim value with accuracy _slightly_
|
// Scale the aim value with accuracy _slightly_
|
||||||
aimValue *= 0.5 + accuracy / 2.0;
|
aimValue *= 0.5 + accuracy / 2.0;
|
||||||
// It is important to also consider accuracy difficulty when doing that
|
// It is important to also consider accuracy difficulty when doing that
|
||||||
@ -145,9 +150,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
double approachRateFactor = 0.0;
|
double approachRateFactor = 0.0;
|
||||||
if (Attributes.ApproachRate > 10.33)
|
if (Attributes.ApproachRate > 10.33)
|
||||||
approachRateFactor += 0.4 * (Attributes.ApproachRate - 10.33);
|
approachRateFactor = Attributes.ApproachRate - 10.33;
|
||||||
|
|
||||||
speedValue *= 1.0 + Math.Min(approachRateFactor, approachRateFactor * (totalHits / 1000.0));
|
double approachRateTotalHitsFactor = 1.0 / (1.0 + Math.Exp(-(0.007 * (totalHits - 400))));
|
||||||
|
|
||||||
|
speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
|
||||||
|
|
||||||
if (mods.Any(m => m is OsuModHidden))
|
if (mods.Any(m => m is OsuModHidden))
|
||||||
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
|
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
/// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Aim : StrainSkill
|
public class Aim : OsuStrainSkill
|
||||||
{
|
{
|
||||||
private const double angle_bonus_begin = Math.PI / 3;
|
private const double angle_bonus_begin = Math.PI / 3;
|
||||||
private const double timing_threshold = 107;
|
private const double timing_threshold = 107;
|
||||||
@ -47,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
Math.Max(osuPrevious.JumpDistance - scale, 0)
|
Math.Max(osuPrevious.JumpDistance - scale, 0)
|
||||||
* Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2)
|
* Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2)
|
||||||
* Math.Max(osuCurrent.JumpDistance - scale, 0));
|
* Math.Max(osuCurrent.JumpDistance - scale, 0));
|
||||||
result = 1.5 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime);
|
result = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
61
osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
Normal file
61
osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// 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 System.Collections.Generic;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||||
|
{
|
||||||
|
public abstract class OsuStrainSkill : StrainSkill
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
|
||||||
|
/// This is done in order to decrease their impact on the overall difficulty of the map for this skill.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual int ReducedSectionCount => 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The baseline multiplier applied to the section with the biggest strain.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual double ReducedStrainBaseline => 0.75;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The final multiplier to be applied to <see cref="DifficultyValue"/> after all other calculations.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual double DifficultyMultiplier => 1.06;
|
||||||
|
|
||||||
|
protected OsuStrainSkill(Mod[] mods)
|
||||||
|
: base(mods)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override double DifficultyValue()
|
||||||
|
{
|
||||||
|
double difficulty = 0;
|
||||||
|
double weight = 1;
|
||||||
|
|
||||||
|
List<double> strains = GetCurrentStrainPeaks().OrderByDescending(d => d).ToList();
|
||||||
|
|
||||||
|
// We are reducing the highest strains first to account for extreme difficulty spikes
|
||||||
|
for (int i = 0; i < Math.Min(strains.Count, ReducedSectionCount); i++)
|
||||||
|
{
|
||||||
|
double scale = Math.Log10(Interpolation.Lerp(1, 10, Math.Clamp((float)i / ReducedSectionCount, 0, 1)));
|
||||||
|
strains[i] *= Interpolation.Lerp(ReducedStrainBaseline, 1.0, scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Difficulty is the weighted sum of the highest strains from every section.
|
||||||
|
// We're sorting from highest to lowest strain.
|
||||||
|
foreach (double strain in strains.OrderByDescending(d => d))
|
||||||
|
{
|
||||||
|
difficulty += strain * weight;
|
||||||
|
weight *= DecayWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return difficulty * DifficultyMultiplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit.
|
/// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Speed : StrainSkill
|
public class Speed : OsuStrainSkill
|
||||||
{
|
{
|
||||||
private const double single_spacing_threshold = 125;
|
private const double single_spacing_threshold = 125;
|
||||||
|
|
||||||
@ -23,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
|
|
||||||
protected override double SkillMultiplier => 1400;
|
protected override double SkillMultiplier => 1400;
|
||||||
protected override double StrainDecayBase => 0.3;
|
protected override double StrainDecayBase => 0.3;
|
||||||
|
protected override int ReducedSectionCount => 5;
|
||||||
|
protected override double DifficultyMultiplier => 1.04;
|
||||||
|
|
||||||
private const double min_speed_bonus = 75; // ~200BPM
|
private const double min_speed_bonus = 75; // ~200BPM
|
||||||
private const double max_speed_bonus = 45; // ~330BPM
|
private const double max_speed_bonus = 45; // ~330BPM
|
||||||
|
@ -129,9 +129,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
public bool OnPressed(PlatformAction action)
|
public bool OnPressed(PlatformAction action)
|
||||||
{
|
{
|
||||||
switch (action.ActionMethod)
|
switch (action)
|
||||||
{
|
{
|
||||||
case PlatformActionMethod.Delete:
|
case PlatformAction.Delete:
|
||||||
return DeleteSelected();
|
return DeleteSelected();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,8 +35,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
Quad quad = selectedMovableObjects.Length > 0 ? getSurroundingQuad(selectedMovableObjects) : new Quad();
|
Quad quad = selectedMovableObjects.Length > 0 ? getSurroundingQuad(selectedMovableObjects) : new Quad();
|
||||||
|
|
||||||
SelectionBox.CanRotate = quad.Width > 0 || quad.Height > 0;
|
SelectionBox.CanRotate = quad.Width > 0 || quad.Height > 0;
|
||||||
SelectionBox.CanScaleX = quad.Width > 0;
|
SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0;
|
||||||
SelectionBox.CanScaleY = quad.Height > 0;
|
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0;
|
||||||
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
|
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,32 +76,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
if (h is Slider slider)
|
if (h is Slider slider)
|
||||||
{
|
{
|
||||||
var points = slider.Path.ControlPoints.ToArray();
|
slider.Path.Reverse(out Vector2 offset);
|
||||||
Vector2 endPos = points.Last().Position.Value;
|
slider.Position += offset;
|
||||||
|
|
||||||
slider.Path.ControlPoints.Clear();
|
|
||||||
|
|
||||||
slider.Position += endPos;
|
|
||||||
|
|
||||||
PathType? lastType = null;
|
|
||||||
|
|
||||||
for (var i = 0; i < points.Length; i++)
|
|
||||||
{
|
|
||||||
var p = points[i];
|
|
||||||
p.Position.Value -= endPos;
|
|
||||||
|
|
||||||
// propagate types forwards to last null type
|
|
||||||
if (i == points.Length - 1)
|
|
||||||
p.Type.Value = lastType;
|
|
||||||
else if (p.Type.Value != null)
|
|
||||||
{
|
|
||||||
var newType = p.Type.Value;
|
|
||||||
p.Type.Value = lastType;
|
|
||||||
lastType = newType;
|
|
||||||
}
|
|
||||||
|
|
||||||
slider.Path.ControlPoints.Insert(0, p);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.Utils;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Mods
|
namespace osu.Game.Rulesets.Osu.Mods
|
||||||
{
|
{
|
||||||
@ -15,23 +14,13 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
{
|
{
|
||||||
public override double ScoreMultiplier => 1.06;
|
public override double ScoreMultiplier => 1.06;
|
||||||
|
|
||||||
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModMirror)).ToArray();
|
||||||
|
|
||||||
public void ApplyToHitObject(HitObject hitObject)
|
public void ApplyToHitObject(HitObject hitObject)
|
||||||
{
|
{
|
||||||
var osuObject = (OsuHitObject)hitObject;
|
var osuObject = (OsuHitObject)hitObject;
|
||||||
|
|
||||||
osuObject.Position = new Vector2(osuObject.Position.X, OsuPlayfield.BASE_SIZE.Y - osuObject.Y);
|
OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
|
||||||
|
|
||||||
if (!(hitObject is Slider slider))
|
|
||||||
return;
|
|
||||||
|
|
||||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
|
||||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
|
||||||
|
|
||||||
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray();
|
|
||||||
foreach (var point in controlPoints)
|
|
||||||
point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y);
|
|
||||||
|
|
||||||
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
50
osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs
Normal file
50
osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// 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.Framework.Bindables;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Utils;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Mods
|
||||||
|
{
|
||||||
|
public class OsuModMirror : ModMirror, IApplicableToHitObject
|
||||||
|
{
|
||||||
|
public override string Description => "Flip objects on the chosen axes.";
|
||||||
|
public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) };
|
||||||
|
|
||||||
|
[SettingSource("Mirrored axes", "Choose which axes objects are mirrored over.")]
|
||||||
|
public Bindable<MirrorType> Reflection { get; } = new Bindable<MirrorType>();
|
||||||
|
|
||||||
|
public void ApplyToHitObject(HitObject hitObject)
|
||||||
|
{
|
||||||
|
var osuObject = (OsuHitObject)hitObject;
|
||||||
|
|
||||||
|
switch (Reflection.Value)
|
||||||
|
{
|
||||||
|
case MirrorType.Horizontal:
|
||||||
|
OsuHitObjectGenerationUtils.ReflectHorizontally(osuObject);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MirrorType.Vertical:
|
||||||
|
OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MirrorType.Both:
|
||||||
|
OsuHitObjectGenerationUtils.ReflectHorizontally(osuObject);
|
||||||
|
OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum MirrorType
|
||||||
|
{
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
Both
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
osu.Game.Rulesets.Osu/Mods/OsuModMuted.cs
Normal file
12
osu.Game.Rulesets.Osu/Mods/OsuModMuted.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// 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.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Mods
|
||||||
|
{
|
||||||
|
public class OsuModMuted : ModMuted<OsuHitObject>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,39 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Audio;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Beatmaps.Timing;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Overlays.Settings;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Rulesets.Osu.Utils;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Mods
|
namespace osu.Game.Rulesets.Osu.Mods
|
||||||
{
|
{
|
||||||
public class OsuModTarget : Mod
|
public class OsuModTarget : ModWithVisibilityAdjustment, IApplicableToDrawableRuleset<OsuHitObject>,
|
||||||
|
IApplicableToHealthProcessor, IApplicableToDifficulty, IApplicableFailOverride,
|
||||||
|
IHasSeed, IHidesApproachCircles
|
||||||
{
|
{
|
||||||
public override string Name => "Target";
|
public override string Name => "Target";
|
||||||
public override string Acronym => "TP";
|
public override string Acronym => "TP";
|
||||||
@ -15,5 +41,466 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override IconUsage? Icon => OsuIcon.ModTarget;
|
public override IconUsage? Icon => OsuIcon.ModTarget;
|
||||||
public override string Description => @"Practice keeping up with the beat of the song.";
|
public override string Description => @"Practice keeping up with the beat of the song.";
|
||||||
public override double ScoreMultiplier => 1;
|
public override double ScoreMultiplier => 1;
|
||||||
|
|
||||||
|
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles) };
|
||||||
|
|
||||||
|
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
|
||||||
|
public Bindable<int?> Seed { get; } = new Bindable<int?>
|
||||||
|
{
|
||||||
|
Default = null,
|
||||||
|
Value = null
|
||||||
|
};
|
||||||
|
|
||||||
|
#region Constants
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Jump distance for circles in the last combo
|
||||||
|
/// </summary>
|
||||||
|
private const float max_base_distance = 333f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum allowed jump distance after multipliers are applied
|
||||||
|
/// </summary>
|
||||||
|
private const float distance_cap = 380f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The extent of rotation towards playfield centre when a circle is near the edge
|
||||||
|
/// </summary>
|
||||||
|
private const float edge_rotation_multiplier = 0.75f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of recent circles to check for overlap
|
||||||
|
/// </summary>
|
||||||
|
private const int overlap_check_count = 5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Duration of the undimming animation
|
||||||
|
/// </summary>
|
||||||
|
private const double undim_duration = 96;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Acceptable difference for timing comparisons
|
||||||
|
/// </summary>
|
||||||
|
private const double timing_precision = 1;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Private Fields
|
||||||
|
|
||||||
|
private ControlPointInfo controlPointInfo;
|
||||||
|
|
||||||
|
private List<OsuHitObject> originalHitObjects;
|
||||||
|
|
||||||
|
private Random rng;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Sudden Death (IApplicableFailOverride)
|
||||||
|
|
||||||
|
public bool PerformFail() => true;
|
||||||
|
|
||||||
|
public bool RestartOnFail => false;
|
||||||
|
|
||||||
|
public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
|
||||||
|
{
|
||||||
|
// Sudden death
|
||||||
|
healthProcessor.FailConditions += (_, result)
|
||||||
|
=> result.Type.AffectsCombo()
|
||||||
|
&& !result.IsHit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Reduce AR (IApplicableToDifficulty)
|
||||||
|
|
||||||
|
public void ReadFromDifficulty(BeatmapDifficulty difficulty)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyToDifficulty(BeatmapDifficulty difficulty)
|
||||||
|
{
|
||||||
|
// Decrease AR to increase preempt time
|
||||||
|
difficulty.ApproachRate *= 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Circle Transforms (ModWithVisibilityAdjustment)
|
||||||
|
|
||||||
|
protected override void ApplyIncreasedVisibilityState(DrawableHitObject drawable, ArmedState state)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ApplyNormalVisibilityState(DrawableHitObject drawable, ArmedState state)
|
||||||
|
{
|
||||||
|
if (!(drawable is DrawableHitCircle circle)) return;
|
||||||
|
|
||||||
|
double startTime = circle.HitObject.StartTime;
|
||||||
|
double preempt = circle.HitObject.TimePreempt;
|
||||||
|
|
||||||
|
using (circle.BeginAbsoluteSequence(startTime - preempt))
|
||||||
|
{
|
||||||
|
// initial state
|
||||||
|
circle.ScaleTo(0.5f)
|
||||||
|
.FadeColour(OsuColour.Gray(0.5f));
|
||||||
|
|
||||||
|
// scale to final size
|
||||||
|
circle.ScaleTo(1f, preempt);
|
||||||
|
|
||||||
|
// Remove approach circles
|
||||||
|
circle.ApproachCircle.Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (circle.BeginAbsoluteSequence(startTime - controlPointInfo.TimingPointAt(startTime).BeatLength - undim_duration))
|
||||||
|
circle.FadeColour(Color4.White, undim_duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Beatmap Generation (IApplicableToBeatmap)
|
||||||
|
|
||||||
|
public override void ApplyToBeatmap(IBeatmap beatmap)
|
||||||
|
{
|
||||||
|
Seed.Value ??= RNG.Next();
|
||||||
|
rng = new Random(Seed.Value.Value);
|
||||||
|
|
||||||
|
var osuBeatmap = (OsuBeatmap)beatmap;
|
||||||
|
|
||||||
|
if (osuBeatmap.HitObjects.Count == 0) return;
|
||||||
|
|
||||||
|
controlPointInfo = osuBeatmap.ControlPointInfo;
|
||||||
|
originalHitObjects = osuBeatmap.HitObjects.OrderBy(x => x.StartTime).ToList();
|
||||||
|
|
||||||
|
var hitObjects = generateBeats(osuBeatmap)
|
||||||
|
.Select(beat =>
|
||||||
|
{
|
||||||
|
var newCircle = new HitCircle();
|
||||||
|
newCircle.ApplyDefaults(controlPointInfo, osuBeatmap.BeatmapInfo.BaseDifficulty);
|
||||||
|
newCircle.StartTime = beat;
|
||||||
|
return (OsuHitObject)newCircle;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
addHitSamples(hitObjects);
|
||||||
|
|
||||||
|
fixComboInfo(hitObjects);
|
||||||
|
|
||||||
|
randomizeCirclePos(hitObjects);
|
||||||
|
|
||||||
|
osuBeatmap.HitObjects = hitObjects;
|
||||||
|
|
||||||
|
base.ApplyToBeatmap(beatmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<double> generateBeats(IBeatmap beatmap)
|
||||||
|
{
|
||||||
|
var startTime = originalHitObjects.First().StartTime;
|
||||||
|
var endTime = originalHitObjects.Last().GetEndTime();
|
||||||
|
|
||||||
|
var beats = beatmap.ControlPointInfo.TimingPoints
|
||||||
|
// Ignore timing points after endTime
|
||||||
|
.Where(timingPoint => !definitelyBigger(timingPoint.Time, endTime))
|
||||||
|
// Generate the beats
|
||||||
|
.SelectMany(timingPoint => getBeatsForTimingPoint(timingPoint, endTime))
|
||||||
|
// Remove beats before startTime
|
||||||
|
.Where(beat => almostBigger(beat, startTime))
|
||||||
|
// Remove beats during breaks
|
||||||
|
.Where(beat => !isInsideBreakPeriod(beatmap.Breaks, beat))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Remove beats that are too close to the next one (e.g. due to timing point changes)
|
||||||
|
for (var i = beats.Count - 2; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var beat = beats[i];
|
||||||
|
|
||||||
|
if (!definitelyBigger(beats[i + 1] - beat, beatmap.ControlPointInfo.TimingPointAt(beat).BeatLength / 2))
|
||||||
|
beats.RemoveAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return beats;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addHitSamples(IEnumerable<OsuHitObject> hitObjects)
|
||||||
|
{
|
||||||
|
foreach (var obj in hitObjects)
|
||||||
|
{
|
||||||
|
var samples = getSamplesAtTime(originalHitObjects, obj.StartTime);
|
||||||
|
|
||||||
|
// If samples aren't available at the exact start time of the object,
|
||||||
|
// use samples (without additions) in the closest original hit object instead
|
||||||
|
obj.Samples = samples ?? getClosestHitObject(originalHitObjects, obj.StartTime).Samples.Where(s => !HitSampleInfo.AllAdditions.Contains(s.Name)).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fixComboInfo(List<OsuHitObject> hitObjects)
|
||||||
|
{
|
||||||
|
// Copy combo indices from an original object at the same time or from the closest preceding object
|
||||||
|
// (Objects lying between two combos are assumed to belong to the preceding combo)
|
||||||
|
hitObjects.ForEach(newObj =>
|
||||||
|
{
|
||||||
|
var closestOrigObj = originalHitObjects.FindLast(y => almostBigger(newObj.StartTime, y.StartTime));
|
||||||
|
|
||||||
|
// It shouldn't be possible for closestOrigObj to be null
|
||||||
|
// But if it is, obj should be in the first combo
|
||||||
|
newObj.ComboIndex = closestOrigObj?.ComboIndex ?? 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// The copied combo indices may not be continuous if the original map starts and ends a combo in between beats
|
||||||
|
// e.g. A stream with each object starting a new combo
|
||||||
|
// So combo indices need to be reprocessed to ensure continuity
|
||||||
|
// Other kinds of combo info are also added in the process
|
||||||
|
var combos = hitObjects.GroupBy(x => x.ComboIndex).ToList();
|
||||||
|
|
||||||
|
for (var i = 0; i < combos.Count; i++)
|
||||||
|
{
|
||||||
|
var group = combos[i].ToList();
|
||||||
|
group.First().NewCombo = true;
|
||||||
|
group.Last().LastInCombo = true;
|
||||||
|
|
||||||
|
for (var j = 0; j < group.Count; j++)
|
||||||
|
{
|
||||||
|
var x = group[j];
|
||||||
|
x.ComboIndex = i;
|
||||||
|
x.IndexInCurrentCombo = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void randomizeCirclePos(IReadOnlyList<OsuHitObject> hitObjects)
|
||||||
|
{
|
||||||
|
if (hitObjects.Count == 0) return;
|
||||||
|
|
||||||
|
float nextSingle(float max = 1f) => (float)(rng.NextDouble() * max);
|
||||||
|
|
||||||
|
const float two_pi = MathF.PI * 2;
|
||||||
|
|
||||||
|
var direction = two_pi * nextSingle();
|
||||||
|
var maxComboIndex = hitObjects.Last().ComboIndex;
|
||||||
|
|
||||||
|
for (var i = 0; i < hitObjects.Count; i++)
|
||||||
|
{
|
||||||
|
var obj = hitObjects[i];
|
||||||
|
var lastPos = i == 0
|
||||||
|
? Vector2.Divide(OsuPlayfield.BASE_SIZE, 2)
|
||||||
|
: hitObjects[i - 1].Position;
|
||||||
|
|
||||||
|
var distance = maxComboIndex == 0
|
||||||
|
? (float)obj.Radius
|
||||||
|
: mapRange(obj.ComboIndex, 0, maxComboIndex, (float)obj.Radius, max_base_distance);
|
||||||
|
if (obj.NewCombo) distance *= 1.5f;
|
||||||
|
if (obj.Kiai) distance *= 1.2f;
|
||||||
|
distance = Math.Min(distance_cap, distance);
|
||||||
|
|
||||||
|
// Attempt to place the circle at a place that does not overlap with previous ones
|
||||||
|
|
||||||
|
var tryCount = 0;
|
||||||
|
|
||||||
|
// for checking overlap
|
||||||
|
var precedingObjects = hitObjects.SkipLast(hitObjects.Count - i).TakeLast(overlap_check_count).ToList();
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (tryCount > 0) direction = two_pi * nextSingle();
|
||||||
|
|
||||||
|
var relativePos = new Vector2(
|
||||||
|
distance * MathF.Cos(direction),
|
||||||
|
distance * MathF.Sin(direction)
|
||||||
|
);
|
||||||
|
// Rotate the new circle away from playfield border
|
||||||
|
relativePos = OsuHitObjectGenerationUtils.RotateAwayFromEdge(lastPos, relativePos, edge_rotation_multiplier);
|
||||||
|
direction = MathF.Atan2(relativePos.Y, relativePos.X);
|
||||||
|
|
||||||
|
var newPosition = Vector2.Add(lastPos, relativePos);
|
||||||
|
|
||||||
|
obj.Position = newPosition;
|
||||||
|
|
||||||
|
clampToPlayfield(obj);
|
||||||
|
|
||||||
|
tryCount++;
|
||||||
|
if (tryCount % 10 == 0) distance *= 0.9f;
|
||||||
|
} while (distance >= obj.Radius * 2 && checkForOverlap(precedingObjects, obj));
|
||||||
|
|
||||||
|
if (obj.LastInCombo)
|
||||||
|
direction = two_pi * nextSingle();
|
||||||
|
else
|
||||||
|
direction += distance / distance_cap * (nextSingle() * two_pi - MathF.PI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Metronome (IApplicableToDrawableRuleset)
|
||||||
|
|
||||||
|
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||||
|
{
|
||||||
|
drawableRuleset.Overlays.Add(new Metronome(drawableRuleset.Beatmap.HitObjects.First().StartTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helper Subroutines
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if a given time is inside a <see cref="BreakPeriod"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The given time is also considered to be inside a break if it is earlier than the
|
||||||
|
/// start time of the first original hit object after the break.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="breaks">The breaks of the beatmap.</param>
|
||||||
|
/// <param name="time">The time to be checked.</param>=
|
||||||
|
private bool isInsideBreakPeriod(IEnumerable<BreakPeriod> breaks, double time)
|
||||||
|
{
|
||||||
|
return breaks.Any(breakPeriod =>
|
||||||
|
{
|
||||||
|
var firstObjAfterBreak = originalHitObjects.First(obj => almostBigger(obj.StartTime, breakPeriod.EndTime));
|
||||||
|
|
||||||
|
return almostBigger(time, breakPeriod.StartTime)
|
||||||
|
&& definitelyBigger(firstObjAfterBreak.StartTime, time);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<double> getBeatsForTimingPoint(TimingControlPoint timingPoint, double mapEndTime)
|
||||||
|
{
|
||||||
|
var beats = new List<double>();
|
||||||
|
int i = 0;
|
||||||
|
var currentTime = timingPoint.Time;
|
||||||
|
|
||||||
|
while (!definitelyBigger(currentTime, mapEndTime) && controlPointInfo.TimingPointAt(currentTime) == timingPoint)
|
||||||
|
{
|
||||||
|
beats.Add(Math.Floor(currentTime));
|
||||||
|
i++;
|
||||||
|
currentTime = timingPoint.Time + i * timingPoint.BeatLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
return beats;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OsuHitObject getClosestHitObject(List<OsuHitObject> hitObjects, double time)
|
||||||
|
{
|
||||||
|
var precedingIndex = hitObjects.FindLastIndex(h => h.StartTime < time);
|
||||||
|
|
||||||
|
if (precedingIndex == hitObjects.Count - 1) return hitObjects[precedingIndex];
|
||||||
|
|
||||||
|
// return the closest preceding/succeeding hit object, whoever is closer in time
|
||||||
|
return hitObjects[precedingIndex + 1].StartTime - time < time - hitObjects[precedingIndex].StartTime
|
||||||
|
? hitObjects[precedingIndex + 1]
|
||||||
|
: hitObjects[precedingIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get samples (if any) for a specific point in time.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Samples will be returned if a hit circle or a slider node exists at that point of time.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="hitObjects">The list of hit objects in a beatmap, ordered by StartTime</param>
|
||||||
|
/// <param name="time">The point in time to get samples for</param>
|
||||||
|
/// <returns>Hit samples</returns>
|
||||||
|
private IList<HitSampleInfo> getSamplesAtTime(IEnumerable<OsuHitObject> hitObjects, double time)
|
||||||
|
{
|
||||||
|
// Get a hit object that
|
||||||
|
// either has StartTime equal to the target time
|
||||||
|
// or has a repeat node at the target time
|
||||||
|
var sampleObj = hitObjects.FirstOrDefault(hitObject =>
|
||||||
|
{
|
||||||
|
if (almostEquals(time, hitObject.StartTime))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!(hitObject is IHasRepeats s))
|
||||||
|
return false;
|
||||||
|
// If time is outside the duration of the IHasRepeats,
|
||||||
|
// then this hitObject isn't the one we want
|
||||||
|
if (!almostBigger(time, hitObject.StartTime)
|
||||||
|
|| !almostBigger(s.EndTime, time))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return nodeIndexFromTime(s, time - hitObject.StartTime) != -1;
|
||||||
|
});
|
||||||
|
if (sampleObj == null) return null;
|
||||||
|
|
||||||
|
IList<HitSampleInfo> samples;
|
||||||
|
|
||||||
|
if (sampleObj is IHasRepeats slider)
|
||||||
|
samples = slider.NodeSamples[nodeIndexFromTime(slider, time - sampleObj.StartTime)];
|
||||||
|
else
|
||||||
|
samples = sampleObj.Samples;
|
||||||
|
|
||||||
|
return samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the repeat node at a point in time.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="curve">The slider.</param>
|
||||||
|
/// <param name="timeSinceStart">The time since the start time of the slider.</param>
|
||||||
|
/// <returns>Index of the node. -1 if there isn't a node at the specific time.</returns>
|
||||||
|
private int nodeIndexFromTime(IHasRepeats curve, double timeSinceStart)
|
||||||
|
{
|
||||||
|
double spanDuration = curve.Duration / curve.SpanCount();
|
||||||
|
double nodeIndex = timeSinceStart / spanDuration;
|
||||||
|
|
||||||
|
if (almostEquals(nodeIndex, Math.Round(nodeIndex)))
|
||||||
|
return (int)Math.Round(nodeIndex);
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool checkForOverlap(IEnumerable<OsuHitObject> objectsToCheck, OsuHitObject target)
|
||||||
|
{
|
||||||
|
return objectsToCheck.Any(h => Vector2.Distance(h.Position, target.Position) < target.Radius * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move the hit object into playfield, taking its radius into account.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">The hit object to be clamped.</param>
|
||||||
|
private void clampToPlayfield(OsuHitObject obj)
|
||||||
|
{
|
||||||
|
var position = obj.Position;
|
||||||
|
var radius = (float)obj.Radius;
|
||||||
|
|
||||||
|
if (position.Y < radius)
|
||||||
|
position.Y = radius;
|
||||||
|
else if (position.Y > OsuPlayfield.BASE_SIZE.Y - radius)
|
||||||
|
position.Y = OsuPlayfield.BASE_SIZE.Y - radius;
|
||||||
|
|
||||||
|
if (position.X < radius)
|
||||||
|
position.X = radius;
|
||||||
|
else if (position.X > OsuPlayfield.BASE_SIZE.X - radius)
|
||||||
|
position.X = OsuPlayfield.BASE_SIZE.X - radius;
|
||||||
|
|
||||||
|
obj.Position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Re-maps a number from one range to another.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The number to be re-mapped.</param>
|
||||||
|
/// <param name="fromLow">Beginning of the original range.</param>
|
||||||
|
/// <param name="fromHigh">End of the original range.</param>
|
||||||
|
/// <param name="toLow">Beginning of the new range.</param>
|
||||||
|
/// <param name="toHigh">End of the new range.</param>
|
||||||
|
/// <returns>The re-mapped number.</returns>
|
||||||
|
private static float mapRange(float value, float fromLow, float fromHigh, float toLow, float toHigh)
|
||||||
|
{
|
||||||
|
return (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + toLow;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool almostBigger(double value1, double value2)
|
||||||
|
{
|
||||||
|
return Precision.AlmostBigger(value1, value2, timing_precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool definitelyBigger(double value1, double value2)
|
||||||
|
{
|
||||||
|
return Precision.DefinitelyBigger(value1, value2, timing_precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool almostEquals(double value1, double value2)
|
||||||
|
{
|
||||||
|
return Precision.AlmostEquals(value1, value2, timing_precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,6 +97,14 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
set => ComboIndexBindable.Value = value;
|
set => ComboIndexBindable.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Bindable<int> ComboIndexWithOffsetsBindable { get; } = new Bindable<int>();
|
||||||
|
|
||||||
|
public int ComboIndexWithOffsets
|
||||||
|
{
|
||||||
|
get => ComboIndexWithOffsetsBindable.Value;
|
||||||
|
set => ComboIndexWithOffsetsBindable.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>();
|
public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>();
|
||||||
|
|
||||||
public bool LastInCombo
|
public bool LastInCombo
|
||||||
|
@ -166,6 +166,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
new OsuModDifficultyAdjust(),
|
new OsuModDifficultyAdjust(),
|
||||||
new OsuModClassic(),
|
new OsuModClassic(),
|
||||||
new OsuModRandom(),
|
new OsuModRandom(),
|
||||||
|
new OsuModMirror(),
|
||||||
};
|
};
|
||||||
|
|
||||||
case ModType.Automation:
|
case ModType.Automation:
|
||||||
@ -188,6 +189,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
new OsuModTraceable(),
|
new OsuModTraceable(),
|
||||||
new OsuModBarrelRoll(),
|
new OsuModBarrelRoll(),
|
||||||
new OsuModApproachDifferent(),
|
new OsuModApproachDifferent(),
|
||||||
|
new OsuModMuted(),
|
||||||
};
|
};
|
||||||
|
|
||||||
case ModType.System:
|
case ModType.System:
|
||||||
|
@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
|
|
||||||
private void onJudgementLoaded(DrawableOsuJudgement judgement)
|
private void onJudgementLoaded(DrawableOsuJudgement judgement)
|
||||||
{
|
{
|
||||||
judgementAboveHitObjectLayer.Add(judgement.GetProxyAboveHitObjectsContent());
|
judgementAboveHitObjectLayer.Add(judgement.ProxiedAboveHitObjectsContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
@ -150,6 +150,10 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
DrawableOsuJudgement explosion = poolDictionary[result.Type].Get(doj => doj.Apply(result, judgedObject));
|
DrawableOsuJudgement explosion = poolDictionary[result.Type].Get(doj => doj.Apply(result, judgedObject));
|
||||||
|
|
||||||
judgementLayer.Add(explosion);
|
judgementLayer.Add(explosion);
|
||||||
|
|
||||||
|
// the proxied content is added to judgementAboveHitObjectLayer once, on first load, and never removed from it.
|
||||||
|
// ensure that ordering is consistent with expectations (latest judgement should be front-most).
|
||||||
|
judgementAboveHitObjectLayer.ChangeChildDepth(explosion.ProxiedAboveHitObjectsContent, (float)-result.TimeAbsolute);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos);
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos);
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Overlays.Settings;
|
using osu.Game.Overlays.Settings;
|
||||||
using osu.Game.Rulesets.Osu.Configuration;
|
using osu.Game.Rulesets.Osu.Configuration;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
{
|
{
|
||||||
public class OsuSettingsSubsection : RulesetSettingsSubsection
|
public class OsuSettingsSubsection : RulesetSettingsSubsection
|
||||||
{
|
{
|
||||||
protected override string Header => "osu!";
|
protected override LocalisableString Header => "osu!";
|
||||||
|
|
||||||
public OsuSettingsSubsection(Ruleset ruleset)
|
public OsuSettingsSubsection(Ruleset ruleset)
|
||||||
: base(ruleset)
|
: base(ruleset)
|
||||||
|
@ -2,7 +2,11 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Utils
|
namespace osu.Game.Rulesets.Osu.Utils
|
||||||
@ -100,5 +104,47 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
initial.Length * MathF.Sin(finalAngleRad)
|
initial.Length * MathF.Sin(finalAngleRad)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reflects the position of the <see cref="OsuHitObject"/> in the playfield horizontally.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="osuObject">The object to reflect.</param>
|
||||||
|
public static void ReflectHorizontally(OsuHitObject osuObject)
|
||||||
|
{
|
||||||
|
osuObject.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - osuObject.X, osuObject.Position.Y);
|
||||||
|
|
||||||
|
if (!(osuObject is Slider slider))
|
||||||
|
return;
|
||||||
|
|
||||||
|
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
|
||||||
|
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
|
||||||
|
|
||||||
|
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray();
|
||||||
|
foreach (var point in controlPoints)
|
||||||
|
point.Position.Value = new Vector2(-point.Position.Value.X, point.Position.Value.Y);
|
||||||
|
|
||||||
|
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reflects the position of the <see cref="OsuHitObject"/> in the playfield vertically.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="osuObject">The object to reflect.</param>
|
||||||
|
public static void ReflectVertically(OsuHitObject osuObject)
|
||||||
|
{
|
||||||
|
osuObject.Position = new Vector2(osuObject.Position.X, OsuPlayfield.BASE_SIZE.Y - osuObject.Y);
|
||||||
|
|
||||||
|
if (!(osuObject is Slider slider))
|
||||||
|
return;
|
||||||
|
|
||||||
|
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
||||||
|
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
||||||
|
|
||||||
|
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray();
|
||||||
|
foreach (var point in controlPoints)
|
||||||
|
point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y);
|
||||||
|
|
||||||
|
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Extensions;
|
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@ -31,10 +30,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
|
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
|
||||||
{
|
{
|
||||||
mods = Score.Mods;
|
mods = Score.Mods;
|
||||||
countGreat = Score.Statistics.GetOrDefault(HitResult.Great);
|
countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||||
countOk = Score.Statistics.GetOrDefault(HitResult.Ok);
|
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||||
countMeh = Score.Statistics.GetOrDefault(HitResult.Meh);
|
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||||
countMiss = Score.Statistics.GetOrDefault(HitResult.Miss);
|
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||||
|
|
||||||
// Custom multipliers for NoFail and SpunOut.
|
// Custom multipliers for NoFail and SpunOut.
|
||||||
double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
|
double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
|
||||||
|
@ -2,10 +2,30 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Mods
|
namespace osu.Game.Rulesets.Taiko.Mods
|
||||||
{
|
{
|
||||||
public class TaikoModClassic : ModClassic
|
public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject>, IUpdatableByPlayfield
|
||||||
{
|
{
|
||||||
|
private DrawableTaikoRuleset drawableTaikoRuleset;
|
||||||
|
|
||||||
|
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
|
||||||
|
{
|
||||||
|
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(Playfield playfield)
|
||||||
|
{
|
||||||
|
// Classic taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
|
||||||
|
const float scroll_rate = 10;
|
||||||
|
|
||||||
|
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
|
||||||
|
float ratio = drawableTaikoRuleset.DrawHeight / 480;
|
||||||
|
|
||||||
|
drawableTaikoRuleset.TimeRange.Value = (playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,23 +12,11 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Mods
|
namespace osu.Game.Rulesets.Taiko.Mods
|
||||||
{
|
{
|
||||||
public class TaikoModHidden : ModHidden, IApplicableToDifficulty
|
public class TaikoModHidden : ModHidden
|
||||||
{
|
{
|
||||||
public override string Description => @"Beats fade out before you hit them!";
|
public override string Description => @"Beats fade out before you hit them!";
|
||||||
public override double ScoreMultiplier => 1.06;
|
public override double ScoreMultiplier => 1.06;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// In osu-stable, the hit position is 160, so the active playfield is essentially 160 pixels shorter
|
|
||||||
/// than the actual screen width. The normalized playfield height is 480, so on a 4:3 screen the
|
|
||||||
/// playfield ratio of the active area up to the hit position will actually be (640 - 160) / 480 = 1.
|
|
||||||
/// For custom resolutions/aspect ratios (x:y), the screen width given the normalized height becomes 480 * x / y instead,
|
|
||||||
/// and the playfield ratio becomes (480 * x / y - 160) / 480 = x / y - 1/3.
|
|
||||||
/// This constant is equal to the playfield ratio on 4:3 screens divided by the playfield ratio on 16:9 screens.
|
|
||||||
/// </summary>
|
|
||||||
private const double hd_sv_scale = (4.0 / 3.0 - 1.0 / 3.0) / (16.0 / 9.0 - 1.0 / 3.0);
|
|
||||||
|
|
||||||
private double originalSliderMultiplier;
|
|
||||||
|
|
||||||
private ControlPointInfo controlPointInfo;
|
private ControlPointInfo controlPointInfo;
|
||||||
|
|
||||||
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||||
@ -41,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
|||||||
double beatLength = controlPointInfo.TimingPointAt(position).BeatLength;
|
double beatLength = controlPointInfo.TimingPointAt(position).BeatLength;
|
||||||
double speedMultiplier = controlPointInfo.DifficultyPointAt(position).SpeedMultiplier;
|
double speedMultiplier = controlPointInfo.DifficultyPointAt(position).SpeedMultiplier;
|
||||||
|
|
||||||
return originalSliderMultiplier * speedMultiplier * TimingControlPoint.DEFAULT_BEAT_LENGTH / beatLength;
|
return speedMultiplier * TimingControlPoint.DEFAULT_BEAT_LENGTH / beatLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||||
@ -69,22 +57,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ReadFromDifficulty(BeatmapDifficulty difficulty)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ApplyToDifficulty(BeatmapDifficulty difficulty)
|
|
||||||
{
|
|
||||||
// needs to be read after all processing has been run (TaikoBeatmapConverter applies an adjustment which would otherwise be omitted).
|
|
||||||
originalSliderMultiplier = difficulty.SliderMultiplier;
|
|
||||||
|
|
||||||
// osu-stable has an added playfield cover that essentially forces a 4:3 playfield ratio, by cutting off all objects past that size.
|
|
||||||
// This is not yet implemented; instead a playfield adjustment container is present which maintains a 16:9 ratio.
|
|
||||||
// For now, increase the slider multiplier proportionally so that the notes stay on the screen for the same amount of time as on stable.
|
|
||||||
// Note that this means that the notes will scroll faster as they have a longer distance to travel on the screen in that same amount of time.
|
|
||||||
difficulty.SliderMultiplier /= hd_sv_scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void ApplyToBeatmap(IBeatmap beatmap)
|
public override void ApplyToBeatmap(IBeatmap beatmap)
|
||||||
{
|
{
|
||||||
controlPointInfo = beatmap.ControlPointInfo;
|
controlPointInfo = beatmap.ControlPointInfo;
|
||||||
|
12
osu.Game.Rulesets.Taiko/Mods/TaikoModMuted.cs
Normal file
12
osu.Game.Rulesets.Taiko/Mods/TaikoModMuted.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// 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.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Mods
|
||||||
|
{
|
||||||
|
public class TaikoModMuted : ModMuted<TaikoHitObject>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -149,7 +149,8 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
case ModType.Fun:
|
case ModType.Fun:
|
||||||
return new Mod[]
|
return new Mod[]
|
||||||
{
|
{
|
||||||
new MultiMod(new ModWindUp(), new ModWindDown())
|
new MultiMod(new ModWindUp(), new ModWindDown()),
|
||||||
|
new TaikoModMuted(),
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
@ -24,12 +25,14 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
{
|
{
|
||||||
public class DrawableTaikoRuleset : DrawableScrollingRuleset<TaikoHitObject>
|
public class DrawableTaikoRuleset : DrawableScrollingRuleset<TaikoHitObject>
|
||||||
{
|
{
|
||||||
private SkinnableDrawable scroller;
|
public new BindableDouble TimeRange => base.TimeRange;
|
||||||
|
|
||||||
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
|
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
|
||||||
|
|
||||||
protected override bool UserScrollSpeedAdjustment => false;
|
protected override bool UserScrollSpeedAdjustment => false;
|
||||||
|
|
||||||
|
private SkinnableDrawable scroller;
|
||||||
|
|
||||||
public DrawableTaikoRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
public DrawableTaikoRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||||
: base(ruleset, beatmap, mods)
|
: base(ruleset, beatmap, mods)
|
||||||
{
|
{
|
||||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default height of a <see cref="TaikoPlayfield"/> when inside a <see cref="DrawableTaikoRuleset"/>.
|
/// Default height of a <see cref="TaikoPlayfield"/> when inside a <see cref="DrawableTaikoRuleset"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const float DEFAULT_HEIGHT = 178;
|
public const float DEFAULT_HEIGHT = 212;
|
||||||
|
|
||||||
private Container<HitExplosion> hitExplosionContainer;
|
private Container<HitExplosion> hitExplosionContainer;
|
||||||
private Container<KiaiHitExplosion> kiaiExplosionContainer;
|
private Container<KiaiHitExplosion> kiaiExplosionContainer;
|
||||||
|
@ -323,12 +323,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
new OsuBeatmapProcessor(converted).PreProcess();
|
new OsuBeatmapProcessor(converted).PreProcess();
|
||||||
new OsuBeatmapProcessor(converted).PostProcess();
|
new OsuBeatmapProcessor(converted).PostProcess();
|
||||||
|
|
||||||
Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndex);
|
Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndexWithOffsets);
|
||||||
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndex);
|
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndexWithOffsets);
|
||||||
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndex);
|
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndexWithOffsets);
|
||||||
Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndex);
|
Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndexWithOffsets);
|
||||||
Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndex);
|
Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndexWithOffsets);
|
||||||
Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndex);
|
Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndexWithOffsets);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,12 +346,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
new CatchBeatmapProcessor(converted).PreProcess();
|
new CatchBeatmapProcessor(converted).PreProcess();
|
||||||
new CatchBeatmapProcessor(converted).PostProcess();
|
new CatchBeatmapProcessor(converted).PostProcess();
|
||||||
|
|
||||||
Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndex);
|
Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndexWithOffsets);
|
||||||
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndex);
|
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndexWithOffsets);
|
||||||
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndex);
|
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndexWithOffsets);
|
||||||
Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndex);
|
Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndexWithOffsets);
|
||||||
Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndex);
|
Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndexWithOffsets);
|
||||||
Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndex);
|
Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndexWithOffsets);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,6 +100,14 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
set => ComboIndexBindable.Value = value;
|
set => ComboIndexBindable.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Bindable<int> ComboIndexWithOffsetsBindable { get; } = new Bindable<int>();
|
||||||
|
|
||||||
|
public int ComboIndexWithOffsets
|
||||||
|
{
|
||||||
|
get => ComboIndexWithOffsetsBindable.Value;
|
||||||
|
set => ComboIndexWithOffsetsBindable.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>();
|
public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>();
|
||||||
|
|
||||||
public bool LastInCombo
|
public bool LastInCombo
|
||||||
@ -129,14 +137,8 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
{
|
{
|
||||||
switch (lookup)
|
switch (lookup)
|
||||||
{
|
{
|
||||||
case GlobalSkinColours global:
|
case SkinComboColourLookup comboColour:
|
||||||
switch (global)
|
return SkinUtils.As<TValue>(new Bindable<Color4>(ComboColours[comboColour.ColourIndex % ComboColours.Count]));
|
||||||
{
|
|
||||||
case GlobalSkinColours.ComboColours:
|
|
||||||
return SkinUtils.As<TValue>(new Bindable<IReadOnlyList<Color4>>(ComboColours));
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
|
@ -248,13 +248,13 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestCreateCopyIsDeepClone()
|
public void TestDeepClone()
|
||||||
{
|
{
|
||||||
var cpi = new ControlPointInfo();
|
var cpi = new ControlPointInfo();
|
||||||
|
|
||||||
cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
|
cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
|
||||||
|
|
||||||
var cpiCopy = cpi.CreateCopy();
|
var cpiCopy = cpi.DeepClone();
|
||||||
|
|
||||||
cpiCopy.Add(2000, new TimingControlPoint { BeatLength = 500 });
|
cpiCopy.Add(2000, new TimingControlPoint { BeatLength = 500 });
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Globalization;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
|
|
||||||
@ -20,7 +19,7 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
[TestCase(1, "100.00%")]
|
[TestCase(1, "100.00%")]
|
||||||
public void TestAccuracyFormatting(double input, string expectedOutput)
|
public void TestAccuracyFormatting(double input, string expectedOutput)
|
||||||
{
|
{
|
||||||
Assert.AreEqual(expectedOutput, input.FormatAccuracy(CultureInfo.InvariantCulture));
|
Assert.AreEqual(expectedOutput, input.FormatAccuracy().ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
33
osu.Game.Tests/NonVisual/ScoreInfoTest.cs
Normal file
33
osu.Game.Tests/NonVisual/ScoreInfoTest.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.NonVisual
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class ScoreInfoTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestDeepClone()
|
||||||
|
{
|
||||||
|
var score = new ScoreInfo();
|
||||||
|
|
||||||
|
score.Statistics.Add(HitResult.Good, 10);
|
||||||
|
score.Rank = ScoreRank.B;
|
||||||
|
|
||||||
|
var scoreCopy = score.DeepClone();
|
||||||
|
|
||||||
|
score.Statistics[HitResult.Good]++;
|
||||||
|
score.Rank = ScoreRank.X;
|
||||||
|
|
||||||
|
Assert.That(scoreCopy.Statistics[HitResult.Good], Is.EqualTo(10));
|
||||||
|
Assert.That(score.Statistics[HitResult.Good], Is.EqualTo(11));
|
||||||
|
|
||||||
|
Assert.That(scoreCopy.Rank, Is.EqualTo(ScoreRank.B));
|
||||||
|
Assert.That(score.Rank, Is.EqualTo(ScoreRank.X));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
osu.Game.Tests/NonVisual/TimeDisplayExtensionTest.cs
Normal file
41
osu.Game.Tests/NonVisual/TimeDisplayExtensionTest.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Game.Extensions;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.NonVisual
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class TimeDisplayExtensionTest
|
||||||
|
{
|
||||||
|
private static readonly object[][] editor_formatted_duration_tests =
|
||||||
|
{
|
||||||
|
new object[] { new TimeSpan(0, 0, 0, 0, 50), "00:00:050" },
|
||||||
|
new object[] { new TimeSpan(0, 0, 0, 10, 50), "00:10:050" },
|
||||||
|
new object[] { new TimeSpan(0, 0, 5, 10), "05:10:000" },
|
||||||
|
new object[] { new TimeSpan(0, 1, 5, 10), "65:10:000" },
|
||||||
|
};
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(editor_formatted_duration_tests))]
|
||||||
|
public void TestEditorFormat(TimeSpan input, string expectedOutput)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(expectedOutput, input.ToEditorFormattedString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly object[][] formatted_duration_tests =
|
||||||
|
{
|
||||||
|
new object[] { new TimeSpan(0, 0, 10), "00:10" },
|
||||||
|
new object[] { new TimeSpan(0, 5, 10), "05:10" },
|
||||||
|
new object[] { new TimeSpan(1, 5, 10), "01:05:10" },
|
||||||
|
new object[] { new TimeSpan(1, 1, 5, 10), "01:01:05:10" },
|
||||||
|
};
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(formatted_duration_tests))]
|
||||||
|
public void TestFormattedDuration(TimeSpan input, string expectedOutput)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(expectedOutput, input.ToFormattedDuration().ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -168,8 +168,8 @@ namespace osu.Game.Tests.Online
|
|||||||
|
|
||||||
public override async Task<BeatmapSetInfo> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
|
public override async Task<BeatmapSetInfo> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await AllowImport.Task;
|
await AllowImport.Task.ConfigureAwait(false);
|
||||||
return await (CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken));
|
return await (CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +35,8 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
CanRotate = true,
|
CanRotate = true,
|
||||||
CanScaleX = true,
|
CanScaleX = true,
|
||||||
CanScaleY = true,
|
CanScaleY = true,
|
||||||
|
CanFlipX = true,
|
||||||
|
CanFlipY = true,
|
||||||
|
|
||||||
OnRotation = handleRotation,
|
OnRotation = handleRotation,
|
||||||
OnScale = handleScale
|
OnScale = handleScale
|
||||||
|
@ -13,8 +13,8 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
{
|
{
|
||||||
public TestSceneEditorComposeRadioButtons()
|
public TestSceneEditorComposeRadioButtons()
|
||||||
{
|
{
|
||||||
RadioButtonCollection collection;
|
EditorRadioButtonCollection collection;
|
||||||
Add(collection = new RadioButtonCollection
|
Add(collection = new EditorRadioButtonCollection
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user