diff --git a/.github/ISSUE_TEMPLATE/00-mobile-issues.md b/.github/ISSUE_TEMPLATE/00-mobile-issues.md
deleted file mode 100644
index f171e80b8b..0000000000
--- a/.github/ISSUE_TEMPLATE/00-mobile-issues.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: Mobile Report
-about: ⚠ Due to current development priorities we are not accepting mobile reports at this time (unless you're willing to fix them yourself!)
----
-
-⚠ **PLEASE READ** ⚠: Due to prioritising finishing the client for desktop first we are not accepting reports related to mobile platforms for the time being, unless you're willing to fix them.
-If you'd like to report a problem or suggest a feature and then work on it, feel free to open an issue and highlight that you'd like to address it yourself in the issue body; mobile pull requests are also welcome.
-Otherwise, please check back in the future when the focus of development shifts towards mobile!
diff --git a/.github/ISSUE_TEMPLATE/01-bug-issues.md b/.github/ISSUE_TEMPLATE/01-bug-issues.md
index c8c41e5a78..0b80ce44dd 100644
--- a/.github/ISSUE_TEMPLATE/01-bug-issues.md
+++ b/.github/ISSUE_TEMPLATE/01-bug-issues.md
@@ -8,4 +8,9 @@ about: Issues regarding encountered bugs.
**osu!lazer version:**
-**Logs:**
+**Logs:**
+
diff --git a/.github/ISSUE_TEMPLATE/02-crash-issues.md b/.github/ISSUE_TEMPLATE/02-crash-issues.md
index 8ad27e9e31..ada8de73c0 100644
--- a/.github/ISSUE_TEMPLATE/02-crash-issues.md
+++ b/.github/ISSUE_TEMPLATE/02-crash-issues.md
@@ -8,6 +8,11 @@ about: Issues regarding crashes or permanent freezes.
**osu!lazer version:**
-**Logs:**
+**Logs:**
+
**Computer Specifications:**
diff --git a/.idea/.idea.osu.Desktop/.idea/modules.xml b/.idea/.idea.osu.Desktop/.idea/modules.xml
index fe63f5faf3..366f172c30 100644
--- a/.idea/.idea.osu.Desktop/.idea/modules.xml
+++ b/.idea/.idea.osu.Desktop/.idea/modules.xml
@@ -2,6 +2,7 @@
+
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 6480612b2e..4e8af405a2 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -11,11 +11,6 @@
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Debug)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
- }
- },
"console": "internalConsole"
},
{
@@ -28,11 +23,6 @@
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Release)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
- }
- },
"console": "internalConsole"
},
{
@@ -45,11 +35,6 @@
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build tests (Debug)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
- }
- },
"console": "internalConsole"
},
{
@@ -62,11 +47,6 @@
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build tests (Release)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
- }
- },
"console": "internalConsole"
},
{
@@ -80,11 +60,6 @@
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Debug)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
- }
- },
"console": "internalConsole"
},
{
@@ -98,11 +73,6 @@
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Release)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
- }
- },
"console": "internalConsole"
},
{
@@ -116,11 +86,6 @@
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build tournament tests (Debug)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
- }
- },
"console": "internalConsole"
},
{
@@ -134,11 +99,6 @@
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build tournament tests (Release)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
- }
- },
"console": "internalConsole"
},
{
@@ -169,4 +129,4 @@
"externalConsole": false
}
]
-}
\ No newline at end of file
+}
diff --git a/README.md b/README.md
index 67027bb9f3..59d72247f5 100644
--- a/README.md
+++ b/README.md
@@ -13,35 +13,36 @@ Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the
## Status
-This project is still heavily under development, but is in a state where users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve over the coming months and hopefully bring some new unique features to the table.
+This project is under heavy development, but is in a stable state. Users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve to the point of eventually replacing the existing stable client as an update.
-We are accepting bug reports (please report with as much detail as possible). Feature requests are welcome as long as you read and understand the contribution guidelines listed below.
+We are accepting bug reports (please report with as much detail as possible and follow the existing issue templates). Feature requests are also welcome, but understand that our focus is on completing the game to feature parity before adding new features. A few resources are available as starting points to getting involved and understanding the project:
-Detailed changelogs are published on the [official osu! site](https://osu.ppy.sh/home/changelog).
-
-## Requirements
-
-- A desktop platform with the [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download) or higher installed.
-- When running on Linux, please have a system-wide FFmpeg installation available to support video decoding.
-- When running on Windows 7 or 8.1, **[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/dependencies?tabs=netcore31&pivots=os-windows)** may be required to correctly run .NET Core applications if your operating system is not up-to-date with the latest service packs.
-- When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/).
-- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
+- 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).
+- Read peppy's [latest blog post](https://blog.ppy.sh/a-definitive-lazer-faq/) exploring where lazer is currently and the roadmap going forward.
## Running osu!
-### Releases
-
-If you are not interested in developing the game, you can still consume our [binary releases](https://github.com/ppy/osu/releases).
+If you are looking to install or test osu! without setting up a development environment, you can consume our [binary releases](https://github.com/ppy/osu/releases). Handy links below will download the latest version for your operating system of choice:
**Latest build:**
-| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [iOS(iOS 10+)](https://osu.ppy.sh/home/testflight) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
-| ------------- | ------------- | ------------- | ------------- |
+| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS(iOS 10+)](https://osu.ppy.sh/home/testflight) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
+| ------------- | ------------- | ------------- | ------------- | ------------- |
-- **Linux** users are recommended to self-compile until we have official deployment in place.
+- When running on Windows 7 or 8.1, **[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/dependencies?tabs=netcore31&pivots=os-windows)** may be required to correctly run .NET Core applications if your operating system is not up-to-date with the latest service packs.
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
+## Developing or debugging
+
+Please make sure you have the following prerequisites:
+
+- A desktop platform with the [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download) or higher installed.
+- When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/).
+- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
+- When running on Linux, please have a system-wide FFmpeg installation available to support video decoding.
+
### Downloading the source code
Clone the repository:
@@ -92,7 +93,7 @@ JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it
We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention of having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time to ensure no effort is wasted.
-If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues) (especially those with the ["good first issue"](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) label).
+If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues) (especially those with the ["good first issue"](https://github.com/ppy/osu/issues?q=is%3Aopen+label%3Agood-first-issue+sort%3Aupdated-desc) label).
Before starting, please make sure you are familiar with the [development and testing](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) procedure we have set up. New component development, and where possible, bug fixing and debugging existing components **should always be done under VisualTests**.
diff --git a/build/InspectCode.cake b/build/InspectCode.cake
index 06c56dce87..2e7a1d1b28 100644
--- a/build/InspectCode.cake
+++ b/build/InspectCode.cake
@@ -1,5 +1,5 @@
#addin "nuget:?package=CodeFileSanity&version=0.0.33"
-#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2019.3.0"
+#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2019.3.2"
#tool "nuget:?package=NVika.MSBuild&version=1.0.1"
var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First();
diff --git a/osu.Android.props b/osu.Android.props
index 5497a82a7a..942970c890 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -25,7 +25,6 @@
portable
False
DEBUG;TRACE
- false
false
true
false
@@ -34,7 +33,6 @@
false
None
True
- true
false
False
true
@@ -53,7 +51,7 @@
-
-
+
+
diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs
index a91c010809..84f215f930 100644
--- a/osu.Android/OsuGameAndroid.cs
+++ b/osu.Android/OsuGameAndroid.cs
@@ -30,11 +30,6 @@ namespace osu.Android
}
}
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- Add(new SimpleUpdateManager());
- }
+ protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
}
}
\ No newline at end of file
diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj
index ac3905a372..0598a50530 100644
--- a/osu.Android/osu.Android.csproj
+++ b/osu.Android/osu.Android.csproj
@@ -13,6 +13,7 @@
osu.Android
Properties\AndroidManifest.xml
armeabi-v7a;x86;arm64-v8a
+ false
cjk;mideast;other;rare;west
@@ -52,4 +53,4 @@
-
\ No newline at end of file
+
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index f70cc24159..f05ee48914 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -47,20 +47,25 @@ namespace osu.Desktop
return null;
}
+ protected override UpdateManager CreateUpdateManager()
+ {
+ switch (RuntimeInfo.OS)
+ {
+ case RuntimeInfo.Platform.Windows:
+ return new SquirrelUpdateManager();
+
+ default:
+ return new SimpleUpdateManager();
+ }
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
if (!noVersionOverlay)
- {
LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add);
- if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
- Add(new SquirrelUpdateManager());
- else
- Add(new SimpleUpdateManager());
- }
-
LoadComponentAsync(new DiscordRichPresence(), Add);
}
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index b9294088f4..c34e1e1221 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -29,7 +29,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
new file mode 100644
index 0000000000..47e91e50d4
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
@@ -0,0 +1,54 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Rulesets.Catch.Mods;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Tests.Mods
+{
+ public class TestSceneCatchModPerfect : ModPerfectTestScene
+ {
+ public TestSceneCatchModPerfect()
+ : base(new CatchRuleset(), new CatchModPerfect())
+ {
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestBananaShower(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new BananaShower { StartTime = 1000, EndTime = 3000 }, false), shouldMiss);
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestFruit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Fruit { StartTime = 1000 }), shouldMiss);
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestJuiceStream(bool shouldMiss)
+ {
+ var stream = new JuiceStream
+ {
+ StartTime = 1000,
+ Path = new SliderPath(PathType.Linear, new[]
+ {
+ Vector2.Zero,
+ new Vector2(100, 0),
+ })
+ };
+
+ CreateHitObjectTest(new HitObjectTestData(stream), shouldMiss);
+ }
+
+ // We only care about testing misses, hits are tested via JuiceStream
+ [TestCase(true)]
+ public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Droplet { StartTime = 1000 }), shouldMiss);
+
+ // We only care about testing misses, hits are tested via JuiceStream
+ [TestCase(true)]
+ public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss);
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-apple-overlay@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-apple-overlay@2x.png
new file mode 100644
index 0000000000..4233d9bb6e
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-apple-overlay@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-apple@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-apple@2x.png
new file mode 100644
index 0000000000..043bfbfae1
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-apple@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-bananas-overlay@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-bananas-overlay@2x.png
new file mode 100644
index 0000000000..4233d9bb6e
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-bananas-overlay@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-bananas@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-bananas@2x.png
new file mode 100644
index 0000000000..043bfbfae1
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-bananas@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-catcher-idle@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-catcher-idle@2x.png
new file mode 100644
index 0000000000..76949ccfcc
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-catcher-idle@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-drop@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-drop@2x.png
new file mode 100644
index 0000000000..ec2fdbdbdb
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-drop@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-grapes-overlay@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-grapes-overlay@2x.png
new file mode 100644
index 0000000000..4233d9bb6e
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-grapes-overlay@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-grapes@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-grapes@2x.png
new file mode 100644
index 0000000000..043bfbfae1
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-grapes@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-orange-overlay@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-orange-overlay@2x.png
new file mode 100644
index 0000000000..4233d9bb6e
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-orange-overlay@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-orange@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-orange@2x.png
new file mode 100644
index 0000000000..043bfbfae1
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-orange@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-pear-overlay@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-pear-overlay@2x.png
new file mode 100644
index 0000000000..4233d9bb6e
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-pear-overlay@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-pear@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-pear@2x.png
new file mode 100644
index 0000000000..043bfbfae1
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/metrics-skin/fruit-pear@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-apple-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-apple-overlay.png
new file mode 100644
index 0000000000..8d9608cfc9
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-apple-overlay.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-apple.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-apple.png
new file mode 100644
index 0000000000..be1bda0383
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-apple.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas-overlay.png
new file mode 100644
index 0000000000..3a6612378e
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas-overlay.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas.png
new file mode 100644
index 0000000000..afb8698b2d
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-drop.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-drop.png
new file mode 100644
index 0000000000..12c74f46e2
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-drop.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-grapes-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-grapes-overlay.png
new file mode 100644
index 0000000000..bb37ba1920
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-grapes-overlay.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-grapes.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-grapes.png
new file mode 100644
index 0000000000..10699b1f31
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-grapes.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-orange-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-orange-overlay.png
new file mode 100644
index 0000000000..e86aa6e7e3
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-orange-overlay.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-orange.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-orange.png
new file mode 100644
index 0000000000..42cc80399f
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-orange.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-pear-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-pear-overlay.png
new file mode 100644
index 0000000000..5c479da954
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-pear-overlay.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-pear.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-pear.png
new file mode 100644
index 0000000000..9fe400bdd1
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-pear.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-plate.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-plate.png
new file mode 100644
index 0000000000..1da1fdde85
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-plate.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-ryuuta.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-ryuuta.png
new file mode 100644
index 0000000000..f732092379
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-ryuuta.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit0.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit0.png
new file mode 100644
index 0000000000..2d312ceefd
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit0.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit100.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit100.png
new file mode 100644
index 0000000000..7884dc072d
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit100.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit300.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit300.png
new file mode 100644
index 0000000000..3e4ec2e047
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit300.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit50.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit50.png
new file mode 100644
index 0000000000..f02ad11a17
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/hit50.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-apple-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-apple-overlay.png
new file mode 100755
index 0000000000..fe567d158d
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-apple-overlay.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-apple.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-apple.png
new file mode 100755
index 0000000000..17f3be9c26
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-apple.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-bananas-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-bananas-overlay.png
new file mode 100755
index 0000000000..2c94ea78bf
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-bananas-overlay.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-bananas.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-bananas.png
new file mode 100755
index 0000000000..2c94ea78bf
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-bananas.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png
new file mode 100755
index 0000000000..1eea5c2083
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-0@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-0@2x.png
new file mode 100644
index 0000000000..786e5cc25a
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-0@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-1@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-1@2x.png
new file mode 100644
index 0000000000..e93530fb16
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-1@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-2@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-2@2x.png
new file mode 100644
index 0000000000..6f51257742
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-2@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-3@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-3@2x.png
new file mode 100644
index 0000000000..953a04d4e4
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-3@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-4@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-4@2x.png
new file mode 100644
index 0000000000..66a3cf9e0b
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-4@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-5@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-5@2x.png
new file mode 100644
index 0000000000..ec4487f8fb
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-5@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png
new file mode 100755
index 0000000000..31be03b014
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-drop-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-drop-overlay.png
new file mode 100755
index 0000000000..56bf4a92fb
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-drop-overlay.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-drop.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-drop.png
new file mode 100755
index 0000000000..f259684055
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-drop.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-grapes-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-grapes-overlay.png
new file mode 100755
index 0000000000..17f3be9c26
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-grapes-overlay.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-grapes.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-grapes.png
new file mode 100755
index 0000000000..3dc60464cf
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-grapes.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-orange-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-orange-overlay.png
new file mode 100755
index 0000000000..3dc60464cf
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-orange-overlay.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-orange.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-orange.png
new file mode 100755
index 0000000000..3dc60464cf
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-orange.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-pear-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-pear-overlay.png
new file mode 100755
index 0000000000..3dc60464cf
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-pear-overlay.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-pear.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-pear.png
new file mode 100755
index 0000000000..3dc60464cf
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-pear.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
index 74a9c05bf9..ed7bfb9a44 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
@@ -7,7 +7,6 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
-using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
using osuTK;
@@ -51,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests
return beatmap;
}
- protected override Player CreatePlayer(Ruleset ruleset)
+ protected override TestPlayer CreatePlayer(Ruleset ruleset)
{
SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray();
return base.CreatePlayer(ruleset);
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
index 0ad72412fc..024c4cefb0 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
@@ -6,7 +6,7 @@ using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.Objects.Drawable;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Tests.Visual;
@@ -18,7 +18,9 @@ namespace osu.Game.Rulesets.Catch.Tests
public override IReadOnlyList RequiredTypes => new[]
{
typeof(BananaShower),
+ typeof(Banana),
typeof(DrawableBananaShower),
+ typeof(DrawableBanana),
typeof(CatchRuleset),
typeof(DrawableCatchRuleset),
@@ -29,6 +31,12 @@ namespace osu.Game.Rulesets.Catch.Tests
{
}
+ [Test]
+ public void TestBananaShower()
+ {
+ AddUntilStep("player is done", () => !Player.ValidForResume);
+ }
+
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap
@@ -40,7 +48,7 @@ namespace osu.Game.Rulesets.Catch.Tests
}
};
- beatmap.HitObjects.Add(new BananaShower { StartTime = 200, Duration = 5000, NewCombo = true });
+ beatmap.HitObjects.Add(new BananaShower { StartTime = 200, Duration = 3000, NewCombo = true });
return beatmap;
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index 9b529a2e4c..fe0d512166 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -3,104 +3,32 @@
using NUnit.Framework;
using osu.Framework.Allocation;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Tests.Visual;
using System;
using System.Collections.Generic;
-using osu.Game.Skinning;
-using osu.Framework.Graphics.Shapes;
-using osuTK.Graphics;
-using osu.Framework.Audio.Sample;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics.Textures;
-using osu.Game.Audio;
-using osu.Game.Graphics.Sprites;
+using osu.Framework.Graphics;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneCatcher : OsuTestScene
+ public class TestSceneCatcher : SkinnableTestScene
{
public override IReadOnlyList RequiredTypes => new[]
{
- typeof(CatcherSprite),
+ typeof(CatcherArea),
+ typeof(CatcherSprite)
};
- private readonly Container container;
-
- public TestSceneCatcher()
- {
- Child = container = new Container
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- };
- }
-
[BackgroundDependencyLoader]
private void load()
{
- AddStep("show default catcher implementation", () => { container.Child = new CatcherSprite(); });
-
- AddStep("show custom catcher implementation", () =>
+ SetContents(() => new Catcher
{
- container.Child = new CatchCustomSkinSourceContainer
- {
- Child = new CatcherSprite()
- };
+ RelativePositionAxes = Axes.None,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
});
}
-
- private class CatcherCustomSkin : Container
- {
- public CatcherCustomSkin()
- {
- RelativeSizeAxes = Axes.Both;
-
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.Blue
- },
- new OsuSpriteText
- {
- Text = "custom"
- }
- };
- }
- }
-
- [Cached(typeof(ISkinSource))]
- private class CatchCustomSkinSourceContainer : Container, ISkinSource
- {
- public event Action SourceChanged
- {
- add { }
- remove { }
- }
-
- public Drawable GetDrawableComponent(ISkinComponent component)
- {
- switch (component.LookupName)
- {
- case "Gameplay/catch/fruit-catcher-idle":
- return new CatcherCustomSkin();
- }
-
- return null;
- }
-
- public SampleChannel GetSample(ISampleInfo sampleInfo) =>
- throw new NotImplementedException();
-
- public Texture GetTexture(string componentName) =>
- throw new NotImplementedException();
-
- public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException();
- }
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index 3ae6886c31..cf68c5424d 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -1,45 +1,87 @@
// Copyright (c) ppy Pty Ltd . 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.Allocation;
+using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.Beatmaps;
+using osu.Game.Rulesets.Catch.Judgements;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneCatcherArea : OsuTestScene
+ public class TestSceneCatcherArea : SkinnableTestScene
{
private RulesetInfo catchRuleset;
- private TestCatcherArea catcherArea;
-
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(CatcherArea),
- };
public TestSceneCatcherArea()
{
AddSliderStep("CircleSize", 0, 8, 5, createCatcher);
- AddToggleStep("Hyperdash", t => catcherArea.ToggleHyperDash(t));
+ AddToggleStep("Hyperdash", t =>
+ CreatedDrawables.OfType().Select(i => i.Child)
+ .OfType().ForEach(c => c.ToggleHyperDash(t)));
+
+ AddRepeatStep("catch fruit", () => catchFruit(new TestFruit(false)
+ {
+ X = this.ChildrenOfType().First().MovableCatcher.X
+ }), 20);
+ AddRepeatStep("catch fruit last in combo", () => catchFruit(new TestFruit(false)
+ {
+ X = this.ChildrenOfType().First().MovableCatcher.X,
+ LastInCombo = true,
+ }), 20);
+ AddRepeatStep("catch kiai fruit", () => catchFruit(new TestFruit(true)
+ {
+ X = this.ChildrenOfType().First().MovableCatcher.X,
+ }), 20);
+ AddRepeatStep("miss fruit", () => catchFruit(new Fruit
+ {
+ X = this.ChildrenOfType().First().MovableCatcher.X + 100,
+ LastInCombo = true,
+ }, true), 20);
+ }
+
+ private void catchFruit(Fruit fruit, bool miss = false)
+ {
+ this.ChildrenOfType().ForEach(area =>
+ {
+ DrawableFruit drawable = new DrawableFruit(fruit);
+ area.Add(drawable);
+
+ Schedule(() =>
+ {
+ area.AttemptCatch(fruit);
+ area.OnResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great });
+
+ drawable.Expire();
+ });
+ });
}
private void createCatcher(float size)
{
- Child = new CatchInputManager(catchRuleset)
+ SetContents(() => new CatchInputManager(catchRuleset)
{
RelativeSizeAxes = Axes.Both,
- Child = catcherArea = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size })
+ Child = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size })
{
Anchor = Anchor.CentreLeft,
- Origin = Anchor.TopLeft
+ Origin = Anchor.TopLeft,
+ CreateDrawableRepresentation = ((DrawableRuleset)catchRuleset.CreateInstance().CreateDrawableRulesetWith(new CatchBeatmap())).CreateDrawableRepresentation
},
- };
+ });
}
[BackgroundDependencyLoader]
@@ -48,6 +90,17 @@ namespace osu.Game.Rulesets.Catch.Tests
catchRuleset = rulesets.GetRuleset(2);
}
+ public class TestFruit : Fruit
+ {
+ public TestFruit(bool kiai)
+ {
+ var kiaiCpi = new ControlPointInfo();
+ kiaiCpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
+
+ ApplyDefaultsToSelf(kiaiCpi, new BeatmapDifficulty());
+ }
+ }
+
private class TestCatcherArea : CatcherArea
{
public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
@@ -55,6 +108,8 @@ namespace osu.Game.Rulesets.Catch.Tests
{
}
+ public new Catcher MovableCatcher => base.MovableCatcher;
+
public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperDashState(status ? 2 : 1);
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
index 1eb913e900..df5494aab0 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
@@ -4,13 +4,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Allocation;
+using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.Objects.Drawable;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public override IReadOnlyList RequiredTypes => new[]
{
- typeof(CatcherArea.Catcher),
+ typeof(Catcher),
typeof(DrawableCatchRuleset),
typeof(DrawableFruit),
typeof(DrawableJuiceStream),
@@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Catch.Tests
private DrawableCatchRuleset drawableRuleset;
private double playfieldTime => drawableRuleset.Playfield.Time.Current;
- [BackgroundDependencyLoader]
- private void load()
+ [SetUp]
+ public void Setup() => Schedule(() =>
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint());
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Catch.Tests
ControlPointInfo = controlPointInfo
});
- Add(new Container
+ Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -66,16 +66,49 @@ namespace osu.Game.Rulesets.Catch.Tests
{
drawableRuleset = new DrawableCatchRuleset(new CatchRuleset(), beatmap.GetPlayableBeatmap(new CatchRuleset().RulesetInfo))
}
- });
+ };
+ });
+
+ [Test]
+ public void TestFruits()
+ {
+ AddStep("hit fruits", () => spawnFruits(true));
+ AddUntilStep("wait for completion", () => playfieldIsEmpty);
+ AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle);
AddStep("miss fruits", () => spawnFruits());
- AddStep("hit fruits", () => spawnFruits(true));
- AddStep("miss juicestream", () => spawnJuiceStream());
- AddStep("hit juicestream", () => spawnJuiceStream(true));
- AddStep("miss bananas", () => spawnBananas());
- AddStep("hit bananas", () => spawnBananas(true));
+ AddUntilStep("wait for completion", () => playfieldIsEmpty);
+ AddAssert("catcher state is failed", () => catcherState == CatcherAnimationState.Fail);
}
+ [Test]
+ public void TestJuicestream()
+ {
+ AddStep("hit juicestream", () => spawnJuiceStream(true));
+ AddUntilStep("wait for completion", () => playfieldIsEmpty);
+ AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle);
+
+ AddStep("miss juicestream", () => spawnJuiceStream());
+ AddUntilStep("wait for completion", () => playfieldIsEmpty);
+ AddAssert("catcher state is failed", () => catcherState == CatcherAnimationState.Fail);
+ }
+
+ [Test]
+ public void TestBananas()
+ {
+ AddStep("hit bananas", () => spawnBananas(true));
+ AddUntilStep("wait for completion", () => playfieldIsEmpty);
+ AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle);
+
+ AddStep("miss bananas", () => spawnBananas());
+ AddUntilStep("wait for completion", () => playfieldIsEmpty);
+ AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle);
+ }
+
+ private bool playfieldIsEmpty => !((CatchPlayfield)drawableRuleset.Playfield).AllHitObjects.Any(h => h.IsAlive);
+
+ private CatcherAnimationState catcherState => ((CatchPlayfield)drawableRuleset.Playfield).CatcherArea.MovableCatcher.CurrentState;
+
private void spawnFruits(bool hit = false)
{
for (int i = 1; i <= 4; i++)
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
index 44517382f7..82d5aa936f 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
@@ -5,78 +5,114 @@ using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.Objects.Drawable;
-using osu.Game.Rulesets.Catch.Objects.Drawable.Pieces;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneFruitObjects : OsuTestScene
+ public class TestSceneFruitObjects : SkinnableTestScene
{
public override IReadOnlyList RequiredTypes => new[]
{
typeof(CatchHitObject),
typeof(Fruit),
+ typeof(FruitPiece),
typeof(Droplet),
+ typeof(Banana),
+ typeof(BananaShower),
typeof(DrawableCatchHitObject),
typeof(DrawableFruit),
typeof(DrawableDroplet),
- typeof(BananaShower),
+ typeof(DrawableBanana),
+ typeof(DrawableBananaShower),
typeof(Pulp),
};
- public TestSceneFruitObjects()
+ protected override void LoadComplete()
{
- Add(new GridContainer
- {
- RelativeSizeAxes = Axes.Both,
- Content = new[]
- {
- new Drawable[]
- {
- createDrawable(0),
- createDrawable(1),
- createDrawable(2),
- },
- new Drawable[]
- {
- createDrawable(3),
- createDrawable(4),
- createDrawable(5),
- },
- }
- });
+ base.LoadComplete();
+
+ foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
+ AddStep($"show {rep}", () => SetContents(() => createDrawable(rep)));
+
+ AddStep("show droplet", () => SetContents(createDrawableDroplet));
+
+ AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet));
+
+ foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
+ AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawable(rep, true)));
}
- private DrawableFruit createDrawable(int index)
+ private Drawable createDrawableTinyDroplet()
{
- Fruit fruit = index == 5
- ? new Banana
- {
- StartTime = 1000000000000,
- IndexInBeatmap = index,
- Scale = 1.5f,
- }
- : new Fruit
- {
- StartTime = 1000000000000,
- IndexInBeatmap = index,
- Scale = 1.5f,
- };
+ var droplet = new TinyDroplet
+ {
+ StartTime = Clock.CurrentTime,
+ Scale = 1.5f,
+ };
- return new DrawableFruit(fruit)
+ return new DrawableTinyDroplet(droplet)
{
Anchor = Anchor.Centre,
- RelativePositionAxes = Axes.Both,
+ RelativePositionAxes = Axes.None,
Position = Vector2.Zero,
Alpha = 1,
LifetimeStart = double.NegativeInfinity,
LifetimeEnd = double.PositiveInfinity,
};
}
+
+ private Drawable createDrawableDroplet()
+ {
+ var droplet = new Droplet
+ {
+ StartTime = Clock.CurrentTime,
+ Scale = 1.5f,
+ };
+
+ return new DrawableDroplet(droplet)
+ {
+ Anchor = Anchor.Centre,
+ RelativePositionAxes = Axes.None,
+ Position = Vector2.Zero,
+ Alpha = 1,
+ LifetimeStart = double.NegativeInfinity,
+ LifetimeEnd = double.PositiveInfinity,
+ };
+ }
+
+ private Drawable createDrawable(FruitVisualRepresentation rep, bool hyperdash = false)
+ {
+ Fruit fruit = new TestCatchFruit(rep)
+ {
+ Scale = 1.5f,
+ HyperDashTarget = hyperdash ? new Banana() : null
+ };
+
+ return new DrawableFruit(fruit)
+ {
+ Anchor = Anchor.Centre,
+ RelativePositionAxes = Axes.None,
+ Position = Vector2.Zero,
+ Alpha = 1,
+ LifetimeStart = double.NegativeInfinity,
+ LifetimeEnd = double.PositiveInfinity,
+ };
+ }
+
+ public class TestCatchFruit : Fruit
+ {
+ public TestCatchFruit(FruitVisualRepresentation rep)
+ {
+ VisualRepresentation = rep;
+ StartTime = 1000000000000;
+ }
+
+ public override FruitVisualRepresentation VisualRepresentation { get; }
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
index da36673930..49ff9df4d7 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
@@ -1,16 +1,28 @@
// Copyright (c) ppy Pty Ltd . 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.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Visual;
+using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
public class TestSceneHyperDash : PlayerTestScene
{
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(CatcherArea),
+ };
+
public TestSceneHyperDash()
: base(new CatchRuleset())
{
@@ -22,8 +34,19 @@ namespace osu.Game.Rulesets.Catch.Tests
public void TestHyperDash()
{
AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash);
+ AddUntilStep("wait for right movement", () => getCatcher().Scale.X > 0); // don't check hyperdashing as it happens too fast.
+
+ AddUntilStep("wait for left movement", () => getCatcher().Scale.X < 0);
+
+ for (int i = 0; i < 3; i++)
+ {
+ AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing);
+ AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing);
+ }
}
+ private Catcher getCatcher() => Player.ChildrenOfType().First().MovableCatcher;
+
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap
@@ -35,17 +58,52 @@ namespace osu.Game.Rulesets.Catch.Tests
}
};
- // Should produce a hyper-dash
- beatmap.HitObjects.Add(new Fruit { StartTime = 816, X = 308 / 512f, NewCombo = true });
- beatmap.HitObjects.Add(new Fruit { StartTime = 1008, X = 56 / 512f, });
+ // Should produce a hyper-dash (edge case test)
+ beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56 / 512f, NewCombo = true });
+ beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308 / 512f, NewCombo = true });
- for (int i = 0; i < 512; i++)
- {
- if (i % 5 < 3)
- beatmap.HitObjects.Add(new Fruit { X = i % 10 < 5 ? 0.02f : 0.98f, StartTime = 2000 + i * 100, NewCombo = i % 8 == 0 });
- }
+ double startTime = 3000;
+
+ const float left_x = 0.02f;
+ const float right_x = 0.98f;
+
+ createObjects(() => new Fruit { X = left_x });
+ createObjects(() => new TestJuiceStream(right_x), 1);
+ createObjects(() => new TestJuiceStream(left_x), 1);
+ createObjects(() => new Fruit { X = right_x });
+ createObjects(() => new Fruit { X = left_x });
+ createObjects(() => new Fruit { X = right_x });
+ createObjects(() => new TestJuiceStream(left_x), 1);
return beatmap;
+
+ void createObjects(Func createObject, int count = 3)
+ {
+ const float spacing = 140;
+
+ for (int i = 0; i < count; i++)
+ {
+ var hitObject = createObject();
+ hitObject.StartTime = startTime + i * spacing;
+ beatmap.HitObjects.Add(hitObject);
+ }
+
+ startTime += 700;
+ }
+ }
+
+ private class TestJuiceStream : JuiceStream
+ {
+ public TestJuiceStream(float x)
+ {
+ X = x;
+
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(Vector2.Zero),
+ new PathControlPoint(new Vector2(30, 0)),
+ });
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs
new file mode 100644
index 0000000000..cbc87459e1
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs
@@ -0,0 +1,56 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneJuiceStream : PlayerTestScene
+ {
+ public TestSceneJuiceStream()
+ : base(new CatchRuleset())
+ {
+ }
+
+ [Test]
+ public void TestJuiceStreamEndingCombo()
+ {
+ AddUntilStep("player is done", () => !Player.ValidForResume);
+ }
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BaseDifficulty = new BeatmapDifficulty { CircleSize = 5, SliderMultiplier = 2 },
+ Ruleset = ruleset
+ },
+ HitObjects = new List
+ {
+ new JuiceStream
+ {
+ X = 0.5f,
+ Path = new SliderPath(PathType.Linear, new[]
+ {
+ Vector2.Zero,
+ new Vector2(0, 100)
+ }),
+ StartTime = 200
+ },
+ new Banana
+ {
+ X = 0.5f,
+ StartTime = 1000,
+ NewCombo = true
+ }
+ }
+ };
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 9559d13328..8c371db257 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index db52fbac1b..7c81bcdf0c 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -28,15 +28,18 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
ApplyPositionOffsets(Beatmap);
- initialiseHyperDash((List)Beatmap.HitObjects);
-
int index = 0;
foreach (var obj in Beatmap.HitObjects.OfType())
{
- obj.IndexInBeatmap = index++;
+ obj.IndexInBeatmap = index;
+ foreach (var nested in obj.NestedHitObjects.OfType())
+ nested.IndexInBeatmap = index;
+
if (obj.LastInCombo && obj.NestedHitObjects.LastOrDefault() is IHasComboInformation lastNested)
lastNested.LastInCombo = true;
+
+ index++;
}
}
@@ -71,6 +74,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
break;
case JuiceStream juiceStream:
+ // Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead.
+ lastPosition = juiceStream.X + juiceStream.Path.ControlPoints[^1].Position.Value.X / CatchPlayfield.BASE_WIDTH;
+
+ // Todo: BUG!! Stable attempted to use the end time of the stream, but referenced it too early in execution and used the start time instead.
+ lastStartTime = juiceStream.StartTime;
+
foreach (var nested in juiceStream.NestedHitObjects)
{
var catchObject = (CatchHitObject)nested;
@@ -85,20 +94,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
break;
}
}
+
+ initialiseHyperDash(beatmap);
}
private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng)
{
- if (hitObject is JuiceStream stream)
- {
- lastPosition = stream.EndX;
- lastStartTime = stream.EndTime;
- return;
- }
-
- if (!(hitObject is Fruit))
- return;
-
float offsetPosition = hitObject.X;
double startTime = hitObject.StartTime;
@@ -111,7 +112,9 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
}
float positionDiff = offsetPosition - lastPosition.Value;
- double timeDiff = startTime - lastStartTime;
+
+ // Todo: BUG!! Stable calculated time deltas as ints, which affects randomisation. This should be changed to a double.
+ int timeDiff = (int)(startTime - lastStartTime);
if (timeDiff > 1000)
{
@@ -127,7 +130,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
return;
}
- if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3d)
+ // ReSharper disable once PossibleLossOfFraction
+ if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3)
applyOffset(ref offsetPosition, positionDiff);
hitObject.XOffset = offsetPosition - hitObject.X;
@@ -186,14 +190,14 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
}
}
- private void initialiseHyperDash(List objects)
+ private static void initialiseHyperDash(IBeatmap beatmap)
{
List objectWithDroplets = new List();
- foreach (var currentObject in objects)
+ foreach (var currentObject in beatmap.HitObjects)
{
- if (currentObject is Fruit)
- objectWithDroplets.Add(currentObject);
+ if (currentObject is Fruit fruitObject)
+ objectWithDroplets.Add(fruitObject);
if (currentObject is JuiceStream)
{
@@ -207,7 +211,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
- double halfCatcherWidth = CatcherArea.GetCatcherSize(Beatmap.BeatmapInfo.BaseDifficulty) / 2;
+ double halfCatcherWidth = CatcherArea.GetCatcherSize(beatmap.BeatmapInfo.BaseDifficulty) / 2;
int lastDirection = 0;
double lastExcess = halfCatcherWidth;
@@ -216,10 +220,14 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
CatchHitObject currentObject = objectWithDroplets[i];
CatchHitObject nextObject = objectWithDroplets[i + 1];
+ // Reset variables in-case values have changed (e.g. after applying HR)
+ currentObject.HyperDashTarget = null;
+ currentObject.DistanceToHyperDash = 0;
+
int thisDirection = nextObject.X > currentObject.X ? 1 : -1;
double timeToNext = nextObject.StartTime - currentObject.StartTime - 1000f / 60f / 4; // 1/4th of a frame of grace time, taken from osu-stable
double distanceToNext = Math.Abs(nextObject.X - currentObject.X) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth);
- float distanceToHyper = (float)(timeToNext * CatcherArea.Catcher.BASE_SPEED - distanceToNext);
+ float distanceToHyper = (float)(timeToNext * Catcher.BASE_SPEED - distanceToNext);
if (distanceToHyper < 0)
{
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index e5c3647f99..b9d791fdb1 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -21,6 +21,8 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System;
+using osu.Game.Rulesets.Catch.Skinning;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
{
@@ -141,6 +143,8 @@ namespace osu.Game.Rulesets.Catch
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
+ public override ISkin CreateLegacySkinProvider(ISkinSource source) => new CatchLegacySkinTransformer(source);
+
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new CatchPerformanceCalculator(this, beatmap, score);
public int LegacyID => 2;
diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
index 7e482d4045..80390705fe 100644
--- a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
+++ b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
@@ -5,5 +5,14 @@ namespace osu.Game.Rulesets.Catch
{
public enum CatchSkinComponents
{
+ FruitBananas,
+ FruitApple,
+ FruitGrapes,
+ FruitOrange,
+ FruitPear,
+ Droplet,
+ CatcherIdle,
+ CatcherFail,
+ CatcherKiai
}
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index 44e1a8e5cc..5880a227c2 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
protected override int SectionLength => 750;
+ private float halfCatcherWidth;
+
public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
@@ -48,14 +50,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
- float halfCatchWidth;
-
- using (var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty))
- {
- halfCatchWidth = catcher.CatchWidth * 0.5f;
- halfCatchWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay.
- }
-
CatchHitObject lastObject = null;
// In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream.
@@ -69,16 +63,25 @@ namespace osu.Game.Rulesets.Catch.Difficulty
continue;
if (lastObject != null)
- yield return new CatchDifficultyHitObject(hitObject, lastObject, clockRate, halfCatchWidth);
+ yield return new CatchDifficultyHitObject(hitObject, lastObject, clockRate, halfCatcherWidth);
lastObject = hitObject;
}
}
- protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
+ protected override Skill[] CreateSkills(IBeatmap beatmap)
{
- new Movement(),
- };
+ using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty))
+ {
+ halfCatcherWidth = catcher.CatchWidth * 0.5f;
+ halfCatcherWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay.
+ }
+
+ return new Skill[]
+ {
+ new Movement(halfCatcherWidth),
+ };
+ }
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
{
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
index 7cd569035b..fd164907e0 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -20,9 +20,16 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
protected override double DecayWeight => 0.94;
+ protected readonly float HalfCatcherWidth;
+
private float? lastPlayerPosition;
private float lastDistanceMoved;
+ public Movement(float halfCatcherWidth)
+ {
+ HalfCatcherWidth = halfCatcherWidth;
+ }
+
protected override double StrainValueOf(DifficultyHitObject current)
{
var catchCurrent = (CatchDifficultyHitObject)current;
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
index 8377b3786a..e2465d727e 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
@@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModDifficultyAdjust : ModDifficultyAdjust
{
- [SettingSource("Fruit Size", "Override a beatmap's set CS.")]
+ [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
public BindableNumber CircleSize { get; } = new BindableFloat
{
Precision = 0.1f,
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Mods
Value = 5,
};
- [SettingSource("Approach Rate", "Override a beatmap's set AR.")]
+ [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
public BindableNumber ApproachRate { get; } = new BindableFloat
{
Precision = 0.1f,
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
index 606a935229..ee88edbea1 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
@@ -3,7 +3,7 @@
using System.Linq;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Catch.Objects.Drawable;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs
index fb92399102..e3391c47f1 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs
@@ -1,11 +1,17 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Catch.Judgements;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModPerfect : ModPerfect
{
+ protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
+ => !(result.Judgement is CatchBananaJudgement)
+ && base.FailCondition(healthProcessor, result);
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
index 4c72b9fd3e..1ef235f764 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Mods
private class MouseInputHelper : Drawable, IKeyBindingHandler, IRequireHighFrequencyMousePosition
{
- private readonly CatcherArea.Catcher catcher;
+ private readonly Catcher catcher;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
index 267e6d12c7..c3488aec11 100644
--- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects
@@ -11,6 +12,8 @@ namespace osu.Game.Rulesets.Catch.Objects
public override bool LastInCombo => true;
+ public override Judgement CreateJudgement() => new IgnoreJudgement();
+
protected override void CreateNestedHitObjects()
{
base.CreateNestedHitObjects();
@@ -36,7 +39,11 @@ namespace osu.Game.Rulesets.Catch.Objects
}
}
- public double EndTime => StartTime + Duration;
+ public double EndTime
+ {
+ get => StartTime + Duration;
+ set => Duration = value - StartTime;
+ }
public double Duration { get; set; }
}
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index e4ad49ea50..f3b566f340 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
public abstract class CatchHitObject : HitObject, IHasXPosition, IHasComboInformation
{
- public const double OBJECT_RADIUS = 44;
+ public const float OBJECT_RADIUS = 64;
private float x;
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public int IndexInBeatmap { get; set; }
- public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(ComboIndex % 4);
+ public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(IndexInBeatmap % 4);
public virtual bool NewCombo { get; set; }
@@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Catch.Objects
TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
- Scale = 1.0f - 0.7f * (difficulty.CircleSize - 5) / 5;
+ Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2;
}
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
@@ -100,8 +100,8 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Pear,
Grape,
- Raspberry,
Pineapple,
+ Raspberry,
Banana // banananananannaanana
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBanana.cs
deleted file mode 100644
index 5afdb14888..0000000000
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBanana.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-namespace osu.Game.Rulesets.Catch.Objects.Drawable
-{
- public class DrawableBanana : DrawableFruit
- {
- public DrawableBanana(Banana h)
- : base(h)
- {
- }
- }
-}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs
deleted file mode 100644
index 059310d671..0000000000
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Game.Rulesets.Catch.Objects.Drawable.Pieces;
-using osuTK;
-
-namespace osu.Game.Rulesets.Catch.Objects.Drawable
-{
- public class DrawableDroplet : PalpableCatchHitObject
- {
- private Pulp pulp;
-
- public override bool StaysOnPlate => false;
-
- public DrawableDroplet(Droplet h)
- : base(h)
- {
- Origin = Anchor.Centre;
- Size = new Vector2((float)CatchHitObject.OBJECT_RADIUS) / 4;
- Masking = false;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- AddInternal(pulp = new Pulp { Size = Size });
-
- AccentColour.BindValueChanged(colour => { pulp.AccentColour = colour.NewValue; }, true);
- }
- }
-}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs
deleted file mode 100644
index 53a018c9f4..0000000000
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs
+++ /dev/null
@@ -1,316 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using osu.Framework.Allocation;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Effects;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Utils;
-using osu.Game.Rulesets.Catch.Objects.Drawable.Pieces;
-using osuTK;
-using osuTK.Graphics;
-
-namespace osu.Game.Rulesets.Catch.Objects.Drawable
-{
- public class DrawableFruit : PalpableCatchHitObject
- {
- private Circle border;
-
- private const float drawable_radius = (float)CatchHitObject.OBJECT_RADIUS * radius_adjust;
-
- ///
- /// Because we're adding a border around the fruit, we need to scale down some.
- ///
- private const float radius_adjust = 1.1f;
-
- public DrawableFruit(Fruit h)
- : base(h)
- {
- Origin = Anchor.Centre;
-
- Size = new Vector2(drawable_radius);
- Masking = false;
-
- Rotation = (float)(RNG.NextDouble() - 0.5f) * 40;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- // todo: this should come from the skin.
- AccentColour.Value = colourForRepresentation(HitObject.VisualRepresentation);
-
- AddRangeInternal(new[]
- {
- createPulp(HitObject.VisualRepresentation),
- border = new Circle
- {
- EdgeEffect = new EdgeEffectParameters
- {
- Hollow = !HitObject.HyperDash,
- Type = EdgeEffectType.Glow,
- Radius = 4 * radius_adjust,
- Colour = HitObject.HyperDash ? Color4.Red : AccentColour.Value.Darken(1).Opacity(0.6f)
- },
- Size = new Vector2(Height),
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- BorderColour = Color4.White,
- BorderThickness = 3f * radius_adjust,
- Children = new Framework.Graphics.Drawable[]
- {
- new Box
- {
- AlwaysPresent = true,
- Colour = AccentColour.Value,
- Alpha = 0,
- RelativeSizeAxes = Axes.Both
- }
- }
- },
- });
-
- if (HitObject.HyperDash)
- {
- AddInternal(new Pulp
- {
- RelativePositionAxes = Axes.Both,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- AccentColour = Color4.Red,
- Blending = BlendingParameters.Additive,
- Alpha = 0.5f,
- Scale = new Vector2(1.333f)
- });
- }
- }
-
- private Framework.Graphics.Drawable createPulp(FruitVisualRepresentation representation)
- {
- const float large_pulp_3 = 8f * radius_adjust;
- const float distance_from_centre_3 = 0.15f;
-
- const float large_pulp_4 = large_pulp_3 * 0.925f;
- const float distance_from_centre_4 = distance_from_centre_3 / 0.925f;
-
- const float small_pulp = large_pulp_3 / 2;
-
- static Vector2 positionAt(float angle, float distance) => new Vector2(
- distance * MathF.Sin(angle * MathF.PI / 180),
- distance * MathF.Cos(angle * MathF.PI / 180));
-
- switch (representation)
- {
- default:
- return new Container();
-
- case FruitVisualRepresentation.Raspberry:
- return new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Framework.Graphics.Drawable[]
- {
- new Pulp
- {
- AccentColour = AccentColour.Value,
- Size = new Vector2(small_pulp),
- Y = -0.34f,
- },
- new Pulp
- {
- AccentColour = AccentColour.Value,
- Size = new Vector2(large_pulp_4),
- Position = positionAt(0, distance_from_centre_4),
- },
- new Pulp
- {
- AccentColour = AccentColour.Value,
- Size = new Vector2(large_pulp_4),
- Position = positionAt(90, distance_from_centre_4),
- },
- new Pulp
- {
- AccentColour = AccentColour.Value,
- Size = new Vector2(large_pulp_4),
- Position = positionAt(180, distance_from_centre_4),
- },
- new Pulp
- {
- Size = new Vector2(large_pulp_4),
- AccentColour = AccentColour.Value,
- Position = positionAt(270, distance_from_centre_4),
- },
- }
- };
-
- case FruitVisualRepresentation.Pineapple:
- return new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Framework.Graphics.Drawable[]
- {
- new Pulp
- {
- AccentColour = AccentColour.Value,
- Size = new Vector2(small_pulp),
- Y = -0.3f,
- },
- new Pulp
- {
- AccentColour = AccentColour.Value,
- Size = new Vector2(large_pulp_4),
- Position = positionAt(45, distance_from_centre_4),
- },
- new Pulp
- {
- AccentColour = AccentColour.Value,
- Size = new Vector2(large_pulp_4),
- Position = positionAt(135, distance_from_centre_4),
- },
- new Pulp
- {
- AccentColour = AccentColour.Value,
- Size = new Vector2(large_pulp_4),
- Position = positionAt(225, distance_from_centre_4),
- },
- new Pulp
- {
- Size = new Vector2(large_pulp_4),
- AccentColour = AccentColour.Value,
- Position = positionAt(315, distance_from_centre_4),
- },
- }
- };
-
- case FruitVisualRepresentation.Pear:
- return new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Framework.Graphics.Drawable[]
- {
- new Pulp
- {
- AccentColour = AccentColour.Value,
- Size = new Vector2(small_pulp),
- Y = -0.33f,
- },
- new Pulp
- {
- AccentColour = AccentColour.Value,
- Size = new Vector2(large_pulp_3),
- Position = positionAt(60, distance_from_centre_3),
- },
- new Pulp
- {
- AccentColour = AccentColour.Value,
- Size = new Vector2(large_pulp_3),
- Position = positionAt(180, distance_from_centre_3),
- },
- new Pulp
- {
- Size = new Vector2(large_pulp_3),
- AccentColour = AccentColour.Value,
- Position = positionAt(300, distance_from_centre_3),
- },
- }
- };
-
- case FruitVisualRepresentation.Grape:
- return new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Framework.Graphics.Drawable[]
- {
- new Pulp
- {
- AccentColour = AccentColour.Value,
- Size = new Vector2(small_pulp),
- Y = -0.25f,
- },
- new Pulp
- {
- AccentColour = AccentColour.Value,
- Size = new Vector2(large_pulp_3),
- Position = positionAt(0, distance_from_centre_3),
- },
- new Pulp
- {
- AccentColour = AccentColour.Value,
- Size = new Vector2(large_pulp_3),
- Position = positionAt(120, distance_from_centre_3),
- },
- new Pulp
- {
- Size = new Vector2(large_pulp_3),
- AccentColour = AccentColour.Value,
- Position = positionAt(240, distance_from_centre_3),
- },
- }
- };
-
- case FruitVisualRepresentation.Banana:
- return new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Framework.Graphics.Drawable[]
- {
- new Pulp
- {
- AccentColour = AccentColour.Value,
- Size = new Vector2(small_pulp),
- Y = -0.3f
- },
- new Pulp
- {
- AccentColour = AccentColour.Value,
- Size = new Vector2(large_pulp_4 * 0.8f, large_pulp_4 * 2.5f),
- Y = 0.05f,
- },
- }
- };
- }
- }
-
- protected override void Update()
- {
- base.Update();
-
- border.Alpha = (float)Math.Clamp((HitObject.StartTime - Time.Current) / 500, 0, 1);
- }
-
- private Color4 colourForRepresentation(FruitVisualRepresentation representation)
- {
- switch (representation)
- {
- default:
- case FruitVisualRepresentation.Pear:
- return new Color4(17, 136, 170, 255);
-
- case FruitVisualRepresentation.Grape:
- return new Color4(204, 102, 0, 255);
-
- case FruitVisualRepresentation.Raspberry:
- return new Color4(121, 9, 13, 255);
-
- case FruitVisualRepresentation.Pineapple:
- return new Color4(102, 136, 0, 255);
-
- case FruitVisualRepresentation.Banana:
- switch (RNG.Next(0, 3))
- {
- default:
- return new Color4(255, 240, 0, 255);
-
- case 1:
- return new Color4(255, 192, 0, 255);
-
- case 2:
- return new Color4(214, 221, 28, 255);
- }
- }
- }
- }
-}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/BananaPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/BananaPiece.cs
new file mode 100644
index 0000000000..ebb0bf0f2c
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/BananaPiece.cs
@@ -0,0 +1,31 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ public class BananaPiece : PulpFormation
+ {
+ public BananaPiece()
+ {
+ InternalChildren = new Drawable[]
+ {
+ new Pulp
+ {
+ AccentColour = { BindTarget = AccentColour },
+ Size = new Vector2(SMALL_PULP),
+ Y = -0.3f
+ },
+ new Pulp
+ {
+ AccentColour = { BindTarget = AccentColour },
+ Size = new Vector2(LARGE_PULP_4 * 0.8f, LARGE_PULP_4 * 2.5f),
+ Y = 0.05f,
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
new file mode 100644
index 0000000000..01b76ceed9
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
@@ -0,0 +1,58 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Framework.Utils;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ public class DrawableBanana : DrawableFruit
+ {
+ public DrawableBanana(Banana h)
+ : base(h)
+ {
+ }
+
+ private Color4? colour;
+
+ protected override Color4 GetComboColour(IReadOnlyList comboColours)
+ {
+ // override any external colour changes with banananana
+ return colour ??= getBananaColour();
+ }
+
+ protected override void UpdateInitialTransforms()
+ {
+ base.UpdateInitialTransforms();
+
+ const float end_scale = 0.6f;
+ const float random_scale_range = 1.6f;
+
+ ScaleContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RNG.NextSingle()))
+ .Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt);
+
+ ScaleContainer.RotateTo(getRandomAngle())
+ .Then()
+ .RotateTo(getRandomAngle(), HitObject.TimePreempt);
+
+ float getRandomAngle() => 180 * (RNG.NextSingle() * 2 - 1);
+ }
+
+ private Color4 getBananaColour()
+ {
+ switch (RNG.Next(0, 3))
+ {
+ default:
+ return new Color4(255, 240, 0, 255);
+
+ case 1:
+ return new Color4(255, 192, 0, 255);
+
+ case 2:
+ return new Color4(214, 221, 28, 255);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs
similarity index 97%
rename from osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs
rename to osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs
index ea415e18fa..4ce80aceb8 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs
@@ -7,7 +7,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-namespace osu.Game.Rulesets.Catch.Objects.Drawable
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public class DrawableBananaShower : DrawableCatchHitObject
{
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
similarity index 69%
rename from osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs
rename to osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
index b7c05392f3..6844be5941 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
@@ -2,24 +2,51 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osuTK;
+using System.Collections.Generic;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
-namespace osu.Game.Rulesets.Catch.Objects.Drawable
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public abstract class PalpableCatchHitObject : DrawableCatchHitObject
where TObject : CatchHitObject
{
public override bool CanBePlated => true;
+ protected Container ScaleContainer { get; private set; }
+
protected PalpableCatchHitObject(TObject hitObject)
: base(hitObject)
{
- Scale = new Vector2(HitObject.Scale);
+ Origin = Anchor.Centre;
+ Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
+ Masking = false;
}
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddRangeInternal(new Drawable[]
+ {
+ ScaleContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ }
+ });
+
+ ScaleContainer.Scale = new Vector2(HitObject.Scale);
+ }
+
+ protected override Color4 GetComboColour(IReadOnlyList comboColours) =>
+ comboColours[(HitObject.IndexInBeatmap + 1) % comboColours.Count];
}
public abstract class DrawableCatchHitObject : DrawableCatchHitObject
@@ -41,6 +68,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
public virtual bool StaysOnPlate => CanBePlated;
+ public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
+
protected DrawableCatchHitObject(CatchHitObject hitObject)
: base(hitObject)
{
@@ -62,10 +91,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss);
}
- protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
-
- protected override void UpdateInitialTransforms() => this.FadeInFromZero(200);
-
protected override void UpdateStateTransforms(ArmedState state)
{
var endTime = HitObject.GetEndTime();
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
new file mode 100644
index 0000000000..cad8892283
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
@@ -0,0 +1,42 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ public class DrawableDroplet : PalpableCatchHitObject
+ {
+ public override bool StaysOnPlate => false;
+
+ public DrawableDroplet(Droplet h)
+ : base(h)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ ScaleContainer.Child = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new Pulp
+ {
+ Size = Size / 4,
+ AccentColour = { BindTarget = AccentColour }
+ });
+ }
+
+ protected override void UpdateInitialTransforms()
+ {
+ base.UpdateInitialTransforms();
+
+ // roughly matches osu-stable
+ float startRotation = RNG.NextSingle() * 20;
+ double duration = HitObject.TimePreempt + 2000;
+
+ ScaleContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
new file mode 100644
index 0000000000..fae5a10d04
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
@@ -0,0 +1,51 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Utils;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ public class DrawableFruit : PalpableCatchHitObject
+ {
+ public DrawableFruit(Fruit h)
+ : base(h)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ ScaleContainer.Child = new SkinnableDrawable(
+ new CatchSkinComponent(getComponent(HitObject.VisualRepresentation)), _ => new FruitPiece());
+
+ ScaleContainer.Rotation = (float)(RNG.NextDouble() - 0.5f) * 40;
+ }
+
+ private CatchSkinComponents getComponent(FruitVisualRepresentation hitObjectVisualRepresentation)
+ {
+ switch (hitObjectVisualRepresentation)
+ {
+ case FruitVisualRepresentation.Pear:
+ return CatchSkinComponents.FruitPear;
+
+ case FruitVisualRepresentation.Grape:
+ return CatchSkinComponents.FruitGrapes;
+
+ case FruitVisualRepresentation.Pineapple:
+ return CatchSkinComponents.FruitApple;
+
+ case FruitVisualRepresentation.Raspberry:
+ return CatchSkinComponents.FruitOrange;
+
+ case FruitVisualRepresentation.Banana:
+ return CatchSkinComponents.FruitBananas;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(hitObjectVisualRepresentation), hitObjectVisualRepresentation, null);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
similarity index 77%
rename from osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs
rename to osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
index a24821b3ce..7bc016d94f 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
@@ -6,19 +6,22 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
+using osuTK;
-namespace osu.Game.Rulesets.Catch.Objects.Drawable
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public class DrawableJuiceStream : DrawableCatchHitObject
{
private readonly Func> createDrawableRepresentation;
private readonly Container dropletContainer;
+ public override Vector2 OriginPosition => base.OriginPosition - new Vector2(0, CatchHitObject.OBJECT_RADIUS);
+
public DrawableJuiceStream(JuiceStream s, Func> createDrawableRepresentation = null)
: base(s)
{
this.createDrawableRepresentation = createDrawableRepresentation;
- RelativeSizeAxes = Axes.Both;
+ RelativeSizeAxes = Axes.X;
Origin = Anchor.BottomLeft;
X = 0;
@@ -27,6 +30,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
+ hitObject.Origin = Anchor.BottomCentre;
+
base.AddNestedHitObject(hitObject);
dropletContainer.Add(hitObject);
}
@@ -42,10 +47,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
switch (hitObject)
{
case CatchHitObject catchObject:
- return createDrawableRepresentation?.Invoke(catchObject)?.With(o => ((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false);
+ return createDrawableRepresentation?.Invoke(catchObject)?.With(o =>
+ ((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false);
}
- return base.CreateNestedHitObject(hitObject);
+ throw new ArgumentException($"{nameof(hitObject)} must be of type {nameof(CatchHitObject)}.");
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableTinyDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs
similarity index 60%
rename from osu.Game.Rulesets.Catch/Objects/Drawable/DrawableTinyDroplet.cs
rename to osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs
index d41aea1e7b..ae775684d8 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableTinyDroplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs
@@ -1,16 +1,21 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osuTK;
+using osu.Framework.Allocation;
-namespace osu.Game.Rulesets.Catch.Objects.Drawable
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public class DrawableTinyDroplet : DrawableDroplet
{
public DrawableTinyDroplet(TinyDroplet h)
: base(h)
{
- Size = new Vector2((float)CatchHitObject.OBJECT_RADIUS) / 8;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ ScaleContainer.Scale /= 2;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
new file mode 100644
index 0000000000..5797588ded
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
@@ -0,0 +1,116 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ internal class FruitPiece : CompositeDrawable
+ {
+ ///
+ /// Because we're adding a border around the fruit, we need to scale down some.
+ ///
+ public const float RADIUS_ADJUST = 1.1f;
+
+ private Circle border;
+
+ private CatchHitObject hitObject;
+
+ private readonly IBindable accentColour = new Bindable();
+
+ public FruitPiece()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableObject)
+ {
+ DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject;
+ hitObject = drawableCatchObject.HitObject;
+
+ accentColour.BindTo(drawableCatchObject.AccentColour);
+
+ AddRangeInternal(new[]
+ {
+ getFruitFor(drawableCatchObject.HitObject.VisualRepresentation),
+ border = new Circle
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ BorderColour = Color4.White,
+ BorderThickness = 6f * RADIUS_ADJUST,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ AlwaysPresent = true,
+ Alpha = 0,
+ RelativeSizeAxes = Axes.Both
+ }
+ }
+ },
+ });
+
+ if (hitObject.HyperDash)
+ {
+ AddInternal(new Circle
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ BorderColour = Color4.Red,
+ BorderThickness = 12f * RADIUS_ADJUST,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ AlwaysPresent = true,
+ Alpha = 0.3f,
+ Blending = BlendingParameters.Additive,
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Red,
+ }
+ }
+ });
+ }
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ border.Alpha = (float)Math.Clamp((hitObject.StartTime - Time.Current) / 500, 0, 1);
+ }
+
+ private Drawable getFruitFor(FruitVisualRepresentation representation)
+ {
+ switch (representation)
+ {
+ case FruitVisualRepresentation.Pear:
+ return new PearPiece();
+
+ case FruitVisualRepresentation.Grape:
+ return new GrapePiece();
+
+ case FruitVisualRepresentation.Pineapple:
+ return new PineapplePiece();
+
+ case FruitVisualRepresentation.Banana:
+ return new BananaPiece();
+
+ case FruitVisualRepresentation.Raspberry:
+ return new RaspberryPiece();
+ }
+
+ return Empty();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/GrapePiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/GrapePiece.cs
new file mode 100644
index 0000000000..1d1faf893b
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/GrapePiece.cs
@@ -0,0 +1,43 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ public class GrapePiece : PulpFormation
+ {
+ public GrapePiece()
+ {
+ InternalChildren = new Drawable[]
+ {
+ new Pulp
+ {
+ AccentColour = { BindTarget = AccentColour },
+ Size = new Vector2(SMALL_PULP),
+ Y = -0.25f,
+ },
+ new Pulp
+ {
+ AccentColour = { BindTarget = AccentColour },
+ Size = new Vector2(LARGE_PULP_3),
+ Position = PositionAt(0, DISTANCE_FROM_CENTRE_3),
+ },
+ new Pulp
+ {
+ AccentColour = { BindTarget = AccentColour },
+ Size = new Vector2(LARGE_PULP_3),
+ Position = PositionAt(120, DISTANCE_FROM_CENTRE_3),
+ },
+ new Pulp
+ {
+ Size = new Vector2(LARGE_PULP_3),
+ AccentColour = { BindTarget = AccentColour },
+ Position = PositionAt(240, DISTANCE_FROM_CENTRE_3),
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/PearPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/PearPiece.cs
new file mode 100644
index 0000000000..7f14217cda
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/PearPiece.cs
@@ -0,0 +1,43 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ public class PearPiece : PulpFormation
+ {
+ public PearPiece()
+ {
+ InternalChildren = new Drawable[]
+ {
+ new Pulp
+ {
+ AccentColour = { BindTarget = AccentColour },
+ Size = new Vector2(SMALL_PULP),
+ Y = -0.33f,
+ },
+ new Pulp
+ {
+ AccentColour = { BindTarget = AccentColour },
+ Size = new Vector2(LARGE_PULP_3),
+ Position = PositionAt(60, DISTANCE_FROM_CENTRE_3),
+ },
+ new Pulp
+ {
+ AccentColour = { BindTarget = AccentColour },
+ Size = new Vector2(LARGE_PULP_3),
+ Position = PositionAt(180, DISTANCE_FROM_CENTRE_3),
+ },
+ new Pulp
+ {
+ Size = new Vector2(LARGE_PULP_3),
+ AccentColour = { BindTarget = AccentColour },
+ Position = PositionAt(300, DISTANCE_FROM_CENTRE_3),
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs
similarity index 62%
rename from osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs
rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs
index 1e9daf18db..1e7506a257 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs
@@ -1,16 +1,16 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics;
using osuTK.Graphics;
-namespace osu.Game.Rulesets.Catch.Objects.Drawable.Pieces
+namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{
- public class Pulp : Circle, IHasAccentColour
+ public class Pulp : Circle
{
public Pulp()
{
@@ -22,32 +22,23 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable.Pieces
Colour = Color4.White.Opacity(0.9f);
}
- private Color4 accentColour;
+ public readonly Bindable AccentColour = new Bindable();
- public Color4 AccentColour
+ protected override void LoadComplete()
{
- get => accentColour;
- set
- {
- accentColour = value;
- if (IsLoaded) updateAccentColour();
- }
+ base.LoadComplete();
+
+ AccentColour.BindValueChanged(updateAccentColour, true);
}
- private void updateAccentColour()
+ private void updateAccentColour(ValueChangedEvent colour)
{
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = Size.X / 2,
- Colour = accentColour.Darken(0.2f).Opacity(0.75f)
+ Colour = colour.NewValue.Darken(0.2f).Opacity(0.75f)
};
}
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
- updateAccentColour();
- }
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/PineapplePiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/PineapplePiece.cs
new file mode 100644
index 0000000000..c328ba1837
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/PineapplePiece.cs
@@ -0,0 +1,49 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ public class PineapplePiece : PulpFormation
+ {
+ public PineapplePiece()
+ {
+ InternalChildren = new Drawable[]
+ {
+ new Pulp
+ {
+ AccentColour = { BindTarget = AccentColour },
+ Size = new Vector2(SMALL_PULP),
+ Y = -0.3f,
+ },
+ new Pulp
+ {
+ AccentColour = { BindTarget = AccentColour },
+ Size = new Vector2(LARGE_PULP_4),
+ Position = PositionAt(45, DISTANCE_FROM_CENTRE_4),
+ },
+ new Pulp
+ {
+ AccentColour = { BindTarget = AccentColour },
+ Size = new Vector2(LARGE_PULP_4),
+ Position = PositionAt(135, DISTANCE_FROM_CENTRE_4),
+ },
+ new Pulp
+ {
+ AccentColour = { BindTarget = AccentColour },
+ Size = new Vector2(LARGE_PULP_4),
+ Position = PositionAt(225, DISTANCE_FROM_CENTRE_4),
+ },
+ new Pulp
+ {
+ Size = new Vector2(LARGE_PULP_4),
+ AccentColour = { BindTarget = AccentColour },
+ Position = PositionAt(315, DISTANCE_FROM_CENTRE_4),
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/PulpFormation.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/PulpFormation.cs
new file mode 100644
index 0000000000..be70c3400c
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/PulpFormation.cs
@@ -0,0 +1,43 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ public abstract class PulpFormation : CompositeDrawable
+ {
+ protected readonly IBindable AccentColour = new Bindable();
+
+ protected const float LARGE_PULP_3 = 16f * FruitPiece.RADIUS_ADJUST;
+ protected const float DISTANCE_FROM_CENTRE_3 = 0.15f;
+
+ protected const float LARGE_PULP_4 = LARGE_PULP_3 * 0.925f;
+ protected const float DISTANCE_FROM_CENTRE_4 = DISTANCE_FROM_CENTRE_3 / 0.925f;
+
+ protected const float SMALL_PULP = LARGE_PULP_3 / 2;
+
+ protected PulpFormation()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ protected static Vector2 PositionAt(float angle, float distance) => new Vector2(
+ distance * MathF.Sin(angle * MathF.PI / 180),
+ distance * MathF.Cos(angle * MathF.PI / 180));
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableObject)
+ {
+ DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject;
+ AccentColour.BindTo(drawableCatchObject.AccentColour);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/RaspberryPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/RaspberryPiece.cs
new file mode 100644
index 0000000000..22ce3ba5b3
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/RaspberryPiece.cs
@@ -0,0 +1,49 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ public class RaspberryPiece : PulpFormation
+ {
+ public RaspberryPiece()
+ {
+ InternalChildren = new Drawable[]
+ {
+ new Pulp
+ {
+ AccentColour = { BindTarget = AccentColour },
+ Size = new Vector2(SMALL_PULP),
+ Y = -0.34f,
+ },
+ new Pulp
+ {
+ AccentColour = { BindTarget = AccentColour },
+ Size = new Vector2(LARGE_PULP_4),
+ Position = PositionAt(0, DISTANCE_FROM_CENTRE_4),
+ },
+ new Pulp
+ {
+ AccentColour = { BindTarget = AccentColour },
+ Size = new Vector2(LARGE_PULP_4),
+ Position = PositionAt(90, DISTANCE_FROM_CENTRE_4),
+ },
+ new Pulp
+ {
+ AccentColour = { BindTarget = AccentColour },
+ Size = new Vector2(LARGE_PULP_4),
+ Position = PositionAt(180, DISTANCE_FROM_CENTRE_4),
+ },
+ new Pulp
+ {
+ Size = new Vector2(LARGE_PULP_4),
+ AccentColour = { BindTarget = AccentColour },
+ Position = PositionAt(270, DISTANCE_FROM_CENTRE_4),
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index a4ed966abb..01011645bd 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -7,6 +7,7 @@ using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -19,10 +20,12 @@ namespace osu.Game.Rulesets.Catch.Objects
///
private const float base_scoring_distance = 100;
+ public override Judgement CreateJudgement() => new IgnoreJudgement();
+
public int RepeatCount { get; set; }
- public double Velocity;
- public double TickDistance;
+ public double Velocity { get; private set; }
+ public double TickDistance { get; private set; }
///
/// The length of one span of this .
@@ -46,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
base.CreateNestedHitObjects();
- var tickSamples = Samples.Select(s => new HitSampleInfo
+ var dropletSamples = Samples.Select(s => new HitSampleInfo
{
Bank = s.Bank,
Name = @"slidertick",
@@ -72,10 +75,9 @@ namespace osu.Game.Rulesets.Catch.Objects
{
AddNested(new TinyDroplet
{
- Samples = tickSamples,
StartTime = t + lastEvent.Value.Time,
X = X + Path.PositionAt(
- lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X / CatchPlayfield.BASE_WIDTH,
+ lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X / CatchPlayfield.BASE_WIDTH,
});
}
}
@@ -90,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Objects
case SliderEventType.Tick:
AddNested(new Droplet
{
- Samples = tickSamples,
+ Samples = dropletSamples,
StartTime = e.Time,
X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
});
@@ -110,7 +112,11 @@ namespace osu.Game.Rulesets.Catch.Objects
}
}
- public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
+ public double EndTime
+ {
+ get => StartTime + this.SpanCount() * Path.Distance / Velocity;
+ set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
+ }
public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH;
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
index 4649dcae90..b90b5812a6 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Replays
public override Replay Generate()
{
// todo: add support for HT DT
- const double dash_speed = CatcherArea.Catcher.BASE_SPEED;
+ const double dash_speed = Catcher.BASE_SPEED;
const double movement_speed = dash_speed / 2;
float lastPosition = 0.5f;
double lastTime = 0;
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
new file mode 100644
index 0000000000..65e6e6f209
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
@@ -0,0 +1,70 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Humanizer;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Audio;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Skinning
+{
+ public class CatchLegacySkinTransformer : ISkin
+ {
+ private readonly ISkin source;
+
+ public CatchLegacySkinTransformer(ISkinSource source)
+ {
+ this.source = source;
+ }
+
+ public Drawable GetDrawableComponent(ISkinComponent component)
+ {
+ if (!(component is CatchSkinComponent catchSkinComponent))
+ return null;
+
+ switch (catchSkinComponent.Component)
+ {
+ case CatchSkinComponents.FruitApple:
+ case CatchSkinComponents.FruitBananas:
+ case CatchSkinComponents.FruitOrange:
+ case CatchSkinComponents.FruitGrapes:
+ case CatchSkinComponents.FruitPear:
+ var lookupName = catchSkinComponent.Component.ToString().Kebaberize();
+ if (GetTexture(lookupName) != null)
+ return new LegacyFruitPiece(lookupName);
+
+ break;
+
+ case CatchSkinComponents.Droplet:
+ if (GetTexture("fruit-drop") != null)
+ return new LegacyFruitPiece("fruit-drop") { Scale = new Vector2(0.8f) };
+
+ break;
+
+ case CatchSkinComponents.CatcherIdle:
+ return this.GetAnimation("fruit-catcher-idle", true, true, true) ??
+ this.GetAnimation("fruit-ryuuta", true, true, true);
+
+ case CatchSkinComponents.CatcherFail:
+ return this.GetAnimation("fruit-catcher-fail", true, true, true) ??
+ this.GetAnimation("fruit-ryuuta", true, true, true);
+
+ case CatchSkinComponents.CatcherKiai:
+ return this.GetAnimation("fruit-catcher-kiai", true, true, true) ??
+ this.GetAnimation("fruit-ryuuta", true, true, true);
+ }
+
+ return null;
+ }
+
+ public Texture GetTexture(string componentName) => source.GetTexture(componentName);
+
+ public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample);
+
+ public IBindable GetConfig(TLookup lookup) => source.GetConfig(lookup);
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
new file mode 100644
index 0000000000..25ee0811d0
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
@@ -0,0 +1,79 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Skinning
+{
+ internal class LegacyFruitPiece : CompositeDrawable
+ {
+ private readonly string lookupName;
+
+ private readonly IBindable accentColour = new Bindable();
+ private Sprite colouredSprite;
+
+ public LegacyFruitPiece(string lookupName)
+ {
+ this.lookupName = lookupName;
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableObject, ISkinSource skin)
+ {
+ DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject;
+
+ accentColour.BindTo(drawableCatchObject.AccentColour);
+
+ InternalChildren = new Drawable[]
+ {
+ colouredSprite = new Sprite
+ {
+ Texture = skin.GetTexture(lookupName),
+ Colour = drawableObject.AccentColour.Value,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ new Sprite
+ {
+ Texture = skin.GetTexture($"{lookupName}-overlay"),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ };
+
+ if (drawableCatchObject.HitObject.HyperDash)
+ {
+ var hyperDash = new Sprite
+ {
+ Texture = skin.GetTexture(lookupName),
+ Colour = Color4.Red,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Blending = BlendingParameters.Additive,
+ Depth = 1,
+ Alpha = 0.7f,
+ Scale = new Vector2(1.2f)
+ };
+
+ AddInternal(hyperDash);
+ }
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ accentColour.BindValueChanged(colour => colouredSprite.Colour = colour.NewValue, true);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index 589503c35b..2319c5ac1f 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -6,7 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.Objects.Drawable;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
@@ -20,7 +20,9 @@ namespace osu.Game.Rulesets.Catch.UI
internal readonly CatcherArea CatcherArea;
- public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || CatcherArea.ReceivePositionalInputAt(screenSpacePos);
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
+ // only check the X position; handle all vertical space.
+ base.ReceivePositionalInputAt(new Vector2(screenSpacePos.X, ScreenSpaceDrawQuad.Centre.Y));
public CatchPlayfield(BeatmapDifficulty difficulty, Func> createDrawableRepresentation)
{
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
new file mode 100644
index 0000000000..e361b29a9d
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -0,0 +1,458 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Animations;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ public class Catcher : Container, IKeyBindingHandler
+ {
+ ///
+ /// Whether we are hyper-dashing or not.
+ ///
+ public bool HyperDashing => hyperDashModifier != 1;
+
+ ///
+ /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
+ ///
+ public const double BASE_SPEED = 1.0 / 512;
+
+ public Container ExplodingFruitTarget;
+
+ public Container AdditiveTarget;
+
+ public CatcherAnimationState CurrentState { get; private set; }
+
+ ///
+ /// Width of the area that can be used to attempt catches during gameplay.
+ ///
+ internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X);
+
+ protected bool Dashing
+ {
+ get => dashing;
+ set
+ {
+ if (value == dashing) return;
+
+ dashing = value;
+
+ Trail |= dashing;
+ }
+ }
+
+ ///
+ /// Activate or deactivate the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met.
+ ///
+ protected bool Trail
+ {
+ get => trail;
+ set
+ {
+ if (value == trail || AdditiveTarget == null) return;
+
+ trail = value;
+
+ if (Trail)
+ beginTrail();
+ }
+ }
+
+ private Container caughtFruit;
+
+ private CatcherSprite catcherIdle;
+ private CatcherSprite catcherKiai;
+ private CatcherSprite catcherFail;
+
+ private CatcherSprite currentCatcher;
+
+ private int currentDirection;
+
+ private bool dashing;
+
+ private bool trail;
+
+ private double hyperDashModifier = 1;
+ private int hyperDashDirection;
+ private float hyperDashTargetPosition;
+
+ public Catcher(BeatmapDifficulty difficulty = null)
+ {
+ RelativePositionAxes = Axes.X;
+ X = 0.5f;
+
+ Origin = Anchor.TopCentre;
+
+ Size = new Vector2(CatcherArea.CATCHER_SIZE);
+ if (difficulty != null)
+ Scale = new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Children = new Drawable[]
+ {
+ caughtFruit = new Container
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.BottomCentre,
+ },
+ catcherIdle = new CatcherSprite(CatcherAnimationState.Idle)
+ {
+ Anchor = Anchor.TopCentre,
+ Alpha = 0,
+ },
+ catcherKiai = new CatcherSprite(CatcherAnimationState.Kiai)
+ {
+ Anchor = Anchor.TopCentre,
+ Alpha = 0,
+ },
+ catcherFail = new CatcherSprite(CatcherAnimationState.Fail)
+ {
+ Anchor = Anchor.TopCentre,
+ Alpha = 0,
+ }
+ };
+
+ updateCatcher();
+ }
+
+ ///
+ /// Add a caught fruit to the catcher's stack.
+ ///
+ /// The fruit that was caught.
+ public void PlaceOnPlate(DrawableCatchHitObject fruit)
+ {
+ var ourRadius = fruit.DisplayRadius;
+ float theirRadius = 0;
+
+ const float allowance = 6;
+
+ while (caughtFruit.Any(f =>
+ f.LifetimeEnd == double.MaxValue &&
+ Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2)))
+ {
+ var diff = (ourRadius + theirRadius) / allowance;
+ fruit.X += (RNG.NextSingle() - 0.5f) * 2 * diff;
+ fruit.Y -= RNG.NextSingle() * diff;
+ }
+
+ fruit.X = Math.Clamp(fruit.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
+
+ caughtFruit.Add(fruit);
+
+ Add(new HitExplosion(fruit)
+ {
+ X = fruit.X,
+ Scale = new Vector2(fruit.HitObject.Scale)
+ });
+ }
+
+ ///
+ /// Let the catcher attempt to catch a fruit.
+ ///
+ /// The fruit to catch.
+ /// Whether the catch is possible.
+ public bool AttemptCatch(CatchHitObject fruit)
+ {
+ var halfCatchWidth = CatchWidth * 0.5f;
+
+ // this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
+ var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH;
+ var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH;
+
+ var validCatch =
+ catchObjectPosition >= catcherPosition - halfCatchWidth &&
+ catchObjectPosition <= catcherPosition + halfCatchWidth;
+
+ // only update hyperdash state if we are catching a fruit.
+ // exceptions are Droplets and JuiceStreams.
+ if (!(fruit is Fruit)) return validCatch;
+
+ if (validCatch && fruit.HyperDash)
+ {
+ var target = fruit.HyperDashTarget;
+ var timeDifference = target.StartTime - fruit.StartTime;
+ double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition;
+ var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
+
+ SetHyperDashState(Math.Abs(velocity), target.X);
+ }
+ else
+ SetHyperDashState();
+
+ if (validCatch)
+ updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle);
+ else if (!(fruit is Banana))
+ updateState(CatcherAnimationState.Fail);
+
+ return validCatch;
+ }
+
+ ///
+ /// Set hyper-dash state.
+ ///
+ /// The speed multiplier. If this is less or equals to 1, this catcher will be non-hyper-dashing state.
+ /// When this catcher crosses this position, this catcher ends hyper-dashing.
+ public void SetHyperDashState(double modifier = 1, float targetPosition = -1)
+ {
+ const float hyper_dash_transition_length = 180;
+
+ var wasHyperDashing = HyperDashing;
+
+ if (modifier <= 1 || X == targetPosition)
+ {
+ hyperDashModifier = 1;
+ hyperDashDirection = 0;
+
+ if (wasHyperDashing)
+ {
+ this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint);
+ this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint);
+ Trail &= Dashing;
+ }
+ }
+ else
+ {
+ hyperDashModifier = modifier;
+ hyperDashDirection = Math.Sign(targetPosition - X);
+ hyperDashTargetPosition = targetPosition;
+
+ if (!wasHyperDashing)
+ {
+ this.FadeColour(Color4.OrangeRed, hyper_dash_transition_length, Easing.OutQuint);
+ this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint);
+ Trail = true;
+
+ var hyperDashEndGlow = createAdditiveSprite();
+
+ hyperDashEndGlow.MoveToOffset(new Vector2(0, -10), 1200, Easing.In);
+ hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.95f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In);
+ hyperDashEndGlow.FadeOut(1200);
+ hyperDashEndGlow.Expire(true);
+ }
+ }
+ }
+
+ public bool OnPressed(CatchAction action)
+ {
+ switch (action)
+ {
+ case CatchAction.MoveLeft:
+ currentDirection--;
+ return true;
+
+ case CatchAction.MoveRight:
+ currentDirection++;
+ return true;
+
+ case CatchAction.Dash:
+ Dashing = true;
+ return true;
+ }
+
+ return false;
+ }
+
+ public void OnReleased(CatchAction action)
+ {
+ switch (action)
+ {
+ case CatchAction.MoveLeft:
+ currentDirection++;
+ break;
+
+ case CatchAction.MoveRight:
+ currentDirection--;
+ break;
+
+ case CatchAction.Dash:
+ Dashing = false;
+ break;
+ }
+ }
+
+ public void UpdatePosition(float position)
+ {
+ position = Math.Clamp(position, 0, 1);
+
+ if (position == X)
+ return;
+
+ Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y);
+ X = position;
+ }
+
+ ///
+ /// Drop any fruit off the plate.
+ ///
+ public void Drop()
+ {
+ foreach (var f in caughtFruit.ToArray())
+ Drop(f);
+ }
+
+ ///
+ /// Explode any fruit off the plate.
+ ///
+ public void Explode()
+ {
+ foreach (var f in caughtFruit.ToArray())
+ Explode(f);
+ }
+
+ public void Drop(DrawableHitObject fruit)
+ {
+ removeFromPlateWithTransform(fruit, f =>
+ {
+ f.MoveToY(f.Y + 75, 750, Easing.InSine);
+ f.FadeOut(750);
+ });
+ }
+
+ public void Explode(DrawableHitObject fruit)
+ {
+ var originalX = fruit.X * Scale.X;
+
+ removeFromPlateWithTransform(fruit, f =>
+ {
+ f.MoveToY(f.Y - 50, 250, Easing.OutSine).Then().MoveToY(f.Y + 50, 500, Easing.InSine);
+ f.MoveToX(f.X + originalX * 6, 1000);
+ f.FadeOut(750);
+ });
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (currentDirection == 0) return;
+
+ var direction = Math.Sign(currentDirection);
+
+ var dashModifier = Dashing ? 1 : 0.5;
+ var speed = BASE_SPEED * dashModifier * hyperDashModifier;
+
+ UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed));
+
+ // Correct overshooting.
+ if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
+ (hyperDashDirection < 0 && hyperDashTargetPosition > X))
+ {
+ X = hyperDashTargetPosition;
+ SetHyperDashState();
+ }
+ }
+
+ private void updateCatcher()
+ {
+ currentCatcher?.Hide();
+
+ switch (CurrentState)
+ {
+ default:
+ currentCatcher = catcherIdle;
+ break;
+
+ case CatcherAnimationState.Fail:
+ currentCatcher = catcherFail;
+ break;
+
+ case CatcherAnimationState.Kiai:
+ currentCatcher = catcherKiai;
+ break;
+ }
+
+ currentCatcher.Show();
+ (currentCatcher.Drawable as IAnimation)?.GotoFrame(0);
+ }
+
+ private void beginTrail()
+ {
+ if (!dashing && !HyperDashing)
+ {
+ Trail = false;
+ return;
+ }
+
+ var additive = createAdditiveSprite();
+
+ additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint);
+ additive.Expire(true);
+
+ Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50);
+ }
+
+ private void updateState(CatcherAnimationState state)
+ {
+ if (CurrentState == state)
+ return;
+
+ CurrentState = state;
+ updateCatcher();
+ }
+
+ private CatcherTrailSprite createAdditiveSprite()
+ {
+ var tex = (currentCatcher.Drawable as TextureAnimation)?.CurrentFrame ?? ((Sprite)currentCatcher.Drawable).Texture;
+
+ var sprite = new CatcherTrailSprite(tex)
+ {
+ Anchor = Anchor,
+ Scale = Scale,
+ Colour = HyperDashing ? Color4.Red : Color4.White,
+ Blending = BlendingParameters.Additive,
+ RelativePositionAxes = RelativePositionAxes,
+ Position = Position
+ };
+
+ AdditiveTarget?.Add(sprite);
+
+ return sprite;
+ }
+
+ private void removeFromPlateWithTransform(DrawableHitObject fruit, Action action)
+ {
+ if (ExplodingFruitTarget != null)
+ {
+ fruit.Anchor = Anchor.TopLeft;
+ fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
+
+ if (!caughtFruit.Remove(fruit))
+ // we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling).
+ // this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice.
+ return;
+
+ ExplodingFruitTarget.Add(fruit);
+ }
+
+ var actionTime = Clock.CurrentTime;
+
+ fruit.ApplyCustomUpdateState += onFruitOnApplyCustomUpdateState;
+ onFruitOnApplyCustomUpdateState(fruit, fruit.State.Value);
+
+ void onFruitOnApplyCustomUpdateState(DrawableHitObject o, ArmedState state)
+ {
+ using (fruit.BeginAbsoluteSequence(actionTime))
+ action(fruit);
+
+ fruit.Expire();
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs b/osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs
new file mode 100644
index 0000000000..566e9d1911
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs
@@ -0,0 +1,12 @@
+// Copyright (c) ppy Pty Ltd . 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 CatcherAnimationState
+ {
+ Idle,
+ Fail,
+ Kiai
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index 1de0b6bfa3..e0d9ff759d 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -2,22 +2,17 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Linq;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Input.Bindings;
-using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.Objects.Drawable;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI
{
@@ -25,8 +20,6 @@ namespace osu.Game.Rulesets.Catch.UI
{
public const float CATCHER_SIZE = 106.75f;
- protected internal readonly Catcher MovableCatcher;
-
public Func> CreateDrawableRepresentation;
public Container ExplodingFruitTarget
@@ -34,6 +27,8 @@ namespace osu.Game.Rulesets.Catch.UI
set => MovableCatcher.ExplodingFruitTarget = value;
}
+ private DrawableCatchHitObject lastPlateableFruit;
+
public CatcherArea(BeatmapDifficulty difficulty = null)
{
RelativeSizeAxes = Axes.X;
@@ -44,10 +39,16 @@ namespace osu.Game.Rulesets.Catch.UI
};
}
- private DrawableCatchHitObject lastPlateableFruit;
+ public static float GetCatcherSize(BeatmapDifficulty difficulty)
+ {
+ return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
+ }
public void OnResult(DrawableCatchHitObject fruit, JudgementResult result)
{
+ if (result.Judgement is IgnoreJudgement)
+ return;
+
void runAfterLoaded(Action action)
{
if (lastPlateableFruit == null)
@@ -63,6 +64,7 @@ namespace osu.Game.Rulesets.Catch.UI
if (result.IsHit && fruit.CanBePlated)
{
+ // create a new (cloned) fruit to stay on the plate. the original is faded out immediately.
var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit.HitObject);
if (caughtFruit == null) return;
@@ -73,11 +75,11 @@ namespace osu.Game.Rulesets.Catch.UI
caughtFruit.Anchor = Anchor.TopCentre;
caughtFruit.Origin = Anchor.Centre;
- caughtFruit.Scale *= 0.7f;
+ caughtFruit.Scale *= 0.5f;
caughtFruit.LifetimeStart = caughtFruit.HitObject.StartTime;
caughtFruit.LifetimeEnd = double.MaxValue;
- MovableCatcher.Add(caughtFruit);
+ MovableCatcher.PlaceOnPlate(caughtFruit);
lastPlateableFruit = caughtFruit;
if (!fruit.StaysOnPlate)
@@ -86,13 +88,22 @@ namespace osu.Game.Rulesets.Catch.UI
if (fruit.HitObject.LastInCombo)
{
- if (((CatchJudgement)result.Judgement).ShouldExplodeFor(result))
+ if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
runAfterLoaded(() => MovableCatcher.Explode());
else
MovableCatcher.Drop();
}
}
+ public void OnReleased(CatchAction action)
+ {
+ }
+
+ public bool AttemptCatch(CatchHitObject obj)
+ {
+ return MovableCatcher.AttemptCatch(obj);
+ }
+
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
@@ -103,365 +114,6 @@ namespace osu.Game.Rulesets.Catch.UI
MovableCatcher.X = state.CatcherX.Value;
}
- public void OnReleased(CatchAction action)
- {
- }
-
- public bool AttemptCatch(CatchHitObject obj) => MovableCatcher.AttemptCatch(obj);
-
- public static float GetCatcherSize(BeatmapDifficulty difficulty)
- {
- return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
- }
-
- public class Catcher : Container, IKeyBindingHandler
- {
- ///
- /// Width of the area that can be used to attempt catches during gameplay.
- ///
- internal float CatchWidth => CATCHER_SIZE * Math.Abs(Scale.X);
-
- private Container caughtFruit;
-
- public Container ExplodingFruitTarget;
-
- public Container AdditiveTarget;
-
- public Catcher(BeatmapDifficulty difficulty = null)
- {
- RelativePositionAxes = Axes.X;
- X = 0.5f;
-
- Origin = Anchor.TopCentre;
- Anchor = Anchor.TopLeft;
-
- Size = new Vector2(CATCHER_SIZE);
- if (difficulty != null)
- Scale = new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- Children = new[]
- {
- caughtFruit = new Container
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.BottomCentre,
- },
- createCatcherSprite(),
- };
- }
-
- private int currentDirection;
-
- private bool dashing;
-
- protected bool Dashing
- {
- get => dashing;
- set
- {
- if (value == dashing) return;
-
- dashing = value;
-
- Trail |= dashing;
- }
- }
-
- private bool trail;
-
- ///
- /// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met.
- ///
- protected bool Trail
- {
- get => trail;
- set
- {
- if (value == trail) return;
-
- trail = value;
-
- if (Trail)
- beginTrail();
- }
- }
-
- private void beginTrail()
- {
- Trail &= dashing || HyperDashing;
- Trail &= AdditiveTarget != null;
-
- if (!Trail) return;
-
- var additive = createCatcherSprite();
-
- additive.Anchor = Anchor;
- additive.OriginPosition += new Vector2(DrawWidth / 2, 0); // also temporary to align sprite correctly.
- additive.Position = Position;
- additive.Scale = Scale;
- additive.Colour = HyperDashing ? Color4.Red : Color4.White;
- additive.RelativePositionAxes = RelativePositionAxes;
- additive.Blending = BlendingParameters.Additive;
-
- AdditiveTarget.Add(additive);
-
- additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint);
- additive.Expire(true);
-
- Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50);
- }
-
- private Drawable createCatcherSprite() => new CatcherSprite();
-
- ///
- /// Add a caught fruit to the catcher's stack.
- ///
- /// The fruit that was caught.
- public void Add(DrawableHitObject fruit)
- {
- float ourRadius = fruit.DrawSize.X / 2 * fruit.Scale.X;
- float theirRadius = 0;
-
- const float allowance = 6;
-
- while (caughtFruit.Any(f =>
- f.LifetimeEnd == double.MaxValue &&
- Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2)))
- {
- float diff = (ourRadius + theirRadius) / allowance;
- fruit.X += (RNG.NextSingle() - 0.5f) * 2 * diff;
- fruit.Y -= RNG.NextSingle() * diff;
- }
-
- fruit.X = Math.Clamp(fruit.X, -CATCHER_SIZE / 2, CATCHER_SIZE / 2);
-
- caughtFruit.Add(fruit);
- }
-
- ///
- /// Let the catcher attempt to catch a fruit.
- ///
- /// The fruit to catch.
- /// Whether the catch is possible.
- public bool AttemptCatch(CatchHitObject fruit)
- {
- float halfCatchWidth = CatchWidth * 0.5f;
-
- // this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
- var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH;
- var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH;
-
- var validCatch =
- catchObjectPosition >= catcherPosition - halfCatchWidth &&
- catchObjectPosition <= catcherPosition + halfCatchWidth;
-
- if (validCatch && fruit.HyperDash)
- {
- var target = fruit.HyperDashTarget;
- double timeDifference = target.StartTime - fruit.StartTime;
- double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition;
- double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
-
- SetHyperDashState(Math.Abs(velocity), target.X);
- }
- else
- {
- SetHyperDashState();
- }
-
- return validCatch;
- }
-
- private double hyperDashModifier = 1;
- private int hyperDashDirection;
- private float hyperDashTargetPosition;
-
- ///
- /// Whether we are hyper-dashing or not.
- ///
- public bool HyperDashing => hyperDashModifier != 1;
-
- ///
- /// Set hyper-dash state.
- ///
- /// The speed multiplier. If this is less or equals to 1, this catcher will be non-hyper-dashing state.
- /// When this catcher crosses this position, this catcher ends hyper-dashing.
- public void SetHyperDashState(double modifier = 1, float targetPosition = -1)
- {
- const float hyper_dash_transition_length = 180;
-
- bool previouslyHyperDashing = HyperDashing;
-
- if (modifier <= 1 || X == targetPosition)
- {
- hyperDashModifier = 1;
- hyperDashDirection = 0;
-
- if (previouslyHyperDashing)
- {
- this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint);
- this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint);
- Trail &= Dashing;
- }
- }
- else
- {
- hyperDashModifier = modifier;
- hyperDashDirection = Math.Sign(targetPosition - X);
- hyperDashTargetPosition = targetPosition;
-
- if (!previouslyHyperDashing)
- {
- this.FadeColour(Color4.OrangeRed, hyper_dash_transition_length, Easing.OutQuint);
- this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint);
- Trail = true;
- }
- }
- }
-
- public bool OnPressed(CatchAction action)
- {
- switch (action)
- {
- case CatchAction.MoveLeft:
- currentDirection--;
- return true;
-
- case CatchAction.MoveRight:
- currentDirection++;
- return true;
-
- case CatchAction.Dash:
- Dashing = true;
- return true;
- }
-
- return false;
- }
-
- public void OnReleased(CatchAction action)
- {
- switch (action)
- {
- case CatchAction.MoveLeft:
- currentDirection++;
- break;
-
- case CatchAction.MoveRight:
- currentDirection--;
- break;
-
- case CatchAction.Dash:
- Dashing = false;
- break;
- }
- }
-
- ///
- /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
- ///
- public const double BASE_SPEED = 1.0 / 512;
-
- protected override void Update()
- {
- base.Update();
-
- if (currentDirection == 0) return;
-
- var direction = Math.Sign(currentDirection);
-
- double dashModifier = Dashing ? 1 : 0.5;
- double speed = BASE_SPEED * dashModifier * hyperDashModifier;
-
- UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed));
-
- // Correct overshooting.
- if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
- (hyperDashDirection < 0 && hyperDashTargetPosition > X))
- {
- X = hyperDashTargetPosition;
- SetHyperDashState();
- }
- }
-
- ///
- /// Drop any fruit off the plate.
- ///
- public void Drop()
- {
- var fruit = caughtFruit.ToArray();
-
- foreach (var f in fruit)
- {
- if (ExplodingFruitTarget != null)
- {
- f.Anchor = Anchor.TopLeft;
- f.Position = caughtFruit.ToSpaceOfOtherDrawable(f.DrawPosition, ExplodingFruitTarget);
-
- caughtFruit.Remove(f);
-
- ExplodingFruitTarget.Add(f);
- }
-
- f.MoveToY(f.Y + 75, 750, Easing.InSine);
- f.FadeOut(750);
-
- // todo: this shouldn't exist once DrawableHitObject's ClearTransformsAfter overrides are repaired.
- f.LifetimeStart = Time.Current;
- f.Expire();
- }
- }
-
- ///
- /// Explode any fruit off the plate.
- ///
- public void Explode()
- {
- foreach (var f in caughtFruit.ToArray())
- Explode(f);
- }
-
- public void Explode(DrawableHitObject fruit)
- {
- var originalX = fruit.X * Scale.X;
-
- if (ExplodingFruitTarget != null)
- {
- fruit.Anchor = Anchor.TopLeft;
- fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
-
- if (!caughtFruit.Remove(fruit))
- // we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling).
- // this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice.
- return;
-
- ExplodingFruitTarget.Add(fruit);
- }
-
- fruit.ClearTransforms();
- fruit.MoveToY(fruit.Y - 50, 250, Easing.OutSine).Then().MoveToY(fruit.Y + 50, 500, Easing.InSine);
- fruit.MoveToX(fruit.X + originalX * 6, 1000);
- fruit.FadeOut(750);
-
- // todo: this shouldn't exist once DrawableHitObject's ClearTransformsAfter overrides are repaired.
- fruit.LifetimeStart = Time.Current;
- fruit.Expire();
- }
-
- public void UpdatePosition(float position)
- {
- position = Math.Clamp(position, 0, 1);
-
- if (position == X)
- return;
-
- Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y);
- X = position;
- }
- }
+ protected internal readonly Catcher MovableCatcher;
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
index 025fa9c56e..52eb8d597e 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
@@ -3,31 +3,57 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
- public class CatcherSprite : CompositeDrawable
+ public class CatcherSprite : SkinnableDrawable
{
- public CatcherSprite()
+ protected override bool ApplySizeRestrictionsToDefault => true;
+
+ public CatcherSprite(CatcherAnimationState state)
+ : base(new CatchSkinComponent(componentFromState(state)), _ =>
+ new DefaultCatcherSprite(state), confineMode: ConfineMode.ScaleDownToFit)
{
+ RelativeSizeAxes = Axes.None;
Size = new Vector2(CatcherArea.CATCHER_SIZE);
// Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
- OriginPosition = new Vector2(-0.02f, 0.06f) * CatcherArea.CATCHER_SIZE;
+ OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE;
}
- [BackgroundDependencyLoader]
- private void load()
+ private static CatchSkinComponents componentFromState(CatcherAnimationState state)
{
- InternalChild = new SkinnableSprite("Gameplay/catch/fruit-catcher-idle", confineMode: ConfineMode.ScaleDownToFit)
+ switch (state)
{
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- };
+ case CatcherAnimationState.Fail:
+ return CatchSkinComponents.CatcherFail;
+
+ case CatcherAnimationState.Kiai:
+ return CatchSkinComponents.CatcherKiai;
+
+ default:
+ return CatchSkinComponents.CatcherIdle;
+ }
+ }
+
+ private class DefaultCatcherSprite : Sprite
+ {
+ private readonly CatcherAnimationState state;
+
+ public DefaultCatcherSprite(CatcherAnimationState state)
+ {
+ this.state = state;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ Texture = textures.Get($"Gameplay/catch/fruit-catcher-{state.ToString().ToLower()}");
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs
new file mode 100644
index 0000000000..56cb7dbfda
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ public class CatcherTrailSprite : Sprite
+ {
+ public CatcherTrailSprite(Texture texture)
+ {
+ Texture = texture;
+
+ Size = new Vector2(CatcherArea.CATCHER_SIZE);
+
+ // 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;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
index fdd820b891..fd8a1d175d 100644
--- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
@@ -8,7 +8,7 @@ using osu.Game.Configuration;
using osu.Game.Input.Handlers;
using osu.Game.Replays;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.Objects.Drawable;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
new file mode 100644
index 0000000000..04a86f83be
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
@@ -0,0 +1,122 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ public class HitExplosion : CompositeDrawable
+ {
+ private readonly CircularContainer largeFaint;
+
+ public HitExplosion(DrawableCatchHitObject fruit)
+ {
+ Size = new Vector2(20);
+ Anchor = Anchor.TopCentre;
+ Origin = Anchor.BottomCentre;
+
+ Color4 objectColour = fruit.AccentColour.Value;
+
+ // scale roughly in-line with visual appearance of notes
+
+ const float angle_variangle = 15; // should be less than 45
+
+ const float roundness = 100;
+
+ const float initial_height = 10;
+
+ var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1);
+
+ InternalChildren = new Drawable[]
+ {
+ largeFaint = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ // we want our size to be very small so the glow dominates it.
+ Size = new Vector2(0.8f),
+ Blending = BlendingParameters.Additive,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
+ Roundness = 160,
+ Radius = 200,
+ },
+ },
+ new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Blending = BlendingParameters.Additive,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
+ Roundness = 20,
+ Radius = 50,
+ },
+ },
+ new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Size = new Vector2(0.01f, initial_height),
+ Blending = BlendingParameters.Additive,
+ Rotation = RNG.NextSingle(-angle_variangle, angle_variangle),
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = colour,
+ Roundness = roundness,
+ Radius = 40,
+ },
+ },
+ new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Size = new Vector2(0.01f, initial_height),
+ Blending = BlendingParameters.Additive,
+ Rotation = RNG.NextSingle(-angle_variangle, angle_variangle),
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = colour,
+ Roundness = roundness,
+ Radius = 40,
+ },
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ const double duration = 400;
+
+ largeFaint
+ .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
+ .FadeOut(duration * 2);
+
+ this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out);
+ Expire(true);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs
new file mode 100644
index 0000000000..607d42a1bb
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs
@@ -0,0 +1,26 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Rulesets.Mania.Mods;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests.Mods
+{
+ public class TestSceneManiaModPerfect : ModPerfectTestScene
+ {
+ public TestSceneManiaModPerfect()
+ : base(new ManiaRuleset(), new ManiaModPerfect())
+ {
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Note { StartTime = 1000 }), shouldMiss);
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/SkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/SkinnableTestScene.cs
deleted file mode 100644
index 80b1b3df8e..0000000000
--- a/osu.Game.Rulesets.Mania.Tests/SkinnableTestScene.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using osu.Framework.Allocation;
-using osu.Framework.Audio;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Skinning;
-using osu.Game.Tests.Visual;
-
-namespace osu.Game.Rulesets.Mania.Tests
-{
- public abstract class SkinnableTestScene : OsuGridTestScene
- {
- private Skin defaultSkin;
-
- protected SkinnableTestScene()
- : base(1, 2)
- {
- }
-
- [BackgroundDependencyLoader]
- private void load(AudioManager audio, SkinManager skinManager)
- {
- defaultSkin = skinManager.GetSkin(DefaultLegacySkin.Info);
- }
-
- public void SetContents(Func creationFunction)
- {
- Cell(0).Child = createProvider(null, creationFunction);
- Cell(1).Child = createProvider(defaultSkin, creationFunction);
- }
-
- private Drawable createProvider(Skin skin, Func creationFunction)
- {
- var mainProvider = new SkinProvidingContainer(skin);
-
- return mainProvider
- .WithChild(new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider))
- {
- Child = creationFunction()
- });
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs
index eea1a36a19..692d079c16 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
+using osu.Game.Tests.Visual;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index dea6e6c0fb..6855b99f28 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
index bcbc1ee527..7bbde400ea 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.UpdatePosition(screenSpacePosition);
- if (PlacementBegun)
+ if (PlacementActive)
{
var endTime = TimeAt(screenSpacePosition);
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
index 7a3b42914e..a3657d3bb9 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
@@ -56,13 +56,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
protected override void OnMouseUp(MouseUpEvent e)
{
- EndPlacement();
+ EndPlacement(true);
base.OnMouseUp(e);
}
public override void UpdatePosition(Vector2 screenSpacePosition)
{
- if (!PlacementBegun)
+ if (!PlacementActive)
Column = ColumnAt(screenSpacePosition);
if (Column == null) return;
diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteJudgement.cs
deleted file mode 100644
index e8b48768a1..0000000000
--- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteJudgement.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Rulesets.Scoring;
-
-namespace osu.Game.Rulesets.Mania.Judgements
-{
- public class HoldNoteJudgement : ManiaJudgement
- {
- public override bool AffectsCombo => false;
-
- protected override int NumericResultFor(HitResult result) => 0;
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
index 6893e1e73b..86a00271e9 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
@@ -3,8 +3,8 @@
using System;
using osu.Framework.Bindables;
-using osu.Framework.Caching;
using osu.Framework.Graphics;
+using osu.Framework.Layout;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
using osuTK;
@@ -22,21 +22,13 @@ namespace osu.Game.Rulesets.Mania.Mods
private class ManiaFlashlight : Flashlight
{
- private readonly Cached flashlightProperties = new Cached();
+ private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize);
public ManiaFlashlight()
{
FlashlightSize = new Vector2(0, default_flashlight_size);
- }
- public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true)
- {
- if ((invalidation & Invalidation.DrawSize) > 0)
- {
- flashlightProperties.Invalidate();
- }
-
- return base.Invalidate(invalidation, source, shallPropagate);
+ AddLayout(flashlightProperties);
}
protected override void Update()
diff --git a/osu.Game.Rulesets.Mania/Objects/BarLine.cs b/osu.Game.Rulesets.Mania/Objects/BarLine.cs
index 0981b028b2..09a746042b 100644
--- a/osu.Game.Rulesets.Mania/Objects/BarLine.cs
+++ b/osu.Game.Rulesets.Mania/Objects/BarLine.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Objects
@@ -8,5 +9,7 @@ namespace osu.Game.Rulesets.Mania.Objects
public class BarLine : ManiaHitObject, IBarLine
{
public bool Major { get; set; }
+
+ public override Judgement CreateJudgement() => new IgnoreJudgement();
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
index 56bc797c7f..08b5b75f9c 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
@@ -71,8 +71,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
}
- protected override void UpdateStateTransforms(ArmedState state)
- {
- }
+ protected override void UpdateStateTransforms(ArmedState state) => this.FadeOut(150);
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs
index 31a4857805..43f9ae2783 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs
@@ -2,13 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osu.Framework.Caching;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Layout;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
@@ -65,6 +65,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
}
}
};
+
+ AddLayout(subtractionCache);
}
protected override void LoadComplete()
@@ -100,15 +102,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
}
}
- private readonly Cached subtractionCache = new Cached();
-
- public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true)
- {
- if ((invalidation & Invalidation.DrawSize) > 0)
- subtractionCache.Invalidate();
-
- return base.Invalidate(invalidation, source, shallPropagate);
- }
+ private readonly LayoutValue subtractionCache = new LayoutValue(Invalidation.DrawSize);
protected override void Update()
{
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
index bdba813eed..049bf55f90 100644
--- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
@@ -4,7 +4,6 @@
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@@ -15,7 +14,11 @@ namespace osu.Game.Rulesets.Mania.Objects
///
public class HoldNote : ManiaHitObject, IHasEndTime
{
- public double EndTime => StartTime + Duration;
+ public double EndTime
+ {
+ get => StartTime + Duration;
+ set => Duration = value - StartTime;
+ }
private double duration;
@@ -99,7 +102,7 @@ namespace osu.Game.Rulesets.Mania.Objects
}
}
- public override Judgement CreateJudgement() => new HoldNoteJudgement();
+ public override Judgement CreateJudgement() => new IgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs
index a28de7ea58..bfe9f1085b 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs
@@ -115,9 +115,8 @@ namespace osu.Game.Rulesets.Mania.UI
{
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
- AutoSizeAxes = Axes.Both,
+ RelativeSizeAxes = Axes.Both,
Y = HIT_TARGET_POSITION + 150,
- BypassAutoSizeAxes = Axes.Both
},
topLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs
new file mode 100644
index 0000000000..69415b70e3
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs
@@ -0,0 +1,86 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModDifficultyAdjust : ModTestScene
+ {
+ public TestSceneOsuModDifficultyAdjust()
+ : base(new OsuRuleset())
+ {
+ }
+
+ [Test]
+ public void TestNoAdjustment() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModDifficultyAdjust(),
+ Autoplay = true,
+ PassCondition = checkSomeHit
+ });
+
+ [Test]
+ public void TestCircleSize1() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModDifficultyAdjust { CircleSize = { Value = 1 } },
+ Autoplay = true,
+ PassCondition = () => checkSomeHit() && checkObjectsScale(0.78f)
+ });
+
+ [Test]
+ public void TestCircleSize10() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModDifficultyAdjust { CircleSize = { Value = 10 } },
+ Autoplay = true,
+ PassCondition = () => checkSomeHit() && checkObjectsScale(0.15f)
+ });
+
+ [Test]
+ public void TestApproachRate1() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModDifficultyAdjust { ApproachRate = { Value = 1 } },
+ Autoplay = true,
+ PassCondition = () => checkSomeHit() && checkObjectsPreempt(1680)
+ });
+
+ [Test]
+ public void TestApproachRate10() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } },
+ Autoplay = true,
+ PassCondition = () => checkSomeHit() && checkObjectsPreempt(450)
+ });
+
+ private bool checkObjectsPreempt(double target)
+ {
+ var objects = Player.ChildrenOfType();
+ if (!objects.Any())
+ return false;
+
+ return objects.All(o => o.HitObject.TimePreempt == target);
+ }
+
+ private bool checkObjectsScale(float target)
+ {
+ var objects = Player.ChildrenOfType();
+ if (!objects.Any())
+ return false;
+
+ return objects.All(o => Precision.AlmostEquals(o.ChildrenOfType().First().Children.OfType().Single().Scale.X, target));
+ }
+
+ private bool checkSomeHit()
+ {
+ return Player.ScoreProcessor.JudgedHits >= 2;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs
new file mode 100644
index 0000000000..dcf19ad993
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModDoubleTime : ModTestScene
+ {
+ public TestSceneOsuModDoubleTime()
+ : base(new OsuRuleset())
+ {
+ }
+
+ [TestCase(0.5)]
+ [TestCase(1.01)]
+ [TestCase(1.5)]
+ [TestCase(2)]
+ [TestCase(5)]
+ public void TestSpeedChangeCustomisation(double rate)
+ {
+ var mod = new OsuModDoubleTime { SpeedChange = { Value = rate } };
+
+ CreateModTest(new ModTestData
+ {
+ Mod = mod,
+ PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 &&
+ Precision.AlmostEquals(Player.GameplayClockContainer.GameplayClock.Rate, mod.SpeedChange.Value)
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs
new file mode 100644
index 0000000000..b03a894085
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs
@@ -0,0 +1,52 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModPerfect : ModPerfectTestScene
+ {
+ public TestSceneOsuModPerfect()
+ : base(new OsuRuleset(), new OsuModPerfect())
+ {
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestHitCircle(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HitCircle { StartTime = 1000 }), shouldMiss);
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestSlider(bool shouldMiss)
+ {
+ var slider = new Slider
+ {
+ StartTime = 1000,
+ Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
+ };
+
+ CreateHitObjectTest(new HitObjectTestData(slider), shouldMiss);
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestSpinner(bool shouldMiss)
+ {
+ var spinner = new Spinner
+ {
+ StartTime = 1000,
+ EndTime = 3000,
+ Position = new Vector2(256, 192)
+ };
+
+ CreateHitObjectTest(new HitObjectTestData(spinner), shouldMiss);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs
deleted file mode 100644
index d4c3000d3f..0000000000
--- a/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Text.RegularExpressions;
-using osu.Framework.Allocation;
-using osu.Framework.Audio;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Textures;
-using osu.Framework.IO.Stores;
-using osu.Game.Skinning;
-using osu.Game.Tests.Visual;
-
-namespace osu.Game.Rulesets.Osu.Tests
-{
- public abstract class SkinnableTestScene : OsuGridTestScene
- {
- private Skin metricsSkin;
- private Skin defaultSkin;
- private Skin specialSkin;
- private Skin oldSkin;
-
- protected SkinnableTestScene()
- : base(2, 3)
- {
- }
-
- [BackgroundDependencyLoader]
- private void load(AudioManager audio, SkinManager skinManager)
- {
- var dllStore = new DllResourceStore(typeof(SkinnableTestScene).Assembly);
-
- metricsSkin = new TestLegacySkin(new SkinInfo(), new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), audio, true);
- defaultSkin = skinManager.GetSkin(DefaultLegacySkin.Info);
- specialSkin = new TestLegacySkin(new SkinInfo(), new NamespacedResourceStore(dllStore, "Resources/special_skin"), audio, true);
- oldSkin = new TestLegacySkin(new SkinInfo(), new NamespacedResourceStore(dllStore, "Resources/old_skin"), audio, true);
- }
-
- public void SetContents(Func creationFunction)
- {
- Cell(0).Child = createProvider(null, creationFunction);
- Cell(1).Child = createProvider(metricsSkin, creationFunction);
- Cell(2).Child = createProvider(defaultSkin, creationFunction);
- Cell(3).Child = createProvider(specialSkin, creationFunction);
- Cell(4).Child = createProvider(oldSkin, creationFunction);
- }
-
- private Drawable createProvider(Skin skin, Func creationFunction)
- {
- var mainProvider = new SkinProvidingContainer(skin);
-
- return mainProvider
- .WithChild(new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider))
- {
- Child = creationFunction()
- });
- }
-
- private class TestLegacySkin : LegacySkin
- {
- private readonly bool extrapolateAnimations;
-
- public TestLegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager, bool extrapolateAnimations)
- : base(skin, storage, audioManager, "skin.ini")
- {
- this.extrapolateAnimations = extrapolateAnimations;
- }
-
- public override Texture GetTexture(string componentName)
- {
- // extrapolate frames to test longer animations
- if (extrapolateAnimations)
- {
- var match = Regex.Match(componentName, "-([0-9]*)");
-
- if (match.Length > 0 && int.TryParse(match.Groups[1].Value, out var number) && number < 60)
- return base.GetTexture(componentName.Replace($"-{number}", $"-{number % 2}"));
- }
-
- return base.GetTexture(componentName);
- }
- }
- }
-}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
index ac627aa23e..02d4406809 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
@@ -10,6 +10,7 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
index 94ca2d4cd1..87da7ef417 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
@@ -2,9 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Objects;
@@ -114,6 +117,22 @@ namespace osu.Game.Rulesets.Osu.Tests
assertGroups();
}
+ [Test]
+ public void TestStackedObjects()
+ {
+ addObjectsStep(() => new OsuHitObject[]
+ {
+ new HitCircle { Position = new Vector2(300, 100) },
+ new HitCircle
+ {
+ Position = new Vector2(300, 300),
+ StackHeight = 20
+ },
+ });
+
+ assertDirections();
+ }
+
private void addMultipleObjectsStep() => addObjectsStep(() => new OsuHitObject[]
{
new HitCircle { Position = new Vector2(100, 100) },
@@ -207,6 +226,33 @@ namespace osu.Game.Rulesets.Osu.Tests
});
}
+ private void assertDirections()
+ {
+ AddAssert("group directions are correct", () =>
+ {
+ for (int i = 0; i < hitObjectContainer.Count; i++)
+ {
+ DrawableOsuHitObject expectedStart = getObject(i);
+ DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null;
+
+ if (expectedEnd == null)
+ continue;
+
+ var points = getGroup(i).ChildrenOfType().ToArray();
+ if (points.Length == 0)
+ continue;
+
+ float expectedDirection = MathF.Atan2(expectedStart.Position.Y - expectedEnd.Position.Y, expectedStart.Position.X - expectedEnd.Position.X);
+ float realDirection = MathF.Atan2(expectedStart.Position.Y - points[^1].Position.Y, expectedStart.Position.X - points[^1].Position.X);
+
+ if (!Precision.AlmostEquals(expectedDirection, realDirection))
+ throw new AssertionException($"Expected group {i} in direction {expectedDirection}, but was {realDirection}.");
+ }
+
+ return true;
+ });
+ }
+
private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index];
private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index];
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index aa170eae1e..7b96e2ec6a 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -7,7 +7,10 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing.Input;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.UI.Cursor;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
@@ -21,12 +24,50 @@ namespace osu.Game.Rulesets.Osu.Tests
typeof(CursorTrail)
};
- [BackgroundDependencyLoader]
- private void load()
+ [Cached]
+ private GameplayBeatmap gameplayBeatmap;
+
+ private ClickingCursorContainer lastContainer;
+
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
+ public TestSceneGameplayCursor()
+ {
+ gameplayBeatmap = new GameplayBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
+ }
+
+ [TestCase(1, 1)]
+ [TestCase(5, 1)]
+ [TestCase(10, 1)]
+ [TestCase(1, 1.5f)]
+ [TestCase(5, 1.5f)]
+ [TestCase(10, 1.5f)]
+ public void TestSizing(int circleSize, float userScale)
+ {
+ AddStep($"set user scale to {userScale}", () => config.Set(OsuSetting.GameplayCursorSize, userScale));
+ AddStep($"adjust cs to {circleSize}", () => gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize);
+ AddStep("turn on autosizing", () => config.Set(OsuSetting.AutoCursorSize, true));
+
+ AddStep("load content", loadContent);
+
+ AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);
+
+ AddStep("set user scale to 1", () => config.Set(OsuSetting.GameplayCursorSize, 1f));
+ AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize));
+
+ AddStep("turn off autosizing", () => config.Set(OsuSetting.AutoCursorSize, false));
+ AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == 1);
+
+ AddStep($"set user scale to {userScale}", () => config.Set(OsuSetting.GameplayCursorSize, userScale));
+ AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == userScale);
+ }
+
+ private void loadContent()
{
SetContents(() => new MovingCursorInputManager
{
- Child = new ClickingCursorContainer
+ Child = lastContainer = new ClickingCursorContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
index 098e277fff..ae5a28217c 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Mods;
using System.Linq;
using NUnit.Framework;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs
new file mode 100644
index 0000000000..67b6dac787
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs
@@ -0,0 +1,108 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneHitCircleArea : ManualInputManagerTestScene
+ {
+ private HitCircle hitCircle;
+ private DrawableHitCircle drawableHitCircle;
+ private DrawableHitCircle.HitReceptor hitAreaReceptor => drawableHitCircle.HitArea;
+
+ [SetUp]
+ public new void SetUp()
+ {
+ base.SetUp();
+
+ Schedule(() =>
+ {
+ hitCircle = new HitCircle
+ {
+ Position = new Vector2(100, 100),
+ StartTime = Time.Current + 500
+ };
+
+ hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ Child = new SkinProvidingContainer(new DefaultSkin())
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = drawableHitCircle = new DrawableHitCircle(hitCircle)
+ {
+ Size = new Vector2(100)
+ }
+ };
+ });
+ }
+
+ [Test]
+ public void TestCircleHitCentre()
+ {
+ AddStep("move mouse to centre", () => InputManager.MoveMouseTo(hitAreaReceptor.ScreenSpaceDrawQuad.Centre));
+ scheduleHit();
+
+ AddAssert("hit registered", () => hitAreaReceptor.HitAction == OsuAction.LeftButton);
+ }
+
+ [Test]
+ public void TestCircleHitLeftEdge()
+ {
+ AddStep("move mouse to left edge", () =>
+ {
+ var drawQuad = hitAreaReceptor.ScreenSpaceDrawQuad;
+ var mousePosition = new Vector2(drawQuad.TopLeft.X, drawQuad.Centre.Y);
+
+ InputManager.MoveMouseTo(mousePosition);
+ });
+ scheduleHit();
+
+ AddAssert("hit registered", () => hitAreaReceptor.HitAction == OsuAction.LeftButton);
+ }
+
+ [TestCase(0.95f, OsuAction.LeftButton)]
+ [TestCase(1.05f, null)]
+ public void TestHitsCloseToEdge(float relativeDistanceFromCentre, OsuAction? expectedAction)
+ {
+ AddStep("move mouse to top left circle edge", () =>
+ {
+ var drawQuad = hitAreaReceptor.ScreenSpaceDrawQuad;
+ // sqrt(2) / 2 = sin(45deg) = cos(45deg)
+ // draw width halved to get radius
+ float correction = relativeDistanceFromCentre * (float)Math.Sqrt(2) / 2 * (drawQuad.Width / 2);
+ var mousePosition = new Vector2(drawQuad.Centre.X - correction, drawQuad.Centre.Y - correction);
+
+ InputManager.MoveMouseTo(mousePosition);
+ });
+ scheduleHit();
+
+ AddAssert($"hit {(expectedAction == null ? "not " : string.Empty)}registered", () => hitAreaReceptor.HitAction == expectedAction);
+ }
+
+ [Test]
+ public void TestCircleMissBoundingBoxCorner()
+ {
+ AddStep("move mouse to top left corner of bounding box", () => InputManager.MoveMouseTo(hitAreaReceptor.ScreenSpaceDrawQuad.TopLeft));
+ scheduleHit();
+
+ AddAssert("hit not registered", () => hitAreaReceptor.HitAction == null);
+ }
+
+ private void scheduleHit() => AddStep("schedule action", () =>
+ {
+ var delay = hitCircle.StartTime - hitCircle.HitWindows.WindowFor(HitResult.Great) - Time.Current;
+ Scheduler.AddDelayed(() => hitAreaReceptor.OnPressed(OsuAction.LeftButton), delay);
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs
index 4676f14655..3ff37c4147 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs
@@ -7,12 +7,10 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
-using osu.Framework.Graphics;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Screens;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
@@ -21,7 +19,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Tests
{
- public class TestSceneLegacyBeatmapSkin : OsuTestScene
+ public class TestSceneLegacyBeatmapSkin : ScreenTestScene
{
[Resolved]
private AudioManager audio { get; set; }
@@ -65,7 +63,8 @@ namespace osu.Game.Rulesets.Osu.Tests
ExposedPlayer player;
Beatmap.Value = new CustomSkinWorkingBeatmap(audio, beatmapHasColours);
- Child = new OsuScreenStack(player = new ExposedPlayer(userHasCustomColours)) { RelativeSizeAxes = Axes.Both };
+
+ LoadScreen(player = new ExposedPlayer(userHasCustomColours));
return player;
}
@@ -90,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public IReadOnlyList UsableComboColours =>
GameplayClockContainer.ChildrenOfType()
.First()
- .GetConfig>(GlobalSkinConfiguration.ComboColours)?.Value;
+ .GetConfig>(GlobalSkinColours.ComboColours)?.Value;
}
private class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
new file mode 100644
index 0000000000..5f3596976d
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
@@ -0,0 +1,101 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Replays;
+using osu.Game.Rulesets.Osu.Scoring;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Tests.Visual;
+using osu.Game.Users;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneMissHitWindowJudgements : ModTestScene
+ {
+ public TestSceneMissHitWindowJudgements()
+ : base(new OsuRuleset())
+ {
+ }
+
+ [Test]
+ public void TestMissViaEarlyHit()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects = { new HitCircle { Position = new Vector2(256, 192) } }
+ };
+
+ var hitWindows = new OsuHitWindows();
+ hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
+
+ CreateModTest(new ModTestData
+ {
+ Autoplay = false,
+ Mod = new TestAutoMod(),
+ Beatmap = new Beatmap
+ {
+ HitObjects = { new HitCircle { Position = new Vector2(256, 192) } }
+ },
+ PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < -hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss
+ });
+ }
+
+ [Test]
+ public void TestMissViaNotHitting()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects = { new HitCircle { Position = new Vector2(256, 192) } }
+ };
+
+ var hitWindows = new OsuHitWindows();
+ hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
+
+ CreateModTest(new ModTestData
+ {
+ Autoplay = false,
+ Beatmap = beatmap,
+ PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset >= hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss
+ });
+ }
+
+ private class TestAutoMod : OsuModAutoplay
+ {
+ public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ {
+ ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } },
+ Replay = new MissingAutoGenerator(beatmap).Generate()
+ };
+ }
+
+ private class MissingAutoGenerator : OsuAutoGeneratorBase
+ {
+ public new OsuBeatmap Beatmap => (OsuBeatmap)base.Beatmap;
+
+ public MissingAutoGenerator(IBeatmap beatmap)
+ : base(beatmap)
+ {
+ }
+
+ public override Replay Generate()
+ {
+ AddFrameToReplay(new OsuReplayFrame(-100000, new Vector2(256, 500)));
+ AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, new Vector2(256, 500)));
+ AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, new Vector2(256, 500)));
+
+ AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 450, Beatmap.HitObjects[0].StackedPosition));
+ AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 350, Beatmap.HitObjects[0].StackedPosition, OsuAction.LeftButton));
+ AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 325, Beatmap.HitObjects[0].StackedPosition));
+
+ return Replay;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs
index 412effe176..19736a7709 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs
@@ -3,13 +3,13 @@
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
-using osu.Game.Screens.Play;
+using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneOsuFlashlight : TestSceneOsuPlayer
{
- protected override Player CreatePlayer(Ruleset ruleset)
+ protected override TestPlayer CreatePlayer(Ruleset ruleset)
{
SelectedMods.Value = new Mod[] { new OsuModAutoplay(), new OsuModFlashlight(), };
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
index 4da1b1dae0..d39e24fc1f 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
@@ -18,7 +18,6 @@ using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
@@ -56,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void checkNextHitObject(string skin) =>
AddUntilStep($"check skin from {skin}", () =>
{
- var firstObject = ((TestPlayer)Player).DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.OfType().FirstOrDefault();
+ var firstObject = Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.OfType().FirstOrDefault();
if (firstObject == null)
return false;
@@ -75,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved]
private AudioManager audio { get; set; }
- protected override Player CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(testUserSkin);
+ protected override TestPlayer CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(testUserSkin);
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, audio, testBeatmapSkin);
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index e8386363be..defd3a6f22 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -22,6 +22,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
+using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
index b6fc9821a4..94df239267 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
@@ -44,6 +44,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private const double time_during_slide_2 = 3000;
private const double time_during_slide_3 = 3500;
private const double time_during_slide_4 = 3800;
+ private const double time_slider_end = 4000;
private List judgementResults;
private bool allJudgedFired;
@@ -284,6 +285,48 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("Tracking acquired", assertMidSliderJudgements);
}
+ ///
+ /// Scenario:
+ /// - Press a key on the slider head
+ /// - While holding the key, move cursor close to the edge of tracking area
+ /// - Keep the cursor on the edge of tracking area until the slider ends
+ /// Expected Result:
+ /// A passing test case will have the slider track the cursor throughout the whole test.
+ ///
+ [Test]
+ public void TestTrackingAreaEdge()
+ {
+ performTest(new List
+ {
+ new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_slider_start },
+ new OsuReplayFrame { Position = new Vector2(0, OsuHitObject.OBJECT_RADIUS * 1.19f), Actions = { OsuAction.LeftButton }, Time = time_slider_start + 250 },
+ new OsuReplayFrame { Position = new Vector2(slider_path_length, OsuHitObject.OBJECT_RADIUS * 1.199f), Actions = { OsuAction.LeftButton }, Time = time_slider_end },
+ });
+
+ AddAssert("Tracking kept", assertGreatJudge);
+ }
+
+ ///
+ /// Scenario:
+ /// - Press a key on the slider head
+ /// - While holding the key, move cursor just outside the tracking area
+ /// - Keep the cursor just outside the tracking area until the slider ends
+ /// Expected Result:
+ /// A passing test case will have the slider drop the tracking on frame 2.
+ ///
+ [Test]
+ public void TestTrackingAreaOutsideEdge()
+ {
+ performTest(new List
+ {
+ new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_slider_start },
+ new OsuReplayFrame { Position = new Vector2(0, OsuHitObject.OBJECT_RADIUS * 1.21f), Actions = { OsuAction.LeftButton }, Time = time_slider_start + 250 },
+ new OsuReplayFrame { Position = new Vector2(slider_path_length, OsuHitObject.OBJECT_RADIUS * 1.201f), Actions = { OsuAction.LeftButton }, Time = time_slider_end },
+ });
+
+ AddAssert("Tracking dropped", assertMidSliderJudgementFail);
+ }
+
private bool assertGreatJudge() => judgementResults.Last().Type == HitResult.Great;
private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.Great && judgementResults.First().Type == HitResult.Miss;
@@ -294,6 +337,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private ScoreAccessibleReplayPlayer currentPlayer;
+ private const float slider_path_length = 25;
+
private void performTest(List frames)
{
AddStep("load player", () =>
@@ -309,8 +354,8 @@ namespace osu.Game.Rulesets.Osu.Tests
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
- new Vector2(25, 0),
- }, 25),
+ new Vector2(slider_path_length, 0),
+ }, slider_path_length),
}
},
BeatmapInfo =
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index 5cf571d961..ea006ec607 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -11,7 +11,6 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osu.Game.Tests.Visual;
using osuTK;
using System.Collections.Generic;
using System.Linq;
@@ -44,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests
base.SetUpSteps();
AddUntilStep("wait for track to start running", () => track.IsRunning);
- AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)((TestPlayer)Player).DrawableRuleset.Playfield.AllHitObjects.First());
+ AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First());
}
[Test]
@@ -89,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
AddStep($"seek to {time}", () => track.Seek(time));
- AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, ((TestPlayer)Player).DrawableRuleset.FrameStableClock.CurrentTime, 100));
+ AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 1d8c4708c1..217707b180 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -2,9 +2,9 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
index bb47c7e464..407f5f540e 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
@@ -30,12 +30,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
protected override bool OnClick(ClickEvent e)
{
- EndPlacement();
+ EndPlacement(true);
return true;
}
public override void UpdatePosition(Vector2 screenSpacePosition)
{
+ BeginPlacement();
HitObject.Position = ToLocalSpace(screenSpacePosition);
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index 90512849d4..a780653796 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -68,6 +68,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
switch (state)
{
case PlacementState.Initial:
+ BeginPlacement();
HitObject.Position = ToLocalSpace(screenSpacePosition);
break;
@@ -125,14 +126,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void beginCurve()
{
- BeginPlacement();
+ BeginPlacement(commitStart: true);
setState(PlacementState.Body);
}
private void endCurve()
{
updateSlider();
- EndPlacement();
+ EndPlacement(true);
}
protected override void Update()
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
index 5525b8936e..74b563d922 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
@@ -8,6 +9,7 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
{
@@ -29,22 +31,31 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
{
base.Update();
+ if (isPlacingEnd)
+ HitObject.EndTime = Math.Max(HitObject.StartTime, EditorClock.CurrentTime);
+
piece.UpdateFrom(HitObject);
}
- protected override bool OnClick(ClickEvent e)
+ protected override bool OnMouseDown(MouseDownEvent e)
{
if (isPlacingEnd)
{
+ if (e.Button != MouseButton.Right)
+ return false;
+
HitObject.EndTime = EditorClock.CurrentTime;
- EndPlacement();
+ EndPlacement(true);
}
else
{
- isPlacingEnd = true;
+ if (e.Button != MouseButton.Left)
+ return false;
+
+ BeginPlacement(commitStart: true);
piece.FadeTo(1f, 150, Easing.OutQuint);
- BeginPlacement();
+ isPlacingEnd = true;
}
return true;
diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs
index 22b4c3e82e..a8719e0aa8 100644
--- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs
+++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs
@@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.Edit
if (existing == null)
return;
+ hitObject.RemoveTransform(existing);
+
using (hitObject.BeginAbsoluteSequence(existing.StartTime))
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
break;
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index b01488e7c2..cdf78a5902 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -36,6 +36,9 @@ namespace osu.Game.Rulesets.Osu.Edit
protected override DistanceSnapGrid CreateDistanceSnapGrid(IEnumerable selectedHitObjects)
{
+ if (BlueprintContainer.CurrentTool is SpinnerCompositionTool)
+ return null;
+
var objects = selectedHitObjects.ToList();
if (objects.Count == 0)
@@ -89,6 +92,9 @@ namespace osu.Game.Rulesets.Osu.Edit
targetIndex++;
}
+ if (sourceObject is Spinner)
+ return null;
+
return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject);
}
}
diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSliderTailJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSliderTailJudgement.cs
deleted file mode 100644
index 5104d9494b..0000000000
--- a/osu.Game.Rulesets.Osu/Judgements/OsuSliderTailJudgement.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Rulesets.Scoring;
-
-namespace osu.Game.Rulesets.Osu.Judgements
-{
- public class OsuSliderTailJudgement : OsuJudgement
- {
- public override bool AffectsCombo => false;
-
- protected override int NumericResultFor(HitResult result) => 0;
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
index 7eee71be81..75de6896a3 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
@@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModDifficultyAdjust : ModDifficultyAdjust
{
- [SettingSource("Circle Size", "Override a beatmap's set CS.")]
+ [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
public BindableNumber CircleSize { get; } = new BindableFloat
{
Precision = 0.1f,
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
Value = 5,
};
- [SettingSource("Approach Rate", "Override a beatmap's set AR.")]
+ [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
public BindableNumber ApproachRate { get; } = new BindableFloat
{
Precision = 0.1f,
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
index 649b01c132..6286c80d7c 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
@@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI;
using static osu.Game.Input.Handlers.ReplayInputHandler;
@@ -19,69 +20,18 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
- public void Update(Playfield playfield)
- {
- bool requiresHold = false;
- bool requiresHit = false;
+ ///
+ /// How early before a hitobject's start time to trigger a hit.
+ ///
+ private const float relax_leniency = 3;
- const float relax_leniency = 3;
-
- foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
- {
- if (!(drawable is DrawableOsuHitObject osuHit))
- continue;
-
- double time = osuHit.Clock.CurrentTime;
- double relativetime = time - osuHit.HitObject.StartTime;
-
- if (time < osuHit.HitObject.StartTime - relax_leniency) continue;
-
- if ((osuHit.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime) || osuHit.IsHit)
- continue;
-
- if (osuHit is DrawableHitCircle && osuHit.IsHovered)
- {
- Debug.Assert(osuHit.HitObject.HitWindows != null);
- requiresHit |= osuHit.HitObject.HitWindows.CanBeHit(relativetime);
- }
-
- requiresHold |= (osuHit is DrawableSlider slider && (slider.Ball.IsHovered || osuHit.IsHovered)) || osuHit is DrawableSpinner;
- }
-
- if (requiresHit)
- {
- addAction(false);
- addAction(true);
- }
-
- addAction(requiresHold);
- }
-
- private bool wasHit;
+ private bool isDownState;
private bool wasLeft;
private OsuInputManager osuInputManager;
- private void addAction(bool hitting)
- {
- if (wasHit == hitting)
- return;
-
- wasHit = hitting;
-
- var state = new ReplayState
- {
- PressedActions = new List()
- };
-
- if (hitting)
- {
- state.PressedActions.Add(wasLeft ? OsuAction.LeftButton : OsuAction.RightButton);
- wasLeft = !wasLeft;
- }
-
- state.Apply(osuInputManager.CurrentState, osuInputManager);
- }
+ private ReplayState state;
+ private double lastStateChangeTime;
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
@@ -89,5 +39,85 @@ namespace osu.Game.Rulesets.Osu.Mods
osuInputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
osuInputManager.AllowUserPresses = false;
}
+
+ public void Update(Playfield playfield)
+ {
+ bool requiresHold = false;
+ bool requiresHit = false;
+
+ double time = playfield.Clock.CurrentTime;
+
+ foreach (var h in playfield.HitObjectContainer.AliveObjects.OfType())
+ {
+ // we are not yet close enough to the object.
+ if (time < h.HitObject.StartTime - relax_leniency)
+ break;
+
+ // already hit or beyond the hittable end time.
+ if (h.IsHit || (h.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime))
+ continue;
+
+ switch (h)
+ {
+ case DrawableHitCircle circle:
+ handleHitCircle(circle);
+ break;
+
+ case DrawableSlider slider:
+ // Handles cases like "2B" beatmaps, where sliders may be overlapping and simply holding is not enough.
+ if (!slider.HeadCircle.IsHit)
+ handleHitCircle(slider.HeadCircle);
+
+ requiresHold |= slider.Ball.IsHovered || h.IsHovered;
+ break;
+
+ case DrawableSpinner _:
+ requiresHold = true;
+ break;
+ }
+ }
+
+ if (requiresHit)
+ {
+ changeState(false);
+ changeState(true);
+ }
+
+ if (requiresHold)
+ changeState(true);
+ else if (isDownState && time - lastStateChangeTime > AutoGenerator.KEY_UP_DELAY)
+ changeState(false);
+
+ void handleHitCircle(DrawableHitCircle circle)
+ {
+ if (!circle.IsHovered)
+ return;
+
+ Debug.Assert(circle.HitObject.HitWindows != null);
+ requiresHit |= circle.HitObject.HitWindows.CanBeHit(time - circle.HitObject.StartTime);
+ }
+
+ void changeState(bool down)
+ {
+ if (isDownState == down)
+ return;
+
+ isDownState = down;
+ lastStateChangeTime = time;
+
+ state = new ReplayState
+ {
+ PressedActions = new List()
+ };
+
+ if (down)
+ {
+ state.PressedActions.Add(wasLeft ? OsuAction.LeftButton : OsuAction.RightButton);
+ wasLeft = !wasLeft;
+ }
+
+ state?.Apply(osuInputManager.CurrentState, osuInputManager);
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
index cc664ae72e..41daef1f38 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
@@ -27,26 +28,40 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableHitObjects(IEnumerable drawables)
{
foreach (var drawable in drawables)
+ drawable.ApplyCustomUpdateState += applyTransform;
+ }
+
+ private void applyTransform(DrawableHitObject drawable, ArmedState state)
+ {
+ switch (drawable)
{
- var hitObject = (OsuHitObject)drawable.HitObject;
+ case DrawableSliderHead _:
+ case DrawableSliderTail _:
+ case DrawableSliderTick _:
+ case DrawableRepeatPoint _:
+ return;
- float appearDistance = (float)(hitObject.TimePreempt - hitObject.TimeFadeIn) / 2;
+ default:
+ var hitObject = (OsuHitObject)drawable.HitObject;
- Vector2 originalPosition = drawable.Position;
- Vector2 appearOffset = new Vector2(MathF.Cos(theta), MathF.Sin(theta)) * appearDistance;
+ float appearDistance = (float)(hitObject.TimePreempt - hitObject.TimeFadeIn) / 2;
- //the - 1 and + 1 prevents the hit objects to appear in the wrong position.
- double appearTime = hitObject.StartTime - hitObject.TimePreempt - 1;
- double moveDuration = hitObject.TimePreempt + 1;
+ Vector2 originalPosition = drawable.Position;
+ Vector2 appearOffset = new Vector2(MathF.Cos(theta), MathF.Sin(theta)) * appearDistance;
- using (drawable.BeginAbsoluteSequence(appearTime, true))
- {
- drawable
- .MoveToOffset(appearOffset)
- .MoveTo(originalPosition, moveDuration, Easing.InOutSine);
- }
+ //the - 1 and + 1 prevents the hit objects to appear in the wrong position.
+ double appearTime = hitObject.StartTime - hitObject.TimePreempt - 1;
+ double moveDuration = hitObject.TimePreempt + 1;
- theta += (float)hitObject.TimeFadeIn / 1000;
+ using (drawable.BeginAbsoluteSequence(appearTime, true))
+ {
+ drawable
+ .MoveToOffset(appearOffset)
+ .MoveTo(originalPosition, moveDuration, Easing.InOutSine);
+ }
+
+ theta += (float)hitObject.TimeFadeIn / 1000;
+ break;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
index 6c4fbbac17..3e9c0f341b 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
@@ -20,10 +20,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
private const int spacing = 32;
private const double preempt = 800;
+ public override bool RemoveWhenNotAlive => false;
+
///
/// The start time of .
///
- public readonly Bindable StartTime = new Bindable();
+ public readonly Bindable StartTime = new BindableDouble();
///
/// The which s will exit from.
@@ -79,27 +81,31 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
drawableObject.HitObject.DefaultsApplied += scheduleRefresh;
}
- private void scheduleRefresh() => Scheduler.AddOnce(refresh);
+ private void scheduleRefresh()
+ {
+ Scheduler.AddOnce(refresh);
+ }
private void refresh()
{
ClearInternal();
- if (End == null)
- return;
-
OsuHitObject osuStart = Start.HitObject;
- OsuHitObject osuEnd = End.HitObject;
-
- if (osuEnd.NewCombo)
- return;
-
- if (osuStart is Spinner || osuEnd is Spinner)
- return;
-
- Vector2 startPosition = osuStart.EndPosition;
- Vector2 endPosition = osuEnd.Position;
double startTime = osuStart.GetEndTime();
+
+ LifetimeStart = startTime;
+
+ OsuHitObject osuEnd = End?.HitObject;
+
+ if (osuEnd == null || osuEnd.NewCombo || osuStart is Spinner || osuEnd is Spinner)
+ {
+ // ensure we always set a lifetime for full LifetimeManagementContainer benefits
+ LifetimeEnd = LifetimeStart;
+ return;
+ }
+
+ Vector2 startPosition = osuStart.StackedEndPosition;
+ Vector2 endPosition = osuEnd.StackedPosition;
double endTime = osuEnd.StartTime;
Vector2 distanceVector = endPosition - startPosition;
@@ -107,6 +113,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
double duration = endTime - startTime;
+ double? firstTransformStartTime = null;
+ double finalTransformEndTime = startTime;
+
for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing)
{
float fraction = (float)d / distance;
@@ -125,16 +134,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
Scale = new Vector2(1.5f * osuEnd.Scale),
});
+ if (firstTransformStartTime == null)
+ firstTransformStartTime = fadeInTime;
+
using (fp.BeginAbsoluteSequence(fadeInTime))
{
fp.FadeIn(osuEnd.TimeFadeIn);
fp.ScaleTo(osuEnd.Scale, osuEnd.TimeFadeIn, Easing.Out);
fp.MoveTo(pointEndPosition, osuEnd.TimeFadeIn, Easing.Out);
fp.Delay(fadeOutTime - fadeInTime).FadeOut(osuEnd.TimeFadeIn);
- }
- fp.Expire(true);
+ finalTransformEndTime = fadeOutTime + osuEnd.TimeFadeIn;
+ }
}
+
+ // todo: use Expire() on FollowPoints and take lifetime from them when https://github.com/ppy/osu-framework/issues/3300 is fixed.
+ LifetimeStart = firstTransformStartTime ?? startTime;
+ LifetimeEnd = finalTransformEndTime;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
index be192080f9..4d73e711bb 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
///
/// Visualises connections between s.
///
- public class FollowPointRenderer : CompositeDrawable
+ public class FollowPointRenderer : LifetimeManagementContainer
{
///
/// All the s contained by this .
@@ -45,8 +45,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
/// The index of in .
private void addConnection(FollowPointConnection connection)
{
- AddInternal(connection);
-
// Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections
int index = connections.AddInPlace(connection, Comparer.Create((g1, g2) => g1.StartTime.Value.CompareTo(g2.StartTime.Value)));
@@ -74,6 +72,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
FollowPointConnection previousConnection = connections[index - 1];
previousConnection.End = connection.Start;
}
+
+ AddInternal(connection);
}
///
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 3162f4b379..da1e666aba 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly IBindable positionBindable = new Bindable();
private readonly IBindable stackHeightBindable = new Bindable();
- private readonly IBindable scaleBindable = new Bindable();
+ private readonly IBindable scaleBindable = new BindableFloat();
public OsuAction? HitAction => HitArea.HitAction;
@@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public Drawable ProxiedLayer => ApproachCircle;
- public class HitReceptor : Drawable, IKeyBindingHandler
+ public class HitReceptor : CompositeDrawable, IKeyBindingHandler
{
// IsHovered is used
public override bool HandlePositionalInput => true;
@@ -185,6 +185,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
+
+ CornerRadius = OsuHitObject.OBJECT_RADIUS;
+ CornerExponent = 2;
}
public bool OnPressed(OsuAction action)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs
index 20b31c68f2..8fdcd060e7 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
InternalChild = scaleContainer = new ReverseArrowPiece();
}
- private readonly IBindable scaleBindable = new Bindable();
+ private readonly IBindable scaleBindable = new BindableFloat();
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index cd3c572ba0..7403649184 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly IBindable positionBindable = new Bindable();
private readonly IBindable stackHeightBindable = new Bindable();
- private readonly IBindable scaleBindable = new Bindable();
+ private readonly IBindable scaleBindable = new BindableFloat();
public DrawableSlider(Slider s)
: base(s)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
index 9d4d9958a1..60b5c335d6 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
};
}
- private readonly IBindable scaleBindable = new Bindable();
+ private readonly IBindable scaleBindable = new BindableFloat();
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index de11ab6419..0ec7f2ebfe 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly SpriteIcon symbol;
- private readonly Color4 baseColour = OsuColour.FromHex(@"002c3c");
- private readonly Color4 fillColour = OsuColour.FromHex(@"005b7c");
+ private readonly Color4 baseColour = Color4Extensions.FromHex(@"002c3c");
+ private readonly Color4 fillColour = Color4Extensions.FromHex(@"005b7c");
private readonly IBindable positionBindable = new Bindable();
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
index ef7b077480..5a6dd49c44 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -15,16 +16,24 @@ using osu.Game.Rulesets.Osu.Skinning;
using osuTK.Graphics;
using osu.Game.Skinning;
using osuTK;
+using osu.Game.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
- public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition
+ public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour
{
public Func GetInitialHitAction;
+ public Color4 AccentColour
+ {
+ get => ball.Colour;
+ set => ball.Colour = value;
+ }
+
private readonly Slider slider;
- public readonly Drawable FollowCircle;
+ private readonly Drawable followCircle;
private readonly DrawableSlider drawableSlider;
+ private readonly CircularContainer ball;
public SliderBall(Slider slider, DrawableSlider drawableSlider = null)
{
@@ -38,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Children = new[]
{
- FollowCircle = new FollowCircleContainer
+ followCircle = new FollowCircleContainer
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
@@ -46,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Alpha = 0,
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()),
},
- new CircularContainer
+ ball = new CircularContainer
{
Masking = true,
RelativeSizeAxes = Axes.Both,
@@ -95,8 +104,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
tracking = value;
- FollowCircle.ScaleTo(tracking ? 2f : 1, 300, Easing.OutQuint);
- FollowCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint);
+ followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint);
+ followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint);
}
}
@@ -149,7 +158,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
// in valid time range
Time.Current >= slider.StartTime && Time.Current < slider.EndTime &&
// in valid position range
- lastScreenSpaceMousePosition.HasValue && FollowCircle.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) &&
+ lastScreenSpaceMousePosition.HasValue && followCircle.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) &&
// valid action
(actions?.Any(isValidTrackingAction) ?? false);
}
@@ -214,9 +223,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
public class DefaultSliderBall : CompositeDrawable
{
+ private Box box;
+
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject, ISkinSource skin)
{
+ var slider = (DrawableSlider)drawableObject;
+
RelativeSizeAxes = Axes.Both;
float radius = skin.GetConfig(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS;
@@ -231,14 +244,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
BorderThickness = 10,
BorderColour = Color4.White,
Alpha = 1,
- Child = new Box
+ Child = box = new Box
{
+ Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Colour = Color4.White,
- Alpha = 0.4f,
+ AlwaysPresent = true,
+ Alpha = 0
}
};
+
+ slider.Tracking.BindValueChanged(trackingChanged, true);
}
+
+ private void trackingChanged(ValueChangedEvent tracking) =>
+ box.FadeTo(tracking.NewValue ? 0.6f : 0.05f, 200, Easing.OutQuint);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
index 0ba712a83f..15af141c99 100644
--- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public double Radius => OBJECT_RADIUS * Scale;
- public readonly Bindable ScaleBindable = new Bindable(1);
+ public readonly Bindable ScaleBindable = new BindableFloat(1);
public float Scale
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index fe65ab78d1..77f8ec6cc8 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -18,7 +18,12 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public class Slider : OsuHitObject, IHasCurve
{
- public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
+ public double EndTime
+ {
+ get => StartTime + this.SpanCount() * Path.Distance / Velocity;
+ set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
+ }
+
public double Duration => EndTime - StartTime;
private readonly Cached endPositionCache = new Cached();
@@ -81,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Objects
set
{
repeatCount = value;
- endPositionCache.Invalidate();
+ updateNestedPositions();
}
}
@@ -111,8 +116,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public Slider()
{
- SamplesBindable.ItemsAdded += _ => updateNestedSamples();
- SamplesBindable.ItemsRemoved += _ => updateNestedSamples();
+ SamplesBindable.CollectionChanged += (_, __) => updateNestedSamples();
Path.Version.ValueChanged += _ => updateNestedPositions();
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
index c17d2275b8..127c36fcc0 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
@@ -4,7 +4,6 @@
using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
@@ -23,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects
pathVersion.BindValueChanged(_ => Position = slider.EndPosition);
}
- public override Judgement CreateJudgement() => new OsuSliderTailJudgement();
+ public override Judgement CreateJudgement() => new IgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
index 2441a1449d..0b8d03d118 100644
--- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
@@ -13,8 +13,13 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public class Spinner : OsuHitObject, IHasEndTime
{
- public double EndTime { get; set; }
- public double Duration => EndTime - StartTime;
+ public double EndTime
+ {
+ get => StartTime + Duration;
+ set => Duration = value - StartTime;
+ }
+
+ public double Duration { get; set; }
///
/// Number of spins required to finish the spinner without miss.
diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
index a6491bb3f3..6f2998006f 100644
--- a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
+++ b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
new DifficultyRange(HitResult.Great, 80, 50, 20),
new DifficultyRange(HitResult.Good, 140, 100, 60),
new DifficultyRange(HitResult.Meh, 200, 150, 100),
- new DifficultyRange(HitResult.Miss, 200, 200, 200),
+ new DifficultyRange(HitResult.Miss, 400, 400, 400),
};
public override bool IsHitResultAllowed(HitResult result)
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
index 266b619334..d6c3f443eb 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
@@ -46,17 +46,20 @@ namespace osu.Game.Rulesets.Osu.Skinning
switch (osuComponent.Component)
{
case OsuSkinComponents.FollowPoint:
- return this.GetAnimation(component.LookupName, true, false);
+ return this.GetAnimation(component.LookupName, true, false, true);
case OsuSkinComponents.SliderFollowCircle:
- var followCircle = this.GetAnimation("sliderfollowcircle", true, true);
+ var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);
if (followCircle != null)
// follow circles are 2x the hitcircle resolution in legacy skins (since they are scaled down from >1x
followCircle.Scale *= 0.5f;
return followCircle;
case OsuSkinComponents.SliderBall:
- var sliderBallContent = this.GetAnimation("sliderb", true, true, "");
+ var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "");
+
+ // todo: slider ball has a custom frame delay based on velocity
+ // Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME);
if (sliderBallContent != null)
{
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
index 4e86662ec6..37df5ec540 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
@@ -5,7 +5,6 @@ using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using osu.Framework.Allocation;
-using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Batches;
using osu.Framework.Graphics.OpenGL.Vertices;
@@ -14,6 +13,7 @@ using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Input.Events;
+using osu.Framework.Layout;
using osu.Framework.Timing;
using osuTK;
using osuTK.Graphics;
@@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
// -1 signals that the part is unusable, and should not be drawn
parts[i].InvalidationID = -1;
}
+
+ AddLayout(partSizeCache);
}
[BackgroundDependencyLoader]
@@ -72,20 +74,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
}
}
- private readonly Cached partSizeCache = new Cached();
+ private readonly LayoutValue partSizeCache = new LayoutValue(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence);
private Vector2 partSize => partSizeCache.IsValid
? partSizeCache.Value
: (partSizeCache.Value = new Vector2(Texture.DisplayWidth, Texture.DisplayHeight) * DrawInfo.Matrix.ExtractScale().Xy);
- public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true)
- {
- if ((invalidation & (Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence)) > 0)
- partSizeCache.Invalidate();
-
- return base.Invalidate(invalidation, source, shallPropagate);
- }
-
///
/// The amount of time to fade the cursor trail pieces.
///
@@ -97,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{
base.Update();
- Invalidate(Invalidation.DrawNode, shallPropagate: false);
+ Invalidate(Invalidation.DrawNode);
const int fade_clock_reset_threshold = 1000000;
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
index a463542e64..28600ef55b 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
@@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osuTK;
@@ -29,10 +30,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private readonly Drawable cursorTrail;
- public Bindable CursorScale;
+ public Bindable CursorScale = new BindableFloat(1);
+
private Bindable userCursorScale;
private Bindable autoCursorScale;
- private readonly IBindable beatmap = new Bindable();
public OsuCursorContainer()
{
@@ -43,37 +44,16 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
};
}
+ [Resolved(canBeNull: true)]
+ private GameplayBeatmap beatmap { get; set; }
+
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
[BackgroundDependencyLoader(true)]
- private void load(OsuConfigManager config, OsuRulesetConfigManager rulesetConfig, IBindable beatmap)
+ private void load(OsuConfigManager config, OsuRulesetConfigManager rulesetConfig)
{
rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorTrail, showTrail);
-
- this.beatmap.BindTo(beatmap);
- this.beatmap.ValueChanged += _ => calculateScale();
-
- userCursorScale = config.GetBindable(OsuSetting.GameplayCursorSize);
- userCursorScale.ValueChanged += _ => calculateScale();
-
- autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize);
- autoCursorScale.ValueChanged += _ => calculateScale();
-
- CursorScale = new Bindable();
- CursorScale.ValueChanged += e => ActiveCursor.Scale = cursorTrail.Scale = new Vector2(e.NewValue);
-
- calculateScale();
- }
-
- private void calculateScale()
- {
- float scale = userCursorScale.Value;
-
- if (autoCursorScale.Value && beatmap.Value != null)
- {
- // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
- scale *= 1f - 0.7f * (1f + beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY;
- }
-
- CursorScale.Value = scale;
}
protected override void LoadComplete()
@@ -81,6 +61,46 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
base.LoadComplete();
showTrail.BindValueChanged(v => cursorTrail.FadeTo(v.NewValue ? 1 : 0, 200), true);
+
+ userCursorScale = config.GetBindable(OsuSetting.GameplayCursorSize);
+ userCursorScale.ValueChanged += _ => calculateScale();
+
+ autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize);
+ autoCursorScale.ValueChanged += _ => calculateScale();
+
+ CursorScale.ValueChanged += e =>
+ {
+ var newScale = new Vector2(e.NewValue);
+
+ ActiveCursor.Scale = newScale;
+ cursorTrail.Scale = newScale;
+ };
+
+ calculateScale();
+ }
+
+ ///
+ /// Get the scale applicable to the ActiveCursor based on a beatmap's circle size.
+ ///
+ public static float GetScaleForCircleSize(float circleSize) =>
+ 1f - 0.7f * (1f + circleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY;
+
+ private void calculateScale()
+ {
+ float scale = userCursorScale.Value;
+
+ if (autoCursorScale.Value && beatmap != null)
+ {
+ // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
+ scale *= GetScaleForCircleSize(beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
+ }
+
+ CursorScale.Value = scale;
+
+ var newScale = new Vector2(scale);
+
+ ActiveCursor.ScaleTo(newScale, 400, Easing.OutQuint);
+ cursorTrail.Scale = newScale;
}
private int downCount;
diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
index ca3030db3e..abba444c73 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.UI
{
Add(localCursorContainer = new OsuCursorContainer());
- localCursorScale = new Bindable();
+ localCursorScale = new BindableFloat();
localCursorScale.BindTo(localCursorContainer.CursorScale);
localCursorScale.BindValueChanged(scale => cursorScaleContainer.Scale = new Vector2(scale.NewValue), true);
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs
new file mode 100644
index 0000000000..d3be2cdf0d
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs
@@ -0,0 +1,47 @@
+// Copyright (c) ppy Pty Ltd . 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.Rulesets.Taiko.Mods;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Scoring;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Mods
+{
+ public class TestSceneTaikoModPerfect : ModPerfectTestScene
+ {
+ public TestSceneTaikoModPerfect()
+ : base(new TestTaikoRuleset(), new TaikoModPerfect())
+ {
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestHit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new CentreHit { StartTime = 1000 }), shouldMiss);
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestDrumRoll(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new DrumRoll { StartTime = 1000, EndTime = 3000 }), shouldMiss);
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestSwell(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Swell { StartTime = 1000, EndTime = 3000 }), shouldMiss);
+
+ private class TestTaikoRuleset : TaikoRuleset
+ {
+ public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new TestTaikoHealthProcessor();
+
+ private class TestTaikoHealthProcessor : TaikoHealthProcessor
+ {
+ protected override void Reset(bool storeResults)
+ {
+ base.Reset(storeResults);
+
+ Health.Value = 1; // Don't care about the health condition (only the mod condition)
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs
index f27e329e8e..303f0163b1 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs
@@ -1,24 +1,16 @@
// Copyright (c) ppy Pty Ltd . 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.Game.Beatmaps;
-using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Scoring;
-using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects;
-using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
public class TestSceneSwellJudgements : PlayerTestScene
{
- protected new TestPlayer Player => (TestPlayer)base.Player;
-
public TestSceneSwellJudgements()
: base(new TaikoRuleset())
{
@@ -28,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
public void TestZeroTickTimeOffsets()
{
AddUntilStep("gameplay finished", () => Player.ScoreProcessor.HasCompleted);
- AddAssert("all tick offsets are 0", () => Player.Results.Where(r => r.Judgement is TaikoSwellTickJudgement).All(r => r.TimeOffset == 0));
+ AddAssert("all tick offsets are 0", () => Player.Results.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0));
}
protected override bool Autoplay => true;
@@ -50,25 +42,5 @@ namespace osu.Game.Rulesets.Taiko.Tests
return beatmap;
}
-
- protected override Player CreatePlayer(Ruleset ruleset) => new TestPlayer();
-
- protected class TestPlayer : Player
- {
- public readonly List Results = new List();
-
- public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
-
- public TestPlayer()
- : base(false, false)
- {
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- ScoreProcessor.NewJudgement += r => Results.Add(r);
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
index 140433a523..2ab041e191 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
@@ -4,11 +4,9 @@
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Objects;
-using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
@@ -22,10 +20,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
protected override bool AllowFail => true;
- protected override Player CreatePlayer(Ruleset ruleset)
+ protected override TestPlayer CreatePlayer(Ruleset ruleset)
{
SelectedMods.Value = SelectedMods.Value.Concat(new[] { new TaikoModSuddenDeath() }).ToArray();
- return new ScoreAccessiblePlayer();
+ return base.CreatePlayer(ruleset);
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) =>
@@ -49,20 +47,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
AddStep("Setup judgements", () =>
{
judged = false;
- ((ScoreAccessiblePlayer)Player).ScoreProcessor.NewJudgement += b => judged = true;
+ Player.ScoreProcessor.NewJudgement += b => judged = true;
});
AddUntilStep("swell judged", () => judged);
AddAssert("not failed", () => !Player.HasFailed);
}
-
- private class ScoreAccessiblePlayer : TestPlayer
- {
- public ScoreAccessiblePlayer()
- : base(false, false)
- {
- }
-
- public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
- }
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index d728d65bfd..f6054a5d6f 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
index b7db3307ad..1253b7c8ae 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
@@ -2,8 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
-using osu.Framework.Caching;
using osu.Framework.Graphics;
+using osu.Framework.Layout;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
@@ -30,13 +30,15 @@ namespace osu.Game.Rulesets.Taiko.Mods
private class TaikoFlashlight : Flashlight
{
- private readonly Cached flashlightProperties = new Cached();
+ private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize);
private readonly TaikoPlayfield taikoPlayfield;
public TaikoFlashlight(TaikoPlayfield taikoPlayfield)
{
this.taikoPlayfield = taikoPlayfield;
FlashlightSize = new Vector2(0, getSizeFor(0));
+
+ AddLayout(flashlightProperties);
}
private float getSizeFor(int combo)
@@ -56,16 +58,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
protected override string FragmentShader => "CircularFlashlight";
- public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true)
- {
- if ((invalidation & Invalidation.DrawSize) > 0)
- {
- flashlightProperties.Invalidate();
- }
-
- return base.Invalidate(invalidation, source, shallPropagate);
- }
-
protected override void Update()
{
base.Update();
diff --git a/osu.Game.Rulesets.Taiko/Objects/BarLine.cs b/osu.Game.Rulesets.Taiko/Objects/BarLine.cs
index 2afbbc737c..6306195704 100644
--- a/osu.Game.Rulesets.Taiko/Objects/BarLine.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/BarLine.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Taiko.Objects
@@ -8,5 +9,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
public class BarLine : TaikoHitObject, IBarLine
{
public bool Major { get; set; }
+
+ public override Judgement CreateJudgement() => new IgnoreJudgement();
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs
index 1a5a797f28..e9caabbcc8 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs
@@ -54,5 +54,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Alpha = 0.75f
});
}
+
+ protected override void UpdateStateTransforms(ArmedState state) => this.FadeOut(150);
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
index 8956ca9c19..aacd78f176 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
@@ -18,7 +18,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
///
private const float base_distance = 100;
- public double EndTime => StartTime + Duration;
+ public double EndTime
+ {
+ get => StartTime + Duration;
+ set => Duration = value - StartTime;
+ }
public double Duration { get; set; }
diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs
index e60984596d..2f06066a16 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs
@@ -11,7 +11,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
{
public class Swell : TaikoHitObject, IHasEndTime
{
- public double EndTime => StartTime + Duration;
+ public double EndTime
+ {
+ get => StartTime + Duration;
+ set => Duration = value - StartTime;
+ }
public double Duration { get; set; }
diff --git a/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs
index 91f4d3dbe7..bdc0478195 100644
--- a/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs
@@ -3,13 +3,12 @@
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Rulesets.Taiko.Judgements;
namespace osu.Game.Rulesets.Taiko.Objects
{
public class SwellTick : TaikoHitObject
{
- public override Judgement CreateJudgement() => new TaikoSwellTickJudgement();
+ public override Judgement CreateJudgement() => new IgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs
index edb089dbac..dd3c2289ea 100644
--- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs
+++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
@@ -39,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring
{
base.ApplyBeatmap(beatmap);
- hpMultiplier = 1 / (object_count_factor * beatmap.HitObjects.OfType().Count() * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98));
+ hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType().Count()) * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98));
hpMissMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120);
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
index 96ff6b81e3..76b76aa357 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
@@ -99,7 +99,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var storyboard = decoder.Decode(stream);
StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
- Assert.AreEqual(123456, ((StoryboardSprite)background.Elements.Single()).InitialPosition.X);
+ Assert.AreEqual(3456, ((StoryboardSprite)background.Elements.Single()).InitialPosition.X);
}
}
}
diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
index c1bd73ef05..c6095ae404 100644
--- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
+++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
@@ -417,7 +417,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportWithDuplicateHashes()
{
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportNestedStructure)))
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithDuplicateHashes)))
{
try
{
diff --git a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs
index 5a4e76d586..3cb5909ba9 100644
--- a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs
+++ b/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -3,6 +3,8 @@
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
@@ -23,15 +25,31 @@ namespace osu.Game.Tests.Editor
[Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap;
+ protected override Container Content { get; }
+
public TestSceneHitObjectComposerDistanceSnapping()
{
- editorBeatmap = new EditorBeatmap(new OsuBeatmap(), BeatDivisor);
+ base.Content.Add(new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ editorBeatmap = new EditorBeatmap(new OsuBeatmap()),
+ Content = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
+ },
+ });
}
[SetUp]
public void Setup() => Schedule(() =>
{
- Child = composer = new TestHitObjectComposer();
+ Children = new Drawable[]
+ {
+ composer = new TestHitObjectComposer()
+ };
BeatDivisor.Value = 1;
diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
index 244e37f017..885abb61b5 100644
--- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
@@ -154,6 +154,7 @@ namespace osu.Game.Tests.Gameplay
private class JudgeableHitObject : HitObject
{
public override Judgement CreateJudgement() => new Judgement();
+ protected override HitWindows CreateHitWindows() => new HitWindows();
}
}
}
diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
index c6d1f9da29..7a89642e11 100644
--- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
@@ -11,8 +11,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Audio;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
@@ -78,7 +78,7 @@ namespace osu.Game.Tests.Gameplay
}
}
- private class TestHitObjectWithCombo : HitObject, IHasComboInformation
+ private class TestHitObjectWithCombo : ConvertHitObject, IHasComboInformation
{
public bool NewCombo { get; } = false;
public int ComboOffset { get; } = 0;
@@ -126,10 +126,10 @@ namespace osu.Game.Tests.Gameplay
{
switch (lookup)
{
- case GlobalSkinConfiguration global:
+ case GlobalSkinColours global:
switch (global)
{
- case GlobalSkinConfiguration.ComboColours:
+ case GlobalSkinColours.ComboColours:
return SkinUtils.As(new Bindable>(ComboColours));
}
diff --git a/osu.Game.Tests/Resources/Archives/ogg-beatmap.osz b/osu.Game.Tests/Resources/Archives/ogg-beatmap.osz
new file mode 100644
index 0000000000..f264a8dda2
Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/ogg-beatmap.osz differ
diff --git a/osu.Game.Tests/Resources/Archives/ogg-skin.osk b/osu.Game.Tests/Resources/Archives/ogg-skin.osk
new file mode 100644
index 0000000000..d7379446aa
Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/ogg-skin.osk differ
diff --git a/osu.Game.Tests/Resources/variable-with-suffix.osb b/osu.Game.Tests/Resources/variable-with-suffix.osb
index 5c9b46ca98..fd284eb055 100644
--- a/osu.Game.Tests/Resources/variable-with-suffix.osb
+++ b/osu.Game.Tests/Resources/variable-with-suffix.osb
@@ -1,5 +1,5 @@
[Variables]
-$var=1234
+$var=34
[Events]
Sprite,Background,TopCentre,"img.jpg",$var56,240
diff --git a/osu.Game.Tests/ScrollAlgorithms/ConstantScrollTest.cs b/osu.Game.Tests/ScrollAlgorithms/ConstantScrollTest.cs
index d7f709dc03..a6e8622b6f 100644
--- a/osu.Game.Tests/ScrollAlgorithms/ConstantScrollTest.cs
+++ b/osu.Game.Tests/ScrollAlgorithms/ConstantScrollTest.cs
@@ -18,12 +18,21 @@ namespace osu.Game.Tests.ScrollAlgorithms
}
[Test]
- public void TestDisplayStartTime()
+ public void TestPointDisplayStartTime()
{
- Assert.AreEqual(-8000, algorithm.GetDisplayStartTime(2000, 10000));
- Assert.AreEqual(-3000, algorithm.GetDisplayStartTime(2000, 5000));
- Assert.AreEqual(2000, algorithm.GetDisplayStartTime(7000, 5000));
- Assert.AreEqual(7000, algorithm.GetDisplayStartTime(17000, 10000));
+ Assert.AreEqual(-8000, algorithm.GetDisplayStartTime(2000, 0, 10000, 1));
+ Assert.AreEqual(-3000, algorithm.GetDisplayStartTime(2000, 0, 5000, 1));
+ Assert.AreEqual(2000, algorithm.GetDisplayStartTime(7000, 0, 5000, 1));
+ Assert.AreEqual(7000, algorithm.GetDisplayStartTime(17000, 0, 10000, 1));
+ }
+
+ [Test]
+ public void TestObjectDisplayStartTime()
+ {
+ Assert.AreEqual(900, algorithm.GetDisplayStartTime(2000, 50, 1000, 500)); // 2000 - (1 + 50 / 500) * 1000
+ Assert.AreEqual(8900, algorithm.GetDisplayStartTime(10000, 50, 1000, 500)); // 10000 - (1 + 50 / 500) * 1000
+ Assert.AreEqual(13500, algorithm.GetDisplayStartTime(15000, 250, 1000, 500)); // 15000 - (1 + 250 / 500) * 1000
+ Assert.AreEqual(19000, algorithm.GetDisplayStartTime(25000, 100, 5000, 500)); // 25000 - (1 + 100 / 500) * 5000
}
[Test]
diff --git a/osu.Game.Tests/ScrollAlgorithms/OverlappingScrollTest.cs b/osu.Game.Tests/ScrollAlgorithms/OverlappingScrollTest.cs
index 106aa88be3..1429d22c1a 100644
--- a/osu.Game.Tests/ScrollAlgorithms/OverlappingScrollTest.cs
+++ b/osu.Game.Tests/ScrollAlgorithms/OverlappingScrollTest.cs
@@ -27,11 +27,22 @@ namespace osu.Game.Tests.ScrollAlgorithms
}
[Test]
- public void TestDisplayStartTime()
+ public void TestPointDisplayStartTime()
{
- Assert.AreEqual(1000, algorithm.GetDisplayStartTime(2000, 1000)); // Like constant
- Assert.AreEqual(10000, algorithm.GetDisplayStartTime(10500, 1000)); // 10500 - (1000 * 0.5)
- Assert.AreEqual(20000, algorithm.GetDisplayStartTime(22000, 1000)); // 23000 - (1000 / 0.5)
+ Assert.AreEqual(1000, algorithm.GetDisplayStartTime(2000, 0, 1000, 1)); // Like constant
+ Assert.AreEqual(10000, algorithm.GetDisplayStartTime(10500, 0, 1000, 1)); // 10500 - (1000 * 0.5)
+ Assert.AreEqual(20000, algorithm.GetDisplayStartTime(22000, 0, 1000, 1)); // 23000 - (1000 / 0.5)
+ }
+
+ [Test]
+ public void TestObjectDisplayStartTime()
+ {
+ Assert.AreEqual(900, algorithm.GetDisplayStartTime(2000, 50, 1000, 500)); // 2000 - (1 + 50 / 500) * 1000 / 1
+ Assert.AreEqual(9450, algorithm.GetDisplayStartTime(10000, 50, 1000, 500)); // 10000 - (1 + 50 / 500) * 1000 / 2
+ Assert.AreEqual(14250, algorithm.GetDisplayStartTime(15000, 250, 1000, 500)); // 15000 - (1 + 250 / 500) * 1000 / 2
+ Assert.AreEqual(16500, algorithm.GetDisplayStartTime(18000, 250, 2000, 500)); // 18000 - (1 + 250 / 500) * 2000 / 2
+ Assert.AreEqual(17800, algorithm.GetDisplayStartTime(20000, 50, 1000, 500)); // 20000 - (1 + 50 / 500) * 1000 / 0.5
+ Assert.AreEqual(19800, algorithm.GetDisplayStartTime(22000, 50, 1000, 500)); // 22000 - (1 + 50 / 500) * 1000 / 0.5
}
[Test]
diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs
new file mode 100644
index 0000000000..4d3b73fb32
--- /dev/null
+++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio.Track;
+using osu.Framework.Testing;
+using osu.Game.Audio;
+using osu.Game.Beatmaps;
+using osu.Game.IO.Archives;
+using osu.Game.Tests.Resources;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Tests.Skins
+{
+ [HeadlessTest]
+ public class TestSceneBeatmapSkinResources : OsuTestScene
+ {
+ [Resolved]
+ private BeatmapManager beatmaps { get; set; }
+
+ private WorkingBeatmap beatmap;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).Result;
+ beatmap = beatmaps.GetWorkingBeatmap(imported.Beatmaps[0]);
+ }
+
+ [Test]
+ public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo("sample")) != null);
+
+ [Test]
+ public void TestRetrieveOggTrack() => AddAssert("track is non-null", () => !(beatmap.Track is TrackVirtual));
+ }
+}
diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
index ed54cc982d..35313ee858 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
@@ -95,7 +95,7 @@ namespace osu.Game.Tests.Skins
[Test]
public void TestGlobalLookup()
{
- AddAssert("Check combo colours", () => requester.GetConfig>(GlobalSkinConfiguration.ComboColours)?.Value?.Count > 0);
+ AddAssert("Check combo colours", () => requester.GetConfig>(GlobalSkinColours.ComboColours)?.Value?.Count > 0);
}
[Test]
@@ -121,7 +121,7 @@ namespace osu.Game.Tests.Skins
public void TestEmptyComboColours()
{
AddAssert("Check retrieved combo colours is skin default colours", () =>
- requester.GetConfig>(GlobalSkinConfiguration.ComboColours)?.Value?.SequenceEqual(SkinConfiguration.DefaultComboColours) ?? false);
+ requester.GetConfig>(GlobalSkinColours.ComboColours)?.Value?.SequenceEqual(SkinConfiguration.DefaultComboColours) ?? false);
}
[Test]
@@ -136,7 +136,7 @@ namespace osu.Game.Tests.Skins
AddStep("Disallow default colours fallback in source2", () => source2.Configuration.AllowDefaultComboColoursFallback = false);
AddAssert("Check retrieved combo colours from source1", () =>
- requester.GetConfig>(GlobalSkinConfiguration.ComboColours)?.Value?.SequenceEqual(source1.Configuration.ComboColours) ?? false);
+ requester.GetConfig>(GlobalSkinColours.ComboColours)?.Value?.SequenceEqual(source1.Configuration.ComboColours) ?? false);
}
[Test]
diff --git a/osu.Game.Tests/Skins/TestSceneSkinResources.cs b/osu.Game.Tests/Skins/TestSceneSkinResources.cs
new file mode 100644
index 0000000000..107a96292f
--- /dev/null
+++ b/osu.Game.Tests/Skins/TestSceneSkinResources.cs
@@ -0,0 +1,33 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Testing;
+using osu.Game.Audio;
+using osu.Game.IO.Archives;
+using osu.Game.Skinning;
+using osu.Game.Tests.Resources;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Tests.Skins
+{
+ [HeadlessTest]
+ public class TestSceneSkinResources : OsuTestScene
+ {
+ [Resolved]
+ private SkinManager skins { get; set; }
+
+ private ISkin skin;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var imported = skins.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-skin.osk"))).Result;
+ skin = skins.GetSkin(imported);
+ }
+
+ [Test]
+ public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => skin.GetSample(new SampleInfo("sample")) != null);
+ }
+}
diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
index 589ec7e8aa..b51555db3e 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
@@ -27,6 +27,7 @@ using osu.Game.Screens;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings;
+using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Tests.Resources;
using osu.Game.Users;
@@ -49,7 +50,7 @@ namespace osu.Game.Tests.Visual.Background
private DummySongSelect songSelect;
private TestPlayerLoader playerLoader;
- private TestPlayer player;
+ private LoadBlockingTestPlayer player;
private BeatmapManager manager;
private RulesetStore rulesets;
@@ -68,10 +69,10 @@ namespace osu.Game.Tests.Visual.Background
[SetUp]
public virtual void SetUp() => Schedule(() =>
{
- Child = new OsuScreenStack(songSelect = new DummySongSelect())
- {
- RelativeSizeAxes = Axes.Both
- };
+ var stack = new OsuScreenStack { RelativeSizeAxes = Axes.Both };
+ Child = stack;
+
+ stack.Push(songSelect = new DummySongSelect());
});
///
@@ -81,7 +82,7 @@ namespace osu.Game.Tests.Visual.Background
public void PlayerLoaderSettingsHoverTest()
{
setupUserSettings();
- AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new TestPlayer { BlockLoad = true })));
+ AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new LoadBlockingTestPlayer { BlockLoad = true })));
AddUntilStep("Wait for Player Loader to load", () => playerLoader?.IsLoaded ?? false);
AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent());
AddStep("Trigger background preview", () =>
@@ -203,7 +204,7 @@ namespace osu.Game.Tests.Visual.Background
}
///
- /// Check if the visual settings container removes user dim when suspending for
+ /// Check if the visual settings container removes user dim when suspending for
///
[Test]
public void TransitionTest()
@@ -268,7 +269,7 @@ namespace osu.Game.Tests.Visual.Background
{
setupUserSettings();
- AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new TestPlayer(allowPause))));
+ AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new LoadBlockingTestPlayer(allowPause))));
AddUntilStep("Wait for Player Loader to load", () => playerLoader.IsLoaded);
AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
@@ -277,7 +278,7 @@ namespace osu.Game.Tests.Visual.Background
private void setupUserSettings()
{
- AddUntilStep("Song select has selection", () => songSelect.Carousel.SelectedBeatmap != null);
+ AddUntilStep("Song select has selection", () => songSelect.Carousel?.SelectedBeatmap != null);
AddStep("Set default user settings", () =>
{
SelectedMods.Value = SelectedMods.Value.Concat(new[] { new OsuModNoFail() }).ToArray();
@@ -302,8 +303,8 @@ namespace osu.Game.Tests.Visual.Background
}
public readonly Bindable DimEnabled = new Bindable();
- public readonly Bindable DimLevel = new Bindable();
- public readonly Bindable BlurLevel = new Bindable();
+ public readonly Bindable DimLevel = new BindableDouble();
+ public readonly Bindable BlurLevel = new BindableDouble();
public new BeatmapCarousel Carousel => base.Carousel;
@@ -335,7 +336,7 @@ namespace osu.Game.Tests.Visual.Background
public bool IsBackgroundCurrent() => ((FadeAccessibleBackground)Background).IsCurrentScreen();
}
- private class FadeAccessibleResults : SoloResults
+ private class FadeAccessibleResults : ResultsScreen
{
public FadeAccessibleResults(ScoreInfo score)
: base(score)
@@ -347,7 +348,7 @@ namespace osu.Game.Tests.Visual.Background
public bool IsBlurCorrect() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(BACKGROUND_BLUR);
}
- private class TestPlayer : Visual.TestPlayer
+ private class LoadBlockingTestPlayer : TestPlayer
{
protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value);
@@ -360,7 +361,7 @@ namespace osu.Game.Tests.Visual.Background
public readonly Bindable ReplacesBackground = new Bindable();
public readonly Bindable IsPaused = new Bindable();
- public TestPlayer(bool allowPause = true)
+ public LoadBlockingTestPlayer(bool allowPause = true)
: base(allowPause)
{
}
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs b/osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs
index 3c75fd5310..4d8f877575 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
@@ -10,6 +12,17 @@ namespace osu.Game.Tests.Visual.Editor
[TestFixture]
public class TestSceneTimelineBlueprintContainer : TimelineTestScene
{
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(TimelineHitObjectBlueprint),
+ };
+
public override Drawable CreateTestComponent() => new TimelineBlueprintContainer();
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ Clock.Seek(10000);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
index adfed9a299..ae09a7fa47 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
@@ -27,7 +27,12 @@ namespace osu.Game.Tests.Visual.Editor
};
[Cached(typeof(EditorBeatmap))]
- private readonly EditorBeatmap editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+ private readonly EditorBeatmap editorBeatmap;
+
+ public TestSceneTimingScreen()
+ {
+ editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+ }
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs
index b5e526d3c2..7081eb3af5 100644
--- a/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs
+++ b/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs
@@ -13,7 +13,6 @@ using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
@@ -38,13 +37,16 @@ namespace osu.Game.Tests.Visual.Editor
{
Beatmap.Value = new WaveformTestBeatmap(audio);
- var editorBeatmap = new EditorBeatmap((Beatmap)Beatmap.Value.Beatmap, BeatDivisor);
+ var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset);
+
+ var editorBeatmap = new EditorBeatmap(playable);
Dependencies.Cache(editorBeatmap);
Dependencies.CacheAs(editorBeatmap);
AddRange(new Drawable[]
{
+ editorBeatmap,
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
diff --git a/osu.Game/Tests/Visual/AllPlayersTestScene.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
similarity index 59%
rename from osu.Game/Tests/Visual/AllPlayersTestScene.cs
rename to osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
index dd65c8c382..83a7b896d2 100644
--- a/osu.Game/Tests/Visual/AllPlayersTestScene.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
@@ -2,49 +2,66 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
+using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Configuration;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch;
+using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Taiko;
using osu.Game.Screens.Play;
-namespace osu.Game.Tests.Visual
+namespace osu.Game.Tests.Visual.Gameplay
{
///
/// A base class which runs test for all available rulesets.
/// Steps to be run for each ruleset should be added via .
///
- public abstract class AllPlayersTestScene : RateAdjustedBeatmapTestScene
+ public abstract class TestSceneAllRulesetPlayers : RateAdjustedBeatmapTestScene
{
protected Player Player;
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
- foreach (var r in rulesets.AvailableRulesets)
- {
- Player p = null;
- AddStep(r.Name, () => p = loadPlayerFor(r));
- AddUntilStep("player loaded", () =>
- {
- if (p?.IsLoaded == true)
- {
- p = null;
- return true;
- }
-
- return false;
- });
-
- AddCheckSteps();
- }
-
OsuConfigManager manager;
Dependencies.Cache(manager = new OsuConfigManager(LocalStorage));
manager.GetBindable(OsuSetting.DimLevel).Value = 1.0;
}
+ [Test]
+ public void TestOsu() => runForRuleset(new OsuRuleset().RulesetInfo);
+
+ [Test]
+ public void TestTaiko() => runForRuleset(new TaikoRuleset().RulesetInfo);
+
+ [Test]
+ public void TestCatch() => runForRuleset(new CatchRuleset().RulesetInfo);
+
+ [Test]
+ public void TestMania() => runForRuleset(new ManiaRuleset().RulesetInfo);
+
+ private void runForRuleset(RulesetInfo ruleset)
+ {
+ Player p = null;
+ AddStep($"load {ruleset.Name} player", () => p = loadPlayerFor(ruleset));
+ AddUntilStep("player loaded", () =>
+ {
+ if (p?.IsLoaded == true)
+ {
+ p = null;
+ return true;
+ }
+
+ return false;
+ });
+
+ AddCheckSteps();
+ }
+
protected abstract void AddCheckSteps();
private Player loadPlayerFor(RulesetInfo rulesetInfo)
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
index 069b965d9b..afeda5fb7c 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
@@ -3,53 +3,32 @@
using System.ComponentModel;
using System.Linq;
-using osu.Game.Beatmaps;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
-using osu.Game.Storyboards;
namespace osu.Game.Tests.Visual.Gameplay
{
[Description("Player instantiated with an autoplay mod.")]
- public class TestSceneAutoplay : AllPlayersTestScene
+ public class TestSceneAutoplay : TestSceneAllRulesetPlayers
{
- private ClockBackedTestWorkingBeatmap.TrackVirtualManual track;
+ protected new TestPlayer Player => (TestPlayer)base.Player;
protected override Player CreatePlayer(Ruleset ruleset)
{
SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray();
- return new ScoreAccessiblePlayer();
+ return new TestPlayer(false, false);
}
protected override void AddCheckSteps()
{
- AddUntilStep("score above zero", () => ((ScoreAccessiblePlayer)Player).ScoreProcessor.TotalScore.Value > 0);
- AddUntilStep("key counter counted keys", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2));
- AddStep("rewind", () => track.Seek(-10000));
- AddUntilStep("key counter reset", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
- }
-
- protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
- {
- var working = base.CreateWorkingBeatmap(beatmap, storyboard);
-
- track = (ClockBackedTestWorkingBeatmap.TrackVirtualManual)working.Track;
-
- return working;
- }
-
- private class ScoreAccessiblePlayer : TestPlayer
- {
- public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
- public new HUDOverlay HUDOverlay => base.HUDOverlay;
-
- public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer;
-
- public ScoreAccessiblePlayer()
- : base(false, false)
- {
- }
+ AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0);
+ AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2));
+ AddStep("seek to break time", () => Player.GameplayClockContainer.Seek(Player.BreakOverlay.Breaks.First().StartTime));
+ AddUntilStep("wait for seek to complete", () =>
+ Player.HUDOverlay.Progress.ReferenceClock.CurrentTime >= Player.BreakOverlay.Breaks.First().StartTime);
+ AddAssert("test keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting);
+ AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000));
+ AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
index 46f62b9541..b25b81c9af 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
@@ -20,6 +20,7 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
@@ -289,7 +290,7 @@ namespace osu.Game.Tests.Visual.Gameplay
#region HitObject
- private class TestHitObject : HitObject, IHasEndTime
+ private class TestHitObject : ConvertHitObject, IHasEndTime
{
public double EndTime { get; set; }
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
index 81050b1637..de257c9e53 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
@@ -10,7 +10,7 @@ using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual.Gameplay
{
- public class TestSceneFailAnimation : AllPlayersTestScene
+ public class TestSceneFailAnimation : TestSceneAllRulesetPlayers
{
protected override Player CreatePlayer(Ruleset ruleset)
{
@@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public override IReadOnlyList RequiredTypes => new[]
{
- typeof(AllPlayersTestScene),
+ typeof(TestSceneAllRulesetPlayers),
typeof(TestPlayer),
typeof(Player),
};
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
index 2045072c79..d80efb2c6e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
@@ -10,7 +10,7 @@ using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual.Gameplay
{
- public class TestSceneFailJudgement : AllPlayersTestScene
+ public class TestSceneFailJudgement : TestSceneAllRulesetPlayers
{
protected override Player CreatePlayer(Ruleset ruleset)
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
index 78c3b22fb9..310746d179 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . 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;
@@ -11,12 +10,8 @@ using osu.Framework.Utils;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Scoring;
-using osu.Game.Rulesets.UI;
-using osu.Game.Screens.Play;
using osu.Game.Storyboards;
using osuTK;
@@ -24,8 +19,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneGameplayRewinding : PlayerTestScene
{
- private RulesetExposingPlayer player => (RulesetExposingPlayer)Player;
-
[Resolved]
private AudioManager audioManager { get; set; }
@@ -48,13 +41,13 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddUntilStep("wait for track to start running", () => track.IsRunning);
addSeekStep(3000);
- AddAssert("all judged", () => player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged));
- AddUntilStep("key counter counted keys", () => player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses >= 7));
- AddStep("clear results", () => player.AppliedResults.Clear());
+ AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged));
+ AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses >= 7));
+ AddStep("clear results", () => Player.Results.Clear());
addSeekStep(0);
- AddAssert("none judged", () => player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged));
- AddUntilStep("key counters reset", () => player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
- AddAssert("no results triggered", () => player.AppliedResults.Count == 0);
+ AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged));
+ AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
+ AddAssert("no results triggered", () => Player.Results.Count == 0);
}
private void addSeekStep(double time)
@@ -62,13 +55,13 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep($"seek to {time}", () => track.Seek(time));
// Allow a few frames of lenience
- AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
+ AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
- protected override Player CreatePlayer(Ruleset ruleset)
+ protected override TestPlayer CreatePlayer(Ruleset ruleset)
{
SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray();
- return new RulesetExposingPlayer();
+ return base.CreatePlayer(ruleset);
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
@@ -89,29 +82,5 @@ namespace osu.Game.Tests.Visual.Gameplay
return beatmap;
}
-
- private class RulesetExposingPlayer : Player
- {
- public readonly List AppliedResults = new List();
-
- public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
-
- public new HUDOverlay HUDOverlay => base.HUDOverlay;
-
- public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer;
-
- public new DrawableRuleset DrawableRuleset => base.DrawableRuleset;
-
- public RulesetExposingPlayer()
- : base(false, false)
- {
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- ScoreProcessor.NewJudgement += r => AppliedResults.Add(r);
- }
- }
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index ee58219cd3..fc03dc6ed3 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -2,12 +2,16 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -15,7 +19,9 @@ namespace osu.Game.Tests.Visual.Gameplay
{
private HUDOverlay hudOverlay;
- private Drawable hideTarget => hudOverlay.KeyCounter; // best way of checking hideTargets without exposing.
+ // best way to check without exposing.
+ private Drawable hideTarget => hudOverlay.KeyCounter;
+ private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First();
[Resolved]
private OsuConfigManager config { get; set; }
@@ -28,6 +34,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("showhud is set", () => hudOverlay.ShowHud.Value);
AddAssert("hidetarget is visible", () => hideTarget.IsPresent);
+ AddAssert("key counter flow is visible", () => keyCounterFlow.IsPresent);
AddAssert("pause button is visible", () => hudOverlay.HoldToQuit.IsPresent);
}
@@ -50,6 +57,9 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent);
+
+ // Key counter flow container should not be affected by this, only the key counter display will be hidden as checked above.
+ AddAssert("key counter flow not affected", () => keyCounterFlow.IsPresent);
}
[Test]
@@ -68,12 +78,40 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("config unchanged", () => originalConfigValue == config.Get(OsuSetting.ShowInterface));
}
+ [Test]
+ public void TestChangeHUDVisibilityOnHiddenKeyCounter()
+ {
+ bool keyCounterVisibleValue = false;
+
+ createNew();
+ AddStep("save keycounter visible value", () => keyCounterVisibleValue = config.Get(OsuSetting.KeyOverlay));
+
+ AddStep("set keycounter visible false", () =>
+ {
+ config.Set(OsuSetting.KeyOverlay, false);
+ hudOverlay.KeyCounter.AlwaysVisible.Value = false;
+ });
+
+ AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
+ AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
+ AddAssert("key counters hidden", () => !keyCounterFlow.IsPresent);
+
+ AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true);
+ AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent);
+ AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
+
+ AddStep("return value", () => config.Set(OsuSetting.KeyOverlay, keyCounterVisibleValue));
+ }
+
private void createNew(Action action = null)
{
AddStep("create overlay", () =>
{
Child = hudOverlay = new HUDOverlay(null, null, null, Array.Empty());
+ // Add any key just to display the key counter visually.
+ hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
+
action?.Invoke(hudOverlay);
});
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
index 8904b54b0d..1527cba6fc 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
@@ -2,16 +2,17 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
-using osu.Game.Rulesets.Objects;
using System;
using System.Collections.Generic;
using osu.Game.Rulesets.Judgements;
using osu.Framework.Utils;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Threading;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Mania.Scoring;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Scoring;
@@ -43,6 +44,22 @@ namespace osu.Game.Tests.Visual.Gameplay
AddRepeatStep("New max negative", () => newJudgement(-hitWindows.WindowFor(HitResult.Meh)), 20);
AddRepeatStep("New max positive", () => newJudgement(hitWindows.WindowFor(HitResult.Meh)), 20);
AddStep("New fixed judgement (50ms)", () => newJudgement(50));
+
+ AddStep("Judgement barrage", () =>
+ {
+ int runCount = 0;
+
+ ScheduledDelegate del = null;
+
+ del = Scheduler.AddDelayed(() =>
+ {
+ newJudgement(runCount++ / 10f);
+
+ if (runCount == 500)
+ // ReSharper disable once AccessToModifiedClosure
+ del?.Cancel();
+ }, 10, true);
+ });
}
[Test]
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs
index e7b3e007fc..227ada70fe 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs
@@ -47,21 +47,22 @@ namespace osu.Game.Tests.Visual.Gameplay
Key testKey = ((KeyCounterKeyboard)kc.Children.First()).Key;
- AddStep($"Press {testKey} key", () =>
+ void addPressKeyStep()
{
- InputManager.PressKey(testKey);
- InputManager.ReleaseKey(testKey);
- });
+ AddStep($"Press {testKey} key", () =>
+ {
+ InputManager.PressKey(testKey);
+ InputManager.ReleaseKey(testKey);
+ });
+ }
+ addPressKeyStep();
AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 1);
-
- AddStep($"Press {testKey} key", () =>
- {
- InputManager.PressKey(testKey);
- InputManager.ReleaseKey(testKey);
- });
-
+ addPressKeyStep();
AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 2);
+ AddStep("Disable counting", () => testCounter.IsCounting = false);
+ addPressKeyStep();
+ AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses == 2);
Add(kc);
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
index 1a83e35e4f..944e6ca6be 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
@@ -11,7 +11,6 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osuTK;
using osuTK.Input;
@@ -36,6 +35,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public override void SetUpSteps()
{
base.SetUpSteps();
+
AddStep("resume player", () => Player.GameplayClockContainer.Start());
confirmClockRunning(true);
}
@@ -281,14 +281,10 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override bool AllowFail => true;
- protected override Player CreatePlayer(Ruleset ruleset) => new PausePlayer();
+ protected override TestPlayer CreatePlayer(Ruleset ruleset) => new PausePlayer();
protected class PausePlayer : TestPlayer
{
- public new HealthProcessor HealthProcessor => base.HealthProcessor;
-
- public new HUDOverlay HUDOverlay => base.HUDOverlay;
-
public bool FailOverlayVisible => FailOverlay.State.Value == Visibility.Visible;
public bool PauseOverlayVisible => PauseOverlay.State.Value == Visibility.Visible;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs
index 3513b6c25a..a83320048b 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs
@@ -9,15 +9,12 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
-using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual.Gameplay
{
[HeadlessTest] // we alter unsafe properties on the game host to test inactive window state.
public class TestScenePauseWhenInactive : PlayerTestScene
{
- protected new TestPlayer Player => (TestPlayer)base.Player;
-
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = (Beatmap)base.CreateBeatmap(ruleset);
@@ -46,6 +43,6 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("time of pause is after gameplay start time", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= Player.DrawableRuleset.GameplayStartTime);
}
- protected override Player CreatePlayer(Ruleset ruleset) => new TestPlayer(true, true, true);
+ protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, true, true);
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index ad5950d9fc..175f909a5a 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -9,7 +9,6 @@ using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
@@ -91,9 +90,44 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("load dummy beatmap", () => ResetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
- AddRepeatStep("move mouse", () => InputManager.MoveMouseTo(loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft + (loader.VisualSettings.ScreenSpaceDrawQuad.BottomRight - loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft) * RNG.NextSingle()), 20);
+
+ AddUntilStep("wait for load ready", () =>
+ {
+ moveMouse();
+ return player.LoadState == LoadState.Ready;
+ });
+ AddRepeatStep("move mouse", moveMouse, 20);
+
AddAssert("loader still active", () => loader.IsCurrentScreen());
AddUntilStep("loads after idle", () => !loader.IsCurrentScreen());
+
+ void moveMouse()
+ {
+ InputManager.MoveMouseTo(
+ loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft
+ + (loader.VisualSettings.ScreenSpaceDrawQuad.BottomRight - loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft)
+ * RNG.NextSingle());
+ }
+ }
+
+ [Test]
+ public void TestBlockLoadViaFocus()
+ {
+ OsuFocusedOverlayContainer overlay = null;
+
+ AddStep("load dummy beatmap", () => ResetPlayer(false));
+ AddUntilStep("wait for current", () => loader.IsCurrentScreen());
+
+ AddStep("show focused overlay", () => { container.Add(overlay = new ChangelogOverlay { State = { Value = Visibility.Visible } }); });
+ AddUntilStep("overlay visible", () => overlay.IsPresent);
+
+ AddUntilStep("wait for load ready", () => player.LoadState == LoadState.Ready);
+ AddRepeatStep("twiddle thumbs", () => { }, 20);
+
+ AddAssert("loader still active", () => loader.IsCurrentScreen());
+
+ AddStep("hide overlay", () => overlay.Hide());
+ AddUntilStep("loads after idle", () => !loader.IsCurrentScreen());
}
[Test]
@@ -159,13 +193,22 @@ namespace osu.Game.Tests.Visual.Gameplay
}
[Test]
- public void TestMutedNotificationMasterVolume() => addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, null, () => audioManager.Volume.IsDefault);
+ public void TestMutedNotificationMasterVolume()
+ {
+ addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, null, () => audioManager.Volume.IsDefault);
+ }
[Test]
- public void TestMutedNotificationTrackVolume() => addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, null, () => audioManager.VolumeTrack.IsDefault);
+ public void TestMutedNotificationTrackVolume()
+ {
+ addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, null, () => audioManager.VolumeTrack.IsDefault);
+ }
[Test]
- public void TestMutedNotificationMuteButton() => addVolumeSteps("mute button", null, () => container.VolumeOverlay.IsMuted.Value = true, () => !container.VolumeOverlay.IsMuted.Value);
+ public void TestMutedNotificationMuteButton()
+ {
+ addVolumeSteps("mute button", null, () => container.VolumeOverlay.IsMuted.Value = true, () => !container.VolumeOverlay.IsMuted.Value);
+ }
///
/// Created for avoiding copy pasting code for the same steps.
@@ -179,7 +222,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("reset notification lock", () => sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce).Value = false);
AddStep("load player", () => ResetPlayer(false, beforeLoad, afterLoad));
- AddUntilStep("wait for player", () => player.IsLoaded);
+ AddUntilStep("wait for player", () => player.LoadState == LoadState.Ready);
AddAssert("check for notification", () => container.NotificationOverlay.UnreadCount.Value == 1);
AddStep("click notification", () =>
@@ -193,6 +236,8 @@ namespace osu.Game.Tests.Visual.Gameplay
});
AddAssert("check " + volumeName, assert);
+
+ AddUntilStep("wait for player load", () => player.IsLoaded);
}
private class TestPlayerLoaderContainer : Container
@@ -207,9 +252,11 @@ namespace osu.Game.Tests.Visual.Gameplay
{
RelativeSizeAxes = Axes.Both;
+ OsuScreenStack stack;
+
InternalChildren = new Drawable[]
{
- new OsuScreenStack(screen)
+ stack = new OsuScreenStack
{
RelativeSizeAxes = Axes.Both,
},
@@ -224,6 +271,8 @@ namespace osu.Game.Tests.Visual.Gameplay
Origin = Anchor.TopLeft,
}
};
+
+ stack.Push(screen);
}
}
@@ -257,17 +306,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
}
- private class TestPlayer : Visual.TestPlayer
- {
- public new Bindable> Mods => base.Mods;
-
- public TestPlayer(bool allowPause = true, bool showResults = true)
- : base(allowPause, showResults)
- {
- }
- }
-
- protected class SlowLoadPlayer : Visual.TestPlayer
+ protected class SlowLoadPlayer : TestPlayer
{
public readonly ManualResetEventSlim AllowLoad = new ManualResetEventSlim(false);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerReferenceLeaking.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerReferenceLeaking.cs
index 4d701f56a9..8f767659c6 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerReferenceLeaking.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerReferenceLeaking.cs
@@ -10,7 +10,7 @@ using osu.Game.Storyboards;
namespace osu.Game.Tests.Visual.Gameplay
{
- public class TestScenePlayerReferenceLeaking : AllPlayersTestScene
+ public class TestScenePlayerReferenceLeaking : TestSceneAllRulesetPlayers
{
private readonly WeakList workingWeakReferences = new WeakList();
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
index 36335bc54a..e82722e7a2 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
@@ -13,7 +13,7 @@ using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual.Gameplay
{
[Description("Player instantiated with a replay.")]
- public class TestSceneReplay : AllPlayersTestScene
+ public class TestSceneReplay : TestSceneAllRulesetPlayers
{
protected override Player CreatePlayer(Ruleset ruleset)
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
index 8cb44de8cb..c9561a70fa 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
@@ -11,7 +11,7 @@ using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Game.Rulesets;
-using osu.Game.Screens.Ranking.Pages;
+using osu.Game.Screens.Ranking;
namespace osu.Game.Tests.Visual.Gameplay
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs
index ffd6f55b53..030d420ec0 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs
@@ -3,9 +3,6 @@
using NUnit.Framework;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Utils;
-using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Play.HUD;
using osuTK;
@@ -45,32 +42,12 @@ namespace osu.Game.Tests.Visual.Gameplay
};
Add(accuracyCounter);
- StarCounter stars = new StarCounter
- {
- Origin = Anchor.BottomLeft,
- Anchor = Anchor.BottomLeft,
- Position = new Vector2(20, -160),
- CountStars = 5,
- };
- Add(stars);
-
- SpriteText starsLabel = new OsuSpriteText
- {
- Origin = Anchor.BottomLeft,
- Anchor = Anchor.BottomLeft,
- Position = new Vector2(20, -190),
- Text = stars.CountStars.ToString("0.00"),
- };
- Add(starsLabel);
-
AddStep(@"Reset all", delegate
{
score.Current.Value = 0;
comboCounter.Current.Value = 0;
numerator = denominator = 0;
accuracyCounter.SetFraction(0, 0);
- stars.CountStars = 0;
- starsLabel.Text = stars.CountStars.ToString("0.00");
});
AddStep(@"Hit! :D", delegate
@@ -88,20 +65,6 @@ namespace osu.Game.Tests.Visual.Gameplay
denominator++;
accuracyCounter.SetFraction(numerator, denominator);
});
-
- AddStep(@"Alter stars", delegate
- {
- stars.CountStars = RNG.NextSingle() * (stars.StarCount + 1);
- starsLabel.Text = stars.CountStars.ToString("0.00");
- });
-
- AddStep(@"Stop counters", delegate
- {
- score.StopRolling();
- comboCounter.StopRolling();
- accuracyCounter.StopRolling();
- stars.StopAnimation();
- });
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
index 8629522dc2..d03716db2e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading;
+using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -19,6 +20,7 @@ using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -30,7 +32,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached(typeof(IReadOnlyList))]
private IReadOnlyList mods { get; set; } = Array.Empty();
- private const int spawn_interval = 5000;
+ private const int time_range = 5000;
+ private const int spawn_rate = time_range / 10;
private readonly ScrollingTestContainer[] scrollContainers = new ScrollingTestContainer[4];
private readonly TestPlayfield[] playfields = new TestPlayfield[4];
@@ -50,13 +53,13 @@ namespace osu.Game.Tests.Visual.Gameplay
{
RelativeSizeAxes = Axes.Both,
Child = playfields[0] = new TestPlayfield(),
- TimeRange = spawn_interval
+ TimeRange = time_range
},
scrollContainers[1] = new ScrollingTestContainer(ScrollingDirection.Down)
{
RelativeSizeAxes = Axes.Both,
Child = playfields[1] = new TestPlayfield(),
- TimeRange = spawn_interval
+ TimeRange = time_range
},
},
new Drawable[]
@@ -65,13 +68,13 @@ namespace osu.Game.Tests.Visual.Gameplay
{
RelativeSizeAxes = Axes.Both,
Child = playfields[2] = new TestPlayfield(),
- TimeRange = spawn_interval
+ TimeRange = time_range
},
scrollContainers[3] = new ScrollingTestContainer(ScrollingDirection.Right)
{
RelativeSizeAxes = Axes.Both,
Child = playfields[3] = new TestPlayfield(),
- TimeRange = spawn_interval
+ TimeRange = time_range
}
}
}
@@ -84,31 +87,55 @@ namespace osu.Game.Tests.Visual.Gameplay
{
scrollContainers.ForEach(c => c.ControlPoints.Add(new MultiplierControlPoint(0)));
- for (int i = 0; i <= spawn_interval; i += 1000)
+ for (int i = spawn_rate / 2; i <= time_range; i += spawn_rate)
addHitObject(Time.Current + i);
hitObjectSpawnDelegate?.Cancel();
- hitObjectSpawnDelegate = Scheduler.AddDelayed(() => addHitObject(Time.Current + spawn_interval), 1000, true);
+ hitObjectSpawnDelegate = Scheduler.AddDelayed(() => addHitObject(Time.Current + time_range), spawn_rate, true);
}
+ private IList testControlPoints => new List
+ {
+ new MultiplierControlPoint(time_range) { DifficultyPoint = { SpeedMultiplier = 1.25 } },
+ new MultiplierControlPoint(1.5 * time_range) { DifficultyPoint = { SpeedMultiplier = 1 } },
+ new MultiplierControlPoint(2 * time_range) { DifficultyPoint = { SpeedMultiplier = 1.5 } }
+ };
+
[Test]
public void TestScrollAlgorithms()
{
- AddStep("Constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant));
- AddStep("Overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping));
- AddStep("Sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential));
+ AddStep("constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant));
+ AddStep("overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping));
+ AddStep("sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential));
- AddSliderStep("Time range", 100, 10000, spawn_interval, v => scrollContainers.Where(c => c != null).ForEach(c => c.TimeRange = v));
- AddStep("Add control point", () => addControlPoint(Time.Current + spawn_interval));
+ AddSliderStep("time range", 100, 10000, time_range, v => scrollContainers.Where(c => c != null).ForEach(c => c.TimeRange = v));
+
+ AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current));
}
[Test]
- public void TestScrollLifetime()
+ public void TestConstantScrollLifetime()
{
- AddStep("Set constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant));
+ AddStep("set constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant));
// scroll container time range must be less than the rate of spawning hitobjects
// otherwise the hitobjects will spawn already partly visible on screen and look wrong
- AddStep("Set time range", () => scrollContainers.ForEach(c => c.TimeRange = spawn_interval / 2.0));
+ AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range / 2.0));
+ }
+
+ [Test]
+ public void TestSequentialScrollLifetime()
+ {
+ AddStep("set sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential));
+ AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range / 2.0));
+ AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current));
+ }
+
+ [Test]
+ public void TestOverlappingScrollLifetime()
+ {
+ AddStep("set overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping));
+ AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range / 2.0));
+ AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current));
}
private void addHitObject(double time)
@@ -122,28 +149,27 @@ namespace osu.Game.Tests.Visual.Gameplay
});
}
- private void addControlPoint(double time)
+ private TestDrawableControlPoint createDrawablePoint(TestPlayfield playfield, double t)
{
- scrollContainers.ForEach(c =>
+ var obj = new TestDrawableControlPoint(playfield.Direction, t);
+ setAnchor(obj, playfield);
+ return obj;
+ }
+
+ private void addControlPoints(IList controlPoints, double sequenceStartTime)
+ {
+ controlPoints.ForEach(point => point.StartTime += sequenceStartTime);
+
+ scrollContainers.ForEach(container =>
{
- c.ControlPoints.Add(new MultiplierControlPoint(time) { DifficultyPoint = { SpeedMultiplier = 3 } });
- c.ControlPoints.Add(new MultiplierControlPoint(time + 2000) { DifficultyPoint = { SpeedMultiplier = 2 } });
- c.ControlPoints.Add(new MultiplierControlPoint(time + 3000) { DifficultyPoint = { SpeedMultiplier = 1 } });
+ container.ControlPoints.AddRange(controlPoints);
});
- playfields.ForEach(p =>
+ foreach (var playfield in playfields)
{
- TestDrawableControlPoint createDrawablePoint(double t)
- {
- var obj = new TestDrawableControlPoint(p.Direction, t);
- setAnchor(obj, p);
- return obj;
- }
-
- p.Add(createDrawablePoint(time));
- p.Add(createDrawablePoint(time + 2000));
- p.Add(createDrawablePoint(time + 3000));
- });
+ foreach (var controlPoint in controlPoints)
+ playfield.Add(createDrawablePoint(playfield, controlPoint.StartTime));
+ }
}
private void setAnchor(DrawableHitObject obj, TestPlayfield playfield)
@@ -236,7 +262,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AutoSizeAxes = Axes.Both;
- AddInternal(new Box { Size = new Vector2(75) });
+ AddInternal(new Box
+ {
+ Size = new Vector2(75),
+ Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)
+ });
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs
new file mode 100644
index 0000000000..709e71d195
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs
@@ -0,0 +1,57 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Utils;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ [TestFixture]
+ public class TestSceneStarCounter : OsuTestScene
+ {
+ public TestSceneStarCounter()
+ {
+ StarCounter stars = new StarCounter
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Current = 5,
+ };
+
+ Add(stars);
+
+ SpriteText starsLabel = new OsuSpriteText
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Scale = new Vector2(2),
+ Y = 50,
+ Text = stars.Current.ToString("0.00"),
+ };
+
+ Add(starsLabel);
+
+ AddRepeatStep(@"random value", delegate
+ {
+ stars.Current = RNG.NextSingle() * (stars.StarCount + 1);
+ starsLabel.Text = stars.Current.ToString("0.00");
+ }, 10);
+
+ AddStep(@"Stop animation", delegate
+ {
+ stars.StopAnimation();
+ });
+
+ AddStep(@"Reset", delegate
+ {
+ stars.Current = 0;
+ starsLabel.Text = stars.Current.ToString("0.00");
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
index d03d341ee4..5870ef9813 100644
--- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
+++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Menus
protected IntroTestScene()
{
- Drawable introStack = null;
+ OsuScreenStack introStack = null;
Children = new Drawable[]
{
@@ -57,10 +57,12 @@ namespace osu.Game.Tests.Visual.Menus
introStack?.Expire();
- Add(introStack = new OsuScreenStack(CreateScreen())
+ Add(introStack = new OsuScreenStack
{
RelativeSizeAxes = Axes.Both,
});
+
+ introStack.Push(CreateScreen());
});
}
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs b/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs
index 681bf1b40b..49fab08ded 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Menus
API.LocalUser.Value = new User
{
Username = API.LocalUser.Value.Username,
- Id = API.LocalUser.Value.Id,
+ Id = API.LocalUser.Value.Id + 1,
IsSupporter = !API.LocalUser.Value.IsSupporter,
};
});
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs
similarity index 77%
rename from osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs
rename to osu.Game.Tests/Visual/Menus/TestSceneLoader.cs
index 61fed3013e..b3064ba9be 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs
@@ -1,12 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Linq;
using System.Threading;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Screens;
+using osu.Framework.Testing;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Screens;
using osu.Game.Screens.Menu;
using osuTK.Graphics;
@@ -14,14 +17,14 @@ using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Menus
{
[TestFixture]
- public class TestSceneLoaderAnimation : ScreenTestScene
+ public class TestSceneLoader : ScreenTestScene
{
private TestLoader loader;
[Cached]
private OsuLogo logo;
- public TestSceneLoaderAnimation()
+ public TestSceneLoader()
{
Child = logo = new OsuLogo
{
@@ -42,33 +45,33 @@ namespace osu.Game.Tests.Visual.Menus
LoadScreen(loader);
});
+
+ AddAssert("spinner did not display", () => loader.LoadingSpinner?.Alpha == 0);
+
+ AddUntilStep("loaded", () => loader.ScreenLoaded);
+ AddUntilStep("not current", () => !loader.IsCurrentScreen());
}
[Test]
public void TestDelayedLoad()
{
AddStep("begin loading", () => LoadScreen(loader = new TestLoader()));
- AddUntilStep("wait for logo visible", () => loader.Logo?.Alpha > 0);
+ AddUntilStep("wait for spinner visible", () => loader.LoadingSpinner?.Alpha > 0);
AddStep("finish loading", () => loader.AllowLoad.Set());
- AddUntilStep("loaded", () => loader.Logo != null && loader.ScreenLoaded);
- AddUntilStep("logo gone", () => loader.Logo?.Alpha == 0);
+ AddUntilStep("spinner gone", () => loader.LoadingSpinner?.Alpha == 0);
+ AddUntilStep("loaded", () => loader.ScreenLoaded);
+ AddUntilStep("not current", () => !loader.IsCurrentScreen());
}
private class TestLoader : Loader
{
public readonly ManualResetEventSlim AllowLoad = new ManualResetEventSlim();
- public OsuLogo Logo;
+ public LoadingSpinner LoadingSpinner => this.ChildrenOfType().FirstOrDefault();
private TestScreen screen;
public bool ScreenLoaded => screen.IsCurrentScreen();
- protected override void LogoArriving(OsuLogo logo, bool resuming)
- {
- Logo = logo;
- base.LogoArriving(logo, resuming);
- }
-
protected override OsuScreen CreateLoadableScreen() => screen = new TestScreen();
protected override ShaderPrecompiler CreateShaderPrecompiler() => new TestShaderPrecompiler(AllowLoad);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
new file mode 100644
index 0000000000..9fbe8f7ffe
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
@@ -0,0 +1,255 @@
+// Copyright (c) ppy Pty Ltd . 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Screens.Multi;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneDrawableRoomPlaylist : ManualInputManagerTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(DrawableRoomPlaylist),
+ typeof(DrawableRoomPlaylistItem)
+ };
+
+ private TestPlaylist playlist;
+
+ [Test]
+ public void TestNonEditableNonSelectable()
+ {
+ createPlaylist(false, false);
+
+ moveToItem(0);
+ assertHandleVisibility(0, false);
+ assertDeleteButtonVisibility(0, false);
+
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
+ }
+
+ [Test]
+ public void TestEditable()
+ {
+ createPlaylist(true, false);
+
+ moveToItem(0);
+ assertHandleVisibility(0, true);
+ assertDeleteButtonVisibility(0, true);
+
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
+ }
+
+ [Test]
+ public void TestSelectable()
+ {
+ createPlaylist(false, true);
+
+ moveToItem(0);
+ assertHandleVisibility(0, false);
+ assertDeleteButtonVisibility(0, false);
+
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
+ }
+
+ [Test]
+ public void TestEditableSelectable()
+ {
+ createPlaylist(true, true);
+
+ moveToItem(0);
+ assertHandleVisibility(0, true);
+ assertDeleteButtonVisibility(0, true);
+
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
+ }
+
+ [Test]
+ public void TestSelectionNotLostAfterRearrangement()
+ {
+ createPlaylist(true, true);
+
+ moveToItem(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ moveToDragger(0);
+ AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
+ moveToDragger(1, new Vector2(0, 5));
+ AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
+
+ AddAssert("item 1 is selected", () => playlist.SelectedItem.Value == playlist.Items[1]);
+ }
+
+ [Test]
+ public void TestItemRemovedOnDeletion()
+ {
+ PlaylistItem selectedItem = null;
+
+ createPlaylist(true, true);
+
+ moveToItem(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ AddStep("retrieve selection", () => selectedItem = playlist.SelectedItem.Value);
+
+ moveToDeleteButton(0);
+ AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("item removed", () => !playlist.Items.Contains(selectedItem));
+ }
+
+ [Test]
+ public void TestNextItemSelectedAfterDeletion()
+ {
+ createPlaylist(true, true);
+
+ moveToItem(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ moveToDeleteButton(0);
+ AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
+ }
+
+ [Test]
+ public void TestLastItemSelectedAfterLastItemDeleted()
+ {
+ createPlaylist(true, true);
+
+ AddWaitStep("wait for flow", 5); // Items may take 1 update frame to flow. A wait count of 5 is guaranteed to result in the flow being updated as desired.
+ AddStep("scroll to bottom", () => playlist.ChildrenOfType>().First().ScrollToEnd(false));
+
+ moveToItem(19);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ moveToDeleteButton(19);
+ AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("item 18 is selected", () => playlist.SelectedItem.Value == playlist.Items[18]);
+ }
+
+ [Test]
+ public void TestSelectionResetWhenAllItemsDeleted()
+ {
+ createPlaylist(true, true);
+
+ AddStep("remove all but one item", () =>
+ {
+ playlist.Items.RemoveRange(1, playlist.Items.Count - 1);
+ });
+
+ moveToItem(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ moveToDeleteButton(0);
+ AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
+ }
+
+ // Todo: currently not possible due to bindable list shortcomings (https://github.com/ppy/osu-framework/issues/3081)
+ // [Test]
+ public void TestNextItemSelectedAfterExternalDeletion()
+ {
+ createPlaylist(true, true);
+
+ moveToItem(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ AddStep("remove item 0", () => playlist.Items.RemoveAt(0));
+
+ AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
+ }
+
+ [Test]
+ public void TestChangeBeatmapAndRemove()
+ {
+ createPlaylist(true, true);
+
+ AddStep("change beatmap of first item", () => playlist.Items[0].BeatmapID = 30);
+ moveToDeleteButton(0);
+ AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
+ }
+
+ private void moveToItem(int index, Vector2? offset = null)
+ => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType>().ElementAt(index), offset));
+
+ private void moveToDragger(int index, Vector2? offset = null) => AddStep($"move mouse to dragger {index}", () =>
+ {
+ var item = playlist.ChildrenOfType>().ElementAt(index);
+ InputManager.MoveMouseTo(item.ChildrenOfType.PlaylistItemHandle>().Single(), offset);
+ });
+
+ private void moveToDeleteButton(int index, Vector2? offset = null) => AddStep($"move mouse to delete button {index}", () =>
+ {
+ var item = playlist.ChildrenOfType>().ElementAt(index);
+ InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0), offset);
+ });
+
+ private void assertHandleVisibility(int index, bool visible)
+ => AddAssert($"handle {index} {(visible ? "is" : "is not")} visible",
+ () => (playlist.ChildrenOfType.PlaylistItemHandle>().ElementAt(index).Alpha > 0) == visible);
+
+ private void assertDeleteButtonVisibility(int index, bool visible)
+ => AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible", () => (playlist.ChildrenOfType().ElementAt(2 + index * 2).Alpha > 0) == visible);
+
+ private void createPlaylist(bool allowEdit, bool allowSelection)
+ {
+ AddStep("create playlist", () =>
+ {
+ Child = playlist = new TestPlaylist(allowEdit, allowSelection)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(500, 300)
+ };
+
+ for (int i = 0; i < 20; i++)
+ {
+ playlist.Items.Add(new PlaylistItem
+ {
+ ID = i,
+ Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ RequiredMods =
+ {
+ new OsuModHardRock(),
+ new OsuModDoubleTime(),
+ new OsuModAutoplay()
+ }
+ });
+ }
+ });
+
+ AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
+ }
+
+ private class TestPlaylist : DrawableRoomPlaylist
+ {
+ public new IReadOnlyDictionary> ItemMap => base.ItemMap;
+
+ public TestPlaylist(bool allowEdit, bool allowSelection)
+ : base(allowEdit, allowSelection)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs
new file mode 100644
index 0000000000..1e1bc9725c
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs
@@ -0,0 +1,59 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Multiplayer.RoomStatuses;
+using osu.Game.Screens.Multi.Lounge.Components;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneLoungeRoomInfo : MultiplayerTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(RoomInfo)
+ };
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Room.CopyFrom(new Room());
+
+ Child = new RoomInfo
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 500
+ };
+ });
+
+ public override void SetUpSteps()
+ {
+ // Todo: Temp
+ }
+
+ [Test]
+ public void TestNonSelectedRoom()
+ {
+ AddStep("set null room", () => Room.RoomID.Value = null);
+ }
+
+ [Test]
+ public void TestOpenRoom()
+ {
+ AddStep("set open room", () =>
+ {
+ Room.RoomID.Value = 0;
+ Room.Name.Value = "Room 0";
+ Room.Host.Value = new User { Username = "peppy", Id = 2 };
+ Room.EndDate.Value = DateTimeOffset.Now.AddMonths(1);
+ Room.Status.Value = new RoomStatusOpen();
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
index eb4dc909df..b5d946d049 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
@@ -4,11 +4,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch;
+using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Multi;
using osu.Game.Screens.Multi.Lounge.Components;
using osu.Game.Users;
@@ -27,11 +32,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Cached(Type = typeof(IRoomManager))]
private TestRoomManager roomManager = new TestRoomManager();
+ private RoomsContainer container;
+
[BackgroundDependencyLoader]
private void load()
{
- RoomsContainer container;
-
Child = container = new RoomsContainer
{
Anchor = Anchor.Centre,
@@ -39,24 +44,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
Width = 0.5f,
JoinRequested = joinRequested
};
+ }
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
AddStep("clear rooms", () => roomManager.Rooms.Clear());
+ }
- AddStep("add rooms", () =>
- {
- for (int i = 0; i < 3; i++)
- {
- roomManager.Rooms.Add(new Room
- {
- RoomID = { Value = i },
- Name = { Value = $"Room {i}" },
- Host = { Value = new User { Username = "Host" } },
- EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) }
- });
- }
- });
+ [Test]
+ public void TestBasicListChanges()
+ {
+ addRooms(3);
- AddAssert("has 2 rooms", () => container.Rooms.Count == 3);
+ AddAssert("has 3 rooms", () => container.Rooms.Count == 3);
AddStep("remove first room", () => roomManager.Rooms.Remove(roomManager.Rooms.FirstOrDefault()));
AddAssert("has 2 rooms", () => container.Rooms.Count == 2);
AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0));
@@ -68,6 +70,73 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("first room joined", () => roomManager.Rooms.First().Status.Value is JoinedRoomStatus);
}
+ [Test]
+ public void TestStringFiltering()
+ {
+ addRooms(4);
+
+ AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4);
+
+ AddStep("filter one room", () => container.Filter(new FilterCriteria { SearchString = "1" }));
+
+ AddUntilStep("1 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 1);
+
+ AddStep("remove filter", () => container.Filter(null));
+
+ AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4);
+ }
+
+ [Test]
+ public void TestRulesetFiltering()
+ {
+ addRooms(2, new OsuRuleset().RulesetInfo);
+ addRooms(3, new CatchRuleset().RulesetInfo);
+
+ AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5);
+
+ AddStep("filter osu! rooms", () => container.Filter(new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo }));
+
+ AddUntilStep("2 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2);
+
+ AddStep("filter catch rooms", () => container.Filter(new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo }));
+
+ AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3);
+ }
+
+ private void addRooms(int count, RulesetInfo ruleset = null)
+ {
+ AddStep("add rooms", () =>
+ {
+ for (int i = 0; i < count; i++)
+ {
+ var room = new Room
+ {
+ RoomID = { Value = i },
+ Name = { Value = $"Room {i}" },
+ Host = { Value = new User { Username = "Host" } },
+ EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) }
+ };
+
+ if (ruleset != null)
+ {
+ room.Playlist.Add(new PlaylistItem
+ {
+ Ruleset = { Value = ruleset },
+ Beatmap =
+ {
+ Value = new BeatmapInfo
+ {
+ Metadata = new BeatmapMetadata()
+ }
+ }
+ });
+ }
+
+ roomManager.Rooms.Add(room);
+ }
+ });
+ }
+
private void joinRequested(Room room) => room.Status.Value = new JoinedRoomStatus();
private class TestRoomManager : IRoomManager
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs
new file mode 100644
index 0000000000..24d9f5ab12
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs
@@ -0,0 +1,56 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Screens.Multi.Components;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneMatchBeatmapDetailArea : MultiplayerTestScene
+ {
+ [Resolved]
+ private BeatmapManager beatmapManager { get; set; }
+
+ [Resolved]
+ private RulesetStore rulesetStore { get; set; }
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Room.Playlist.Clear();
+
+ Child = new MatchBeatmapDetailArea
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(500),
+ CreateNewItem = createNewItem
+ };
+ });
+
+ private void createNewItem()
+ {
+ Room.Playlist.Add(new PlaylistItem
+ {
+ ID = Room.Playlist.Count,
+ Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ RequiredMods =
+ {
+ new OsuModHardRock(),
+ new OsuModDoubleTime(),
+ new OsuModAutoplay()
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapPanel.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapPanel.cs
deleted file mode 100644
index 1e3e06ce7a..0000000000
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapPanel.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (c) ppy Pty Ltd . 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.Beatmaps;
-using osu.Game.Online.Multiplayer;
-using osu.Game.Screens.Multi.Match.Components;
-using osu.Framework.Graphics;
-using osu.Framework.Utils;
-using osu.Game.Audio;
-using osu.Framework.Allocation;
-
-namespace osu.Game.Tests.Visual.Multiplayer
-{
- [Cached(typeof(IPreviewTrackOwner))]
- public class TestSceneMatchBeatmapPanel : MultiplayerTestScene, IPreviewTrackOwner
- {
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(MatchBeatmapPanel)
- };
-
- [Resolved]
- private PreviewTrackManager previewTrackManager { get; set; }
-
- public TestSceneMatchBeatmapPanel()
- {
- Add(new MatchBeatmapPanel
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- });
-
- Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 1763072 } });
- Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 2101557 } });
- Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 1973466 } });
- Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 2109801 } });
- Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 1922035 } });
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- AddStep("Select random beatmap", () =>
- {
- Room.CurrentItem.Value = Room.Playlist[RNG.Next(Room.Playlist.Count)];
- previewTrackManager.StopAnyPlaying(this);
- });
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs
index e42042f2ea..cf40995fc0 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs
@@ -5,10 +5,10 @@ using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
-using osu.Game.Online.Multiplayer.GameTypes;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Multi.Match.Components;
+using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -23,16 +23,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Room.Playlist.Add(new PlaylistItem
{
- Beatmap = new BeatmapInfo
+ Beatmap =
{
- Metadata = new BeatmapMetadata
+ Value = new BeatmapInfo
{
- Title = "Title",
- Artist = "Artist",
- AuthorString = "Author",
- },
- Version = "Version",
- Ruleset = new OsuRuleset().RulesetInfo
+ Metadata = new BeatmapMetadata
+ {
+ Title = "Title",
+ Artist = "Artist",
+ AuthorString = "Author",
+ },
+ Version = "Version",
+ Ruleset = new OsuRuleset().RulesetInfo
+ }
},
RequiredMods =
{
@@ -42,7 +45,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
- Room.Type.Value = new GameTypeTimeshift();
+ Room.Name.Value = "A very awesome room";
+ Room.Host.Value = new User { Id = 2, Username = "peppy" };
Child = new Header();
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHostInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHostInfo.cs
deleted file mode 100644
index 808a45cdf0..0000000000
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHostInfo.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (c) ppy Pty Ltd . 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.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Game.Screens.Multi.Match.Components;
-using osu.Game.Users;
-
-namespace osu.Game.Tests.Visual.Multiplayer
-{
- public class TestSceneMatchHostInfo : OsuTestScene
- {
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(HostInfo)
- };
-
- private readonly Bindable host = new Bindable(new User { Username = "SomeHost" });
-
- public TestSceneMatchHostInfo()
- {
- HostInfo hostInfo;
-
- Child = hostInfo = new HostInfo
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre
- };
-
- hostInfo.Host.BindTo(host);
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchInfo.cs
deleted file mode 100644
index a6c036a876..0000000000
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchInfo.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using NUnit.Framework;
-using osu.Framework.Allocation;
-using osu.Game.Beatmaps;
-using osu.Game.Online.Multiplayer;
-using osu.Game.Online.Multiplayer.RoomStatuses;
-using osu.Game.Rulesets;
-using osu.Game.Screens.Multi.Match.Components;
-
-namespace osu.Game.Tests.Visual.Multiplayer
-{
- [TestFixture]
- public class TestSceneMatchInfo : MultiplayerTestScene
- {
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(Info),
- typeof(HeaderButton),
- typeof(ReadyButton),
- typeof(MatchBeatmapPanel)
- };
-
- [BackgroundDependencyLoader]
- private void load(RulesetStore rulesets)
- {
- Add(new Info());
-
- AddStep(@"set name", () => Room.Name.Value = @"Room Name?");
- AddStep(@"set availability", () => Room.Availability.Value = RoomAvailability.FriendsOnly);
- AddStep(@"set status", () => Room.Status.Value = new RoomStatusPlaying());
- AddStep(@"set beatmap", () =>
- {
- Room.Playlist.Clear();
- Room.Playlist.Add(new PlaylistItem
- {
- Beatmap = new BeatmapInfo
- {
- StarDifficulty = 2.4,
- Ruleset = rulesets.GetRuleset(0),
- Metadata = new BeatmapMetadata
- {
- Title = @"My Song",
- Artist = @"VisualTests",
- AuthorString = @"osu!lazer",
- },
- }
- });
- });
-
- AddStep(@"change name", () => Room.Name.Value = @"Room Name!");
- AddStep(@"change availability", () => Room.Availability.Value = RoomAvailability.InviteOnly);
- AddStep(@"change status", () => Room.Status.Value = new RoomStatusOpen());
- AddStep(@"null beatmap", () => Room.Playlist.Clear());
- AddStep(@"change beatmap", () =>
- {
- Room.Playlist.Clear();
- Room.Playlist.Add(new PlaylistItem
- {
- Beatmap = new BeatmapInfo
- {
- StarDifficulty = 4.2,
- Ruleset = rulesets.GetRuleset(3),
- Metadata = new BeatmapMetadata
- {
- Title = @"Your Song",
- Artist = @"Tester",
- AuthorString = @"Someone",
- },
- }
- });
- });
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboardChatDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboardChatDisplay.cs
new file mode 100644
index 0000000000..e46386b263
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboardChatDisplay.cs
@@ -0,0 +1,39 @@
+// Copyright (c) ppy Pty Ltd . 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.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Screens.Multi.Match.Components;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneMatchLeaderboardChatDisplay : MultiplayerTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(LeaderboardChatDisplay)
+ };
+
+ protected override bool UseOnlineAPI => true;
+
+ public TestSceneMatchLeaderboardChatDisplay()
+ {
+ Room.RoomID.Value = 7;
+
+ Add(new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(500),
+ Child = new LeaderboardChatDisplay
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchParticipants.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchParticipants.cs
deleted file mode 100644
index 1ac914e27d..0000000000
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchParticipants.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using NUnit.Framework;
-using osu.Framework.Graphics;
-using osu.Game.Screens.Multi.Match.Components;
-using osu.Game.Users;
-
-namespace osu.Game.Tests.Visual.Multiplayer
-{
- [TestFixture]
- public class TestSceneMatchParticipants : MultiplayerTestScene
- {
- public TestSceneMatchParticipants()
- {
- Add(new Participants { RelativeSizeAxes = Axes.Both });
-
- AddStep(@"set max to null", () => Room.MaxParticipants.Value = null);
- AddStep(@"set users", () => Room.Participants.Value = new[]
- {
- new User
- {
- Username = @"Feppla",
- Id = 4271601,
- Country = new Country { FlagName = @"SE" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
- IsSupporter = true,
- },
- new User
- {
- Username = @"Xilver",
- Id = 3099689,
- Country = new Country { FlagName = @"IL" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
- IsSupporter = true,
- },
- new User
- {
- Username = @"Wucki",
- Id = 5287410,
- Country = new Country { FlagName = @"FI" },
- CoverUrl = @"https://assets.ppy.sh/user-profile-covers/5287410/5cfeaa9dd41cbce038ecdc9d781396ed4b0108089170bf7f50492ef8eadeb368.jpeg",
- IsSupporter = true,
- },
- });
-
- AddStep(@"set max", () => Room.MaxParticipants.Value = 10);
- AddStep(@"clear users", () => Room.Participants.Value = System.Array.Empty());
- AddStep(@"set max to null", () => Room.MaxParticipants.Value = null);
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchResults.cs
deleted file mode 100644
index 58e9240026..0000000000
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchResults.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright (c) ppy Pty Ltd . 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.Allocation;
-using osu.Game.Beatmaps;
-using osu.Game.Online.API;
-using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Scoring;
-using osu.Game.Screens.Multi.Match.Components;
-using osu.Game.Screens.Multi.Ranking;
-using osu.Game.Screens.Multi.Ranking.Pages;
-using osu.Game.Screens.Multi.Ranking.Types;
-using osu.Game.Screens.Ranking;
-using osu.Game.Users;
-
-namespace osu.Game.Tests.Visual.Multiplayer
-{
- public class TestSceneMatchResults : MultiplayerTestScene
- {
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(MatchResults),
- typeof(RoomLeaderboardPageInfo),
- typeof(RoomLeaderboardPage)
- };
-
- [Resolved]
- private BeatmapManager beatmaps { get; set; }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0);
- if (beatmapInfo != null)
- Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
-
- Room.RoomID.Value = 1;
- Room.Name.Value = "an awesome room";
-
- LoadScreen(new TestMatchResults(new ScoreInfo
- {
- User = new User { Id = 10 },
- }));
- }
-
- private class TestMatchResults : MatchResults
- {
- public TestMatchResults(ScoreInfo score)
- : base(score)
- {
- }
-
- protected override IEnumerable CreateResultPages() => new[] { new TestRoomLeaderboardPageInfo(Score, Beatmap.Value) };
- }
-
- private class TestRoomLeaderboardPageInfo : RoomLeaderboardPageInfo
- {
- private readonly ScoreInfo score;
- private readonly WorkingBeatmap beatmap;
-
- public TestRoomLeaderboardPageInfo(ScoreInfo score, WorkingBeatmap beatmap)
- : base(score, beatmap)
- {
- this.score = score;
- this.beatmap = beatmap;
- }
-
- public override ResultsPage CreatePage() => new TestRoomLeaderboardPage(score, beatmap);
- }
-
- private class TestRoomLeaderboardPage : RoomLeaderboardPage
- {
- public TestRoomLeaderboardPage(ScoreInfo score, WorkingBeatmap beatmap)
- : base(score, beatmap)
- {
- }
-
- protected override MatchLeaderboard CreateLeaderboard() => new TestMatchLeaderboard();
- }
-
- private class TestMatchLeaderboard : RoomLeaderboardPage.ResultsMatchLeaderboard
- {
- protected override APIRequest FetchScores(Action> scoresCallback)
- {
- var scores = Enumerable.Range(0, 50).Select(createRoomScore).ToArray();
-
- scoresCallback?.Invoke(scores);
- ScoresLoaded?.Invoke(scores);
-
- return null;
- }
-
- private APIUserScoreAggregate createRoomScore(int id) => new APIUserScoreAggregate
- {
- User = new User { Id = id, Username = $"User {id}" },
- Accuracy = 0.98,
- TotalScore = 987654,
- TotalAttempts = 13,
- CompletedBeatmaps = 5
- };
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs
index 8d842fc865..047e9d860d 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("set name", () => Room.Name.Value = "Room name");
AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value);
- AddStep("set beatmap", () => Room.Playlist.Add(new PlaylistItem { Beatmap = CreateBeatmap(Ruleset.Value).BeatmapInfo }));
+ AddStep("set beatmap", () => Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }));
AddAssert("button enabled", () => settings.ApplyButton.Enabled.Value);
AddStep("clear name", () => Room.Name.Value = "");
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs
new file mode 100644
index 0000000000..2c6f34d8a6
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs
@@ -0,0 +1,158 @@
+// Copyright (c) ppy Pty Ltd . 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.IO;
+using System.Linq;
+using System.Text;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Extensions;
+using osu.Framework.Platform;
+using osu.Framework.Screens;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Multi.Components;
+using osu.Game.Screens.Select;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneMatchSongSelect : MultiplayerTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(MatchSongSelect),
+ typeof(MatchBeatmapDetailArea),
+ };
+
+ [Resolved]
+ private BeatmapManager beatmapManager { get; set; }
+
+ private BeatmapManager manager;
+
+ private RulesetStore rulesets;
+
+ private TestMatchSongSelect songSelect;
+
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, AudioManager audio)
+ {
+ Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
+ Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
+
+ var beatmaps = new List();
+
+ for (int i = 0; i < 6; i++)
+ {
+ int beatmapId = 10 * 10 + i;
+
+ int length = RNG.Next(30000, 200000);
+ double bpm = RNG.NextSingle(80, 200);
+
+ beatmaps.Add(new BeatmapInfo
+ {
+ Ruleset = new OsuRuleset().RulesetInfo,
+ OnlineBeatmapID = beatmapId,
+ Path = "normal.osu",
+ Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
+ Length = length,
+ BPM = bpm,
+ BaseDifficulty = new BeatmapDifficulty
+ {
+ OverallDifficulty = 3.5f,
+ },
+ });
+ }
+
+ manager.Import(new BeatmapSetInfo
+ {
+ OnlineBeatmapSetID = 10,
+ Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(),
+ Metadata = new BeatmapMetadata
+ {
+ // Create random metadata, then we can check if sorting works based on these
+ Artist = "Some Artist " + RNG.Next(0, 9),
+ Title = $"Some Song (set id 10), max bpm {beatmaps.Max(b => b.BPM):0.#})",
+ AuthorString = "Some Guy " + RNG.Next(0, 9),
+ },
+ Beatmaps = beatmaps,
+ DateAdded = DateTimeOffset.UtcNow,
+ }).Wait();
+ }
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("reset", () =>
+ {
+ Ruleset.Value = new OsuRuleset().RulesetInfo;
+ Beatmap.SetDefault();
+ });
+
+ AddStep("create song select", () => LoadScreen(songSelect = new TestMatchSongSelect()));
+ AddUntilStep("wait for present", () => songSelect.IsCurrentScreen());
+ }
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Room.Playlist.Clear();
+ });
+
+ [Test]
+ public void TestItemAddedIfEmptyOnStart()
+ {
+ AddStep("finalise selection", () => songSelect.FinaliseSelection());
+ AddAssert("playlist has 1 item", () => Room.Playlist.Count == 1);
+ }
+
+ [Test]
+ public void TestItemAddedWhenCreateNewItemClicked()
+ {
+ AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
+ AddAssert("playlist has 1 item", () => Room.Playlist.Count == 1);
+ }
+
+ [Test]
+ public void TestItemNotAddedIfExistingOnStart()
+ {
+ AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
+ AddStep("finalise selection", () => songSelect.FinaliseSelection());
+ AddAssert("playlist has 1 item", () => Room.Playlist.Count == 1);
+ }
+
+ [Test]
+ public void TestAddSameItemMultipleTimes()
+ {
+ AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
+ AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
+ AddAssert("playlist has 2 items", () => Room.Playlist.Count == 2);
+ }
+
+ [Test]
+ public void TestAddItemAfterRearrangement()
+ {
+ AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
+ AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
+ AddStep("rearrange", () =>
+ {
+ var item = Room.Playlist[0];
+ Room.Playlist.RemoveAt(0);
+ Room.Playlist.Add(item);
+ });
+
+ AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
+ AddAssert("new item has id 2", () => Room.Playlist.Last().ID == 2);
+ }
+
+ private class TestMatchSongSelect : MatchSongSelect
+ {
+ public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs
new file mode 100644
index 0000000000..7f79e306ad
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs
@@ -0,0 +1,121 @@
+// Copyright (c) ppy Pty Ltd . 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.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Screens;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Multi;
+using osu.Game.Screens.Multi.Match;
+using osu.Game.Screens.Multi.Match.Components;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Users;
+using osuTK.Input;
+using Header = osu.Game.Screens.Multi.Match.Components.Header;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneMatchSubScreen : MultiplayerTestScene
+ {
+ protected override bool UseOnlineAPI => true;
+
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(Screens.Multi.Multiplayer),
+ typeof(MatchSubScreen),
+ typeof(Header),
+ typeof(Footer)
+ };
+
+ [Cached(typeof(IRoomManager))]
+ private readonly TestRoomManager roomManager = new TestRoomManager();
+
+ [Resolved]
+ private BeatmapManager beatmaps { get; set; }
+
+ [Resolved]
+ private RulesetStore rulesets { get; set; }
+
+ private TestMatchSubScreen match;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Room.CopyFrom(new Room());
+ });
+
+ [SetUpSteps]
+ public void SetupSteps()
+ {
+ AddStep("load match", () => LoadScreen(match = new TestMatchSubScreen(Room)));
+ AddUntilStep("wait for load", () => match.IsCurrentScreen());
+ }
+
+ [Test]
+ public void TestPlaylistItemSelectedOnCreate()
+ {
+ AddStep("set room properties", () =>
+ {
+ Room.Name.Value = "my awesome room";
+ Room.Host.Value = new User { Id = 2, Username = "peppy" };
+ Room.Playlist.Add(new PlaylistItem
+ {
+ Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo }
+ });
+ });
+
+ AddStep("move mouse to create button", () =>
+ {
+ var footer = match.ChildrenOfType